Repository: freqtrade/freqtrade

Issue:
Backtesting / Hyperopt - track rejected trades (due to max_open_trades reached)
Have backtesting and hyperopt report the number of simultaneous open trades, which would help me optimize how much I should allocate per trade. Right now I have to manually adjust this value and rerun the backtest.

Note I am not asking to backtest or hyperopt different values for this, merely a report of how many were used and perhaps a report on how many trades were rejected due to maximum open positions.
Backtesting / Hyperopt - track rejected trades (due to max_open_trades reached)
Have backtesting and hyperopt report the number of simultaneous open trades, which would help me optimize how much I should allocate per trade. Right now I have to manually adjust this value and rerun the backtest.

Note I am not asking to backtest or hyperopt different values for this, merely a report of how many were used and perhaps a report on how many trades were rejected due to maximum open positions.


Discussion:
What value was used is in the logs (before the backtesting), as that value comes from the config.
e.g. 

```
2020-06-01 13:49:42,511 - freqtrade.configuration.configuration - INFO - Using max_open_trades: 3 ...
```

---

Counting rejected signals might be difficult as there are 2 reasons entry signals are rejected, either because this pair is already open, or because max_open_trades is reached...

I've renamed the issue now to reflect this 2nd point - as i think that can be a good enhancement (the first part is already there, so is not a real enhancement).
I also thought about this, can be a good enhancement.

What value was used is in the logs (before the backtesting), as that value comes from the config.
e.g. 

```
2020-06-01 13:49:42,511 - freqtrade.configuration.configuration - INFO - Using max_open_trades: 3 ...
```

---

Counting rejected signals might be difficult as there are 2 reasons entry signals are rejected, either because this pair is already open, or because max_open_trades is reached...

I've renamed the issue now to reflect this 2nd point - as i think that can be a good enhancement (the first part is already there, so is not a real enhancement).
I also thought about this, can be a good enhancement.


Resolving patch:
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -177,6 +177,7 @@ def prepare_backtest(self, enable_protections):
         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,14 @@ def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]],
                     trades.append(trade1)
         return trades
 
+    def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool:
+        # 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
+        return False
+
     def backtest(self, processed: Dict,
                  start_date: datetime, end_date: datetime,
                  max_open_trades: int = 0, position_stacking: bool = False,
@@ -397,11 +406,14 @@ def backtest(self, processed: Dict,
                 # 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 +451,7 @@ def backtest(self, processed: Dict,
             'results': results,
             'config': self.strategy.config,
             'locks': PairLocks.get_all_locks(),
+            '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
--- 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_signals'],
         'max_open_trades': max_open_trades,
         'max_open_trades_setting': (config['max_open_trades']
                                     if config['max_open_trades'] != float('inf') else -1),
@@ -561,7 +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'])),
-
             ('', ''),  # Empty line to improve readability
             ('Best Pair', f"{strat_results['best_pair']['key']} "
                           f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
@@ -580,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'],


Test patch:
diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py
--- 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
--- 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
--- 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,
         }