From 7f125315b059cc529750415945366474efa5ac7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 09:36:02 +0200 Subject: [PATCH 1/2] Track Rejected Trades closes #3423 --- freqtrade/optimize/backtesting.py | 26 +++++++++++++++++++++----- freqtrade/optimize/optimize_reports.py | 2 ++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aff09921c..642ed2b76 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -177,6 +177,7 @@ class Backtesting: Trade.use_db = False PairLocks.reset_locks() Trade.reset_trades() + self.rejected_trades = 0 self.dataprovider.clear_cache() def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: @@ -336,6 +337,17 @@ class Backtesting: trades.append(trade1) return trades + def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: + if max_open_trades <= 0: + # Always allow trades when max_open_trades is enabled. + return True + if open_trade_count < max_open_trades: + + return True + # Rejected trade + self.rejected_trades += 1 + return False + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, @@ -397,11 +409,14 @@ class Backtesting: # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row - if ((position_stacking or len(open_trades[pair]) == 0) - and (max_open_trades <= 0 or open_trade_count_start < max_open_trades) - and tmp != end_date - and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 - and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): + if ( + (position_stacking or len(open_trades[pair]) == 0) + and self.trade_slot_available(max_open_trades, open_trade_count_start) + and tmp != end_date + and row[BUY_IDX] == 1 + and row[SELL_IDX] != 1 + and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) + ): trade = self._enter_trade(pair, row) if trade: # TODO: hacky workaround to avoid opening > max_open_trades @@ -439,6 +454,7 @@ class Backtesting: 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), + 'rejected': self.rejected_trades, 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ce6758210..ddccfd1dc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -355,6 +355,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], + 'rejected_signals': content['rejected'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -561,6 +562,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), + ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " From a39860e0de12e1d3c4590b235dc07c74460e7210 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 May 2021 09:46:51 +0200 Subject: [PATCH 2/2] Add tests for rejected signals --- docs/backtesting.md | 3 +++ freqtrade/optimize/backtesting.py | 9 +++------ freqtrade/optimize/optimize_reports.py | 5 ++--- tests/optimize/test_backtesting.py | 3 +++ tests/optimize/test_hyperopt.py | 2 ++ tests/optimize/test_optimize_reports.py | 2 ++ 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index e720206bb..2027c2079 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -298,6 +298,7 @@ A backtesting result will look like that: | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | Zero Duration Trades | 4.6% (20) | +| Rejected Buy signals | 3089 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -386,6 +387,7 @@ It contains some useful key metrics about performance of your strategy on backte | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | Zero Duration Trades | 4.6% (20) | +| Rejected Buy signals | 3089 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -416,6 +418,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results. +- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached. - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 642ed2b76..cbc0995aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -338,11 +338,8 @@ class Backtesting: return trades def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: - if max_open_trades <= 0: - # Always allow trades when max_open_trades is enabled. - return True - if open_trade_count < max_open_trades: - + # Always allow trades when max_open_trades is enabled. + if max_open_trades <= 0 or open_trade_count < max_open_trades: return True # Rejected trade self.rejected_trades += 1 @@ -454,7 +451,7 @@ class Backtesting: 'results': results, 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), - 'rejected': self.rejected_trades, + 'rejected_signals': self.rejected_trades, 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ddccfd1dc..090af4a85 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -355,7 +355,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'starting_balance': starting_balance, 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], - 'rejected_signals': content['rejected'], + 'rejected_signals': content['rejected_signals'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -562,8 +562,6 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), ('Total trade volume', round_coin_value(strat_results['total_volume'], strat_results['stake_currency'])), - ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), - ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), @@ -582,6 +580,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('Zero Duration Trades', zero_duration_trades), + ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 03a65b159..632d458ce 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -831,6 +831,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'results': pd.DataFrame(columns=BT_DATA_COLUMNS), 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', @@ -938,12 +939,14 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'results': result1, 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, }, { 'results': result2, 'config': default_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, } ]) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b80ede734..d7eb8bf67 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -441,6 +441,7 @@ def test_hyperopt_format_results(hyperopt): 'config': hyperopt.config, 'locks': [], 'final_balance': 0.02, + 'rejected_signals': 2, 'backtest_start_time': 1619718665, 'backtest_end_time': 1619718665, } @@ -593,6 +594,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: }), 'config': hyperopt_conf, 'locks': [], + 'rejected_signals': 20, 'final_balance': 1000, } diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 575bb9092..f9dac3397 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -79,6 +79,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): 'config': default_conf, 'locks': [], 'final_balance': 1000.02, + 'rejected_signals': 20, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, } @@ -126,6 +127,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): 'config': default_conf, 'locks': [], 'final_balance': 1000.02, + 'rejected_signals': 20, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, }