Skip to content

Commit 8af0631

Browse files
authored
Merge pull request freqtrade#12599 from freqtrade/fix/dynamic_funding_fees
Adjust to dynamic funding fees
2 parents 6848f91 + 2e3d276 commit 8af0631

33 files changed

+452
-114
lines changed

docs/commands/download-data.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ usage: freqtrade download-data [-h] [-v] [--no-color] [--logfile FILE] [-V]
1111
[--data-format-ohlcv {json,jsongz,feather,parquet}]
1212
[--data-format-trades {json,jsongz,feather,parquet}]
1313
[--trading-mode {spot,margin,futures}]
14+
[--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]]
1415
[--prepend]
1516
1617
options:
@@ -50,6 +51,11 @@ options:
5051
`feather`).
5152
--trading-mode, --tradingmode {spot,margin,futures}
5253
Select Trading mode
54+
--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]
55+
Select candle type to download. Defaults to the
56+
necessary candles for the selected trading mode (e.g.
57+
'spot' or ('futures', 'funding_rate' and 'mark') for
58+
futures).
5359
--prepend Allow data prepending. (Data-appending is disabled)
5460
5561
Common arguments:

docs/data-download.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ freqtrade download-data --exchange binance --pairs ".*/USDT"
6060
* Given starting points are ignored if data is already available, downloading only missing data up to today.
6161
* 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.
6262
* 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.
63+
* 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`.
6364

6465
??? Note "Permission denied errors"
6566
If your configuration directory `user_data` was made by docker, you may get the following error:

docs/deprecated.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,40 @@ Please use configuration based [log setup](advanced-setup.md#advanced-logging) i
9898

9999
The edge module has been deprecated in 2023.9 and removed in 2025.6.
100100
All functionalities of edge have been removed, and having edge configured will result in an error.
101+
102+
## Adjustment to dynamic funding rate handling
103+
104+
With version 2025.12, the handling of dynamic funding rates has been adjusted to also support dynamic funding rates down to 1h funding intervals.
105+
As a consequence, the mark and funding rate timeframes have been changed to 1h for every supported futures exchange.
106+
107+
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.
108+
You can either re-download everything (`freqtrade download-data [...] --erase` - :warning: can take a long time) - or download the updated data selectively.
109+
110+
### Strategy
111+
112+
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.
113+
The same is true for `dp.get_pair_dataframe(metadata["pair"], "8h", candle_type="funding_rate")` - which will need to be switched to 1h.
114+
115+
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.
116+
117+
### Selective data re-download
118+
119+
The script below should serve as an example - you may need to adjust the timeframe and exchange to your needs!
120+
121+
``` bash
122+
# Cleanup no longer needed data
123+
rm user_data/data/<exchange>/futures/*-mark-*
124+
rm user_data/data/<exchange>/futures/*-funding_rate-*
125+
126+
# download new data (only required once to fix the mark and funding fee data)
127+
freqtrade download-data -t 1h --trading-mode futures --candle-types funding_rate mark [...] --timerange <full timerange you've got other data for>
128+
129+
```
130+
131+
The result of the above will be that your funding_rates and mark data will have the 1h timeframe.
132+
you can verify this with `freqtrade list-data --exchange <yourexchange> --show`.
133+
134+
!!! Note "Additional arguments"
135+
Additional arguments to the above commands may be necessary, like configuration files or explicit user_data if they deviate from the default.
136+
137+
**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.

freqtrade/commands/arguments.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
from argparse import ArgumentParser, Namespace, _ArgumentGroup
6+
from copy import deepcopy
67
from functools import partial
78
from pathlib import Path
89
from typing import Any
@@ -174,6 +175,7 @@
174175
"dataformat_ohlcv",
175176
"dataformat_trades",
176177
"trading_mode",
178+
"candle_types",
177179
"prepend_data",
178180
]
179181

@@ -348,7 +350,11 @@ def _parse_args(self) -> Namespace:
348350
def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
349351
for val in optionlist:
350352
opt = AVAILABLE_CLI_OPTIONS[val]
351-
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
353+
options = deepcopy(opt.kwargs)
354+
help_text = options.pop("help", None)
355+
if opt.fthelp and isinstance(opt.fthelp, dict) and hasattr(parser, "prog"):
356+
help_text = opt.fthelp.get(parser.prog, help_text)
357+
parser.add_argument(*opt.cli, dest=val, help=help_text, **options)
352358

353359
def _build_subcommands(self) -> None:
354360
"""

freqtrade/commands/cli_options.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,14 @@ def check_int_nonzero(value: str) -> int:
3838

3939
class Arg:
4040
# Optional CLI arguments
41-
def __init__(self, *args, **kwargs):
41+
def __init__(self, *args, fthelp: dict[str, str] | None = None, **kwargs):
42+
"""
43+
CLI Arguments - used to build subcommand parsers consistently.
44+
:param fthelp: dict - fthelp per command - should be "freqtrade <command>": help_text
45+
If not provided or not found, 'help' from kwargs is used instead.
46+
"""
4247
self.cli = args
48+
self.fthelp = fthelp
4349
self.kwargs = kwargs
4450

4551

@@ -422,6 +428,14 @@ def __init__(self, *args, **kwargs):
422428
),
423429
"candle_types": Arg(
424430
"--candle-types",
431+
fthelp={
432+
"freqtrade download-data": (
433+
"Select candle type to download. "
434+
"Defaults to the necessary candles for the selected trading mode "
435+
"(e.g. 'spot' or ('futures', 'funding_rate' and 'mark') for futures)."
436+
),
437+
"_": "Select candle type to convert. Defaults to all available types.",
438+
},
425439
help="Select candle type to convert. Defaults to all available types.",
426440
choices=[c.value for c in CandleType],
427441
nargs="+",

freqtrade/data/converter/converter.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def ohlcv_to_dataframe(
3838
cols = DEFAULT_DATAFRAME_COLUMNS
3939
df = DataFrame(ohlcv, columns=cols)
4040

41-
df["date"] = to_datetime(df["date"], unit="ms", utc=True)
41+
# Floor date to seconds to account for exchange imprecisions
42+
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s")
4243

4344
# Some exchanges return int values for Volume and even for OHLC.
4445
# Convert them since TA-LIB indicators used in the strategy assume floats

freqtrade/data/dataprovider.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,22 @@ def get_required_startup(self, timeframe: str) -> int:
348348
)
349349
return total_candles
350350

351+
def __fix_funding_rate_timeframe(
352+
self, pair: str, timeframe: str | None, candle_type: str
353+
) -> str | None:
354+
if (
355+
candle_type == CandleType.FUNDING_RATE
356+
and (ff_tf := self.get_funding_rate_timeframe()) != timeframe
357+
):
358+
# TODO: does this message make sense? might be pointless as funding fees don't
359+
# have a timeframe
360+
logger.warning(
361+
f"{pair}, {timeframe} requested - funding rate timeframe not matching {ff_tf}."
362+
)
363+
return ff_tf
364+
365+
return timeframe
366+
351367
def get_pair_dataframe(
352368
self, pair: str, timeframe: str | None = None, candle_type: str = ""
353369
) -> DataFrame:
@@ -361,6 +377,7 @@ def get_pair_dataframe(
361377
:return: Dataframe for this pair
362378
:param candle_type: '', mark, index, premiumIndex, or funding_rate
363379
"""
380+
timeframe = self.__fix_funding_rate_timeframe(pair, timeframe, candle_type)
364381
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
365382
# Get live OHLCV data.
366383
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
@@ -620,3 +637,12 @@ def check_delisting(self, pair: str) -> datetime | None:
620637
except ExchangeError:
621638
logger.warning(f"Could not fetch market data for {pair}. Assuming no delisting.")
622639
return None
640+
641+
def get_funding_rate_timeframe(self) -> str:
642+
"""
643+
Get the funding rate timeframe from exchange options
644+
:return: Timeframe string
645+
"""
646+
if self._exchange is None:
647+
raise OperationalException(NO_EXCHANGE_EXCEPTION)
648+
return self._exchange.get_option("funding_fee_timeframe")

freqtrade/data/history/datahandlers/idatahandler.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ def ohlcv_load(
397397
pairdf = self._ohlcv_load(
398398
pair, timeframe, timerange=timerange_startup, candle_type=candle_type
399399
)
400+
if not pairdf.empty and candle_type == CandleType.FUNDING_RATE:
401+
# Funding rate data is sometimes off by a couple of ms - floor to seconds
402+
pairdf["date"] = pairdf["date"].dt.floor("s")
400403
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
401404
return pairdf
402405
else:
@@ -508,8 +511,15 @@ def fix_funding_fee_timeframe(self, ff_timeframe: str):
508511
Applies to bybit and okx, where funding-fee and mark candles have different timeframes.
509512
"""
510513
paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES)
514+
ff_timeframe_s = timeframe_to_seconds(ff_timeframe)
515+
511516
funding_rate_combs = [
512-
f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe
517+
f
518+
for f in paircombs
519+
if f[2] == CandleType.FUNDING_RATE
520+
and f[1] != ff_timeframe
521+
# Only allow smaller timeframes to move from smaller to larger timeframes
522+
and timeframe_to_seconds(f[1]) < ff_timeframe_s
513523
]
514524

515525
if funding_rate_combs:

freqtrade/data/history/history_utils.py

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ def _download_pair_history(
353353

354354
def refresh_backtest_ohlcv_data(
355355
exchange: Exchange,
356+
*,
356357
pairs: list[str],
357358
timeframes: list[str],
358359
datadir: Path,
@@ -363,6 +364,7 @@ def refresh_backtest_ohlcv_data(
363364
data_format: str | None = None,
364365
prepend: bool = False,
365366
progress_tracker: CustomProgress | None = None,
367+
candle_types: list[CandleType] | None = None,
366368
no_parallel_download: bool = False,
367369
) -> list[str]:
368370
"""
@@ -375,10 +377,44 @@ def refresh_backtest_ohlcv_data(
375377
pairs_not_available = []
376378
fast_candles: dict[PairWithTimeframe, DataFrame] = {}
377379
data_handler = get_datahandler(datadir, data_format)
378-
candle_type = CandleType.get_default(trading_mode)
380+
def_candletype = CandleType.SPOT if trading_mode != "futures" else CandleType.FUTURES
381+
if trading_mode != "futures":
382+
# Ignore user passed candle types for non-futures trading
383+
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
384+
else:
385+
# Filter out SPOT candle type for futures trading
386+
candle_types = (
387+
[ct for ct in candle_types if ct != CandleType.SPOT] if candle_types else None
388+
)
389+
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
390+
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
391+
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
392+
393+
if candle_types:
394+
for ct in candle_types:
395+
exchange.verify_candle_type_support(ct)
396+
timeframes_with_candletype = [
397+
(tf, ct)
398+
for ct in candle_types
399+
for tf in timeframes
400+
if ct != CandleType.FUNDING_RATE
401+
]
402+
else:
403+
# Default behavior
404+
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
405+
timeframes_with_candletype.append((tf_mark, fr_candle_type))
406+
if not candle_types or CandleType.FUNDING_RATE in candle_types:
407+
# All exchanges need FundingRate for futures trading.
408+
# The timeframe is aligned to the mark-price timeframe.
409+
timeframes_with_candletype.append((tf_funding_rate, CandleType.FUNDING_RATE))
410+
# Deduplicate list ...
411+
timeframes_with_candletype = list(dict.fromkeys(timeframes_with_candletype))
412+
logger.debug(
413+
"Downloading %s.", ", ".join(f'"{tf} {ct}"' for tf, ct in timeframes_with_candletype)
414+
)
415+
379416
with progress_tracker as progress:
380-
tf_length = len(timeframes) if trading_mode != "futures" else len(timeframes) + 2
381-
timeframe_task = progress.add_task("Timeframe", total=tf_length)
417+
timeframe_task = progress.add_task("Timeframe", total=len(timeframes_with_candletype))
382418
pair_task = progress.add_task("Downloading data...", total=len(pairs))
383419

384420
for pair in pairs:
@@ -389,7 +425,7 @@ def refresh_backtest_ohlcv_data(
389425
pairs_not_available.append(f"{pair}: Pair not available on exchange.")
390426
logger.info(f"Skipping pair {pair}...")
391427
continue
392-
for timeframe in timeframes:
428+
for timeframe, candle_type in timeframes_with_candletype:
393429
# Get fast candles via parallel method on first loop through per timeframe
394430
# and candle type. Downloads all the pairs in the list and stores them.
395431
# Also skips if only 1 pair/timeframe combination is scheduled for download.
@@ -416,7 +452,7 @@ def refresh_backtest_ohlcv_data(
416452
# get the already downloaded pair candles if they exist
417453
pair_candles = fast_candles.pop((pair, timeframe, candle_type), None)
418454

419-
progress.update(timeframe_task, description=f"Timeframe {timeframe}")
455+
progress.update(timeframe_task, description=f"Timeframe {timeframe} {candle_type}")
420456
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
421457
_download_pair_history(
422458
pair=pair,
@@ -432,33 +468,6 @@ def refresh_backtest_ohlcv_data(
432468
pair_candles=pair_candles, # optional pass of dataframe of parallel candles
433469
)
434470
progress.update(timeframe_task, advance=1)
435-
if trading_mode == "futures":
436-
# Predefined candletype (and timeframe) depending on exchange
437-
# Downloads what is necessary to backtest based on futures data.
438-
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
439-
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
440-
441-
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
442-
# All exchanges need FundingRate for futures trading.
443-
# The timeframe is aligned to the mark-price timeframe.
444-
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
445-
for candle_type_f, tf in combs:
446-
logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.")
447-
_download_pair_history(
448-
pair=pair,
449-
datadir=datadir,
450-
exchange=exchange,
451-
timerange=timerange,
452-
data_handler=data_handler,
453-
timeframe=str(tf),
454-
new_pairs_days=new_pairs_days,
455-
candle_type=candle_type_f,
456-
erase=erase,
457-
prepend=prepend,
458-
)
459-
progress.update(
460-
timeframe_task, advance=1, description=f"Timeframe {candle_type_f}, {tf}"
461-
)
462471

463472
progress.update(pair_task, advance=1)
464473
progress.update(timeframe_task, description="Timeframe")
@@ -804,6 +813,7 @@ def download_data(
804813
trading_mode=config.get("trading_mode", "spot"),
805814
prepend=config.get("prepend_data", False),
806815
progress_tracker=progress_tracker,
816+
candle_types=config.get("candle_types"),
807817
no_parallel_download=config.get("no_parallel_download", False),
808818
)
809819
finally:

freqtrade/exchange/bitget.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ class Bitget(Exchange):
3535
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
3636
}
3737
_ft_has_futures: FtHas = {
38-
"mark_ohlcv_timeframe": "4h",
3938
"funding_fee_candle_limit": 100,
4039
"has_delisting": True,
4140
}

0 commit comments

Comments
 (0)