Skip to content

Commit 78acaf8

Browse files
authored
Merge pull request freqtrade#12214 from mrpabloyeah/fix-shufflefilter-behavior-in-backtesting
Fix ShuffleFilter behavior in backtesting
2 parents a1dad06 + c655181 commit 78acaf8

File tree

12 files changed

+176
-36
lines changed

12 files changed

+176
-36
lines changed

docs/commands/backtesting.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
1010
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
1111
[-p PAIRS [PAIRS ...]] [--eps]
1212
[--enable-protections]
13+
[--enable-dynamic-pairlist]
1314
[--dry-run-wallet DRY_RUN_WALLET]
1415
[--timeframe-detail TIMEFRAME_DETAIL]
1516
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
@@ -44,9 +45,14 @@ options:
4445
Allow buying the same pair multiple times (position
4546
stacking).
4647
--enable-protections, --enableprotections
47-
Enable protections for backtesting.Will slow
48+
Enable protections for backtesting. Will slow
4849
backtesting down by a considerable amount, but will
4950
include configured protections
51+
--enable-dynamic-pairlist
52+
Enables dynamic pairlist refreshes in backtesting. The
53+
pairlist will be generated for each new candle if
54+
you're using a pairlist handler that supports this
55+
feature, for example, ShuffleFilter.
5056
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
5157
Starting balance, used for backtesting / hyperopt and
5258
dry-runs.

docs/commands/hyperopt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ options:
4444
Allow buying the same pair multiple times (position
4545
stacking).
4646
--enable-protections, --enableprotections
47-
Enable protections for backtesting.Will slow
47+
Enable protections for backtesting. Will slow
4848
backtesting down by a considerable amount, but will
4949
include configured protections
5050
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET

docs/commands/lookahead-analysis.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--no-color] [--logfile FILE]
1111
[--stake-amount STAKE_AMOUNT]
1212
[--fee FLOAT] [-p PAIRS [PAIRS ...]]
1313
[--enable-protections]
14+
[--enable-dynamic-pairlist]
1415
[--dry-run-wallet DRY_RUN_WALLET]
1516
[--timeframe-detail TIMEFRAME_DETAIL]
1617
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
@@ -44,9 +45,14 @@ options:
4445
Limit command to these pairs. Pairs are space-
4546
separated.
4647
--enable-protections, --enableprotections
47-
Enable protections for backtesting.Will slow
48+
Enable protections for backtesting. Will slow
4849
backtesting down by a considerable amount, but will
4950
include configured protections
51+
--enable-dynamic-pairlist
52+
Enables dynamic pairlist refreshes in backtesting. The
53+
pairlist will be generated for each new candle if
54+
you're using a pairlist handler that supports this
55+
feature, for example, ShuffleFilter.
5056
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
5157
Starting balance, used for backtesting / hyperopt and
5258
dry-runs.

freqtrade/commands/arguments.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
*ARGS_COMMON_OPTIMIZE,
5050
"position_stacking",
5151
"enable_protections",
52+
"enable_dynamic_pairlist",
5253
"dry_run_wallet",
5354
"timeframe_detail",
5455
"strategy_list",

