Merge branch 'feat/short' into pr/samgermain/5780
This commit is contained in:
@@ -52,6 +52,8 @@ USERPATH_STRATEGIES = 'strategies'
|
||||
USERPATH_NOTEBOOKS = 'notebooks'
|
||||
|
||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||
|
||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||
|
||||
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
||||
@@ -316,10 +318,16 @@ CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'enabled': {'type': 'boolean'},
|
||||
'url': {'type': 'string'},
|
||||
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||
'retries': {'type': 'integer', 'minimum': 0},
|
||||
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||
'webhookbuy': {'type': 'object'},
|
||||
'webhookbuycancel': {'type': 'object'},
|
||||
'webhookbuyfill': {'type': 'object'},
|
||||
'webhooksell': {'type': 'object'},
|
||||
'webhooksellcancel': {'type': 'object'},
|
||||
'webhooksellfill': {'type': 'object'},
|
||||
'webhookstatus': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
|
@@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
||||
'fee_open', 'fee_close', 'trade_duration',
|
||||
'profit_ratio', 'profit_abs', 'sell_reason',
|
||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag',
|
||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
||||
'is_short'
|
||||
]
|
||||
# TODO-lev: usage of the above might need compatibility code (buy_tag, is_short?, ...?)
|
||||
|
@@ -77,10 +77,10 @@ class HDF5DataHandler(IDataHandler):
|
||||
|
||||
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||
|
||||
ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc')
|
||||
ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date'])
|
||||
|
||||
ds.close()
|
||||
_data.loc[:, self._columns].to_hdf(
|
||||
filename, key, mode='a', complevel=9, complib='blosc',
|
||||
format='table', data_columns=['date']
|
||||
)
|
||||
|
||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
||||
timerange: Optional[TimeRange] = None, candle_type: str = '') -> pd.DataFrame:
|
||||
@@ -172,11 +172,11 @@ class HDF5DataHandler(IDataHandler):
|
||||
"""
|
||||
key = self._pair_trades_key(pair)
|
||||
|
||||
ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair),
|
||||
mode='a', complevel=9, complib='blosc')
|
||||
ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
|
||||
format='table', data_columns=['timestamp'])
|
||||
ds.close()
|
||||
pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf(
|
||||
self._pair_trades_filename(self._datadir, pair), key,
|
||||
mode='a', complevel=9, complib='blosc',
|
||||
format='table', data_columns=['timestamp']
|
||||
)
|
||||
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
"""
|
||||
|
@@ -23,6 +23,7 @@ BAD_EXCHANGES = {
|
||||
MAP_EXCHANGE_CHILDCLASS = {
|
||||
'binanceus': 'binance',
|
||||
'binanceje': 'binance',
|
||||
'binanceusdm': 'binance',
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1372,7 +1372,7 @@ class Exchange:
|
||||
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||
if raise_:
|
||||
raise
|
||||
continue
|
||||
@@ -1405,7 +1405,7 @@ class Exchange:
|
||||
cached_pairs = []
|
||||
# Gather coroutines to run
|
||||
for pair, timeframe, candle_type in set(pair_list):
|
||||
if ((pair, timeframe, candle_type) not in self._klines
|
||||
if ((pair, timeframe, candle_type) not in self._klines or not cache
|
||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||
if not since_ms and self.required_candle_call_count > 1:
|
||||
# Multiple calls for one pair - to get more history
|
||||
@@ -1428,27 +1428,28 @@ class Exchange:
|
||||
)
|
||||
cached_pairs.append((pair, timeframe, candle_type))
|
||||
|
||||
results = asyncio.get_event_loop().run_until_complete(
|
||||
asyncio.gather(*input_coroutines, return_exceptions=True))
|
||||
|
||||
results_df = {}
|
||||
# handle caching
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
# Deconstruct tuple (has 3 elements)
|
||||
pair, timeframe, c_type, ticks = res
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if ticks:
|
||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
ohlcv_df = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
results_df[(pair, timeframe, c_type)] = ohlcv_df
|
||||
if cache:
|
||||
self._klines[(pair, timeframe, c_type)] = ohlcv_df
|
||||
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||
for input_coro in chunks(input_coroutines, 100):
|
||||
results = asyncio.get_event_loop().run_until_complete(
|
||||
asyncio.gather(*input_coro, return_exceptions=True))
|
||||
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||
continue
|
||||
# Deconstruct tuple (has 4 elements)
|
||||
pair, timeframe, c_type, ticks = res
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if ticks:
|
||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
ohlcv_df = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
results_df[(pair, timeframe, c_type)] = ohlcv_df
|
||||
if cache:
|
||||
self._klines[(pair, timeframe, c_type)] = ohlcv_df
|
||||
# Return cached klines
|
||||
for pair, timeframe, c_type in cached_pairs:
|
||||
results_df[(pair, timeframe, c_type)] = self.klines(
|
||||
|
@@ -322,7 +322,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
f"for order {order.order_id}."
|
||||
)
|
||||
self.update_trade_state(trade, order.order_id,
|
||||
stoploss_order=order.ft_order_side == 'stoploss')
|
||||
stoploss_order=order.ft_order_side == 'stoploss',
|
||||
send_msg=False)
|
||||
|
||||
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
||||
for trade in trades:
|
||||
@@ -333,7 +334,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
f"Updating {trade.enter_side}-fee on trade {trade}"
|
||||
f"for order {order.order_id}."
|
||||
)
|
||||
self.update_trade_state(trade, order.order_id)
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
|
||||
def handle_insufficient_funds(self, trade: Trade):
|
||||
"""
|
||||
@@ -356,7 +357,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if order:
|
||||
logger.info(
|
||||
f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.")
|
||||
self.update_trade_state(trade, order.order_id)
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
|
||||
def refind_lost_order(self, trade):
|
||||
"""
|
||||
@@ -575,8 +576,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
pair: str,
|
||||
stake_amount: float,
|
||||
price: Optional[float] = None,
|
||||
forcebuy: bool = False,
|
||||
*,
|
||||
is_short: bool = False,
|
||||
ordertype: Optional[str] = None,
|
||||
enter_tag: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
@@ -649,12 +651,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
|
||||
amount = (stake_amount / enter_limit_requested) * leverage
|
||||
order_type = self.strategy.order_types['buy']
|
||||
if forcebuy:
|
||||
# Forcebuy can define a different ordertype
|
||||
# TODO-lev: get a forceshort? What is this
|
||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
||||
# TODO-lev: Will this work for shorting?
|
||||
order_type = ordertype or self.strategy.order_types['buy']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||
@@ -736,8 +733,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
exchange=self.exchange.id,
|
||||
open_order_id=order_id,
|
||||
strategy=self.strategy.get_strategy_name(),
|
||||
# TODO-lev: compatibility layer for buy_tag (!)
|
||||
buy_tag=enter_tag,
|
||||
enter_tag=enter_tag,
|
||||
timeframe=timeframe_to_minutes(self.config['timeframe']),
|
||||
leverage=leverage,
|
||||
is_short=is_short,
|
||||
@@ -748,10 +744,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
trade.orders.append(order_obj)
|
||||
|
||||
# Update fees if order is closed
|
||||
if order_status == 'closed':
|
||||
self.update_trade_state(trade, order_id, order)
|
||||
|
||||
Trade.query.session.add(trade)
|
||||
Trade.commit()
|
||||
|
||||
@@ -760,19 +752,31 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
self._notify_enter(trade, order_type)
|
||||
|
||||
# Update fees if order is closed
|
||||
if order_status == 'closed':
|
||||
self.update_trade_state(trade, order_id, order)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_enter(self, trade: Trade, order_type: str) -> None:
|
||||
def _notify_enter(self, trade: Trade, order_type: Optional[str] = None,
|
||||
fill: bool = False) -> None:
|
||||
"""
|
||||
Sends rpc notification when a entry order occurred.
|
||||
"""
|
||||
if fill:
|
||||
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
|
||||
else:
|
||||
msg_type = RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY
|
||||
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'type': msg_type,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'limit': trade.open_rate,
|
||||
'limit': trade.open_rate, # Deprecated (?)
|
||||
'open_rate': trade.open_rate,
|
||||
'order_type': order_type,
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
@@ -794,7 +798,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': msg_type,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'limit': trade.open_rate,
|
||||
@@ -811,23 +816,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': msg_type,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'open_rate': trade.open_rate,
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': trade.amount,
|
||||
'open_date': trade.open_date,
|
||||
}
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
#
|
||||
# SELL / exit positions / close trades logic and methods
|
||||
#
|
||||
@@ -1051,7 +1039,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if should_exit.sell_flag:
|
||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}'
|
||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||
self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag)
|
||||
self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1269,12 +1257,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||
|
||||
def execute_trade_exit(
|
||||
self,
|
||||
trade: Trade,
|
||||
limit: float,
|
||||
sell_reason: SellCheckTuple, # TODO-lev update to exit_reason
|
||||
exit_tag: Optional[str] = None
|
||||
) -> bool:
|
||||
self,
|
||||
trade: Trade,
|
||||
limit: float,
|
||||
sell_reason: SellCheckTuple,
|
||||
*,
|
||||
exit_tag: Optional[str] = None,
|
||||
ordertype: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Executes a trade exit for the given trade and limit
|
||||
:param trade: Trade instance
|
||||
@@ -1317,14 +1307,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||
|
||||
order_type = self.strategy.order_types[exit_type]
|
||||
order_type = ordertype or self.strategy.order_types[exit_type]
|
||||
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
||||
# Emergency sells (default to market!)
|
||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||
if sell_reason.sell_type == SellType.FORCE_SELL:
|
||||
# Force sells (default to the sell_type defined in the strategy,
|
||||
# but we allow this value to be changed)
|
||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||
|
||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||
time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit
|
||||
@@ -1359,16 +1345,16 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.sell_order_status = ''
|
||||
trade.close_rate_requested = limit
|
||||
trade.sell_reason = exit_tag or sell_reason.sell_reason
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
Trade.commit()
|
||||
|
||||
# Lock pair for one candle to prevent immediate re-trading
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
|
||||
self._notify_exit(trade, order_type)
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
Trade.commit()
|
||||
|
||||
return True
|
||||
|
||||
@@ -1399,7 +1385,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
@@ -1443,7 +1430,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'buy_tag': trade.enter_tag,
|
||||
'enter_tag': trade.enter_tag,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||
@@ -1465,13 +1453,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
#
|
||||
|
||||
def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
|
||||
stoploss_order: bool = False) -> bool:
|
||||
stoploss_order: bool = False, send_msg: bool = True) -> bool:
|
||||
"""
|
||||
Checks trades with open orders and updates the amount if necessary
|
||||
Handles closing both buy and sell orders.
|
||||
:param trade: Trade object of the trade we're analyzing
|
||||
:param order_id: Order-id of the order we're analyzing
|
||||
:param action_order: Already acquired order object
|
||||
:param send_msg: Send notification - should always be True except in "recovery" methods
|
||||
:return: True if order has been cancelled without being filled partially, False otherwise
|
||||
"""
|
||||
if not order_id:
|
||||
@@ -1511,13 +1500,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# Updating wallets when order is closed
|
||||
if not trade.is_open:
|
||||
if not stoploss_order and not trade.open_order_id:
|
||||
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||
self._notify_exit(trade, '', True)
|
||||
self.handle_protections(trade.pair)
|
||||
self.wallets.update()
|
||||
elif not trade.open_order_id:
|
||||
elif send_msg and not trade.open_order_id:
|
||||
# Buy fill
|
||||
self._notify_enter_fill(trade)
|
||||
self._notify_enter(trade, fill=True)
|
||||
|
||||
return False
|
||||
|
||||
|
@@ -92,7 +92,8 @@ class Backtesting:
|
||||
self.init_backtest_detail()
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||
raise OperationalException("VolumePairList not allowed for backtesting. "
|
||||
"Please use StaticPairlist instead.")
|
||||
if 'PerformanceFilter' in self.pairlists.name_list:
|
||||
raise OperationalException("PerformanceFilter not allowed for backtesting.")
|
||||
|
||||
@@ -490,11 +491,11 @@ class Backtesting:
|
||||
open_rate=row[OPEN_IDX],
|
||||
open_date=current_time,
|
||||
stake_amount=stake_amount,
|
||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
||||
amount=round((stake_amount / row[OPEN_IDX]) * leverage, 8),
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
buy_tag=row[ENTER_TAG_IDX] if has_enter_tag else None,
|
||||
enter_tag=row[ENTER_TAG_IDX] if has_enter_tag else None,
|
||||
exchange=self._exchange_name,
|
||||
is_short=(direction == 'short'),
|
||||
leverage=leverage,
|
||||
|
@@ -46,20 +46,11 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
||||
'.2f', 'd', 's', 's']
|
||||
|
||||
|
||||
def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
|
||||
def _get_line_header(first_column: str, stake_currency: str, direction: str = 'Buys') -> List[str]:
|
||||
"""
|
||||
Generate header lines (goes in line with _generate_result_line())
|
||||
"""
|
||||
return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Win Draw Loss Win%']
|
||||
|
||||
|
||||
def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]:
|
||||
"""
|
||||
Generate header lines (goes in line with _generate_result_line())
|
||||
"""
|
||||
return [first_column, 'Sells', 'Avg Profit %', 'Cum Profit %',
|
||||
return [first_column, direction, 'Avg Profit %', 'Cum Profit %',
|
||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||
'Win Draw Loss Win%']
|
||||
|
||||
@@ -156,7 +147,7 @@ def generate_tag_metrics(tag_type: str,
|
||||
if skip_nan and result['profit_abs'].isnull().all():
|
||||
continue
|
||||
|
||||
tabular_data.append(_generate_tag_result_line(result, starting_balance, tag))
|
||||
tabular_data.append(_generate_result_line(result, starting_balance, tag))
|
||||
|
||||
# Sort by total profit %:
|
||||
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
||||
@@ -168,39 +159,6 @@ def generate_tag_metrics(tag_type: str,
|
||||
return []
|
||||
|
||||
|
||||
def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
|
||||
"""
|
||||
Generate one result dict, with "first_column" as key.
|
||||
"""
|
||||
profit_sum = result['profit_ratio'].sum()
|
||||
# (end-capital - starting capital) / starting capital
|
||||
profit_total = result['profit_abs'].sum() / starting_balance
|
||||
|
||||
return {
|
||||
'key': first_column,
|
||||
'trades': len(result),
|
||||
'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
|
||||
'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0,
|
||||
'profit_sum': profit_sum,
|
||||
'profit_sum_pct': round(profit_sum * 100.0, 2),
|
||||
'profit_total_abs': result['profit_abs'].sum(),
|
||||
'profit_total': profit_total,
|
||||
'profit_total_pct': round(profit_total * 100.0, 2),
|
||||
'duration_avg': str(timedelta(
|
||||
minutes=round(result['trade_duration'].mean()))
|
||||
) if not result.empty else '0:00',
|
||||
# 'duration_max': str(timedelta(
|
||||
# minutes=round(result['trade_duration'].max()))
|
||||
# ) if not result.empty else '0:00',
|
||||
# 'duration_min': str(timedelta(
|
||||
# minutes=round(result['trade_duration'].min()))
|
||||
# ) if not result.empty else '0:00',
|
||||
'wins': len(result[result['profit_abs'] > 0]),
|
||||
'draws': len(result[result['profit_abs'] == 0]),
|
||||
'losses': len(result[result['profit_abs'] < 0]),
|
||||
}
|
||||
|
||||
|
||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
@@ -422,8 +380,8 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
starting_balance=start_balance,
|
||||
results=results, skip_nan=False)
|
||||
|
||||
buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=start_balance,
|
||||
results=results, skip_nan=False)
|
||||
enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
|
||||
results=results, skip_nan=False)
|
||||
|
||||
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
|
||||
results=results)
|
||||
@@ -448,7 +406,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'best_pair': best_pair,
|
||||
'worst_pair': worst_pair,
|
||||
'results_per_pair': pair_results,
|
||||
'results_per_buy_tag': buy_tag_results,
|
||||
'results_per_enter_tag': enter_tag_results,
|
||||
'sell_reason_summary': sell_reason_stats,
|
||||
'left_open_trades': left_open_results,
|
||||
# 'days_breakdown_stats': days_breakdown_stats,
|
||||
@@ -634,10 +592,10 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
|
||||
:param stake_currency: stake-currency - used to correctly name headers
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
if(tag_type == "buy_tag"):
|
||||
if(tag_type == "enter_tag"):
|
||||
headers = _get_line_header("TAG", stake_currency)
|
||||
else:
|
||||
headers = _get_line_header_sell("TAG", stake_currency)
|
||||
headers = _get_line_header("TAG", stake_currency, 'Sells')
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
output = [
|
||||
[
|
||||
@@ -818,10 +776,12 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
|
||||
print(table)
|
||||
|
||||
if results.get('results_per_buy_tag') is not None:
|
||||
if (results.get('results_per_enter_tag') is not None
|
||||
or results.get('results_per_buy_tag') is not None):
|
||||
# results_per_buy_tag is deprecated and should be removed 2 versions after short golive.
|
||||
table = text_table_tags(
|
||||
"buy_tag",
|
||||
results['results_per_buy_tag'],
|
||||
"enter_tag",
|
||||
results.get('results_per_enter_tag', results.get('results_per_buy_tag')),
|
||||
stake_currency=stake_currency)
|
||||
|
||||
if isinstance(table, str) and len(table) > 0:
|
||||
|
@@ -47,7 +47,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
||||
min_rate = get_column_def(cols, 'min_rate', 'null')
|
||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||
strategy = get_column_def(cols, 'strategy', 'null')
|
||||
buy_tag = get_column_def(cols, 'buy_tag', 'null')
|
||||
enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
|
||||
|
||||
trading_mode = get_column_def(cols, 'trading_mode', 'null')
|
||||
|
||||
@@ -98,7 +98,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
||||
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||
stoploss_order_id, stoploss_last_update,
|
||||
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
|
||||
max_rate, min_rate, sell_reason, sell_order_status, strategy, enter_tag,
|
||||
timeframe, open_trade_value, close_profit_abs,
|
||||
trading_mode, leverage, isolated_liq, is_short,
|
||||
interest_rate, funding_fees
|
||||
@@ -116,7 +116,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
||||
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||
{sell_order_status} sell_order_status,
|
||||
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
|
||||
{strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
|
||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
|
||||
{trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq,
|
||||
{is_short} is_short, {interest_rate} interest_rate,
|
||||
@@ -180,7 +180,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||
table_back_name = get_backup_name(tabs, 'trades_bak')
|
||||
|
||||
# Check for latest column
|
||||
if not has_column(cols, 'funding_fees'):
|
||||
if not has_column(cols, 'enter_tag'):
|
||||
logger.info(f'Running database migration for trades - backup: {table_back_name}')
|
||||
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
||||
# Reread columns - the above recreated the table!
|
||||
|
@@ -264,7 +264,7 @@ class LocalTrade():
|
||||
sell_reason: str = ''
|
||||
sell_order_status: str = ''
|
||||
strategy: str = ''
|
||||
buy_tag: Optional[str] = None
|
||||
enter_tag: Optional[str] = None
|
||||
timeframe: Optional[int] = None
|
||||
|
||||
trading_mode: TradingMode = TradingMode.SPOT
|
||||
@@ -280,6 +280,14 @@ class LocalTrade():
|
||||
# Futures properties
|
||||
funding_fees: Optional[float] = None
|
||||
|
||||
@property
|
||||
def buy_tag(self) -> Optional[str]:
|
||||
"""
|
||||
Compatibility between buy_tag (old) and enter_tag (new)
|
||||
Consider buy_tag deprecated
|
||||
"""
|
||||
return self.enter_tag
|
||||
|
||||
@property
|
||||
def has_no_leverage(self) -> bool:
|
||||
"""Returns true if this is a non-leverage, non-short trade"""
|
||||
@@ -389,7 +397,8 @@ class LocalTrade():
|
||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||
'stake_amount': round(self.stake_amount, 8),
|
||||
'strategy': self.strategy,
|
||||
'buy_tag': self.buy_tag,
|
||||
'buy_tag': self.enter_tag,
|
||||
'enter_tag': self.enter_tag,
|
||||
'timeframe': self.timeframe,
|
||||
|
||||
'fee_open': self.fee_open,
|
||||
@@ -928,7 +937,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
sell_reason = Column(String(100), nullable=True)
|
||||
sell_order_status = Column(String(100), nullable=True)
|
||||
strategy = Column(String(100), nullable=True)
|
||||
buy_tag = Column(String(100), nullable=True)
|
||||
enter_tag = Column(String(100), nullable=True)
|
||||
timeframe = Column(Integer, nullable=True)
|
||||
|
||||
trading_mode = Column(Enum(TradingMode), nullable=True)
|
||||
@@ -1099,7 +1108,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
def get_enter_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, based on buy tag performance
|
||||
Can either be average for all pairs or a specific pair provided
|
||||
@@ -1110,25 +1119,25 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
if(pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
buy_tag_perf = Trade.query.with_entities(
|
||||
Trade.buy_tag,
|
||||
enter_tag_perf = Trade.query.with_entities(
|
||||
Trade.enter_tag,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.buy_tag) \
|
||||
.group_by(Trade.enter_tag) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return [
|
||||
{
|
||||
'buy_tag': buy_tag if buy_tag is not None else "Other",
|
||||
'enter_tag': enter_tag if enter_tag is not None else "Other",
|
||||
'profit_ratio': profit,
|
||||
'profit_pct': round(profit * 100, 2),
|
||||
'profit_abs': profit_abs,
|
||||
'count': count
|
||||
}
|
||||
for buy_tag, profit, profit_abs, count in buy_tag_perf
|
||||
for enter_tag, profit, profit_abs, count in enter_tag_perf
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
@@ -1178,7 +1187,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
|
||||
mix_tag_perf = Trade.query.with_entities(
|
||||
Trade.id,
|
||||
Trade.buy_tag,
|
||||
Trade.enter_tag,
|
||||
Trade.sell_reason,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
@@ -1189,12 +1198,12 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
.all()
|
||||
|
||||
return_list: List[Dict] = []
|
||||
for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
||||
buy_tag = buy_tag if buy_tag is not None else "Other"
|
||||
for id, enter_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
|
||||
enter_tag = enter_tag if enter_tag is not None else "Other"
|
||||
sell_reason = sell_reason if sell_reason is not None else "Other"
|
||||
|
||||
if(sell_reason is not None and buy_tag is not None):
|
||||
mix_tag = buy_tag + " " + sell_reason
|
||||
if(sell_reason is not None and enter_tag is not None):
|
||||
mix_tag = enter_tag + " " + sell_reason
|
||||
i = 0
|
||||
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
||||
return_list.append({'mix_tag': mix_tag,
|
||||
|
@@ -5,6 +5,7 @@ import logging
|
||||
import random
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.enums.runmode import RunMode
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
@@ -18,7 +19,15 @@ class ShuffleFilter(IPairList):
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._seed = pairlistconfig.get('seed')
|
||||
# Apply seed in backtesting mode to get comparable results,
|
||||
# but not in live modes to get a non-repeating order of pairs during live modes.
|
||||
if config.get('runmode') in (RunMode.LIVE, RunMode.DRY_RUN):
|
||||
self._seed = None
|
||||
logger.info("Live mode detected, not applying seed.")
|
||||
else:
|
||||
self._seed = pairlistconfig.get('seed')
|
||||
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
|
||||
|
||||
self._random = random.Random(self._seed)
|
||||
|
||||
@property
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
@@ -125,19 +126,24 @@ class Daily(BaseModel):
|
||||
|
||||
|
||||
class UnfilledTimeout(BaseModel):
|
||||
buy: int
|
||||
sell: int
|
||||
unit: str
|
||||
buy: Optional[int]
|
||||
sell: Optional[int]
|
||||
unit: Optional[str]
|
||||
exit_timeout_count: Optional[int]
|
||||
|
||||
|
||||
class OrderTypeValues(str, Enum):
|
||||
limit = 'limit'
|
||||
market = 'market'
|
||||
|
||||
|
||||
class OrderTypes(BaseModel):
|
||||
buy: str
|
||||
sell: str
|
||||
emergencysell: Optional[str]
|
||||
forcesell: Optional[str]
|
||||
forcebuy: Optional[str]
|
||||
stoploss: str
|
||||
buy: OrderTypeValues
|
||||
sell: OrderTypeValues
|
||||
emergencysell: Optional[OrderTypeValues]
|
||||
forcesell: Optional[OrderTypeValues]
|
||||
forcebuy: Optional[OrderTypeValues]
|
||||
stoploss: OrderTypeValues
|
||||
stoploss_on_exchange: bool
|
||||
stoploss_on_exchange_interval: Optional[int]
|
||||
|
||||
@@ -185,7 +191,8 @@ class TradeSchema(BaseModel):
|
||||
amount_requested: float
|
||||
stake_amount: float
|
||||
strategy: str
|
||||
buy_tag: Optional[str]
|
||||
buy_tag: Optional[str] # Deprecated
|
||||
enter_tag: Optional[str]
|
||||
timeframe: int
|
||||
fee_open: Optional[float]
|
||||
fee_open_cost: Optional[float]
|
||||
@@ -277,10 +284,12 @@ class Logs(BaseModel):
|
||||
class ForceBuyPayload(BaseModel):
|
||||
pair: str
|
||||
price: Optional[float]
|
||||
ordertype: Optional[OrderTypeValues]
|
||||
|
||||
|
||||
class ForceSellPayload(BaseModel):
|
||||
tradeid: str
|
||||
ordertype: Optional[OrderTypeValues]
|
||||
|
||||
|
||||
class BlacklistPayload(BaseModel):
|
||||
|
@@ -29,7 +29,8 @@ logger = logging.getLogger(__name__)
|
||||
# API version
|
||||
# Pre-1.1, no version was provided
|
||||
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
|
||||
API_VERSION = 1.1
|
||||
# 1.11: forcebuy and forcesell accept ordertype
|
||||
API_VERSION = 1.11
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@@ -129,7 +130,8 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
||||
|
||||
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
|
||||
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price)
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype)
|
||||
|
||||
if trade:
|
||||
return ForceBuyResponse.parse_obj(trade.to_json())
|
||||
@@ -139,7 +141,8 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
||||
|
||||
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
||||
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_forcesell(payload.tradeid)
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
return rpc._rpc_forcesell(payload.tradeid, ordertype)
|
||||
|
||||
|
||||
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||
|
@@ -646,7 +646,7 @@ class RPC:
|
||||
|
||||
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||
|
||||
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
||||
def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
|
||||
"""
|
||||
Handler for forcesell <id>.
|
||||
Sells the given trade at current price
|
||||
@@ -671,7 +671,11 @@ class RPC:
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, refresh=False, side=closing_side)
|
||||
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||
self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason)
|
||||
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||
"forcesell", self._freqtrade.strategy.order_types["sell"])
|
||||
|
||||
self._freqtrade.execute_trade_exit(
|
||||
trade, current_rate, sell_reason, ordertype=order_type)
|
||||
# ---- EOF def _exec_forcesell ----
|
||||
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
@@ -699,7 +703,8 @@ class RPC:
|
||||
self._freqtrade.wallets.update()
|
||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||
|
||||
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
|
||||
def _rpc_forcebuy(self, pair: str, price: Optional[float],
|
||||
order_type: Optional[str] = None) -> Optional[Trade]:
|
||||
"""
|
||||
Handler for forcebuy <asset> <price>
|
||||
Buys a pair trade at the given or current price
|
||||
@@ -727,7 +732,10 @@ class RPC:
|
||||
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
||||
|
||||
# execute buy
|
||||
if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True):
|
||||
if not order_type:
|
||||
order_type = self._freqtrade.strategy.order_types.get(
|
||||
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||
if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type):
|
||||
Trade.commit()
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
return trade
|
||||
@@ -782,27 +790,23 @@ class RPC:
|
||||
|
||||
return pair_rates
|
||||
|
||||
def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
def _rpc_enter_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for buy tag performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
buy_tags = Trade.get_buy_tag_performance(pair)
|
||||
|
||||
return buy_tags
|
||||
return Trade.get_enter_tag_performance(pair)
|
||||
|
||||
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for sell reason performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
sell_reasons = Trade.get_sell_reason_performance(pair)
|
||||
|
||||
return sell_reasons
|
||||
return Trade.get_sell_reason_performance(pair)
|
||||
|
||||
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for mix tag (buy_tag + sell_reason) performance.
|
||||
Handler for mix tag (enter_tag + sell_reason) performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
mix_tags = Trade.get_mix_tag_performance(pair)
|
||||
|
@@ -112,6 +112,7 @@ class Telegram(RPCHandler):
|
||||
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
||||
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
|
||||
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||
r'/forcebuy$', r'/help$', r'/version$']
|
||||
# Create keys for generation
|
||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||
@@ -154,7 +155,7 @@ class Telegram(RPCHandler):
|
||||
CommandHandler('trades', self._trades),
|
||||
CommandHandler('delete', self._delete_trade),
|
||||
CommandHandler('performance', self._performance),
|
||||
CommandHandler('buys', self._buy_tag_performance),
|
||||
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
|
||||
CommandHandler('sells', self._sell_reason_performance),
|
||||
CommandHandler('mix_tags', self._mix_tag_performance),
|
||||
CommandHandler('stats', self._stats),
|
||||
@@ -182,7 +183,8 @@ class Telegram(RPCHandler):
|
||||
CallbackQueryHandler(self._profit, pattern='update_profit'),
|
||||
CallbackQueryHandler(self._balance, pattern='update_balance'),
|
||||
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
||||
CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'),
|
||||
CallbackQueryHandler(self._enter_tag_performance,
|
||||
pattern='update_enter_tag_performance'),
|
||||
CallbackQueryHandler(self._sell_reason_performance,
|
||||
pattern='update_sell_reason_performance'),
|
||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||
@@ -226,7 +228,7 @@ class Telegram(RPCHandler):
|
||||
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
|
||||
f" (#{msg['trade_id']})\n"
|
||||
)
|
||||
message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else ""
|
||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
|
||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
|
||||
if msg['type'] == RPCMessageType.BUY_FILL:
|
||||
@@ -251,7 +253,7 @@ class Telegram(RPCHandler):
|
||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||
|
||||
msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None
|
||||
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
||||
msg['emoji'] = self._get_sell_emoji(msg)
|
||||
|
||||
# Check if all sell properties are available.
|
||||
@@ -271,7 +273,7 @@ class Telegram(RPCHandler):
|
||||
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||
f"*Buy Tag:* `{msg['buy_tag']}`\n"
|
||||
f"*Enter Tag:* `{msg['enter_tag']}`\n"
|
||||
f"*Sell Reason:* `{msg['sell_reason']}`\n"
|
||||
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
|
||||
f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
@@ -397,7 +399,7 @@ class Telegram(RPCHandler):
|
||||
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
|
||||
"*Current Pair:* {pair}",
|
||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
||||
"*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "",
|
||||
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
|
||||
"*Open Rate:* `{open_rate:.8f}`",
|
||||
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
||||
"*Current Rate:* `{current_rate:.8f}`",
|
||||
@@ -972,7 +974,7 @@ class Telegram(RPCHandler):
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /buys PAIR .
|
||||
Shows a performance statistic from finished trades
|
||||
@@ -985,11 +987,11 @@ class Telegram(RPCHandler):
|
||||
if context.args and isinstance(context.args[0], str):
|
||||
pair = context.args[0]
|
||||
|
||||
trades = self._rpc._rpc_buy_tag_performance(pair)
|
||||
trades = self._rpc._rpc_enter_tag_performance(pair)
|
||||
output = "<b>Buy Tag Performance:</b>\n"
|
||||
for i, trade in enumerate(trades):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['buy_tag']}\t"
|
||||
f"{i+1}.\t <code>{trade['enter_tag']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
@@ -1001,7 +1003,7 @@ class Telegram(RPCHandler):
|
||||
output += stat_line
|
||||
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||
reload_able=True, callback_path="update_buy_tag_performance",
|
||||
reload_able=True, callback_path="update_enter_tag_performance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
@@ -1277,7 +1279,8 @@ class Telegram(RPCHandler):
|
||||
" *table :* `will display trades in a table`\n"
|
||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
|
||||
# TODO-lev: Update commands and help (?)
|
||||
"*/buys <pair|none>:* `Shows the enter_tag performance`\n"
|
||||
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
||||
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||
|
@@ -2,6 +2,7 @@
|
||||
This module manages webhook communication
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
|
||||
from requests import RequestException, post
|
||||
@@ -28,12 +29,9 @@ class Webhook(RPCHandler):
|
||||
super().__init__(rpc, config)
|
||||
|
||||
self._url = self._config['webhook']['url']
|
||||
|
||||
self._format = self._config['webhook'].get('format', 'form')
|
||||
|
||||
if self._format != 'form' and self._format != 'json':
|
||||
raise NotImplementedError('Unknown webhook format `{}`, possible values are '
|
||||
'`form` (default) and `json`'.format(self._format))
|
||||
self._retries = self._config['webhook'].get('retries', 0)
|
||||
self._retry_delay = self._config['webhook'].get('retry_delay', 0.1)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
@@ -77,13 +75,30 @@ class Webhook(RPCHandler):
|
||||
def _send_msg(self, payload: dict) -> None:
|
||||
"""do the actual call to the webhook"""
|
||||
|
||||
try:
|
||||
if self._format == 'form':
|
||||
post(self._url, data=payload)
|
||||
elif self._format == 'json':
|
||||
post(self._url, json=payload)
|
||||
else:
|
||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
||||
success = False
|
||||
attempts = 0
|
||||
while not success and attempts <= self._retries:
|
||||
if attempts:
|
||||
if self._retry_delay:
|
||||
time.sleep(self._retry_delay)
|
||||
logger.info("Retrying webhook...")
|
||||
|
||||
except RequestException as exc:
|
||||
logger.warning("Could not call webhook url. Exception: %s", exc)
|
||||
attempts += 1
|
||||
|
||||
try:
|
||||
if self._format == 'form':
|
||||
response = post(self._url, data=payload)
|
||||
elif self._format == 'json':
|
||||
response = post(self._url, json=payload)
|
||||
elif self._format == 'raw':
|
||||
response = post(self._url, data=payload['data'],
|
||||
headers={'Content-Type': 'text/plain'})
|
||||
else:
|
||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
||||
|
||||
# Throw a RequestException if the post was not successful
|
||||
response.raise_for_status()
|
||||
success = True
|
||||
|
||||
except RequestException as exc:
|
||||
logger.warning("Could not call webhook url. Exception: %s", exc)
|
||||
|
@@ -87,6 +87,7 @@ class {{ strategy }}(IStrategy):
|
||||
'sell': 'gtc'
|
||||
}
|
||||
{{ plot_config | indent(4) }}
|
||||
|
||||
def informative_pairs(self):
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
|
@@ -79,7 +79,9 @@
|
||||
"source": [
|
||||
"# Load strategy using values set above\n",
|
||||
"from freqtrade.resolvers import StrategyResolver\n",
|
||||
"from freqtrade.data.dataprovider import DataProvider\n",
|
||||
"strategy = StrategyResolver.load_strategy(config)\n",
|
||||
"strategy.dp = DataProvider(config, None, None)\n",
|
||||
"\n",
|
||||
"# Generate buy/sell signals using strategy\n",
|
||||
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",
|
||||
|
@@ -1,18 +1,20 @@
|
||||
|
||||
plot_config = {
|
||||
# Main plot indicators (Moving averages, ...)
|
||||
'main_plot': {
|
||||
'tema': {},
|
||||
'sar': {'color': 'white'},
|
||||
},
|
||||
'subplots': {
|
||||
# Subplots - each dict defines one additional plot
|
||||
"MACD": {
|
||||
'macd': {'color': 'blue'},
|
||||
'macdsignal': {'color': 'orange'},
|
||||
@property
|
||||
def plot_config(self):
|
||||
return {
|
||||
# Main plot indicators (Moving averages, ...)
|
||||
'main_plot': {
|
||||
'tema': {},
|
||||
'sar': {'color': 'white'},
|
||||
},
|
||||
"RSI": {
|
||||
'rsi': {'color': 'red'},
|
||||
'subplots': {
|
||||
# Subplots - each dict defines one additional plot
|
||||
"MACD": {
|
||||
'macd': {'color': 'blue'},
|
||||
'macdsignal': {'color': 'orange'},
|
||||
},
|
||||
"RSI": {
|
||||
'rsi': {'color': 'red'},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user