Skip to content

Commit 04fda25

Browse files
authored
Merge pull request freqtrade#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
2 parents 26f23c1 + 7272403 commit 04fda25

File tree

3 files changed

+30
-10
lines changed

3 files changed

+30
-10
lines changed

freqtrade/optimize/backtesting.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def __init__(self, config: Config, exchange: Exchange | None = None) -> None:
126126

127127
self.config["dry_run"] = True
128128
self.price_pair_prec: dict[str, Series] = {}
129+
self.available_pairs: list[str] = []
129130
self.run_ids: dict[str, str] = {}
130131
self.strategylist: list[IStrategy] = []
131132
self.all_bt_content: dict[str, BacktestContentType] = {}
@@ -176,7 +177,8 @@ def __init__(self, config: Config, exchange: Exchange | None = None) -> None:
176177
self._validate_pairlists_for_backtesting()
177178

178179
self.dataprovider.add_pairlisthandler(self.pairlists)
179-
self.pairlists.refresh_pairlist()
180+
self.dynamic_pairlist: bool = self.config.get("enable_dynamic_pairlist", False)
181+
self.pairlists.refresh_pairlist(only_first=self.dynamic_pairlist)
180182

181183
if len(self.pairlists.whitelist) == 0:
182184
raise OperationalException("No pair in whitelist.")
@@ -211,7 +213,6 @@ def __init__(self, config: Config, exchange: Exchange | None = None) -> None:
211213
self._can_short = self.trading_mode != TradingMode.SPOT
212214
self._position_stacking: bool = self.config.get("position_stacking", False)
213215
self.enable_protections: bool = self.config.get("enable_protections", False)
214-
self.dynamic_pairlist: bool = self.config.get("enable_dynamic_pairlist", False)
215216
migrate_data(config, self.exchange)
216217

217218
self.init_backtest()
@@ -335,10 +336,12 @@ def load_bt_data(self) -> tuple[dict[str, DataFrame], TimeRange]:
335336
self.progress.set_new_value(1)
336337
self._load_bt_data_detail()
337338
self.price_pair_prec = {}
339+
338340
for pair in self.pairlists.whitelist:
339341
if pair in data:
340342
# Load price precision logic
341343
self.price_pair_prec[pair] = get_tick_size_over_time(data[pair])
344+
self.available_pairs.append(pair)
342345
return data, self.timerange
343346

344347
def _load_bt_data_detail(self) -> None:
@@ -1587,7 +1590,7 @@ def time_pair_generator(
15871590
self.check_abort()
15881591

15891592
if self.dynamic_pairlist and self.pairlists:
1590-
self.pairlists.refresh_pairlist()
1593+
self.pairlists.refresh_pairlist(pairs=self.available_pairs)
15911594
pairs = self.pairlists.whitelist
15921595

15931596
# Reset open trade count for this candle

freqtrade/plugins/pairlistmanager.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,20 @@ def short_desc(self) -> list[dict]:
134134
def _get_cached_tickers(self) -> Tickers:
135135
return self._exchange.get_tickers()
136136

137-
def refresh_pairlist(self) -> None:
138-
"""Run pairlist through all configured Pairlist Handlers."""
137+
def refresh_pairlist(self, only_first: bool = False, pairs: list[str] | None = None) -> None:
138+
"""
139+
Run pairlist through all configured Pairlist Handlers.
140+
141+
:param only_first: If True, only run the first PairList handler (the generator)
142+
and skip all subsequent filters. Used during backtesting startup to ensure
143+
historic data is loaded for the complete universe of pairs that the
144+
generator can produce (even if later filters would reduce the list size).
145+
Prevents missing data when a filter returns a variable number of pairs
146+
across refresh cycles.
147+
:param pairs: Optional list of pairs to intersect with the generated pairlist.
148+
Only pairs present both in the generated list and this parameter are kept.
149+
Used in backtesting to filter out pairs with no available data.
150+
"""
139151
# Tickers should be cached to avoid calling the exchange on each call.
140152
tickers: dict = {}
141153
if self._tickers_needed:
@@ -144,10 +156,15 @@ def refresh_pairlist(self) -> None:
144156
# Generate the pairlist with first Pairlist Handler in the chain
145157
pairlist = self._pairlist_handlers[0].gen_pairlist(tickers)
146158

147-
# Process all Pairlist Handlers in the chain
148-
# except for the first one, which is the generator.
149-
for pairlist_handler in self._pairlist_handlers[1:]:
150-
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
159+
# Optional intersection with an explicit list of pairs (used in backtesting)
160+
if pairs is not None:
161+
pairlist = [p for p in pairlist if p in pairs]
162+
163+
if not only_first:
164+
# Process all Pairlist Handlers in the chain
165+
# except for the first one, which is the generator.
166+
for pairlist_handler in self._pairlist_handlers[1:]:
167+
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
151168

152169
# Validation against blacklist happens after the chain of Pairlist Handlers
153170
# to ensure blacklist is respected.

tests/optimize/test_backtesting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2772,7 +2772,7 @@ def test_time_pair_generator_open_trades_first(mocker, default_conf, dynamic_pai
27722772
dummy_row = (end_date, 1.0, 1.1, 0.9, 1.0, 0, 0, 0, 0, None, None)
27732773
data = {pair: [dummy_row] for pair in pairs}
27742774

2775-
def mock_refresh(self):
2775+
def mock_refresh(self, **kwargs):
27762776
# Simulate shuffle
27772777
self._whitelist = pairs[::-1] # ['ETH/BTC', 'NEO/BTC', 'LTC/BTC', 'XRP/BTC']
27782778

0 commit comments

Comments
 (0)