freqtrade/commands/cli_options.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,20 @@ def __init__(self, *args, **kwargs):
184184
"enable_protections": Arg(
185185
"--enable-protections",
186186
"--enableprotections",
187-
help="Enable protections for backtesting."
187+
help="Enable protections for backtesting. "
188188
"Will slow backtesting down by a considerable amount, but will include "
189189
"configured protections",
190190
action="store_true",
191191
default=False,
192192
),
193+
"enable_dynamic_pairlist": Arg(
194+
"--enable-dynamic-pairlist",
195+
help="Enables dynamic pairlist refreshes in backtesting. "
196+
"The pairlist will be generated for each new candle if you're using a "
197+
"pairlist handler that supports this feature, for example, ShuffleFilter.",
198+
action="store_true",
199+
default=False,
200+
),
193201
"strategy_list": Arg(
194202
"--strategy-list",
195203
help="Provide a space-separated list of strategies to backtest. "

freqtrade/configuration/configuration.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,13 @@ def _process_optimize_options(self, config: Config) -> None:
259259
self._args_to_config(
260260
config,
261261
argname="enable_protections",
262-
logstring="Parameter --enable-protections detected, enabling Protections. ...",
262+
logstring="Parameter --enable-protections detected, enabling Protections ...",
263+
)
264+
265+
self._args_to_config(
266+
config,
267+
argname="enable_dynamic_pairlist",
268+
logstring="Parameter --enable-dynamic-pairlist detected, enabling dynamic pairlist ...",
263269
)
264270

265271
if self.args.get("max_open_trades"):

freqtrade/optimize/backtesting.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ def __init__(self, config: Config, exchange: Exchange | None = None) -> None:
211211
self._can_short = self.trading_mode != TradingMode.SPOT
212212
self._position_stacking: bool = self.config.get("position_stacking", False)
213213
self.enable_protections: bool = self.config.get("enable_protections", False)
214+
self.dynamic_pairlist: bool = self.config.get("enable_dynamic_pairlist", False)
214215
migrate_data(config, self.exchange)
215216

216217
self.init_backtest()
@@ -1584,6 +1585,11 @@ def time_pair_generator(
15841585
for current_time in self._time_generator(start_date, end_date):
15851586
# Loop for each main candle.
15861587
self.check_abort()
1588+
1589+
if self.dynamic_pairlist and self.pairlists:
1590+
self.pairlists.refresh_pairlist()
1591+
pairs = self.pairlists.whitelist
1592+
15871593
# Reset open trade count for this candle
15881594
# Critical to avoid exceeding max_open_trades in backtesting
15891595
# when timeframe-detail is used and trades close within the opening candle.

freqtrade/plugins/pairlist/ShuffleFilter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
9393
return pairlist_new
9494
# Shuffle is done inplace
9595
self._random.shuffle(pairlist)
96-
self.__pairlist_cache[pairlist_bef] = pairlist
96+
97+
if self._config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN):
98+
self.__pairlist_cache[pairlist_bef] = pairlist
9799

98100
return pairlist

freqtrade/plugins/pairlist/StaticPairList.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import logging
88
from copy import deepcopy
99

10+
from cachetools import LRUCache
11+
12+
from freqtrade.enums import RunMode
1013
from freqtrade.exchange.exchange_types import Tickers
1114
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
1215

@@ -22,6 +25,8 @@ def __init__(self, *args, **kwargs) -> None:
2225
super().__init__(*args, **kwargs)
2326

2427
self._allow_inactive = self._pairlistconfig.get("allow_inactive", False)
28+
# Pair cache - only used for optimize modes
29+
self._bt_pair_cache: LRUCache = LRUCache(maxsize=1)
2530

2631
@property
2732
def needstickers(self) -> bool:
@@ -60,15 +65,23 @@ def gen_pairlist(self, tickers: Tickers) -> list[str]:
6065
:param tickers: Tickers (from exchange.get_tickers). May be cached.
6166
:return: List of pairs
6267
"""
63-
wl = self.verify_whitelist(
64-
self._config["exchange"]["pair_whitelist"], logger.info, keep_invalid=True
65-
)
66-
if self._allow_inactive:
67-
return wl
68-
else:
69-
# Avoid implicit filtering of "verify_whitelist" to keep
70-
# proper warnings in the log
71-
return self._whitelist_for_active_markets(wl)
68+
pairlist = self._bt_pair_cache.get("pairlist")
69+
70+
if not pairlist:
71+
wl = self.verify_whitelist(
72+
self._config["exchange"]["pair_whitelist"], logger.info, keep_invalid=True
73+
)
74+
if self._allow_inactive:
75+
pairlist = wl
76+
else:
77+
# Avoid implicit filtering of "verify_whitelist" to keep
78+
# proper warnings in the log
79+
pairlist = self._whitelist_for_active_markets(wl)
80+
81+
if self._config["runmode"] in (RunMode.BACKTEST, RunMode.HYPEROPT):
82+
self._bt_pair_cache["pairlist"] = pairlist.copy()
83+
84+
return pairlist
7285

7386
def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
7487
"""

freqtrade/plugins/pairlistmanager.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
from functools import partial
77

8-
from cachetools import TTLCache, cached
8+
from cachetools import LRUCache, TTLCache, cached
99

1010
from freqtrade.constants import Config, ListPairsWithTimeframes
1111
from freqtrade.data.dataprovider import DataProvider
@@ -56,6 +56,7 @@ def __init__(self, exchange, config: Config, dataprovider: DataProvider | None =
5656
)
5757

5858
self._check_backtest()
59+
self._not_expiring_cache: LRUCache = LRUCache(maxsize=1)
5960

6061
refresh_period = config.get("pairlist_refresh_period", 3600)
6162
LoggingMixin.__init__(self, logger, refresh_period)
@@ -109,7 +110,15 @@ def blacklist(self) -> list[str]:
109110
@property
110111
def expanded_blacklist(self) -> list[str]:
111112
"""The expanded blacklist (including wildcard expansion)"""
112-
return expand_pairlist(self._blacklist, self._exchange.get_markets().keys())
113+
eblacklist = self._not_expiring_cache.get("eblacklist")
114+
115+
if not eblacklist:
116+
eblacklist = expand_pairlist(self._blacklist, self._exchange.get_markets().keys())
117+
118+
if self._config["runmode"] in (RunMode.BACKTEST, RunMode.HYPEROPT):
119+
self._not_expiring_cache["eblacklist"] = eblacklist.copy()
120+
121+
return eblacklist
113122

114123
@property
115124
def name_list(self) -> list[str]:
@@ -157,16 +166,17 @@ def verify_blacklist(self, pairlist: list[str], logmethod) -> list[str]:
157166
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`.
158167
:return: pairlist - blacklisted pairs
159168
"""
160-
try:
161-
blacklist = self.expanded_blacklist
162-
except ValueError as err:
163-
logger.error(f"Pair blacklist contains an invalid Wildcard: {err}")
164-
return []
165-
log_once = partial(self.log_once, logmethod=logmethod)
166-
for pair in pairlist.copy():
167-
if pair in blacklist:
168-
log_once(f"Pair {pair} in your blacklist. Removing it from whitelist...")
169-
pairlist.remove(pair)
169+
if self._blacklist:
170+
try:
171+
blacklist = self.expanded_blacklist
172+
except ValueError as err:
173+
logger.error(f"Pair blacklist contains an invalid Wildcard: {err}")
174+
return []
175+
log_once = partial(self.log_once, logmethod=logmethod)
176+
for pair in pairlist.copy():
177+
if pair in blacklist:
178+
log_once(f"Pair {pair} in your blacklist. Removing it from whitelist...")
179+
pairlist.remove(pair)
170180
return pairlist
171181

172182
def verify_whitelist(

0 commit comments

Comments
 (0)