Merge pull request #12529 from mrpabloyeah/fix-backtesting-exception-when-no-data-is-available-for-a-pair

Fix backtesting exception when no data is available for a pair
This commit is contained in:
Matthias
2025-11-27 06:29:24 +01:00
committed by GitHub
3 changed files with 30 additions and 10 deletions

View File

@@ -126,6 +126,7 @@ class Backtesting:
self.config["dry_run"] = True self.config["dry_run"] = True
self.price_pair_prec: dict[str, Series] = {} self.price_pair_prec: dict[str, Series] = {}
self.available_pairs: list[str] = []
self.run_ids: dict[str, str] = {} self.run_ids: dict[str, str] = {}
self.strategylist: list[IStrategy] = [] self.strategylist: list[IStrategy] = []
self.all_bt_content: dict[str, BacktestContentType] = {} self.all_bt_content: dict[str, BacktestContentType] = {}
@@ -176,7 +177,8 @@ class Backtesting:
self._validate_pairlists_for_backtesting() self._validate_pairlists_for_backtesting()
self.dataprovider.add_pairlisthandler(self.pairlists) self.dataprovider.add_pairlisthandler(self.pairlists)
self.pairlists.refresh_pairlist() self.dynamic_pairlist: bool = self.config.get("enable_dynamic_pairlist", False)
self.pairlists.refresh_pairlist(only_first=self.dynamic_pairlist)
if len(self.pairlists.whitelist) == 0: if len(self.pairlists.whitelist) == 0:
raise OperationalException("No pair in whitelist.") raise OperationalException("No pair in whitelist.")
@@ -211,7 +213,6 @@ class Backtesting:
self._can_short = self.trading_mode != TradingMode.SPOT self._can_short = self.trading_mode != TradingMode.SPOT
self._position_stacking: bool = self.config.get("position_stacking", False) self._position_stacking: bool = self.config.get("position_stacking", False)
self.enable_protections: bool = self.config.get("enable_protections", False) self.enable_protections: bool = self.config.get("enable_protections", False)
self.dynamic_pairlist: bool = self.config.get("enable_dynamic_pairlist", False)
migrate_data(config, self.exchange) migrate_data(config, self.exchange)
self.init_backtest() self.init_backtest()
@@ -335,10 +336,12 @@ class Backtesting:
self.progress.set_new_value(1) self.progress.set_new_value(1)
self._load_bt_data_detail() self._load_bt_data_detail()
self.price_pair_prec = {} self.price_pair_prec = {}
for pair in self.pairlists.whitelist: for pair in self.pairlists.whitelist:
if pair in data: if pair in data:
# Load price precision logic # Load price precision logic
self.price_pair_prec[pair] = get_tick_size_over_time(data[pair]) self.price_pair_prec[pair] = get_tick_size_over_time(data[pair])
self.available_pairs.append(pair)
return data, self.timerange return data, self.timerange
def _load_bt_data_detail(self) -> None: def _load_bt_data_detail(self) -> None:
@@ -1587,7 +1590,7 @@ class Backtesting:
self.check_abort() self.check_abort()
if self.dynamic_pairlist and self.pairlists: if self.dynamic_pairlist and self.pairlists:
self.pairlists.refresh_pairlist() self.pairlists.refresh_pairlist(pairs=self.available_pairs)
pairs = self.pairlists.whitelist pairs = self.pairlists.whitelist
# Reset open trade count for this candle # Reset open trade count for this candle

View File

@@ -134,8 +134,20 @@ class PairListManager(LoggingMixin):
def _get_cached_tickers(self) -> Tickers: def _get_cached_tickers(self) -> Tickers:
return self._exchange.get_tickers() return self._exchange.get_tickers()
def refresh_pairlist(self) -> None: def refresh_pairlist(self, only_first: bool = False, pairs: list[str] | None = None) -> None:
"""Run pairlist through all configured Pairlist Handlers.""" """
Run pairlist through all configured Pairlist Handlers.
:param only_first: If True, only run the first PairList handler (the generator)
and skip all subsequent filters. Used during backtesting startup to ensure
historic data is loaded for the complete universe of pairs that the
generator can produce (even if later filters would reduce the list size).
Prevents missing data when a filter returns a variable number of pairs
across refresh cycles.
:param pairs: Optional list of pairs to intersect with the generated pairlist.
Only pairs present both in the generated list and this parameter are kept.
Used in backtesting to filter out pairs with no available data.
"""
# Tickers should be cached to avoid calling the exchange on each call. # Tickers should be cached to avoid calling the exchange on each call.
tickers: dict = {} tickers: dict = {}
if self._tickers_needed: if self._tickers_needed:
@@ -144,6 +156,11 @@ class PairListManager(LoggingMixin):
# Generate the pairlist with first Pairlist Handler in the chain # Generate the pairlist with first Pairlist Handler in the chain
pairlist = self._pairlist_handlers[0].gen_pairlist(tickers) pairlist = self._pairlist_handlers[0].gen_pairlist(tickers)
# Optional intersection with an explicit list of pairs (used in backtesting)
if pairs is not None:
pairlist = [p for p in pairlist if p in pairs]
if not only_first:
# Process all Pairlist Handlers in the chain # Process all Pairlist Handlers in the chain
# except for the first one, which is the generator. # except for the first one, which is the generator.
for pairlist_handler in self._pairlist_handlers[1:]: for pairlist_handler in self._pairlist_handlers[1:]:

View File

@@ -2772,7 +2772,7 @@ def test_time_pair_generator_open_trades_first(mocker, default_conf, dynamic_pai
dummy_row = (end_date, 1.0, 1.1, 0.9, 1.0, 0, 0, 0, 0, None, None) dummy_row = (end_date, 1.0, 1.1, 0.9, 1.0, 0, 0, 0, 0, None, None)
data = {pair: [dummy_row] for pair in pairs} data = {pair: [dummy_row] for pair in pairs}
def mock_refresh(self): def mock_refresh(self, **kwargs):
# Simulate shuffle # Simulate shuffle
self._whitelist = pairs[::-1] # ['ETH/BTC', 'NEO/BTC', 'LTC/BTC', 'XRP/BTC'] self._whitelist = pairs[::-1] # ['ETH/BTC', 'NEO/BTC', 'LTC/BTC', 'XRP/BTC']