From 271fc6b58558f70099f87472b166d2167b5e866d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Dec 2025 21:11:28 +0100 Subject: [PATCH 01/50] feat: don't fill up missing funding-fees after merge --- freqtrade/exchange/exchange.py | 12 ++++++++++-- tests/exchange/test_exchange.py | 9 +++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 717844db7..e71e374a1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -3808,8 +3808,16 @@ class Exchange: combined = mark_rates.merge( funding_rates, on="date", how="left", suffixes=["_mark", "_fund"] ) - combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate) - return combined[relevant_cols] + # Fill only leading missing funding rates so gaps stay untouched + first_valid_idx = combined["open_fund"].first_valid_index() + if first_valid_idx is None: + combined["open_fund"] = futures_funding_rate + else: + is_leading_na = (combined.index <= first_valid_idx) & combined[ + "open_fund" + ].isna() + combined.loc[is_leading_na, "open_fund"] = futures_funding_rate + return combined[relevant_cols].dropna() def calculate_funding_fees( self, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8d7e0b0d8..4daeb1855 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5350,11 +5350,12 @@ def test_combine_funding_and_mark( df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate) if futures_funding_rate is not None: - assert len(df) == 3 + assert len(df) == 2 assert df.iloc[0]["open_fund"] == funding_rate - assert df.iloc[1]["open_fund"] == futures_funding_rate - assert df.iloc[2]["open_fund"] == funding_rate - assert df["date"].to_list() == [prior2_date, prior_date, trade_date] + # assert df.iloc[1]["open_fund"] == futures_funding_rate + assert df.iloc[-1]["open_fund"] == funding_rate + # Mid-candle is dropped ... + assert df["date"].to_list() == [prior2_date, trade_date] else: assert len(df) == 2 assert df["date"].to_list() == [prior2_date, trade_date] From d41acc77f7c4ec9df01b83e8e86423a42a7f678d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Dec 2025 06:55:56 +0100 Subject: [PATCH 02/50] fix: floor funding-rate to seconds to account for slight time offset --- freqtrade/data/history/datahandlers/idatahandler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/data/history/datahandlers/idatahandler.py b/freqtrade/data/history/datahandlers/idatahandler.py index 67ae386c6..2727d1a64 100644 --- a/freqtrade/data/history/datahandlers/idatahandler.py +++ b/freqtrade/data/history/datahandlers/idatahandler.py @@ -397,6 +397,9 @@ class IDataHandler(ABC): pairdf = self._ohlcv_load( pair, timeframe, timerange=timerange_startup, candle_type=candle_type ) + if not pairdf.empty and candle_type == CandleType.FUNDING_RATE: + # Funding rate data is sometimes off by a couple of ms - floor to seconds + pairdf["date"] = pairdf["date"].dt.floor("s") if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): return pairdf else: From 41a82eff21b72e21b2f715a5eab3d0541040df66 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Dec 2025 18:10:16 +0100 Subject: [PATCH 03/50] fix: don't fill up funding fee data Data Timeframes are arbitrary and may vary between pairs or time ranges --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fd78162db..e3e340a1b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -374,6 +374,7 @@ class Backtesting: timerange=self.timerange, startup_candles=0, fail_without_data=True, + fill_up_missing=False, data_format=self.config["dataformat_ohlcv"], candle_type=CandleType.FUNDING_RATE, ) From 2845568f61459fb9378c3299f988085611d9bea1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Dec 2025 20:32:56 +0100 Subject: [PATCH 04/50] feat: limit funding_fee renaming to rename from low to high. --- freqtrade/data/history/datahandlers/idatahandler.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/datahandlers/idatahandler.py b/freqtrade/data/history/datahandlers/idatahandler.py index 2727d1a64..9e7d9de3a 100644 --- a/freqtrade/data/history/datahandlers/idatahandler.py +++ b/freqtrade/data/history/datahandlers/idatahandler.py @@ -511,8 +511,15 @@ class IDataHandler(ABC): Applies to bybit and okx, where funding-fee and mark candles have different timeframes. """ paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES) + ff_timeframe_s = timeframe_to_seconds(ff_timeframe) + funding_rate_combs = [ - f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe + f + for f in paircombs + if f[2] == CandleType.FUNDING_RATE + and f[1] != ff_timeframe + # Only allow smaller timeframes to move from smaller to larger timeframes + and timeframe_to_seconds(f[1]) < ff_timeframe_s ] if funding_rate_combs: From 3f0be5e41fc9b76148c2adb06d8994a4f29eae5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 13:42:16 +0100 Subject: [PATCH 05/50] fix: floor timestamp to seconds no candle has more than second precision. --- freqtrade/data/converter/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/converter/converter.py b/freqtrade/data/converter/converter.py index 4331519c2..290461d39 100644 --- a/freqtrade/data/converter/converter.py +++ b/freqtrade/data/converter/converter.py @@ -38,7 +38,7 @@ def ohlcv_to_dataframe( cols = DEFAULT_DATAFRAME_COLUMNS df = DataFrame(ohlcv, columns=cols) - df["date"] = to_datetime(df["date"], unit="ms", utc=True) + df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s") # Some exchanges return int values for Volume and even for OHLC. # Convert them since TA-LIB indicators used in the strategy assume floats From 07fbf2b4672949c43f01c47b113d59cd61749cf6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 13:46:27 +0100 Subject: [PATCH 06/50] feat: support dynamic funding fees in dry/live mode --- freqtrade/exchange/exchange.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e71e374a1..b41d1a9fb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2744,7 +2744,11 @@ class Exchange: has_cache = cache and (pair, timeframe, c_type) in self._klines # in case of existing cache, fill_missing happens after concatenation ohlcv_df = ohlcv_to_dataframe( - ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete + ticks, + timeframe, + pair=pair, + fill_missing=not has_cache and c_type != CandleType.FUNDING_RATE, + drop_incomplete=drop_incomplete, ) # keeping parsed dataframe in cache if cache: @@ -2755,7 +2759,7 @@ class Exchange: concat([old, ohlcv_df], axis=0), timeframe, pair, - fill_missing=True, + fill_missing=c_type != CandleType.FUNDING_RATE, drop_incomplete=False, ) candle_limit = self.ohlcv_candle_limit(timeframe, self._config["candle_type_def"]) From 17009ac59f539869b687bd2103c8c33d4e4d9bea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 13:53:18 +0100 Subject: [PATCH 07/50] chore: allow non-matching funding timeframe - as timeframe doesn't actually matter any longer. --- freqtrade/exchange/exchange.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b41d1a9fb..381a05044 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2698,7 +2698,7 @@ class Exchange: CandleType.SPOT, CandleType.FUTURES, ) - if invalid_timeframe or invalid_funding: + if invalid_timeframe: timeframes_ = ( ", ".join(self.timeframes) if candle_type != CandleType.FUNDING_RATE @@ -2710,6 +2710,13 @@ class Exchange: f"{timeframes_}." ) continue + if invalid_funding: + # TODO: does this message make sense? would docs be better? + # if any, this should be cached to avoid log spam! + logger.warning( + f"Wrong funding rate timeframe {timeframe} for pair {pair}, " + f"downloading {self.get_option('funding_fee_timeframe')} instead." + ) if ( (pair, timeframe, candle_type) not in self._klines From b70f10dca61f701ab1bae112abbde05d6a55d7a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 14:06:17 +0100 Subject: [PATCH 08/50] chore: simplify warning formatting --- freqtrade/exchange/exchange.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 381a05044..79956e4a0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2699,15 +2699,10 @@ class Exchange: CandleType.FUTURES, ) if invalid_timeframe: - timeframes_ = ( - ", ".join(self.timeframes) - if candle_type != CandleType.FUNDING_RATE - else self.get_option("funding_fee_timeframe") - ) logger.warning( f"Cannot download ({pair}, {timeframe}, {candle_type}) combination as this " f"timeframe is not available on {self.name}. Available timeframes are " - f"{timeframes_}." + f"{', '.join(self.timeframes)}." ) continue if invalid_funding: From 730383ab180b7c6d442e63b846ac56894f14d11f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 14:13:41 +0100 Subject: [PATCH 09/50] feat: auto-download correct funding rate timeframe --- freqtrade/exchange/exchange.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 79956e4a0..dd335b784 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2690,10 +2690,16 @@ class Exchange: input_coroutines: list[Coroutine[Any, Any, OHLCVResponse]] = [] cached_pairs = [] for pair, timeframe, candle_type in set(pair_list): - invalid_funding = ( - candle_type == CandleType.FUNDING_RATE - and timeframe != self.get_option("funding_fee_timeframe") - ) + if candle_type == CandleType.FUNDING_RATE and timeframe != ( + ff_tf := self.get_option("funding_fee_timeframe") + ): + # TODO: does this message make sense? would docs be better? + # if any, this should be cached to avoid log spam! + logger.warning( + f"Wrong funding rate timeframe {timeframe} for pair {pair}, " + f"downloading {ff_tf} instead." + ) + timeframe = ff_tf invalid_timeframe = timeframe not in self.timeframes and candle_type in ( CandleType.SPOT, CandleType.FUTURES, @@ -2705,13 +2711,6 @@ class Exchange: f"{', '.join(self.timeframes)}." ) continue - if invalid_funding: - # TODO: does this message make sense? would docs be better? - # if any, this should be cached to avoid log spam! - logger.warning( - f"Wrong funding rate timeframe {timeframe} for pair {pair}, " - f"downloading {self.get_option('funding_fee_timeframe')} instead." - ) if ( (pair, timeframe, candle_type) not in self._klines From 3bd911982fe850fcbd49e7a5c3c4d3a3992fabb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 14:13:53 +0100 Subject: [PATCH 10/50] feat: add get_funding_rate_timeframe to dataprovider --- freqtrade/data/dataprovider.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index dfab29c08..311086f3b 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -620,3 +620,12 @@ class DataProvider: except ExchangeError: logger.warning(f"Could not fetch market data for {pair}. Assuming no delisting.") return None + + def get_funding_rate_timeframe(self) -> str: + """ + Get the funding rate timeframe from exchange options + :return: Timeframe string + """ + if self._exchange is None: + raise OperationalException(NO_EXCHANGE_EXCEPTION) + return self._exchange.get_option("funding_fee_timeframe") From 40f4ff04c267ecc296de228eea84fa41e27aaa94 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 14:16:27 +0100 Subject: [PATCH 11/50] feat: auto-fix invalid funding rate timeframe in informative decorator --- freqtrade/strategy/informative_decorator.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index ac9a2a1ad..7c7edb973 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -104,8 +104,11 @@ def _create_and_merge_informative_pair( ): asset = inf_data.asset or "" timeframe = inf_data.timeframe + timeframe1 = inf_data.timeframe fmt = inf_data.fmt candle_type = inf_data.candle_type + if candle_type == CandleType.FUNDING_RATE: + timeframe1 = strategy.dp.get_funding_rate_timeframe() config = strategy.config @@ -132,10 +135,10 @@ def _create_and_merge_informative_pair( fmt = "{base}_{quote}_" + fmt # Informatives of other pairs inf_metadata = {"pair": asset, "timeframe": timeframe} - inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe, candle_type) + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe1, candle_type) if inf_dataframe.empty: raise ValueError( - f"Informative dataframe for ({asset}, {timeframe}, {candle_type}) is empty. " + f"Informative dataframe for ({asset}, {timeframe1}, {candle_type}) is empty. " "Can't populate informative indicators." ) inf_dataframe = populate_indicators_fn(strategy, inf_dataframe, inf_metadata) @@ -163,7 +166,7 @@ def _create_and_merge_informative_pair( dataframe, inf_dataframe, strategy.timeframe, - timeframe, + timeframe1, ffill=inf_data.ffill, append_timeframe=False, date_column=date_column, From 3ca8e0fb5c9f47a3f2cadcf90d4d0cdf70734821 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 15:42:40 +0100 Subject: [PATCH 12/50] feat: auto-adjust funding rate timeframe in dataprovider --- freqtrade/data/dataprovider.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 311086f3b..bd5a7a5ba 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -348,6 +348,22 @@ class DataProvider: ) return total_candles + def __fix_funding_rate_timeframe( + self, pair: str, timeframe: str | None, candle_type: str + ) -> str: + if ( + candle_type == CandleType.FUNDING_RATE + and (ff_tf := self.get_funding_rate_timeframe()) != timeframe + ): + # TODO: does this message make sense? might be pointless as funding fees don't + # have a timeframe + logger.warning( + f"{pair}, {timeframe} requested - funding rate timeframe not matching {ff_tf}. " + ) + return ff_tf + + return timeframe + def get_pair_dataframe( self, pair: str, timeframe: str | None = None, candle_type: str = "" ) -> DataFrame: @@ -361,6 +377,7 @@ class DataProvider: :return: Dataframe for this pair :param candle_type: '', mark, index, premiumIndex, or funding_rate """ + timeframe = self.__fix_funding_rate_timeframe(pair, timeframe, candle_type) if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): # Get live OHLCV data. data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type) From 4897080827da16cd50f9199f3a3841f69252408e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 15:53:27 +0100 Subject: [PATCH 13/50] fix: bybit's minimal funding fee interval to 1h --- freqtrade/exchange/bybit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 300344e19..3ca44edbe 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -38,8 +38,8 @@ class Bybit(Exchange): } _ft_has_futures: FtHas = { "ohlcv_has_history": True, - "mark_ohlcv_timeframe": "4h", - "funding_fee_timeframe": "8h", + "mark_ohlcv_timeframe": "1h", + "funding_fee_timeframe": "1h", "funding_fee_candle_limit": 200, "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "limit", "market": "market"}, From cf6b7a847b7c826ec795c1ba06224384b6c0ba26 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 16:01:05 +0100 Subject: [PATCH 14/50] fix: bitget's minimal funding fee interval is 1h --- freqtrade/exchange/bitget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/bitget.py b/freqtrade/exchange/bitget.py index 3351cda88..aba2aa9b9 100644 --- a/freqtrade/exchange/bitget.py +++ b/freqtrade/exchange/bitget.py @@ -35,7 +35,8 @@ class Bitget(Exchange): "order_time_in_force": ["GTC", "FOK", "IOC", "PO"], } _ft_has_futures: FtHas = { - "mark_ohlcv_timeframe": "4h", + "mark_ohlcv_timeframe": "1h", + "funding_fee_timeframe": "1h", "funding_fee_candle_limit": 100, "has_delisting": True, } From 01b0a8fa42c42e48a7f3c1992190ddd2030f5642 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 16:15:32 +0100 Subject: [PATCH 15/50] fix: 1h should be the default for funding/mark candles --- freqtrade/exchange/bitget.py | 2 -- freqtrade/exchange/bybit.py | 2 -- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/exchange/hyperliquid.py | 1 - freqtrade/exchange/kraken.py | 1 - freqtrade/exchange/okx.py | 2 -- 6 files changed, 2 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/bitget.py b/freqtrade/exchange/bitget.py index aba2aa9b9..f0da55cb0 100644 --- a/freqtrade/exchange/bitget.py +++ b/freqtrade/exchange/bitget.py @@ -35,8 +35,6 @@ class Bitget(Exchange): "order_time_in_force": ["GTC", "FOK", "IOC", "PO"], } _ft_has_futures: FtHas = { - "mark_ohlcv_timeframe": "1h", - "funding_fee_timeframe": "1h", "funding_fee_candle_limit": 100, "has_delisting": True, } diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 3ca44edbe..0184c6c42 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -38,8 +38,6 @@ class Bybit(Exchange): } _ft_has_futures: FtHas = { "ohlcv_has_history": True, - "mark_ohlcv_timeframe": "1h", - "funding_fee_timeframe": "1h", "funding_fee_candle_limit": 200, "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "limit", "market": "market"}, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dd335b784..b5b2bff89 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -153,8 +153,8 @@ class Exchange: "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) "l2_limit_upper": None, # Upper limit for L2 limit "mark_ohlcv_price": "mark", - "mark_ohlcv_timeframe": "8h", - "funding_fee_timeframe": "8h", + "mark_ohlcv_timeframe": "1h", + "funding_fee_timeframe": "1h", "ccxt_futures_name": "swap", "needs_trading_fees": False, # use fetch_trading_fees to cache fees "order_props_in_contracts": ["amount", "filled", "remaining"], diff --git a/freqtrade/exchange/hyperliquid.py b/freqtrade/exchange/hyperliquid.py index 212505b72..c72b79755 100644 --- a/freqtrade/exchange/hyperliquid.py +++ b/freqtrade/exchange/hyperliquid.py @@ -37,7 +37,6 @@ class Hyperliquid(Exchange): "stoploss_order_types": {"limit": "limit"}, "stoploss_blocks_assets": False, "stop_price_prop": "stopPrice", - "funding_fee_timeframe": "1h", "funding_fee_candle_limit": 500, "uses_leverage_tiers": False, } diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 1f8b3cc26..60efdf954 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -35,7 +35,6 @@ class Kraken(Exchange): "trades_pagination_arg": "since", "trades_pagination_overlap": False, "trades_has_history": True, - "mark_ohlcv_timeframe": "4h", } _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index c8c1130e5..07760bd32 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -29,8 +29,6 @@ class Okx(Exchange): _ft_has: FtHas = { "ohlcv_candle_limit": 100, # Warning, special case with data prior to X months - "mark_ohlcv_timeframe": "4h", - "funding_fee_timeframe": "8h", "stoploss_order_types": {"limit": "limit"}, "stoploss_on_exchange": True, "trades_has_history": False, # Endpoint doesn't have a "since" parameter From 597cc0592b1f1ff545bab6a24b04a492554f4740 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Dec 2025 20:08:35 +0100 Subject: [PATCH 16/50] test: update funding_rate_migration test --- ...her => XRP_USDT_USDT-1h-funding_rate.feather} | Bin tests/util/test_funding_rate_migration.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) rename tests/testdata/futures/{XRP_USDT_USDT-8h-funding_rate.feather => XRP_USDT_USDT-1h-funding_rate.feather} (100%) diff --git a/tests/testdata/futures/XRP_USDT_USDT-8h-funding_rate.feather b/tests/testdata/futures/XRP_USDT_USDT-1h-funding_rate.feather similarity index 100% rename from tests/testdata/futures/XRP_USDT_USDT-8h-funding_rate.feather rename to tests/testdata/futures/XRP_USDT_USDT-1h-funding_rate.feather diff --git a/tests/util/test_funding_rate_migration.py b/tests/util/test_funding_rate_migration.py index 094ee1562..e81462ba4 100644 --- a/tests/util/test_funding_rate_migration.py +++ b/tests/util/test_funding_rate_migration.py @@ -5,13 +5,14 @@ from freqtrade.util.migrations import migrate_funding_fee_timeframe def test_migrate_funding_rate_timeframe(default_conf_usdt, tmp_path, testdatadir): copytree(testdatadir / "futures", tmp_path / "futures") - file_4h = tmp_path / "futures" / "XRP_USDT_USDT-4h-funding_rate.feather" - file_8h = tmp_path / "futures" / "XRP_USDT_USDT-8h-funding_rate.feather" + file_30m = tmp_path / "futures" / "XRP_USDT_USDT-30m-funding_rate.feather" + file_1h_fr = tmp_path / "futures" / "XRP_USDT_USDT-1h-funding_rate.feather" + file_1h_fr = tmp_path / "futures" / "XRP_USDT_USDT-1h-funding_rate.feather" file_1h = tmp_path / "futures" / "XRP_USDT_USDT-1h-futures.feather" - file_8h.rename(file_4h) + file_1h_fr.rename(file_30m) assert file_1h.exists() - assert file_4h.exists() - assert not file_8h.exists() + assert file_30m.exists() + assert not file_1h_fr.exists() default_conf_usdt["datadir"] = tmp_path @@ -22,7 +23,7 @@ def test_migrate_funding_rate_timeframe(default_conf_usdt, tmp_path, testdatadir migrate_funding_fee_timeframe(default_conf_usdt, None) - assert not file_4h.exists() - assert file_8h.exists() + assert not file_30m.exists() + assert file_1h_fr.exists() # futures files is untouched. assert file_1h.exists() From 5110d0bddedcfca5da8b0adab54e44bed0d9ccb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Dec 2025 15:12:42 +0100 Subject: [PATCH 17/50] test: update a couple of tests for new behavior --- tests/data/test_datahandler.py | 2 +- tests/data/test_history.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 6110d17b8..e9fa95610 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -127,7 +127,7 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): ("XRP/USDT:USDT", "1h", "futures"), ("XRP/USDT:USDT", "1h", "mark"), ("XRP/USDT:USDT", "8h", "mark"), - ("XRP/USDT:USDT", "8h", "funding_rate"), + ("XRP/USDT:USDT", "1h", "funding_rate"), } paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 115e73192..32dc33f07 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -579,8 +579,8 @@ def test_refresh_backtest_ohlcv_data( assert log_has_re(r"Downloading pair ETH/BTC, .* interval 1m\.", caplog) if trademode == "futures": - assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 8h\.", caplog) - assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 4h\.", caplog) + assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 1h\.", caplog) + assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 1h\.", caplog) # Test with only one pair - no parallel download should happen 1 pair/timeframe combination # doesn't justify parallelization From acc69e0d2e9cde65d1e16ff3001414ad70cfc5b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Dec 2025 15:59:45 +0100 Subject: [PATCH 18/50] test: fix a couple more tests --- tests/data/test_datahandler.py | 1 - tests/optimize/test_backtesting.py | 13 ++++++------- .../futures/XRP_USDT_USDT-8h-mark.feather | Bin 7594 -> 0 bytes 3 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 tests/testdata/futures/XRP_USDT_USDT-8h-mark.feather diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index e9fa95610..9d40bf5cb 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -126,7 +126,6 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): ("XRP/USDT:USDT", "5m", "futures"), ("XRP/USDT:USDT", "1h", "futures"), ("XRP/USDT:USDT", "1h", "mark"), - ("XRP/USDT:USDT", "8h", "mark"), ("XRP/USDT:USDT", "1h", "funding_rate"), } diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index b1d13756f..bbf55d2bc 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -970,8 +970,8 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail) @pytest.mark.parametrize( "use_detail,exp_funding_fee, exp_ff_updates", [ - (True, -0.018054162, 10), - (False, -0.01780296, 6), + (True, -0.0180457882, 15), + (False, -0.0178000543, 12), ], ) def test_backtest_one_detail_futures( @@ -1081,8 +1081,8 @@ def test_backtest_one_detail_futures( @pytest.mark.parametrize( "use_detail,entries,max_stake,ff_updates,expected_ff", [ - (True, 50, 3000, 55, -1.18038144), - (False, 6, 360, 11, -0.14679994), + (True, 50, 3000, 78, -1.17988972), + (False, 6, 360, 34, -0.14673681), ], ) def test_backtest_one_detail_futures_funding_fees( @@ -2382,13 +2382,12 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, caplog, testda f"Using data directory: {testdatadir} ...", "Loading data from 2021-11-17 01:00:00 up to 2021-11-21 04:00:00 (4 days).", "Backtesting with data from 2021-11-17 21:00:00 up to 2021-11-21 04:00:00 (3 days).", - "XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00", - "XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00", + "XRP/USDT:USDT, funding_rate, 1h, data starts at 2021-11-18 00:00:00", f"Running backtesting for Strategy {CURRENT_TEST_STRATEGY}", ] for line in exists: - assert log_has(line, caplog) + assert log_has(line, caplog), line captured = capsys.readouterr() assert "BACKTESTING REPORT" in captured.out diff --git a/tests/testdata/futures/XRP_USDT_USDT-8h-mark.feather b/tests/testdata/futures/XRP_USDT_USDT-8h-mark.feather deleted file mode 100644 index f41502690473eada1348441973210c9d7fc0350f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7594 zcmeHMd00)`8(zCvLdG)ZTti(_ha|#xk}(&_l(-TdiBuYBFwYV)U71UzBpT?-lyXfO zlksvTE;@UkI%jW@`TBippVNSQpWi?Jw4Z06cdu{FYkl9l-o1~Lo7>3oQbIoC6{}B( zDWR7!sZR`uJ~6^4ycoS|y9p-swD$IT^cl19rgkIzrFll;v!*Qrd=B*T_x1`VM6F9m zq{P?XTOQ^)Ex<2ywtujsx5dOr$t<+tlO)K?-$yR#Vj=POnk`3zf2f}yrh-Gff_m%s3dwyxt49znnJdpM$3HA4#7T~QJ zLg?(k@al|!sWau%Lg>(D%R{`py+XXOc%($`kHvlceQ4dWV+L6}NEXx@%$K8(U@fub zFUC=A{c0q%aELE<79p<37lWzFv$vb*AN=b#gQC?U0{q1ZjUD5RL+XM<)y`WI5GePj zRlXcWmG!GpO!o`$3bD8Qb_lgHYAp!euP-{A;p;Qw+s>-2-?=ltfO+5cr8R!%uBQ0~ z1pmukR-3`o6Y5J;`wMDYB(em@FY=ZCYtWPBTJWyEWfxL}X2K zT+$I9^qpUTu=kHcehL+g6ddovw2dn_1Ui{S#WGOfWWYzF2RTcK4?$8-Qf!78W82EY{q6|r^fl%%&TS-tkHUW zSJYsHg_2WV2+k?xkw&D%sB7EbXqjtATr((EUtjrY0s1LFdeMj8f!9|Hkke*`a#W50 zrtGnF-BKPF{nJI#)HkH_6AF~ z^3c$}&uO>ccnIKE6pYE|!S$ng@V!O?jGDD$-mZN-U}n4Zp*#%Ty}0Aql|0lRnZ8B7 zngy^|LeX#)Se4hy zwcW0QlncXpu5nVqAL)c2c(xo$4|jOBY)?5HdOvCQ`F~V!c<_^mCWqCq@2;KNVW1i| z#=Vukz_jIG(Pb+kL83L&wqj37fFrR`b2OjkV?B!(dY3oTY|> zxw(5=*r*|3iQf4QQZf{T<)X?n16;;6>DlncQ@e7M62angsA4GjB zg_lLcr<%I6V6`y%_^digXz|qD?AT2%h6uVW6ynm^oMfR+%RQ5=t|*`Z!|P3Bp|knc z+jVAeV1LbV#LbNyv>|*O(~BG|nt3W@vpEMdi|0hGf5yRxo@U2&rLEOPZ_dVw-A-m_>v)-v347|_3a+$zESl#xP3!_M%l%tp{|A+KFq9_}Rs z_BFo8L&8tTKd!?1mts%;7Slk;A$$`ILidy&*Bp7c{2US==c=lm=gy87t&L4cmXF4#R`aDg8!GfY&Y}4+8G? zN(UZ1+c6kvjh5wIjvr`BUYp1J9N~hq)zC`}HZ_c2aQOGBi`0O`(8|MG4H-c*HkQv< z#}VG8kr>Y9fH8F~^#%(Yp1o{a6rhB=r87HMZd1aB(IM@(edfUUT^u&b!U3Guh?^{& zm{kWYEF4C1iCWLWr^?WhT@97cL-ZOYEFVca+~H)T!Xlo=V}~+rpqIhvznio!c&-Gq zKdzbMVHXKSSH8 zo4r5$q7?@dFl>husa6PUoAIK|3eP>g8}6=}d228EnVB(Bo9_k)B;eDxkDcC{cro z@CF~v)nF_}lNwrzr;HkY_aqqwYIsLI=b0MnGkhD%NowdI?zIZW+5UW>aiI$4QObU# z((<1Q+$Uz9Dchrhha(w5e{7ld?0X8$^~T|RN+BwY*s?Hu0}=G~IIZg{z?|+o5?zZm zj7$M#qk1tJrCNGmK^j0_>kikN=XDN7tf5x3E(@6NG%53qy&r*A*%l~b9m9JHsT@{lzr4aeMM zHggb*yuZSORWL$#1zh+iEAnNA0wU=NrBuLx_vOmGUM#G`1Lz%c?9C$+@NiOSnOy;M z=d`R>V$H!YPS38PJqKnwg@;*aictuOEGUCnhSycVL3+;HV4P4sbkQ)hMv8pp*Qcmo zZxreBm;ihGbh*9bMUBI7s4nLGi~i8rVT6aE0Fw2gI2-{e)162Cv6C|55CO#F74^ra zN&h&%FXzGjV88JbYv~W_U@Ynn>g6X*Q+GhYZEX!BQec?P11Lbgv~nG&`Y&xAcU=<3e+G{EdAbn?RS zMCgmkqtn@yc#tYURADGRhMrl_+QFwrdQe?LrH3YRYoy0qWb>rcO6@VL1Q&OPH&H@2 z8j#EWMR}x?d=ws{bFX8ePdrm$jJktLH7Yzrk;p*{k>t2Qrb1GqJMPq>ii(3|WZ~YM zYUvItPf%=VxTHpSps8z2ID`K0vC;t#I8k=EQrW?7)RA%E-Ybg9{6cm#W?dN!iz*?a zaq4gNM;O%~rq`}s-DcYDjMMb#)627V9e3KXeS6sM=Q&Q@yLZoD=a!C|e!8dQ5hpB0 za?Fz~NS^+gce|aiwGKsXk}Zg=Q;%~AsO>-Zj*Yd%SGzlh;}BG{m+UjeH=SGk4`JT@ z=v44OQb>!Y&to@>{@m_b5*SW+b z9;fr7?DBvtwE7gc*n_>lAAK>?l1Ot^gG-2PgtW3ocaarrq5b}GaWg-lo zn|ja|-yrk)>@p?Nqpz(BiOhI+a*{cbopEcIXilWApPe)CrLJ<^4onpFSQ|rRAx9J9 zQ3~l>E9&LoTBTh)t#=Q{&@p1*5vY1GpI;WW8d)t8J`)!G=qglg58IetR@aY6kq;}6I z2QqnYjtoe_#gz5UOSyqX-sCA9U@NIV#868BY!*xu-Yp^F)*b=+-pMs~5db(Sl!-DBY6PF$~c2_2rcnvoi| z7_pj>t@*iC8qWIOrbRMhJK@HrFvh8Q^SX91L}rqDf3GW%8u{cK>BzdBU60y9nqL1P z(UBHCFyKj{Ni^l%Hk<%zT;D*pMgy9OAq}jWr8Wqw;eqqL%Sv3^m`A3cZ&9V zO$<%oRr~vq#zKFM#ri+K`~UdvU-O5+kMI6f|0PL&eD|;ZQ{cyU|L^ Date: Sun, 7 Dec 2025 16:11:47 +0100 Subject: [PATCH 19/50] test: update further tests --- tests/commands/test_commands.py | 4 ++-- tests/util/test_binance_mig.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index a1e541bd1..40127275b 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1801,10 +1801,10 @@ def test_start_list_data(testdatadir, capsys): start_list_data(pargs) captured = capsys.readouterr() - assert "Found 6 pair / timeframe combinations." in captured.out + assert "Found 5 pair / timeframe combinations." in captured.out assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out) assert re.search(r"\n.* XRP/USDT:USDT .* 5m, 1h .* futures |\n", captured.out) - assert re.search(r"\n.* XRP/USDT:USDT .* 1h, 8h .* mark |\n", captured.out) + assert re.search(r"\n.* XRP/USDT:USDT .* 1h.* mark |\n", captured.out) args = [ "list-data", diff --git a/tests/util/test_binance_mig.py b/tests/util/test_binance_mig.py index f700ff73a..db3d8b282 100644 --- a/tests/util/test_binance_mig.py +++ b/tests/util/test_binance_mig.py @@ -20,8 +20,8 @@ def test_binance_mig_data_conversion(default_conf_usdt, tmp_path, testdatadir): files = [ "-1h-mark.feather", "-1h-futures.feather", - "-8h-funding_rate.feather", - "-8h-mark.feather", + "-1h-funding_rate.feather", + "-1h-mark.feather", ] # Copy files to tmpdir and rename to old naming From c1c968666e373e0deb233388dd5ef457a6557a63 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Dec 2025 18:19:22 +0100 Subject: [PATCH 20/50] chore: some minor cleanups --- freqtrade/data/dataprovider.py | 2 +- tests/util/test_funding_rate_migration.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index bd5a7a5ba..9c740709c 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -358,7 +358,7 @@ class DataProvider: # TODO: does this message make sense? might be pointless as funding fees don't # have a timeframe logger.warning( - f"{pair}, {timeframe} requested - funding rate timeframe not matching {ff_tf}. " + f"{pair}, {timeframe} requested - funding rate timeframe not matching {ff_tf}." ) return ff_tf diff --git a/tests/util/test_funding_rate_migration.py b/tests/util/test_funding_rate_migration.py index e81462ba4..69d603ed0 100644 --- a/tests/util/test_funding_rate_migration.py +++ b/tests/util/test_funding_rate_migration.py @@ -7,7 +7,6 @@ def test_migrate_funding_rate_timeframe(default_conf_usdt, tmp_path, testdatadir copytree(testdatadir / "futures", tmp_path / "futures") file_30m = tmp_path / "futures" / "XRP_USDT_USDT-30m-funding_rate.feather" file_1h_fr = tmp_path / "futures" / "XRP_USDT_USDT-1h-funding_rate.feather" - file_1h_fr = tmp_path / "futures" / "XRP_USDT_USDT-1h-funding_rate.feather" file_1h = tmp_path / "futures" / "XRP_USDT_USDT-1h-futures.feather" file_1h_fr.rename(file_30m) assert file_1h.exists() From e6030b7f59d71348c2107124e39059e3197e0f38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Dec 2025 20:01:08 +0100 Subject: [PATCH 21/50] chore: minor adjustments for clarity --- freqtrade/data/converter/converter.py | 1 + freqtrade/data/dataprovider.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/converter/converter.py b/freqtrade/data/converter/converter.py index 290461d39..99301d291 100644 --- a/freqtrade/data/converter/converter.py +++ b/freqtrade/data/converter/converter.py @@ -38,6 +38,7 @@ def ohlcv_to_dataframe( cols = DEFAULT_DATAFRAME_COLUMNS df = DataFrame(ohlcv, columns=cols) + # Floor date to seconds to account for exchange imprecisions df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s") # Some exchanges return int values for Volume and even for OHLC. diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 9c740709c..ed396ba8e 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -350,7 +350,7 @@ class DataProvider: def __fix_funding_rate_timeframe( self, pair: str, timeframe: str | None, candle_type: str - ) -> str: + ) -> str | None: if ( candle_type == CandleType.FUNDING_RATE and (ff_tf := self.get_funding_rate_timeframe()) != timeframe From 62d4da3b94ea7d66d3b3ab0c16e6f04187721b39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 08:43:50 +0100 Subject: [PATCH 22/50] test: add test for get_funding_rate_timeframe --- tests/data/test_dataprovider.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index afbba3d5d..aacc41554 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -636,3 +636,21 @@ def test_check_delisting(mocker, default_conf_usdt): assert res == dt_utc(2025, 10, 2) assert delist_mock2.call_count == 1 + + +def test_get_funding_rate_timeframe(mocker, default_conf_usdt): + default_conf_usdt["trading_mode"] = "futures" + default_conf_usdt["margin_mode"] = "isolated" + exchange = get_patched_exchange(mocker, default_conf_usdt) + mock_get_option = mocker.spy(exchange, "get_option") + dp = DataProvider(default_conf_usdt, exchange) + + assert dp.get_funding_rate_timeframe() == "1h" + mock_get_option.assert_called_once_with("funding_fee_timeframe") + + +def test_get_funding_rate_timeframe_no_exchange(default_conf_usdt): + dp = DataProvider(default_conf_usdt, None) + + with pytest.raises(OperationalException, match=r"Exchange is not available to DataProvider."): + dp.get_funding_rate_timeframe() From 0ec1066b347256da8fbc668fcdecb729476f09ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 08:48:37 +0100 Subject: [PATCH 23/50] test: add test for funding_rate fix --- tests/data/test_dataprovider.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index aacc41554..d3bd5fb98 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -9,7 +9,7 @@ from freqtrade.enums import CandleType, RunMode from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.util import dt_utc -from tests.conftest import EXMS, generate_test_data, get_patched_exchange +from tests.conftest import EXMS, generate_test_data, get_patched_exchange, log_has_re @pytest.mark.parametrize( @@ -185,6 +185,28 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type): assert len(df) == 2 # ohlcv_history is limited to 2 rows now +def test_get_pair_dataframe_funding_rate(mocker, default_conf, ohlcv_history, caplog): + default_conf["runmode"] = RunMode.DRY_RUN + timeframe = "1h" + exchange = get_patched_exchange(mocker, default_conf) + candletype = CandleType.FUNDING_RATE + exchange._klines[("XRP/BTC", timeframe, candletype)] = ohlcv_history + exchange._klines[("UNITTEST/BTC", timeframe, candletype)] = ohlcv_history + + dp = DataProvider(default_conf, exchange) + assert dp.runmode == RunMode.DRY_RUN + assert ohlcv_history.equals( + dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type="funding_rate") + ) + msg = r".*funding rate timeframe not matching" + assert not log_has_re(msg, caplog) + + assert ohlcv_history.equals( + dp.get_pair_dataframe("UNITTEST/BTC", "5h", candle_type="funding_rate") + ) + assert log_has_re(msg, caplog) + + def test_available_pairs(mocker, default_conf, ohlcv_history): exchange = get_patched_exchange(mocker, default_conf) timeframe = default_conf["timeframe"] From f5e6504e22a36c9f2002a859c5960300eb111e34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 09:11:49 +0100 Subject: [PATCH 24/50] test: add test for funding rate exchange fix --- tests/exchange/test_exchange.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4daeb1855..030f04452 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2837,6 +2837,29 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None: assert ohlcv_mock.call_args_list[0][0][0] == pairs +def test_refresh_latest_ohlcv_funding_rate(mocker, default_conf_usdt, caplog) -> None: + ohlcv = generate_test_data_raw("1h", 24, "2025-01-02 12:00:00+00:00") + funding_data = [{"timestamp": x[0], "fundingRate": x[1]} for x in ohlcv] + + caplog.set_level(logging.DEBUG) + exchange = get_patched_exchange(mocker, default_conf_usdt) + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) + exchange._api_async.fetch_funding_rate_history = get_mock_coro(funding_data) + + pairs = [ + ("IOTA/USDT:USDT", "8h", CandleType.FUNDING_RATE), + ("XRP/USDT:USDT", "1h", CandleType.FUNDING_RATE), + ] + # empty dicts + assert not exchange._klines + res = exchange.refresh_latest_ohlcv(pairs, cache=False) + + assert len(res) == len(pairs) + assert log_has_re(r"Wrong funding rate timeframe 8h for pair IOTA/USDT:USDT", caplog) + assert not log_has_re(r"Wrong funding rate timeframe 8h for pair XRP/USDT:USDT", caplog) + assert exchange._api_async.fetch_ohlcv.call_count == 0 + + @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): ohlcv = [ From 9f4e167455b533202f10874c19929b442963fadc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 09:27:48 +0100 Subject: [PATCH 25/50] chore: force keyword usage on refresh_backtest_ohlcv --- freqtrade/data/history/history_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 1467497d6..3e076351b 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -353,6 +353,7 @@ def _download_pair_history( def refresh_backtest_ohlcv_data( exchange: Exchange, + *, pairs: list[str], timeframes: list[str], datadir: Path, From cde886b8844ed8cd073106172d4ed730f2b859fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 10:20:00 +0100 Subject: [PATCH 26/50] chore: use str for safe usage of candle_type --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b5b2bff89..dfa50a6be 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2896,7 +2896,7 @@ class Exchange: ) if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES): - params.update({"price": candle_type.value}) + params.update({"price": str(candle_type)}) if candle_type != CandleType.FUNDING_RATE: data = await self._api_async.fetch_ohlcv( pair, timeframe=timeframe, since=since_ms, limit=candle_limit, params=params From 38e48c0c5ee875f100056b7e48a4eb40403d2aab Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 11:55:26 +0100 Subject: [PATCH 27/50] test: update refresh ohlcv data test --- tests/data/test_history.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 32dc33f07..e09572ad8 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -534,15 +534,15 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No @pytest.mark.parametrize( - "trademode,callcount", + "trademode,callcount, callcount_parallel", [ - ("spot", 4), - ("margin", 4), - ("futures", 8), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls + ("spot", 4, 2), + ("margin", 4, 2), + ("futures", 8, 2), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls ], ) def test_refresh_backtest_ohlcv_data( - mocker, default_conf, markets, caplog, testdatadir, trademode, callcount + mocker, default_conf, markets, caplog, testdatadir, trademode, callcount, callcount_parallel ): caplog.set_level(logging.DEBUG) dl_mock = mocker.patch("freqtrade.data.history.history_utils._download_pair_history") @@ -573,7 +573,8 @@ def test_refresh_backtest_ohlcv_data( ) # Called once per timeframe (as we return an empty dataframe) - assert parallel_mock.call_count == 2 + # called twice for spot/margin and 4 times for futures + assert parallel_mock.call_count == callcount_parallel assert dl_mock.call_count == callcount assert dl_mock.call_args[1]["timerange"].starttype == "date" From 359eba462b23c59136852e14dca1409ba6b6d64d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 12:45:41 +0100 Subject: [PATCH 28/50] feat: add candle_types argument to download-data --- freqtrade/commands/arguments.py | 1 + freqtrade/data/history/history_utils.py | 70 ++++++++++++++----------- tests/data/test_history.py | 2 +- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index ca3b2b422..d6b326050 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -174,6 +174,7 @@ ARGS_DOWNLOAD_DATA = [ "dataformat_ohlcv", "dataformat_trades", "trading_mode", + "candle_types", "prepend_data", ] diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 3e076351b..5c4bde06d 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -364,6 +364,7 @@ def refresh_backtest_ohlcv_data( data_format: str | None = None, prepend: bool = False, progress_tracker: CustomProgress | None = None, + candle_types: list[CandleType] | None = None, no_parallel_download: bool = False, ) -> list[str]: """ @@ -376,10 +377,41 @@ def refresh_backtest_ohlcv_data( pairs_not_available = [] fast_candles: dict[PairWithTimeframe, DataFrame] = {} data_handler = get_datahandler(datadir, data_format) - candle_type = CandleType.get_default(trading_mode) + def_candletype = CandleType.SPOT if trading_mode != "futures" else CandleType.FUTURES + if trading_mode != "futures": + # Ignore user passed candle types for non-futures trading + timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes] + else: + # Filter out SPOT candle type for futures trading + candle_types = ( + [ct for ct in candle_types if ct != CandleType.SPOT] if candle_types else None + ) + fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price")) + tf_funding_rate = exchange.get_option("funding_fee_timeframe") + tf_mark = exchange.get_option("mark_ohlcv_timeframe") + + if candle_types: + timeframes_with_candletype = [ + (tf, ct) + for ct in candle_types + for tf in timeframes + if ct != CandleType.FUNDING_RATE + ] + else: + # Default behavior + timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes] + timeframes_with_candletype.append((tf_mark, fr_candle_type)) + if not candle_types or CandleType.FUNDING_RATE in candle_types: + # All exchanges need FundingRate for futures trading. + # The timeframe is aligned to the mark-price timeframe. + timeframes_with_candletype.append((tf_funding_rate, CandleType.FUNDING_RATE)) + + logger.debug( + "Downloading %s.", ", ".join(f'"{tf} {ct}"' for tf, ct in timeframes_with_candletype) + ) + with progress_tracker as progress: - tf_length = len(timeframes) if trading_mode != "futures" else len(timeframes) + 2 - timeframe_task = progress.add_task("Timeframe", total=tf_length) + timeframe_task = progress.add_task("Timeframe", total=len(timeframes_with_candletype)) pair_task = progress.add_task("Downloading data...", total=len(pairs)) for pair in pairs: @@ -390,7 +422,7 @@ def refresh_backtest_ohlcv_data( pairs_not_available.append(f"{pair}: Pair not available on exchange.") logger.info(f"Skipping pair {pair}...") continue - for timeframe in timeframes: + for timeframe, candle_type in timeframes_with_candletype: # Get fast candles via parallel method on first loop through per timeframe # and candle type. Downloads all the pairs in the list and stores them. # Also skips if only 1 pair/timeframe combination is scheduled for download. @@ -417,7 +449,7 @@ def refresh_backtest_ohlcv_data( # get the already downloaded pair candles if they exist pair_candles = fast_candles.pop((pair, timeframe, candle_type), None) - progress.update(timeframe_task, description=f"Timeframe {timeframe}") + progress.update(timeframe_task, description=f"Timeframe {timeframe} {candle_type}") logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.") _download_pair_history( pair=pair, @@ -433,33 +465,6 @@ def refresh_backtest_ohlcv_data( pair_candles=pair_candles, # optional pass of dataframe of parallel candles ) progress.update(timeframe_task, advance=1) - if trading_mode == "futures": - # Predefined candletype (and timeframe) depending on exchange - # Downloads what is necessary to backtest based on futures data. - tf_mark = exchange.get_option("mark_ohlcv_timeframe") - tf_funding_rate = exchange.get_option("funding_fee_timeframe") - - fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price")) - # All exchanges need FundingRate for futures trading. - # The timeframe is aligned to the mark-price timeframe. - combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark)) - for candle_type_f, tf in combs: - logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.") - _download_pair_history( - pair=pair, - datadir=datadir, - exchange=exchange, - timerange=timerange, - data_handler=data_handler, - timeframe=str(tf), - new_pairs_days=new_pairs_days, - candle_type=candle_type_f, - erase=erase, - prepend=prepend, - ) - progress.update( - timeframe_task, advance=1, description=f"Timeframe {candle_type_f}, {tf}" - ) progress.update(pair_task, advance=1) progress.update(timeframe_task, description="Timeframe") @@ -805,6 +810,7 @@ def download_data( trading_mode=config.get("trading_mode", "spot"), prepend=config.get("prepend_data", False), progress_tracker=progress_tracker, + candle_types=config.get("candle_types"), no_parallel_download=config.get("no_parallel_download", False), ) finally: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index e09572ad8..3051a4140 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -538,7 +538,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No [ ("spot", 4, 2), ("margin", 4, 2), - ("futures", 8, 2), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls + ("futures", 8, 4), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls ], ) def test_refresh_backtest_ohlcv_data( From 994e61fe4219077e59c6b3e756e189a2736db700 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 13:47:26 +0100 Subject: [PATCH 29/50] feat: add (commented) validation for fetch_*_ohlcv methods --- freqtrade/exchange/common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 650db19b7..9dc64eb7e 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -97,6 +97,9 @@ EXCHANGE_HAS_OPTIONAL = [ # 'fetchLeverageTiers', # Futures initialization # 'fetchMarketLeverageTiers', # Futures initialization # 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance... + # "fetchPremiumIndexOHLCV", # Futures additional data + # "fetchMarkOHLCV", # Futures additional data + # "fetchIndexOHLCV", # Futures additional data # ccxt.pro "watchOHLCV", ] From c7636734def052466cf666a770cc70be10422568 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 14:29:16 +0100 Subject: [PATCH 30/50] feat: validate supported candle types when downloading data --- freqtrade/data/history/history_utils.py | 2 ++ freqtrade/exchange/exchange.py | 29 +++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 5c4bde06d..d370a80aa 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -391,6 +391,8 @@ def refresh_backtest_ohlcv_data( tf_mark = exchange.get_option("mark_ohlcv_timeframe") if candle_types: + for ct in candle_types: + exchange.verify_candle_type_support(ct) timeframes_with_candletype = [ (tf, ct) for ct in candle_types diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dfa50a6be..6dd26c1a3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2895,9 +2895,10 @@ class Exchange: timeframe, candle_type=candle_type, since_ms=since_ms ) - if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES): - params.update({"price": str(candle_type)}) if candle_type != CandleType.FUNDING_RATE: + if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES): + self.verify_candle_type_support(candle_type) + params.update({"price": str(candle_type)}) data = await self._api_async.fetch_ohlcv( pair, timeframe=timeframe, since=since_ms, limit=candle_limit, params=params ) @@ -2962,6 +2963,30 @@ class Exchange: data = [[x["timestamp"], x["fundingRate"], 0, 0, 0, 0] for x in data] return data + def verify_candle_type_support(self, candle_type: CandleType) -> None: + """ + Verify that the exchange supports the given candle type. + :param candle_type: CandleType to verify + :raises OperationalException: if the candle type is not supported + """ + if candle_type == CandleType.FUNDING_RATE: + if not self.exchange_has("fetchFundingRateHistory"): + raise OperationalException( + f"Exchange {self._api.name} does not support fetching funding rate history." + ) + elif candle_type not in (CandleType.SPOT, CandleType.FUTURES): + mapping = { + CandleType.MARK: "fetchMarkOHLCV", + CandleType.INDEX: "fetchIndexOHLCV", + CandleType.PREMIUMINDEX: "fetchPremiumIndexOHLCV", + CandleType.FUNDING_RATE: "fetchFundingRateHistory", + } + _method = mapping.get(candle_type, "fetchOHLCV") + if not self.exchange_has(_method): + raise OperationalException( + f"Exchange {self._api.name} does not support fetching {candle_type} candles." + ) + # fetch Trade data stuff def needed_candle_for_trades_ms(self, timeframe: str, candle_type: CandleType) -> int: From f33fd98c8363b35b2d3132f0ad927766387ea2c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 14:43:12 +0100 Subject: [PATCH 31/50] test: Add test for candle type verification --- tests/exchange/test_exchange.py | 49 ++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 030f04452..422b8033f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2388,6 +2388,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_ 5, # volume (in quote currency) ] ] + mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock()) exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) # Monkey-patch async function exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) @@ -2438,6 +2439,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf_usdt, caplog, candle_type) -> ] caplog.set_level(logging.DEBUG) + mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock()) exchange = get_patched_exchange(mocker, default_conf_usdt) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) @@ -2688,6 +2690,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach ohlcv = generate_test_data_raw("1h", 100, start.strftime("%Y-%m-%d")) time_machine.move_to(start + timedelta(hours=99, minutes=30)) + mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock()) exchange = get_patched_exchange(mocker, default_conf) exchange._set_startup_candle_count(default_conf) @@ -5472,8 +5475,13 @@ def test__fetch_and_calculate_funding_fees( api_mock = MagicMock() api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history) api_mock.fetch_ohlcv = get_mock_coro(return_value=mark_ohlcv) - type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True}) - type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True}) + type(api_mock).has = PropertyMock( + return_value={ + "fetchFundingRateHistory": True, + "fetchMarkOHLCV": True, + "fetchOHLCV": True, + } + ) ex = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange) mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["1h", "4h", "8h"])) @@ -5517,8 +5525,13 @@ def test__fetch_and_calculate_funding_fees_datetime_called( api_mock.fetch_funding_rate_history = get_mock_coro( return_value=funding_rate_history_octohourly ) - type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True}) - type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True}) + type(api_mock).has = PropertyMock( + return_value={ + "fetchFundingRateHistory": True, + "fetchMarkOHLCV": True, + "fetchOHLCV": True, + } + ) mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["4h", "8h"])) exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange) d1 = datetime.strptime("2021-08-31 23:00:01 +0000", "%Y-%m-%d %H:%M:%S %z") @@ -6605,3 +6618,31 @@ def test_fetch_funding_rate(default_conf, mocker, exchange_name): with pytest.raises(DependencyException, match=r"Pair XRP/ETH not available"): exchange.fetch_funding_rate(pair="XRP/ETH") + + +def test_verify_candle_type_support(default_conf, mocker): + api_mock = MagicMock() + type(api_mock).has = PropertyMock( + return_value={ + "fetchFundingRateHistory": True, + "fetchIndexOHLCV": True, + "fetchMarkOHLCV": True, + "fetchPremiumIndexOHLCV": False, + } + ) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + # Should pass + exchange.verify_candle_type_support("futures") + exchange.verify_candle_type_support(CandleType.FUTURES) + exchange.verify_candle_type_support(CandleType.FUNDING_RATE) + exchange.verify_candle_type_support(CandleType.SPOT) + exchange.verify_candle_type_support(CandleType.MARK) + + # Should fail: + + with pytest.raises( + OperationalException, + match=r"Exchange .* does not support fetching premiumindex candles\.", + ): + exchange.verify_candle_type_support(CandleType.PREMIUMINDEX) From 80d5b6e24c8aac5169205289b849c0bc31081535 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 16:22:20 +0100 Subject: [PATCH 32/50] test: minor refactor in online tests --- tests/exchange_online/test_ccxt_compat.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index 073ce0a38..cc7f37f4a 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -270,11 +270,13 @@ class TestCCXTExchange: assert exch.klines(pair_tf).iloc[-1]["date"] >= timeframe_to_prev_date(timeframe, now) assert exch.klines(pair_tf)["date"].astype(int).iloc[0] // 1e6 == since_ms - def _ccxt__async_get_candle_history(self, exchange, pair, timeframe, candle_type, factor=0.9): + def _ccxt__async_get_candle_history( + self, exchange, pair: str, timeframe: str, candle_type: CandleType, factor: float = 0.9 + ): timeframe_ms = timeframe_to_msecs(timeframe) now = timeframe_to_prev_date(timeframe, datetime.now(UTC)) - for offset in (360, 120, 30, 10, 5, 2): - since = now - timedelta(days=offset) + for offset_days in (360, 120, 30, 10, 5, 2): + since = now - timedelta(days=offset_days) since_ms = int(since.timestamp() * 1000) res = exchange.loop.run_until_complete( @@ -290,7 +292,7 @@ class TestCCXTExchange: candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor assert len(candles) >= min(candle_count, candle_count1), ( - f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}" + f"{len(candles)} < {candle_count} in {timeframe} {offset_days=} {factor=}" ) # Check if first-timeframe is either the start, or start + 1 assert candles[0][0] == since_ms or (since_ms + timeframe_ms) From 96849fcafe46417452a3dce17230312fefcbdc8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 17:01:36 +0100 Subject: [PATCH 33/50] refactor: provide a non-failing check_candle_support method --- freqtrade/exchange/exchange.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6dd26c1a3..49addd7fe 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2963,17 +2963,15 @@ class Exchange: data = [[x["timestamp"], x["fundingRate"], 0, 0, 0, 0] for x in data] return data - def verify_candle_type_support(self, candle_type: CandleType) -> None: + def check_candle_type_support(self, candle_type: CandleType) -> bool: """ - Verify that the exchange supports the given candle type. + Check that the exchange supports the given candle type. :param candle_type: CandleType to verify - :raises OperationalException: if the candle type is not supported + :return: True if supported, False otherwise """ if candle_type == CandleType.FUNDING_RATE: if not self.exchange_has("fetchFundingRateHistory"): - raise OperationalException( - f"Exchange {self._api.name} does not support fetching funding rate history." - ) + return False elif candle_type not in (CandleType.SPOT, CandleType.FUTURES): mapping = { CandleType.MARK: "fetchMarkOHLCV", @@ -2983,9 +2981,19 @@ class Exchange: } _method = mapping.get(candle_type, "fetchOHLCV") if not self.exchange_has(_method): - raise OperationalException( - f"Exchange {self._api.name} does not support fetching {candle_type} candles." - ) + return False + return True + + def verify_candle_type_support(self, candle_type: CandleType) -> None: + """ + Verify that the exchange supports the given candle type. + :param candle_type: CandleType to verify + :raises OperationalException: if the candle type is not supported + """ + if not self.check_candle_type_support(candle_type): + raise OperationalException( + f"Exchange {self._api.name} does not support fetching {candle_type} candles." + ) # fetch Trade data stuff From 00f687f3f54681eb21f067349d2032ba6a7c2f99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 17:07:26 +0100 Subject: [PATCH 34/50] test: test futures data with online exchanges --- tests/exchange_online/test_ccxt_compat.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index cc7f37f4a..3e9ce3ee5 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -311,6 +311,8 @@ class TestCCXTExchange: [ CandleType.FUTURES, CandleType.FUNDING_RATE, + CandleType.INDEX, + CandleType.PREMIUMINDEX, CandleType.MARK, ], ) @@ -324,6 +326,10 @@ class TestCCXTExchange: timeframe = exchange._ft_has.get( "funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"] ) + else: + # never skip funding rate! + if not exchange.check_candle_type_support(candle_type): + pytest.skip(f"Exchange does not support candle type {candle_type}") self._ccxt__async_get_candle_history( exchange, pair=pair, From 072ed705fcfc8ccee8f82e5398025173c2a4dc67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 19:21:48 +0100 Subject: [PATCH 35/50] test: fix funding_fee online tests --- tests/exchange_online/test_ccxt_compat.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index 3e9ce3ee5..944674da7 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -274,6 +274,7 @@ class TestCCXTExchange: self, exchange, pair: str, timeframe: str, candle_type: CandleType, factor: float = 0.9 ): timeframe_ms = timeframe_to_msecs(timeframe) + timeframe_ms_8h = timeframe_to_msecs("8h") now = timeframe_to_prev_date(timeframe, datetime.now(UTC)) for offset_days in (360, 120, 30, 10, 5, 2): since = now - timedelta(days=offset_days) @@ -291,7 +292,14 @@ class TestCCXTExchange: candles = res[3] candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor - assert len(candles) >= min(candle_count, candle_count1), ( + # funding fees can be 1h or 8h - depending on pair and time. + candle_count2 = (now.timestamp() * 1000 - since_ms) // timeframe_ms_8h * factor + min_value = min( + candle_count, + candle_count1, + candle_count2 if candle_type == CandleType.FUNDING_RATE else candle_count1, + ) + assert len(candles) >= min_value, ( f"{len(candles)} < {candle_count} in {timeframe} {offset_days=} {factor=}" ) # Check if first-timeframe is either the start, or start + 1 From 309985503daafd336f4b2d8aa52da151efcc79b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Dec 2025 20:17:10 +0100 Subject: [PATCH 36/50] test: fix funding_rate_history online test --- tests/exchange_online/test_ccxt_compat.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index 944674da7..9789bfea9 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -353,6 +353,7 @@ class TestCCXTExchange: timeframe_ff = exchange._ft_has.get( "funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"] ) + timeframe_ff_8h = "8h" pair_tf = (pair, timeframe_ff, CandleType.FUNDING_RATE) funding_ohlcv = exchange.refresh_latest_ohlcv( @@ -366,14 +367,26 @@ class TestCCXTExchange: hour1 = timeframe_to_prev_date(timeframe_ff, this_hour - timedelta(minutes=1)) hour2 = timeframe_to_prev_date(timeframe_ff, hour1 - timedelta(minutes=1)) hour3 = timeframe_to_prev_date(timeframe_ff, hour2 - timedelta(minutes=1)) - val0 = rate[rate["date"] == this_hour].iloc[0]["open"] - val1 = rate[rate["date"] == hour1].iloc[0]["open"] - val2 = rate[rate["date"] == hour2].iloc[0]["open"] - val3 = rate[rate["date"] == hour3].iloc[0]["open"] + # Alternative 8h timeframe - funding fee timeframe is not stable. + h8_this_hour = timeframe_to_prev_date(timeframe_ff_8h) + h8_hour1 = timeframe_to_prev_date(timeframe_ff_8h, h8_this_hour - timedelta(minutes=1)) + h8_hour2 = timeframe_to_prev_date(timeframe_ff_8h, h8_hour1 - timedelta(minutes=1)) + h8_hour3 = timeframe_to_prev_date(timeframe_ff_8h, h8_hour2 - timedelta(minutes=1)) + row0 = rate.iloc[-1] + row1 = rate.iloc[-2] + row2 = rate.iloc[-3] + row3 = rate.iloc[-4] + + assert row0["date"] == this_hour or row0["date"] == h8_this_hour + assert row1["date"] == hour1 or row1["date"] == h8_hour1 + assert row2["date"] == hour2 or row2["date"] == h8_hour2 + assert row3["date"] == hour3 or row3["date"] == h8_hour3 # Test For last 4 hours # Avoids random test-failure when funding-fees are 0 for a few hours. - assert val0 != 0.0 or val1 != 0.0 or val2 != 0.0 or val3 != 0.0 + assert ( + row0["open"] != 0.0 or row1["open"] != 0.0 or row2["open"] != 0.0 or row3["open"] != 0.0 + ) # We expect funding rates to be different from 0.0 - or moving around. assert ( rate["open"].max() != 0.0 From 2d3ff2f8ca5919bdd173bf55ead4aa34a56c7e36 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Dec 2025 13:42:59 +0100 Subject: [PATCH 37/50] test: mark-test should use the candle's defined mark price attribute --- tests/exchange_online/test_ccxt_compat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index 9789bfea9..d0dc715b9 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -398,7 +398,10 @@ class TestCCXTExchange: exchange, exchangename = exchange_futures pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"]) since = int((datetime.now(UTC) - timedelta(days=5)).timestamp() * 1000) - pair_tf = (pair, "1h", CandleType.MARK) + candle_type = CandleType.from_string( + exchange.get_option("mark_ohlcv_price", default=CandleType.MARK) + ) + pair_tf = (pair, "1h", candle_type) mark_ohlcv = exchange.refresh_latest_ohlcv([pair_tf], since_ms=since, drop_incomplete=False) From 645a9159e4085ba87884eccaa011627b4401fdc2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Dec 2025 13:43:49 +0100 Subject: [PATCH 38/50] chore: hyperliquid doesn't have mark candles it uses regular futures candles as multiplicator for funding fees. --- freqtrade/exchange/hyperliquid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/hyperliquid.py b/freqtrade/exchange/hyperliquid.py index c72b79755..f5e6fb7ee 100644 --- a/freqtrade/exchange/hyperliquid.py +++ b/freqtrade/exchange/hyperliquid.py @@ -39,6 +39,7 @@ class Hyperliquid(Exchange): "stop_price_prop": "stopPrice", "funding_fee_candle_limit": 500, "uses_leverage_tiers": False, + "mark_ohlcv_price": "futures", } _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [ From bbafb1dabd8fce8ba018f851a4d93f288a4e794f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Dec 2025 13:54:05 +0100 Subject: [PATCH 39/50] fix: deduplicate list before downloading This avoids duplicate downloads, for example on hyperliquid, which uses "futures" as mark candle type. --- freqtrade/data/history/history_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index d370a80aa..13b7101cd 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -407,7 +407,8 @@ def refresh_backtest_ohlcv_data( # All exchanges need FundingRate for futures trading. # The timeframe is aligned to the mark-price timeframe. timeframes_with_candletype.append((tf_funding_rate, CandleType.FUNDING_RATE)) - + # Deduplicate list ... + timeframes_with_candletype = list(dict.fromkeys(timeframes_with_candletype)) logger.debug( "Downloading %s.", ", ".join(f'"{tf} {ct}"' for tf, ct in timeframes_with_candletype) ) From b3a1442a69d13ae0496a4dcf4ca21ec8fd450785 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Dec 2025 18:25:11 +0100 Subject: [PATCH 40/50] feat: allow varying help texts for different subcommands --- freqtrade/commands/arguments.py | 7 ++++++- freqtrade/commands/cli_options.py | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index d6b326050..3ccb854b3 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -3,6 +3,7 @@ This module contains the argument manager class """ from argparse import ArgumentParser, Namespace, _ArgumentGroup +from copy import deepcopy from functools import partial from pathlib import Path from typing import Any @@ -349,7 +350,11 @@ class Arguments: def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None: for val in optionlist: opt = AVAILABLE_CLI_OPTIONS[val] - parser.add_argument(*opt.cli, dest=val, **opt.kwargs) + options = deepcopy(opt.kwargs) + help_text = options.pop("help", None) + if opt.fthelp and isinstance(opt.fthelp, dict): + help_text = opt.fthelp.get(parser.prog, help_text) + parser.add_argument(*opt.cli, dest=val, help=help_text, **options) def _build_subcommands(self) -> None: """ diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index c256c46f3..ebc221523 100755 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -38,8 +38,14 @@ def check_int_nonzero(value: str) -> int: class Arg: # Optional CLI arguments - def __init__(self, *args, **kwargs): + def __init__(self, *args, fthelp: dict[str, str] | None = None, **kwargs): + """ + CLI Arguments - used to build subcommand parsers consistently. + :param fthelp: dict - fthelp per command - should be "freqtrade ": help_text + If not provided or not found, 'help' from kwargs is used instead. + """ self.cli = args + self.fthelp = fthelp self.kwargs = kwargs @@ -422,6 +428,14 @@ AVAILABLE_CLI_OPTIONS = { ), "candle_types": Arg( "--candle-types", + fthelp={ + "freqtrade download-data": ( + "Select candle type to download. " + "Defaults to the necessary candles for the selected trading mode " + "(e.g. 'spot' or ('futures', 'funding_rate' and 'mark') for futures)." + ), + "_": "Select candle type to convert. Defaults to all available types.", + }, help="Select candle type to convert. Defaults to all available types.", choices=[c.value for c in CandleType], nargs="+", From f0f48395c5875a2ddf0074a8383871f2d288280f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Dec 2025 18:25:54 +0100 Subject: [PATCH 41/50] chore: update download-data help text --- docs/commands/download-data.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/commands/download-data.md b/docs/commands/download-data.md index 35f15d19d..dd6474e12 100644 --- a/docs/commands/download-data.md +++ b/docs/commands/download-data.md @@ -11,6 +11,7 @@ usage: freqtrade download-data [-h] [-v] [--no-color] [--logfile FILE] [-V] [--data-format-ohlcv {json,jsongz,feather,parquet}] [--data-format-trades {json,jsongz,feather,parquet}] [--trading-mode {spot,margin,futures}] + [--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]] [--prepend] options: @@ -50,6 +51,11 @@ options: `feather`). --trading-mode, --tradingmode {spot,margin,futures} Select Trading mode + --candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...] + Select candle type to download. Defaults to the + necessary candles for the selected trading mode (e.g. + 'spot' or ('futures', 'funding_rate' and 'mark') for + futures). --prepend Allow data prepending. (Data-appending is disabled) Common arguments: From 46538d9a5b347cfc5ef9ea5b5d5b6fb91744c330 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Dec 2025 19:34:58 +0100 Subject: [PATCH 42/50] fix: verify prog actually exists before using it --- freqtrade/commands/arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 3ccb854b3..f84822ac8 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -352,7 +352,7 @@ class Arguments: opt = AVAILABLE_CLI_OPTIONS[val] options = deepcopy(opt.kwargs) help_text = options.pop("help", None) - if opt.fthelp and isinstance(opt.fthelp, dict): + if opt.fthelp and isinstance(opt.fthelp, dict) and hasattr(parser, "prog"): help_text = opt.fthelp.get(parser.prog, help_text) parser.add_argument(*opt.cli, dest=val, help=help_text, **options) From 6aeab16ce4cfb7ce9bf6009d75041ae743059e79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Dec 2025 20:13:10 +0100 Subject: [PATCH 43/50] test: improve candle type verification test --- tests/exchange/test_exchange.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 422b8033f..4a1f4ae5f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -6646,3 +6646,23 @@ def test_verify_candle_type_support(default_conf, mocker): match=r"Exchange .* does not support fetching premiumindex candles\.", ): exchange.verify_candle_type_support(CandleType.PREMIUMINDEX) + + type(api_mock).has = PropertyMock( + return_value={ + "fetchFundingRateHistory": False, + "fetchIndexOHLCV": False, + "fetchMarkOHLCV": False, + "fetchPremiumIndexOHLCV": True, + } + ) + for candle_type in [ + CandleType.FUNDING_RATE, + CandleType.INDEX, + CandleType.MARK, + ]: + with pytest.raises( + OperationalException, + match=rf"Exchange .* does not support fetching {candle_type.value} candles\.", + ): + exchange.verify_candle_type_support(candle_type) + exchange.verify_candle_type_support(CandleType.PREMIUMINDEX) From d15d08a2d5c9c54b2082c738ddf28a295b0a59b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Dec 2025 20:18:12 +0100 Subject: [PATCH 44/50] test: Improve refresh_backtest test --- tests/data/test_history.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 3051a4140..59606d485 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -546,6 +546,7 @@ def test_refresh_backtest_ohlcv_data( ): caplog.set_level(logging.DEBUG) dl_mock = mocker.patch("freqtrade.data.history.history_utils._download_pair_history") + mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock()) def parallel_mock(pairs, timeframe, candle_type, **kwargs): return {(pair, timeframe, candle_type): DataFrame() for pair in pairs} @@ -600,6 +601,24 @@ def test_refresh_backtest_ohlcv_data( ) assert parallel_mock.call_count == 0 + if trademode == "futures": + dl_mock.reset_mock() + refresh_backtest_ohlcv_data( + exchange=ex, + pairs=[ + "ETH/BTC", + ], + timeframes=["5m", "1h"], + datadir=testdatadir, + timerange=timerange, + erase=False, + trading_mode=trademode, + no_parallel_download=True, + candle_types=["premiumIndex", "funding_rate"], + ) + assert parallel_mock.call_count == 0 + assert dl_mock.call_count == 3 # 2 timeframes premiumIndex + 1x funding_rate + def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): dl_mock = mocker.patch( From 51e0b204b6131d672a1c2688f47f8726abba4a5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Dec 2025 18:30:30 +0100 Subject: [PATCH 45/50] docs: improve download data docs --- docs/data-download.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/data-download.md b/docs/data-download.md index c2b104b1b..4339e9862 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -60,6 +60,7 @@ freqtrade download-data --exchange binance --pairs ".*/USDT" * Given starting points are ignored if data is already available, downloading only missing data up to today. * Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data. * To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. +* When downloading futures data (`--trading-mode futures` or a configuration specifying futures mode), freqtrade will automatically download the necessary candle types (e.g. `mark` and `funding_rate` candles) unless specified otherwise via `--candle-types`. ??? Note "Permission denied errors" If your configuration directory `user_data` was made by docker, you may get the following error: From 83b372a32d2a254da308813b34a03f63ef880d7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Dec 2025 19:30:21 +0100 Subject: [PATCH 46/50] docs: add "Funding fee adjustment" to deprecated docs --- docs/deprecated.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/deprecated.md b/docs/deprecated.md index f6ef8768f..6af1e0d76 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -98,3 +98,33 @@ Please use configuration based [log setup](advanced-setup.md#advanced-logging) i The edge module has been deprecated in 2023.9 and removed in 2025.6. All functionalities of edge have been removed, and having edge configured will result in an error. + +## Adjustment to dynamic funding rate handling + +With version 2025.12, the handling of dynamic funding rates has been adjusted to also support dynamic funding rates down to 1h funding intervals. +As a consequence, the mark and funding rate timeframes have been changed to 1h for every supported futures exchange. + +As the timeframe for both mark and funding_fee candles has changed (usually from 8h to 1h) - already downloaded data will have to be adjusted or partially re-downloaded. +You can either re-download everything (`freqtrade download-data [...] --erase` - :warning: can take a long time) - or download the updated data selectively. + +### Selective data re-download + +The script below should serve as an example - you may need to adjust the timeframe and exchange to your needs! + +``` bash +# Cleanup no longer needed data +rm user_data/data//futures/*-mark-* +rm user_data/data//futures/*-funding_rate-* + +# download new data (only required once to fix the mark and funding fee data) +freqtrade download-data -t 1h --trading-mode futures --candle-types funding_rate mark [...] --timerange + +``` + +The result of the above will be that your funding_rates and mark data will have the 1h timeframe. +you can verify this with `freqtrade list-data --exchange --show`. + +!!! Note "Additional arguments" + Additional arguments to the above commands may be necessary, like configuration files or explicit user_data if they deviate from the default. + +**Hyperliquid** is a special case now - which will no longer require 1h mark data - but will use regular candles instead (this data never existed and is identical to 1h futures candles). As we don't support download-data for hyperliquid (they don't provide historic data) - there won't be actions necessary for hyperliquid users. From 31d3a19836cf5d011d29f9da7f9b60d580e8595b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Dec 2025 13:49:39 +0100 Subject: [PATCH 47/50] feat: support candle_type parameter via API download --- freqtrade/rpc/api_server/api_download_data.py | 2 ++ freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_download_data.py b/freqtrade/rpc/api_server/api_download_data.py index 6e446b063..b755474c9 100644 --- a/freqtrade/rpc/api_server/api_download_data.py +++ b/freqtrade/rpc/api_server/api_download_data.py @@ -63,6 +63,8 @@ def pairlists_evaluate( config_loc["timeframes"] = payload.timeframes config_loc["erase"] = payload.erase config_loc["download_trades"] = payload.download_trades + if payload.candle_types is not None: + config_loc["candle_types"] = payload.candle_types handleExchangePayload(payload, config_loc) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 21a93807c..51507fb04 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -506,6 +506,7 @@ class DownloadDataPayload(ExchangeModePayloadMixin, BaseModel): timerange: str | None = None erase: bool = False download_trades: bool = False + candle_types: list[str] | None = None @model_validator(mode="before") def check_mutually_exclusive(cls, values): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index dc9682b09..3d1ec8433 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -91,7 +91,8 @@ logger = logging.getLogger(__name__) # 2.41: Add download-data endpoint # 2.42: Add /pair_history endpoint with live data # 2.43: Add /profit_all endpoint -API_VERSION = 2.43 +# 2.44: Add candle_types parameter to download-data endpoint +API_VERSION = 2.44 # Public API, requires no auth. router_public = APIRouter() From b406219515a964601b5051cbf56d5c81ae9788d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Dec 2025 15:51:06 +0100 Subject: [PATCH 48/50] test: add candle_types test --- tests/rpc/test_rpc_apiserver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index aa5483e88..c4e4978f9 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -3251,6 +3251,7 @@ def test_api_download_data(botclient, mocker, tmp_path): body = { "pairs": ["ETH/BTC", "XRP/BTC"], "timeframes": ["5m"], + "candle_types": ["spot"], } # Fail, already running From bd5630a104977b504053cd2c6b3cffca79d6dcef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Dec 2025 16:19:37 +0100 Subject: [PATCH 49/50] test: simplify test mock --- tests/exchange/test_exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4a1f4ae5f..4ec983cdf 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2388,8 +2388,8 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_ 5, # volume (in quote currency) ] ] - mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock()) exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) + mocker.patch.object(exchange, "verify_candle_type_support") # Monkey-patch async function exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) @@ -2439,8 +2439,8 @@ def test_refresh_latest_ohlcv(mocker, default_conf_usdt, caplog, candle_type) -> ] caplog.set_level(logging.DEBUG) - mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock()) exchange = get_patched_exchange(mocker, default_conf_usdt) + mocker.patch.object(exchange, "verify_candle_type_support") exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) pairs = [("IOTA/USDT", "5m", candle_type), ("XRP/USDT", "5m", candle_type)] @@ -2690,8 +2690,8 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach ohlcv = generate_test_data_raw("1h", 100, start.strftime("%Y-%m-%d")) time_machine.move_to(start + timedelta(hours=99, minutes=30)) - mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock()) exchange = get_patched_exchange(mocker, default_conf) + mocker.patch.object(exchange, "verify_candle_type_support") exchange._set_startup_candle_count(default_conf) mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100) From 2e3d2763046115e0dee042bb2c7603a229aa1c7c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Dec 2025 17:13:03 +0100 Subject: [PATCH 50/50] docs: Add strategy docs to migrate funding fees --- docs/deprecated.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/deprecated.md b/docs/deprecated.md index 6af1e0d76..ce3bf3b95 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -107,6 +107,13 @@ As a consequence, the mark and funding rate timeframes have been changed to 1h f As the timeframe for both mark and funding_fee candles has changed (usually from 8h to 1h) - already downloaded data will have to be adjusted or partially re-downloaded. You can either re-download everything (`freqtrade download-data [...] --erase` - :warning: can take a long time) - or download the updated data selectively. +### Strategy + +Most strategies should not need adjustments to continue to work as expected - however, strategies using `@informative("8h", candle_type="funding_rate")` or similar will have to switch the timeframe to 1h. +The same is true for `dp.get_pair_dataframe(metadata["pair"], "8h", candle_type="funding_rate")` - which will need to be switched to 1h. + +freqtrade will auto-adjust the timeframe and return `funding_rates` despite the wrongly given timeframe. It'll issue a warning - and may still break your strategy. + ### Selective data re-download The script below should serve as an example - you may need to adjust the timeframe and exchange to your needs!