diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5566127c3..d23b6bdc3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -361,8 +361,12 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time trade.sell_reason = sell.sell_reason - if(sell_row[EXIT_TAG_IDX] is not None): + + # Checks and adds an exit tag, after checking that the length of the + # sell_row has the length for an exit tag column + if(len(sell_row) > EXIT_TAG_IDX and sell_row[EXIT_TAG_IDX] is not None and len(sell_row[EXIT_TAG_IDX]) > 0): trade.sell_reason = sell_row[EXIT_TAG_IDX] + trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 67dacd7c6..6e0926660 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -150,19 +150,22 @@ def generate_tag_metrics(tag_type: str, tabular_data = [] - for tag, count in results[tag_type].value_counts().iteritems(): - result = results[results[tag_type] == tag] - if skip_nan and result['profit_abs'].isnull().all(): - continue + if tag_type in results.columns: + for tag, count in results[tag_type].value_counts().iteritems(): + result = results[results[tag_type] == tag] + 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_tag_result_line(result, starting_balance, tag)) - # Sort by total profit %: - tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) + # Sort by total profit %: + tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) - # Append Total - tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) - return tabular_data + # Append Total + tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) + return tabular_data + else: + return None def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: @@ -732,14 +735,15 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_tags( - "buy_tag", - results['results_per_buy_tag'], - stake_currency=stake_currency) + if(results['results_per_buy_tag'] is not None): + table = text_table_tags( + "buy_tag", + results['results_per_buy_tag'], + stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) - print(table) + if isinstance(table, str) and len(table) > 0: + print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) + print(table) table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], stake_currency=stake_currency) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 341eec5dd..96124ff45 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -107,10 +107,9 @@ class Telegram(RPCHandler): # this needs refactoring of the whole telegram module (same # problem in _help()). valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$', - r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$', - r'/profit$', r'/profit \d+', + r'/trades$', r'/performance$', r'/buys', r'/sells', r'/mix_tags', + r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+', r'/stats$', r'/count$', r'/locks$', r'/balance$', - r'/buys', r'/sells', r'/mix_tags', r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', r'/forcebuy$', r'/help$', r'/version$'] @@ -179,9 +178,9 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._profit, pattern='update_profit'), CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), - CallbackQueryHandler(self._performance, pattern='update_buy_tag_performance'), - CallbackQueryHandler(self._performance, pattern='update_sell_reason_performance'), - CallbackQueryHandler(self._performance, pattern='update_mix_tag_performance'), + CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'), + CallbackQueryHandler(self._sell_reason_performance, pattern='update_sell_reason_performance'), + CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._forcebuy_inline), ] diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 2248cd4c1..9d3ca01a9 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -567,6 +567,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: 195, # Low 201.5, # High '', # Buy Signal Name + '', # Exit Signal Name ] trade = backtesting._enter_trade(pair, row=row) @@ -581,26 +582,27 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: 195, # Low 210.5, # High '', # Buy Signal Name + '', # Exit Signal Name ] row_detail = pd.DataFrame( [ [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc), - 1, 200, 199, 0, 197, 200.1, '', + 1, 200, 199, 0, 197, 200.1, '', '', ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc), - 0, 199, 199.5, 0, 199, 199.7, '', + 0, 199, 199.5, 0, 199, 199.7, '', '', ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc), - 0, 199.5, 200.5, 0, 199, 200.8, '', + 0, 199.5, 200.5, 0, 199, 200.8, '', '', ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc), - 0, 200.5, 210.5, 0, 193, 210.5, '', # ROI sell (?) + 0, 200.5, 210.5, 0, 193, 210.5, '', '', # ROI sell (?) ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc), - 0, 200, 199, 0, 193, 200.1, '', + 0, 200, 199, 0, 193, 200.1, '', '', ], - ], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"] + ], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag", "exit_tag"] ) # No data available. @@ -614,7 +616,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: assert isinstance(trade, LocalTrade) # Assign empty ... no result. backtesting.detail_data[pair] = pd.DataFrame( - [], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"]) + [], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag", "exit_tag"]) res = backtesting._get_sell_trade_entry(trade, row) assert res is None @@ -678,7 +680,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'min_rate': [0.10370188, 0.10300000000000001], 'max_rate': [0.10501, 0.1038888], 'is_open': [False, False], - 'buy_tag': [None, None], + 'buy_tag': [None, None] }) pd.testing.assert_frame_equal(results, expected) data_pair = processed[pair] diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7dde7b803..01d6d92cf 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -33,6 +33,7 @@ class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator """ + def __init__(self, rpc: RPC, config) -> None: super().__init__(rpc, config) self.state = {'called': False} @@ -92,7 +93,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " + "['delete'], ['performance'], ['buys'], ['sells'], ['mix_tags'], " + "['stats'], ['daily'], ['count'], ['locks'], " "['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], " "['show_config', 'show_conf'], ['stopbuy'], " "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" @@ -713,6 +715,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'profit_ratio': 0.0629778, 'stake_currency': 'BTC', 'fiat_currency': 'USD', + 'buy_tag': ANY, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, @@ -776,6 +779,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'profit_ratio': -0.05482878, 'stake_currency': 'BTC', 'fiat_currency': 'USD', + 'buy_tag': ANY, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, @@ -829,6 +833,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'profit_ratio': -0.00408133, 'stake_currency': 'BTC', 'fiat_currency': 'USD', + 'buy_tag': ANY, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, @@ -997,9 +1002,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg = ('
current max total stake\n--------- ----- -------------\n'
' 1 {} {}').format(
- default_conf['max_open_trades'],
- default_conf['stake_amount']
- )
+ default_conf['max_open_trades'],
+ default_conf['stake_amount']
+ )
assert msg in msg_mock.call_args_list[0][0][0]
@@ -1382,6 +1387,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
+ 'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-1),
'close_date': arrow.utcnow(),
@@ -1389,6 +1395,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
+ '*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1:00:00 (60.0 min)`\n'
'*Amount:* `1333.33333333`\n'
@@ -1412,6 +1419,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'profit_amount': -0.05746268,
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
+ 'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
'close_date': arrow.utcnow(),
@@ -1419,6 +1427,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n'
+ '*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
'*Amount:* `1333.33333333`\n'
@@ -1483,6 +1492,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None:
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
+ 'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-1),
'close_date': arrow.utcnow(),
@@ -1574,12 +1584,14 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
+ 'buy_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Profit:* `-57.41%`\n'
+ '*Buy Tag:* `buy_signal1`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `2:35:03 (155.1 min)`\n'
'*Amount:* `1333.33333333`\n'
diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py
index dcb9e3e64..62510b370 100644
--- a/tests/strategy/test_interface.py
+++ b/tests/strategy/test_interface.py
@@ -38,20 +38,27 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
mocked_history['buy'] = 0
mocked_history.loc[1, 'sell'] = 1
- assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None)
+ assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None, None)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 1
- assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None)
+ assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None, None)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 0
- assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None)
+ assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None, None)
mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 1
mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01'
- assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01')
+ assert _STRATEGY.get_signal(
+ 'ETH/BTC',
+ '5m',
+ mocked_history) == (
+ True,
+ False,
+ 'buy_signal_01',
+ None)
def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
@@ -68,17 +75,24 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
def test_get_signal_empty(default_conf, mocker, caplog):
- assert (False, False, None) == _STRATEGY.get_signal(
+ assert (False, False, None, None) == _STRATEGY.get_signal(
'foo', default_conf['timeframe'], DataFrame()
)
assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
caplog.clear()
- assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None)
+ assert (
+ False,
+ False,
+ None,
+ None) == _STRATEGY.get_signal(
+ 'bar',
+ default_conf['timeframe'],
+ None)
assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
caplog.clear()
- assert (False, False, None) == _STRATEGY.get_signal(
+ assert (False, False, None, None) == _STRATEGY.get_signal(
'baz',
default_conf['timeframe'],
DataFrame([])
@@ -118,7 +132,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY, 'assert_df')
- assert (False, False, None) == _STRATEGY.get_signal(
+ assert (False, False, None, None) == _STRATEGY.get_signal(
'xyz',
default_conf['timeframe'],
mocked_history
@@ -140,7 +154,7 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY, 'assert_df')
- assert (True, False, None) == _STRATEGY.get_signal(
+ assert (True, False, None, None) == _STRATEGY.get_signal(
'xyz',
default_conf['timeframe'],
mocked_history
@@ -646,7 +660,7 @@ def test_strategy_safe_wrapper(value):
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value)
- assert type(ret) == type(value)
+ assert isinstance(ret, type(value))
assert ret == value