diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4b8091f87..fd4c98b4d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1102,6 +1102,7 @@ class Backtesting: fee_close=self.fee, is_open=True, enter_tag=entry_tag, + timeframe=self.timeframe_min, exchange=self._exchange_name, is_short=is_short, trading_mode=self.trading_mode, @@ -1357,7 +1358,7 @@ class Backtesting: and (self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0) and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir) ): - if self.trade_slot_available(LocalTrade.bt_open_open_trade_count): + if self.trade_slot_available(LocalTrade.bt_open_open_trade_count_candle): trade = self._enter_trade(pair, row, trade_dir) if trade: self.wallets.update() @@ -1431,6 +1432,10 @@ class Backtesting: ): if is_first_call: self.check_abort() + # Reset open trade count for this candle + # Critical to avoid exceeding max_open_trades in backtesting + # when timeframe-detail is used and trades close within the opening candle. + LocalTrade.bt_open_open_trade_count_candle = LocalTrade.bt_open_open_trade_count strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( current_time=current_time ) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index afae5f4c7..f765b9865 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -379,6 +379,7 @@ class LocalTrade: # Copy of trades_open - but indexed by pair bt_trades_open_pp: dict[str, list["LocalTrade"]] = defaultdict(list) bt_open_open_trade_count: int = 0 + bt_open_open_trade_count_candle: int = 0 bt_total_profit: float = 0 realized_profit: float = 0 @@ -747,6 +748,7 @@ class LocalTrade: LocalTrade.bt_trades_open = [] LocalTrade.bt_trades_open_pp = defaultdict(list) LocalTrade.bt_open_open_trade_count = 0 + LocalTrade.bt_open_open_trade_count_candle = 0 LocalTrade.bt_total_profit = 0 def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None: @@ -1442,6 +1444,11 @@ class LocalTrade: LocalTrade.bt_trades_open.remove(trade) LocalTrade.bt_trades_open_pp[trade.pair].remove(trade) LocalTrade.bt_open_open_trade_count -= 1 + if (trade.close_date_utc - trade.open_date_utc) > timedelta(minutes=trade.timeframe): + # Only subtract trades that are open for more than 1 candle + # To avoid exceeding max_open_trades. + # Must be reset at the start of every candle during backesting. + LocalTrade.bt_open_open_trade_count_candle -= 1 LocalTrade.bt_trades.append(trade) LocalTrade.bt_total_profit += trade.close_profit_abs @@ -1451,6 +1458,7 @@ class LocalTrade: LocalTrade.bt_trades_open.append(trade) LocalTrade.bt_trades_open_pp[trade.pair].append(trade) LocalTrade.bt_open_open_trade_count += 1 + LocalTrade.bt_open_open_trade_count_candle += 1 else: LocalTrade.bt_trades.append(trade) @@ -1459,6 +1467,9 @@ class LocalTrade: LocalTrade.bt_trades_open.remove(trade) LocalTrade.bt_trades_open_pp[trade.pair].remove(trade) LocalTrade.bt_open_open_trade_count -= 1 + # TODO: The below may have odd behavior in case of canceled entries + # It might need to be removed so the trade "counts" as open for this candle. + LocalTrade.bt_open_open_trade_count_candle -= 1 @staticmethod def get_open_trades() -> list[Any]: