From e749051dbc1ffe03e7eea3e7f8c74bc485aa2996 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Sun, 7 Sep 2025 23:28:33 +0200 Subject: [PATCH 01/12] Fix ShuffleFilter behavior in backtesting --- freqtrade/optimize/backtesting.py | 9 +++++++ freqtrade/plugins/pairlist/ShuffleFilter.py | 4 +++- tests/plugins/test_pairlist.py | 26 ++++++++++++++------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 65b4baff2..1ac2b6351 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -173,6 +173,7 @@ class Backtesting: self.disable_database_use() self.init_backtest_detail() self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider) + self.dinamic_pairlist = False self._validate_pairlists_for_backtesting() self.dataprovider.add_pairlisthandler(self.pairlists) @@ -226,6 +227,9 @@ class Backtesting: "PrecisionFilter not allowed for backtesting multiple strategies." ) + if "ShuffleFilter" in self.pairlists.name_list: + self.dinamic_pairlist = True + def log_once(self, msg: str) -> None: """ Partial reimplementation of log_once from the Login mixin. @@ -1582,6 +1586,11 @@ class Backtesting: for current_time in self._time_generator(start_date, end_date): # Loop for each main candle. self.check_abort() + + if self.dinamic_pairlist: + self.pairlists.refresh_pairlist() + pairs = self.pairlists.whitelist + # 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. diff --git a/freqtrade/plugins/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py index be536e705..86d618160 100644 --- a/freqtrade/plugins/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -93,6 +93,8 @@ class ShuffleFilter(IPairList): return pairlist_new # Shuffle is done inplace self._random.shuffle(pairlist) - self.__pairlist_cache[pairlist_bef] = pairlist + + if self._config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN): + self.__pairlist_cache[pairlist_bef] = pairlist return pairlist diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index f2c3c69e3..ad4ed0f47 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -1274,27 +1274,37 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: {"method": "StaticPairList"}, {"method": "ShuffleFilter", "seed": 43}, ] - whitelist_conf["runmode"] = "backtest" + whitelist_conf["runmode"] = RunMode.BACKTEST exchange = get_patched_exchange(mocker, whitelist_conf) plm = PairListManager(exchange, whitelist_conf) assert log_has("Backtesting mode detected, applying seed value: 43", caplog) + plm.refresh_pairlist() + pl1 = deepcopy(plm.whitelist) + plm.refresh_pairlist() + assert plm.whitelist != pl1 + assert set(plm.whitelist) == set(pl1) + + caplog.clear() + whitelist_conf["runmode"] = RunMode.DRY_RUN + plm = PairListManager(exchange, whitelist_conf) + assert not log_has("Backtesting mode detected, applying seed value: 42", caplog) + assert log_has("Live mode detected, not applying seed.", caplog) + with time_machine.travel("2021-09-01 05:01:00 +00:00") as t: plm.refresh_pairlist() pl1 = deepcopy(plm.whitelist) plm.refresh_pairlist() assert plm.whitelist == pl1 + target = plm._pairlist_handlers[1]._random + shuffle_mock = mocker.patch.object(target, "shuffle", wraps=target.shuffle) + t.shift(timedelta(minutes=10)) plm.refresh_pairlist() - assert plm.whitelist != pl1 - - caplog.clear() - whitelist_conf["runmode"] = RunMode.DRY_RUN - plm = PairListManager(exchange, whitelist_conf) - assert not log_has("Backtesting mode detected, applying seed value: 42", caplog) - assert log_has("Live mode detected, not applying seed.", caplog) + assert shuffle_mock.call_count == 1 + assert set(plm.whitelist) == set(pl1) @pytest.mark.usefixtures("init_persistence") From 737d2804d23fc7bd1662dd1999f22108636b318a Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Mon, 8 Sep 2025 17:47:33 +0200 Subject: [PATCH 02/12] Fix spelling mistake --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1ac2b6351..2bb6baeac 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -173,7 +173,7 @@ class Backtesting: self.disable_database_use() self.init_backtest_detail() self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider) - self.dinamic_pairlist = False + self.dynamic_pairlist = False self._validate_pairlists_for_backtesting() self.dataprovider.add_pairlisthandler(self.pairlists) @@ -228,7 +228,7 @@ class Backtesting: ) if "ShuffleFilter" in self.pairlists.name_list: - self.dinamic_pairlist = True + self.dynamic_pairlist = True def log_once(self, msg: str) -> None: """ @@ -1587,7 +1587,7 @@ class Backtesting: # Loop for each main candle. self.check_abort() - if self.dinamic_pairlist: + if self.dynamic_pairlist: self.pairlists.refresh_pairlist() pairs = self.pairlists.whitelist From a8f543ce620e13871d0c10d1f77dcb789611350e Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 9 Sep 2025 03:01:46 +0200 Subject: [PATCH 03/12] Add test_time_pair_generator_refresh_pairlist() --- tests/optimize/test_backtesting.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 292ee92aa..ff6c8c76a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -2715,3 +2715,29 @@ def test_get_backtest_metadata_filename(): filename = "backtest_results_zip.zip" expected = Path("backtest_results_zip.meta.json") assert get_backtest_metadata_filename(filename) == expected + + +@pytest.mark.parametrize("dynamic_pairlist", [True, False]) +def test_time_pair_generator_refresh_pairlist(mocker, default_conf, dynamic_pairlist): + patch_exchange(mocker) + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + backtesting.dynamic_pairlist = dynamic_pairlist + + refresh_mock = mocker.patch( + "freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist" + ) + + # Simulate 2 candles + start_date = datetime(2025, 1, 1, 0, 0, tzinfo=UTC) + end_date = start_date + timedelta(minutes=10) + pairs = default_conf["exchange"]["pair_whitelist"] + data = {pair: [] for pair in pairs} + + # Simulate backtest loop + list(backtesting.time_pair_generator(start_date, end_date, pairs, data)) + + if dynamic_pairlist: + assert refresh_mock.call_count == 2 + else: + assert refresh_mock.call_count == 0 From eea9133b3f75b48b24a0827e001be427163e33fb Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 9 Sep 2025 03:03:07 +0200 Subject: [PATCH 04/12] test_time_pair_generator_open_trades_first() --- tests/optimize/test_backtesting.py | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index ff6c8c76a..0aa0ea7e6 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -2741,3 +2741,47 @@ def test_time_pair_generator_refresh_pairlist(mocker, default_conf, dynamic_pair assert refresh_mock.call_count == 2 else: assert refresh_mock.call_count == 0 + + +@pytest.mark.parametrize("dynamic_pairlist", [True, False]) +def test_time_pair_generator_open_trades_first(mocker, default_conf, dynamic_pairlist): + patch_exchange(mocker) + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + backtesting.dynamic_pairlist = dynamic_pairlist + + pairs = ["XRP/BTC", "LTC/BTC", "NEO/BTC", "ETH/BTC"] + + # Simulate open trades + trades = [ + LocalTrade(pair="XRP/BTC", open_date=datetime.now(tz=UTC), amount=1, open_rate=1), + LocalTrade(pair="NEO/BTC", open_date=datetime.now(tz=UTC), amount=1, open_rate=1), + ] + LocalTrade.bt_trades_open = trades + LocalTrade.bt_trades_open_pp = { + "XRP/BTC": [trades[0]], + "NEO/BTC": [trades[1]], + "LTC/BTC": [], + "ETH/BTC": [], + } + + start_date = datetime(2025, 1, 1, 0, 0, tzinfo=UTC) + end_date = start_date + timedelta(minutes=5) + 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} + + def mock_refresh(self): + # Simulate shuffle + self._whitelist = pairs[::-1] # ['ETH/BTC', 'NEO/BTC', 'LTC/BTC', 'XRP/BTC'] + + mocker.patch("freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist", mock_refresh) + + processed_pairs = [] + for _, pair, _, _, _ in backtesting.time_pair_generator(start_date, end_date, pairs, data): + processed_pairs.append(pair) + + # Open trades first in both cases + if dynamic_pairlist: + assert processed_pairs == ["XRP/BTC", "NEO/BTC", "ETH/BTC", "LTC/BTC"] + else: + assert processed_pairs == ["XRP/BTC", "NEO/BTC", "LTC/BTC", "ETH/BTC"] From 3bce9278bd823c0c55a31280918474a853fdf8d2 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 9 Sep 2025 14:37:08 +0200 Subject: [PATCH 05/12] Add cache for gen_pairlist() in StaticPairList in backtest mode --- freqtrade/plugins/pairlist/StaticPairList.py | 30 ++++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index bff2105b3..9df437260 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -7,6 +7,9 @@ Provides pair white list as it configured in config import logging from copy import deepcopy +from cachetools import LRUCache + +from freqtrade.enums.runmode import RunMode from freqtrade.exchange.exchange_types import Tickers from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting @@ -22,6 +25,7 @@ class StaticPairList(IPairList): super().__init__(*args, **kwargs) self._allow_inactive = self._pairlistconfig.get("allow_inactive", False) + self._pair_cache: LRUCache = LRUCache(maxsize=1) @property def needstickers(self) -> bool: @@ -60,15 +64,23 @@ class StaticPairList(IPairList): :param tickers: Tickers (from exchange.get_tickers). May be cached. :return: List of pairs """ - wl = self.verify_whitelist( - self._config["exchange"]["pair_whitelist"], logger.info, keep_invalid=True - ) - if self._allow_inactive: - return wl - else: - # Avoid implicit filtering of "verify_whitelist" to keep - # proper warnings in the log - return self._whitelist_for_active_markets(wl) + pairlist = self._pair_cache.get("pairlist") + + if not pairlist: + wl = self.verify_whitelist( + self._config["exchange"]["pair_whitelist"], logger.info, keep_invalid=True + ) + if self._allow_inactive: + pairlist = wl + else: + # Avoid implicit filtering of "verify_whitelist" to keep + # proper warnings in the log + pairlist = self._whitelist_for_active_markets(wl) + + if self._config["runmode"] in (RunMode.BACKTEST, RunMode.HYPEROPT): + self._pair_cache["pairlist"] = pairlist.copy() + + return pairlist def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]: """ From 43be752847ccd6c8f12e2c91d4d9867f56245d04 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 9 Sep 2025 19:24:32 +0200 Subject: [PATCH 06/12] Do not verify the backlist if it is empty --- freqtrade/plugins/pairlistmanager.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index bc66d00c3..8d35f5642 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -157,16 +157,17 @@ class PairListManager(LoggingMixin): :param logmethod: Function that'll be called, `logger.info` or `logger.warning`. :return: pairlist - blacklisted pairs """ - try: - blacklist = self.expanded_blacklist - except ValueError as err: - logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") - return [] - log_once = partial(self.log_once, logmethod=logmethod) - for pair in pairlist.copy(): - if pair in blacklist: - log_once(f"Pair {pair} in your blacklist. Removing it from whitelist...") - pairlist.remove(pair) + if self._blacklist: + try: + blacklist = self.expanded_blacklist + except ValueError as err: + logger.error(f"Pair blacklist contains an invalid Wildcard: {err}") + return [] + log_once = partial(self.log_once, logmethod=logmethod) + for pair in pairlist.copy(): + if pair in blacklist: + log_once(f"Pair {pair} in your blacklist. Removing it from whitelist...") + pairlist.remove(pair) return pairlist def verify_whitelist( From 55201b6224a03629e8958fcca8a1f292e3dac448 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 9 Sep 2025 20:17:14 +0200 Subject: [PATCH 07/12] Fix: Hyperopt breaks when self.dynamic_pairlist is set to True --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2bb6baeac..d6336b567 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1587,7 +1587,7 @@ class Backtesting: # Loop for each main candle. self.check_abort() - if self.dynamic_pairlist: + if self.dynamic_pairlist and self.pairlists: self.pairlists.refresh_pairlist() pairs = self.pairlists.whitelist From 5f558137d3317d357cb5cbde160bf50140ff49d3 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Fri, 12 Sep 2025 13:01:50 +0200 Subject: [PATCH 08/12] Add cache for expanded_blacklist() in PairListManager in backtest mode --- freqtrade/plugins/pairlistmanager.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 8d35f5642..9056d842e 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -5,7 +5,7 @@ PairList manager class import logging from functools import partial -from cachetools import TTLCache, cached +from cachetools import LRUCache, TTLCache, cached from freqtrade.constants import Config, ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider @@ -56,6 +56,7 @@ class PairListManager(LoggingMixin): ) self._check_backtest() + self._not_expiring_cache: LRUCache = LRUCache(maxsize=1) refresh_period = config.get("pairlist_refresh_period", 3600) LoggingMixin.__init__(self, logger, refresh_period) @@ -109,7 +110,15 @@ class PairListManager(LoggingMixin): @property def expanded_blacklist(self) -> list[str]: """The expanded blacklist (including wildcard expansion)""" - return expand_pairlist(self._blacklist, self._exchange.get_markets().keys()) + eblacklist = self._not_expiring_cache.get("eblacklist") + + if not eblacklist: + eblacklist = expand_pairlist(self._blacklist, self._exchange.get_markets().keys()) + + if self._config["runmode"] in (RunMode.BACKTEST, RunMode.HYPEROPT): + self._not_expiring_cache["eblacklist"] = eblacklist.copy() + + return eblacklist @property def name_list(self) -> list[str]: From 5dbfc92c5a75332d3a6a47bf1cbca0b15f44d3d7 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Sun, 14 Sep 2025 13:58:53 +0200 Subject: [PATCH 09/12] Add --enable-dynamic-pairlist option in backtesting --- docs/commands/backtesting.md | 8 +++++++- docs/commands/hyperopt.md | 2 +- docs/commands/lookahead-analysis.md | 8 +++++++- freqtrade/commands/arguments.py | 1 + freqtrade/commands/cli_options.py | 10 +++++++++- freqtrade/configuration/configuration.py | 8 +++++++- freqtrade/optimize/backtesting.py | 5 +---- 7 files changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/commands/backtesting.md b/docs/commands/backtesting.md index 7082fb362..56d01f38f 100644 --- a/docs/commands/backtesting.md +++ b/docs/commands/backtesting.md @@ -10,6 +10,7 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [-p PAIRS [PAIRS ...]] [--eps] [--enable-protections] + [--enable-dynamic-pairlist] [--dry-run-wallet DRY_RUN_WALLET] [--timeframe-detail TIMEFRAME_DETAIL] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] @@ -44,9 +45,14 @@ options: Allow buying the same pair multiple times (position stacking). --enable-protections, --enableprotections - Enable protections for backtesting.Will slow + Enable protections for backtesting. Will slow backtesting down by a considerable amount, but will include configured protections + --enable-dynamic-pairlist + Enables dynamic pairlisting in backtesting. The + pairlist will be generated for each new candle if + you're using a pairlist handler that supports this + feature, for example, ShuffleFilter. --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. diff --git a/docs/commands/hyperopt.md b/docs/commands/hyperopt.md index f1c8b28b5..933082b9d 100644 --- a/docs/commands/hyperopt.md +++ b/docs/commands/hyperopt.md @@ -44,7 +44,7 @@ options: Allow buying the same pair multiple times (position stacking). --enable-protections, --enableprotections - Enable protections for backtesting.Will slow + Enable protections for backtesting. Will slow backtesting down by a considerable amount, but will include configured protections --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET diff --git a/docs/commands/lookahead-analysis.md b/docs/commands/lookahead-analysis.md index 880c41ce1..f7354d8ea 100644 --- a/docs/commands/lookahead-analysis.md +++ b/docs/commands/lookahead-analysis.md @@ -11,6 +11,7 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--no-color] [--logfile FILE] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [-p PAIRS [PAIRS ...]] [--enable-protections] + [--enable-dynamic-pairlist] [--dry-run-wallet DRY_RUN_WALLET] [--timeframe-detail TIMEFRAME_DETAIL] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] @@ -43,9 +44,14 @@ options: Limit command to these pairs. Pairs are space- separated. --enable-protections, --enableprotections - Enable protections for backtesting.Will slow + Enable protections for backtesting. Will slow backtesting down by a considerable amount, but will include configured protections + --enable-dynamic-pairlist + Enables dynamic pairlisting in backtesting. The + pairlist will be generated for each new candle if + you're using a pairlist handler that supports this + feature, for example, ShuffleFilter. --dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET Starting balance, used for backtesting / hyperopt and dry-runs. diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 17d8f43d7..e4bdc170d 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -49,6 +49,7 @@ ARGS_BACKTEST = [ *ARGS_COMMON_OPTIMIZE, "position_stacking", "enable_protections", + "enable_dynamic_pairlist", "dry_run_wallet", "timeframe_detail", "strategy_list", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 62a007f59..fc613febe 100755 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -184,12 +184,20 @@ AVAILABLE_CLI_OPTIONS = { "enable_protections": Arg( "--enable-protections", "--enableprotections", - help="Enable protections for backtesting." + help="Enable protections for backtesting. " "Will slow backtesting down by a considerable amount, but will include " "configured protections", action="store_true", default=False, ), + "enable_dynamic_pairlist": Arg( + "--enable-dynamic-pairlist", + help="Enables dynamic pairlisting in backtesting. " + "The pairlist will be generated for each new candle if you're using a " + "pairlist handler that supports this feature, for example, ShuffleFilter.", + action="store_true", + default=False, + ), "strategy_list": Arg( "--strategy-list", help="Provide a space-separated list of strategies to backtest. " diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 7bf448335..cc40edef1 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -256,7 +256,13 @@ class Configuration: self._args_to_config( config, argname="enable_protections", - logstring="Parameter --enable-protections detected, enabling Protections. ...", + logstring="Parameter --enable-protections detected, enabling Protections ...", + ) + + self._args_to_config( + config, + argname="enable_dynamic_pairlist", + logstring="Parameter --enable-dynamic-pairlist detected, enabling dynamic pairlist ...", ) if self.args.get("max_open_trades"): diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d6336b567..5ae91ee30 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -173,7 +173,6 @@ class Backtesting: self.disable_database_use() self.init_backtest_detail() self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider) - self.dynamic_pairlist = False self._validate_pairlists_for_backtesting() self.dataprovider.add_pairlisthandler(self.pairlists) @@ -212,6 +211,7 @@ class Backtesting: self._can_short = self.trading_mode != TradingMode.SPOT self._position_stacking: bool = self.config.get("position_stacking", 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) self.init_backtest() @@ -227,9 +227,6 @@ class Backtesting: "PrecisionFilter not allowed for backtesting multiple strategies." ) - if "ShuffleFilter" in self.pairlists.name_list: - self.dynamic_pairlist = True - def log_once(self, msg: str) -> None: """ Partial reimplementation of log_once from the Login mixin. From 8e3331d0db9e3cad9b1072a3e01f739245c9e4e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Sep 2025 20:16:44 +0200 Subject: [PATCH 10/12] chore: rename variable for clarity --- freqtrade/plugins/pairlist/StaticPairList.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index 9df437260..94ba04235 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -9,7 +9,7 @@ from copy import deepcopy from cachetools import LRUCache -from freqtrade.enums.runmode import RunMode +from freqtrade.enums import RunMode from freqtrade.exchange.exchange_types import Tickers from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting @@ -25,7 +25,8 @@ class StaticPairList(IPairList): super().__init__(*args, **kwargs) self._allow_inactive = self._pairlistconfig.get("allow_inactive", False) - self._pair_cache: LRUCache = LRUCache(maxsize=1) + # Pair cache - only used for optimize modes + self._bt_pair_cache: LRUCache = LRUCache(maxsize=1) @property def needstickers(self) -> bool: @@ -64,7 +65,7 @@ class StaticPairList(IPairList): :param tickers: Tickers (from exchange.get_tickers). May be cached. :return: List of pairs """ - pairlist = self._pair_cache.get("pairlist") + pairlist = self._bt_pair_cache.get("pairlist") if not pairlist: wl = self.verify_whitelist( @@ -78,7 +79,7 @@ class StaticPairList(IPairList): pairlist = self._whitelist_for_active_markets(wl) if self._config["runmode"] in (RunMode.BACKTEST, RunMode.HYPEROPT): - self._pair_cache["pairlist"] = pairlist.copy() + self._bt_pair_cache["pairlist"] = pairlist.copy() return pairlist From e58635a881d593926884e58fdea14e411b1a7746 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Sep 2025 20:23:07 +0200 Subject: [PATCH 11/12] test: minor adjustments to new tests --- tests/optimize/test_backtesting.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 0aa0ea7e6..620e43387 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -27,7 +27,7 @@ from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename, from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.util.datetime_helpers import dt_utc +from freqtrade.util import dt_now, dt_utc from tests.conftest import ( CURRENT_TEST_STRATEGY, EXMS, @@ -2720,9 +2720,10 @@ def test_get_backtest_metadata_filename(): @pytest.mark.parametrize("dynamic_pairlist", [True, False]) def test_time_pair_generator_refresh_pairlist(mocker, default_conf, dynamic_pairlist): patch_exchange(mocker) + default_conf["enable_dynamic_pairlist"] = dynamic_pairlist backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.dynamic_pairlist = dynamic_pairlist + assert backtesting.dynamic_pairlist == dynamic_pairlist refresh_mock = mocker.patch( "freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist" @@ -2746,16 +2747,17 @@ def test_time_pair_generator_refresh_pairlist(mocker, default_conf, dynamic_pair @pytest.mark.parametrize("dynamic_pairlist", [True, False]) def test_time_pair_generator_open_trades_first(mocker, default_conf, dynamic_pairlist): patch_exchange(mocker) + default_conf["enable_dynamic_pairlist"] = dynamic_pairlist backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.dynamic_pairlist = dynamic_pairlist + assert backtesting.dynamic_pairlist == dynamic_pairlist pairs = ["XRP/BTC", "LTC/BTC", "NEO/BTC", "ETH/BTC"] # Simulate open trades trades = [ - LocalTrade(pair="XRP/BTC", open_date=datetime.now(tz=UTC), amount=1, open_rate=1), - LocalTrade(pair="NEO/BTC", open_date=datetime.now(tz=UTC), amount=1, open_rate=1), + LocalTrade(pair="XRP/BTC", open_date=dt_now(), amount=1, open_rate=1), + LocalTrade(pair="NEO/BTC", open_date=dt_now(), amount=1, open_rate=1), ] LocalTrade.bt_trades_open = trades LocalTrade.bt_trades_open_pp = { From c6551819cd123adde8bd28cccb4545e5dc98bf74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Sep 2025 20:23:36 +0200 Subject: [PATCH 12/12] chore: re-word help string --- docs/commands/backtesting.md | 2 +- docs/commands/lookahead-analysis.md | 2 +- freqtrade/commands/cli_options.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/commands/backtesting.md b/docs/commands/backtesting.md index 56d01f38f..925b29da6 100644 --- a/docs/commands/backtesting.md +++ b/docs/commands/backtesting.md @@ -49,7 +49,7 @@ options: backtesting down by a considerable amount, but will include configured protections --enable-dynamic-pairlist - Enables dynamic pairlisting in backtesting. The + Enables dynamic pairlist refreshes in backtesting. The pairlist will be generated for each new candle if you're using a pairlist handler that supports this feature, for example, ShuffleFilter. diff --git a/docs/commands/lookahead-analysis.md b/docs/commands/lookahead-analysis.md index f7354d8ea..4cb6621c2 100644 --- a/docs/commands/lookahead-analysis.md +++ b/docs/commands/lookahead-analysis.md @@ -48,7 +48,7 @@ options: backtesting down by a considerable amount, but will include configured protections --enable-dynamic-pairlist - Enables dynamic pairlisting in backtesting. The + Enables dynamic pairlist refreshes in backtesting. The pairlist will be generated for each new candle if you're using a pairlist handler that supports this feature, for example, ShuffleFilter. diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index fc613febe..9664dc385 100755 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -192,7 +192,7 @@ AVAILABLE_CLI_OPTIONS = { ), "enable_dynamic_pairlist": Arg( "--enable-dynamic-pairlist", - help="Enables dynamic pairlisting in backtesting. " + help="Enables dynamic pairlist refreshes in backtesting. " "The pairlist will be generated for each new candle if you're using a " "pairlist handler that supports this feature, for example, ShuffleFilter.", action="store_true",