Compare commits

..

34 Commits

Author SHA1 Message Date
Matthias
6848f9197e Merge pull request #12613 from freqtrade/fix/binance
Fix binance futures stoploss Order handling
2025-12-13 08:28:21 +01:00
Matthias
6d2c30abca chore: bump ccxt to 4.5.27 2025-12-12 06:54:29 +01:00
Matthias
93bde7dc46 fix: support binance algo orders
closes #12610
2025-12-12 06:52:11 +01:00
Matthias
97e2e0a405 chore: remove hard-pin of pycares 2025-12-11 18:23:58 +01:00
Matthias
12206f028b chore: comment wording improvements 2025-12-11 07:16:36 +01:00
Matthias
4c7944ac77 chore: update comment wording 2025-12-11 07:09:23 +01:00
Matthias
d4ced7b416 docs: improve doc wording 2025-12-11 07:05:58 +01:00
Matthias
451eef5c99 test: further test simplifications 2025-12-11 06:49:56 +01:00
Matthias
ae8f059de0 Merge pull request #12611 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-12-11 06:49:18 +01:00
Matthias
878bd7cbc7 chore: pin pycares for now 2025-12-11 06:35:27 +01:00
Freqtrade Bot
6d017c9a6c chore: update pre-commit hooks 2025-12-11 03:29:24 +00:00
Matthias
4c3d9b8c70 test: simplify some stoploss test setups 2025-12-10 19:28:30 +01:00
Matthias
23a4260859 chore: simplify okx cancel stoploss method 2025-12-10 17:23:10 +01:00
Matthias
5919736904 Merge pull request #12607 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-12-09 07:23:50 +01:00
Freqtrade Bot
732610e200 chore: update pre-commit hooks 2025-12-09 03:27:07 +00:00
Matthias
3689b52309 docs: add Section about very loose stoploss on exchange
closes #12598
2025-12-08 17:21:51 +01:00
Matthias
01fbf31405 chore: don't suggest binance.us supports futures
it doesn't.
2025-12-08 17:15:41 +01:00
Matthias
1e187e0945 Merge pull request #12604 from freqtrade/dependabot/pip/develop/ccxt-4.5.25
chore(deps): bump ccxt from 4.5.24 to 4.5.26
2025-12-08 11:52:15 +01:00
dependabot[bot]
e4fc5df1cf chore(deps): bump ccxt from 4.5.24 to 4.5.25
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.24 to 4.5.25.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.24...v4.5.25)

---
updated-dependencies:
- dependency-name: ccxt
  dependency-version: 4.5.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 09:25:30 +00:00
Matthias
253950deb6 Merge pull request #12600 from freqtrade/dependabot/pip/develop/scipy-ea2b5522bf
chore(deps-dev): bump scipy-stubs from 1.16.3.1 to 1.16.3.2 in the scipy group
2025-12-08 08:38:40 +01:00
Matthias
ed92d6beb9 Merge pull request #12601 from freqtrade/dependabot/pip/develop/ruff-0.14.7
chore(deps-dev): bump ruff from 0.14.6 to 0.14.7
2025-12-08 08:27:20 +01:00
Matthias
ebb362d9fa chore: bump scipy-stubs in pre-commit config 2025-12-08 08:16:30 +01:00
dependabot[bot]
f23fad420e chore(deps-dev): bump ruff from 0.14.6 to 0.14.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.6 to 0.14.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.6...0.14.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 07:11:06 +00:00
Matthias
aaf23221ff Merge pull request #12602 from freqtrade/dependabot/pip/develop/fastapi-0.123.0
chore(deps): bump fastapi from 0.122.0 to 0.123.0
2025-12-08 08:10:21 +01:00
Matthias
156c1a99a9 Merge pull request #12603 from freqtrade/dependabot/pip/develop/mypy-1.19.0
chore(deps-dev): bump mypy from 1.18.2 to 1.19.0
2025-12-08 08:09:35 +01:00
dependabot[bot]
cb55ef5c59 chore(deps-dev): bump mypy from 1.18.2 to 1.19.0
Bumps [mypy](https://github.com/python/mypy) from 1.18.2 to 1.19.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.18.2...v1.19.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.19.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 03:02:14 +00:00
dependabot[bot]
6540fbb8e7 chore(deps): bump fastapi from 0.122.0 to 0.123.0
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.122.0 to 0.123.0.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.122.0...0.123.0)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.123.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 03:02:07 +00:00
dependabot[bot]
412392aea9 chore(deps-dev): bump scipy-stubs in the scipy group
Bumps the scipy group with 1 update: [scipy-stubs](https://github.com/scipy/scipy-stubs).


Updates `scipy-stubs` from 1.16.3.1 to 1.16.3.2
- [Release notes](https://github.com/scipy/scipy-stubs/releases)
- [Commits](https://github.com/scipy/scipy-stubs/compare/v1.16.3.1...v1.16.3.2)

---
updated-dependencies:
- dependency-name: scipy-stubs
  dependency-version: 1.16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: scipy
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 03:01:42 +00:00
Matthias
e3229935f6 Merge pull request #12594 from stremblayiOS/fix-hyperliquid-fetch-positions
Fix IndexError in fetch_positions for Hyperliquid when no pair specified
2025-12-05 20:16:48 +01:00
Matthias
b1ee115b77 Merge pull request #12593 from arawrdn/develop
Update README.md
2025-12-05 19:53:33 +01:00
Matthias
d6060f04bc Merge pull request #12595 from freqtrade/dependabot/pip/urllib3-2.6.0
chore(deps): bump urllib3 from 2.5.0 to 2.6.0
2025-12-05 19:43:22 +01:00
dependabot[bot]
1ae5310d2f chore(deps): bump urllib3 from 2.5.0 to 2.6.0
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 18:27:27 +00:00
stremblayiOS
417a0817a7 Fix IndexError in fetch_positions for Hyperliquid when no pair specified
## Summary

Fix IndexError crash in fetch_positions() when initializing wallets on Hyperliquid exchange.

## Quick changelog

- Changed fetch_positions to pass None instead of empty list when no specific pair is requested
- Fixes compatibility with Hyperliquid CCXT implementation that expects None for all positions

## What's new?

When fetch_positions() is called without a specific pair parameter, the code was passing an empty list [] to the CCXT API.
For Hyperliquid exchange, this causes an IndexError because the exchange's implementation attempts to access symbols[0]
without checking if the list is empty.

The CCXT standard is to pass None (not an empty list) when requesting all positions. This change aligns the code with
the CCXT API convention and prevents the crash on Hyperliquid during wallet initialization.

Error that was occurring:
```
IndexError: list index out of range
  at /root/freqtrade/.venv/lib/python3.11/site-packages/ccxt/hyperliquid.py:3051
  market = self.market(symbols[0])
```

This change does not use AI-generated code.
2025-12-05 18:33:40 +01:00
0xward
8ca25b1757 Update README.md
docs: fix minor typo and inconsistency in README disclaimer (Dry-run to dry-run)
2025-12-05 09:50:16 +07:00
43 changed files with 1714 additions and 2442 deletions

View File

@@ -31,7 +31,7 @@ repos:
- types-requests==2.32.4.20250913
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20251115
- scipy-stubs==1.16.3.1
- scipy-stubs==1.16.3.2
- SQLAlchemy==2.0.44
# stages: [push]
@@ -44,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.14.7'
rev: 'v0.14.8'
hooks:
- id: ruff
- id: ruff-format

View File

@@ -15,7 +15,7 @@ This software is for educational purposes only. Do not risk money which
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
Always start by running a trading bot in Dry-run and do not engage money
Always start by running a trading bot in Dry-Run and do not engage money
before you understand how it works and what profit/loss you should
expect.
@@ -24,7 +24,7 @@ hesitate to read the source code and understand the mechanism of this bot.
## Supported Exchange marketplaces
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
Please read the [exchange-specific notes](docs/exchanges.md) to learn about special configurations that maybe needed for each exchange.
- [X] [Binance](https://www.binance.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)

View File

@@ -11,7 +11,6 @@ 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:
@@ -51,11 +50,6 @@ 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:

View File

@@ -60,7 +60,6 @@ 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:

View File

@@ -98,33 +98,3 @@ 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/<exchange>/futures/*-mark-*
rm user_data/data/<exchange>/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 <full timerange you've got other data for>
```
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 <yourexchange> --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.

View File

@@ -31,9 +31,14 @@ The Order-type will be ignored if only one mode is available.
--8<-- "includes/exchange-features.md"
!!! Note "Tight stoploss"
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
Do not set too low/tight stoploss value when using stop loss on exchange!
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
!!! Warning "Loose stoploss"
Using stoploss on exchange with a very wide stoploss (e.g. -1) may fail to place the stoploss order on exchange due to exchange limitations.
In that case, the bot will fallback to using the `emergency_exit` order type to place a market order as placing the stoploss order failed.
Freqtrade currently does not implement a limitation to avoid this situation, so please ensure your stoploss values are within reasonable limits for your exchange or disable stoploss on exchange.
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
Enable or Disable stop loss on exchange.

View File

@@ -644,7 +644,7 @@ Each of these methods are called right before placing an order on the exchange.
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
!!! Note
Using custom_entry_price, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
When using `custom_entry_price()`, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
### Custom order entry and exit price example

View File

@@ -3,7 +3,6 @@ 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
@@ -175,7 +174,6 @@ ARGS_DOWNLOAD_DATA = [
"dataformat_ohlcv",
"dataformat_trades",
"trading_mode",
"candle_types",
"prepend_data",
]
@@ -350,11 +348,7 @@ class Arguments:
def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val]
options = deepcopy(opt.kwargs)
help_text = options.pop("help", None)
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)
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
def _build_subcommands(self) -> None:
"""

View File

@@ -38,14 +38,8 @@ def check_int_nonzero(value: str) -> int:
class Arg:
# Optional CLI arguments
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 <command>": help_text
If not provided or not found, 'help' from kwargs is used instead.
"""
def __init__(self, *args, **kwargs):
self.cli = args
self.fthelp = fthelp
self.kwargs = kwargs
@@ -428,14 +422,6 @@ 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="+",

View File

@@ -38,8 +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")
df["date"] = to_datetime(df["date"], unit="ms", utc=True)
# Some exchanges return int values for Volume and even for OHLC.
# Convert them since TA-LIB indicators used in the strategy assume floats

View File

@@ -348,22 +348,6 @@ class DataProvider:
)
return total_candles
def __fix_funding_rate_timeframe(
self, pair: str, timeframe: str | None, candle_type: str
) -> str | None:
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:
@@ -377,7 +361,6 @@ 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)
@@ -637,12 +620,3 @@ 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")

View File

@@ -397,9 +397,6 @@ 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:
@@ -511,15 +508,8 @@ 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
# Only allow smaller timeframes to move from smaller to larger timeframes
and timeframe_to_seconds(f[1]) < ff_timeframe_s
f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe
]
if funding_rate_combs:

View File

@@ -353,7 +353,6 @@ def _download_pair_history(
def refresh_backtest_ohlcv_data(
exchange: Exchange,
*,
pairs: list[str],
timeframes: list[str],
datadir: Path,
@@ -364,7 +363,6 @@ 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]:
"""
@@ -377,44 +375,10 @@ def refresh_backtest_ohlcv_data(
pairs_not_available = []
fast_candles: dict[PairWithTimeframe, DataFrame] = {}
data_handler = get_datahandler(datadir, data_format)
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:
for ct in candle_types:
exchange.verify_candle_type_support(ct)
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))
# 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)
)
candle_type = CandleType.get_default(trading_mode)
with progress_tracker as progress:
timeframe_task = progress.add_task("Timeframe", total=len(timeframes_with_candletype))
tf_length = len(timeframes) if trading_mode != "futures" else len(timeframes) + 2
timeframe_task = progress.add_task("Timeframe", total=tf_length)
pair_task = progress.add_task("Downloading data...", total=len(pairs))
for pair in pairs:
@@ -425,7 +389,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, candle_type in timeframes_with_candletype:
for timeframe in timeframes:
# 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.
@@ -452,7 +416,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} {candle_type}")
progress.update(timeframe_task, description=f"Timeframe {timeframe}")
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
_download_pair_history(
pair=pair,
@@ -468,6 +432,33 @@ 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")
@@ -813,7 +804,6 @@ 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:

View File

@@ -4,7 +4,7 @@ from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
from freqtrade.exchange.exchange import Exchange
# isort: on
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.binance import Binance, Binanceus, Binanceusdm
from freqtrade.exchange.bingx import Bingx
from freqtrade.exchange.bitget import Bitget
from freqtrade.exchange.bitmart import Bitmart

View File

@@ -17,7 +17,7 @@ from freqtrade.exchange.binance_public_data import (
download_archive_trades,
)
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import FtHas, Tickers
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas, Tickers
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.util import FtTTLCache
@@ -145,6 +145,20 @@ class Binance(Exchange):
except ccxt.BaseError as e:
raise OperationalException(e) from e
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
if self.trading_mode == TradingMode.FUTURES:
params = params or {}
params.update({"stop": True})
return self.fetch_order(order_id, pair, params)
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
if self.trading_mode == TradingMode.FUTURES:
params = params or {}
params.update({"stop": True})
return self.cancel_order(order_id=order_id, pair=pair, params=params)
def get_historic_ohlcv(
self,
pair: str,
@@ -544,3 +558,26 @@ class Binance(Exchange):
cache[ft_symbol] = delist_dt
return cache.get(pair, None)
class Binanceusdm(Binance):
"""Binacne USDM Exchange
Same as Binance - only futures trading is supported (via ccxt).
Not actually necessary, binance should be preferred.
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
class Binanceus(Binance):
"""Binance US exchange class.
Minimal adjustment to disable futures trading for the US subsidiary of Binance
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.SPOT, MarginMode.NONE),
]

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@ class Bitget(Exchange):
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
}
_ft_has_futures: FtHas = {
"mark_ohlcv_timeframe": "4h",
"funding_fee_candle_limit": 100,
"has_delisting": True,
}

View File

@@ -38,6 +38,8 @@ class Bybit(Exchange):
}
_ft_has_futures: FtHas = {
"ohlcv_has_history": True,
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"funding_fee_candle_limit": 200,
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"},

View File

@@ -45,8 +45,6 @@ BAD_EXCHANGES = {
}
MAP_EXCHANGE_CHILDCLASS = {
"binanceus": "binance",
"binanceusdm": "binance",
"okex": "okx",
"gateio": "gate",
"huboi": "htx",
@@ -54,6 +52,8 @@ MAP_EXCHANGE_CHILDCLASS = {
SUPPORTED_EXCHANGES = [
"binance",
"binanceus",
"binanceusdm",
"bingx",
"bitmart",
"bitget",
@@ -97,9 +97,6 @@ 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",
]

View File

@@ -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": "1h",
"funding_fee_timeframe": "1h",
"mark_ohlcv_timeframe": "8h",
"funding_fee_timeframe": "8h",
"ccxt_futures_name": "swap",
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
"order_props_in_contracts": ["amount", "filled", "remaining"],
@@ -1834,9 +1834,9 @@ class Exchange:
if self._config["dry_run"] or self.trading_mode != TradingMode.FUTURES:
return []
try:
symbols = []
symbols = None
if pair:
symbols.append(pair)
symbols = [pair]
positions: list[CcxtPosition] = self._api.fetch_positions(symbols)
self._log_exchange_response("fetch_positions", positions)
return positions
@@ -2690,25 +2690,24 @@ class Exchange:
input_coroutines: list[Coroutine[Any, Any, OHLCVResponse]] = []
cached_pairs = []
for pair, timeframe, candle_type in set(pair_list):
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_funding = (
candle_type == CandleType.FUNDING_RATE
and timeframe != self.get_option("funding_fee_timeframe")
)
invalid_timeframe = timeframe not in self.timeframes and candle_type in (
CandleType.SPOT,
CandleType.FUTURES,
)
if invalid_timeframe:
if invalid_timeframe or invalid_funding:
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"{', '.join(self.timeframes)}."
f"{timeframes_}."
)
continue
@@ -2745,11 +2744,7 @@ 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 and c_type != CandleType.FUNDING_RATE,
drop_incomplete=drop_incomplete,
ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
)
# keeping parsed dataframe in cache
if cache:
@@ -2760,7 +2755,7 @@ class Exchange:
concat([old, ohlcv_df], axis=0),
timeframe,
pair,
fill_missing=c_type != CandleType.FUNDING_RATE,
fill_missing=True,
drop_incomplete=False,
)
candle_limit = self.ohlcv_candle_limit(timeframe, self._config["candle_type_def"])
@@ -2895,10 +2890,9 @@ 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": candle_type.value})
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
)
@@ -2963,38 +2957,6 @@ class Exchange:
data = [[x["timestamp"], x["fundingRate"], 0, 0, 0, 0] for x in data]
return data
def check_candle_type_support(self, candle_type: CandleType) -> bool:
"""
Check that the exchange supports the given candle type.
:param candle_type: CandleType to verify
:return: True if supported, False otherwise
"""
if candle_type == CandleType.FUNDING_RATE:
if not self.exchange_has("fetchFundingRateHistory"):
return False
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):
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
def needed_candle_for_trades_ms(self, timeframe: str, candle_type: CandleType) -> int:
@@ -3846,16 +3808,8 @@ class Exchange:
combined = mark_rates.merge(
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
)
# 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()
combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
return combined[relevant_cols]
def calculate_funding_fees(
self,

View File

@@ -37,9 +37,9 @@ 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,
"mark_ohlcv_price": "futures",
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [

View File

@@ -35,6 +35,7 @@ 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]] = [

View File

@@ -29,6 +29,8 @@ 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
@@ -264,14 +266,7 @@ class Okx(Exchange):
return order["id"]
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
params1 = {"stop": True}
# 'ordType': 'conditional'
#
return self.cancel_order(
order_id=order_id,
pair=pair,
params=params1,
)
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
orders = []

View File

@@ -2011,14 +2011,14 @@ class FreqtradeBot(LoggingMixin):
def _safe_exit_amount(self, trade: Trade, pair: str, amount: float) -> float:
"""
Get sellable amount.
Get exitable amount.
Should be trade.amount - but will fall back to the available amount if necessary.
This should cover cases where get_real_amount() was not able to update the amount
for whatever reason.
:param trade: Trade we're working with
:param pair: Pair we're trying to sell
:param pair: Pair we're trying to exit
:param amount: amount we expect to be available
:return: amount to sell
:return: amount to exit
:raise: DependencyException: if available balance is not within 2% of the available amount.
"""
# Update wallets to ensure amounts tied up in a stoploss is now free!
@@ -2058,7 +2058,7 @@ class FreqtradeBot(LoggingMixin):
"""
Executes a trade exit for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:param limit: limit rate for the exit order
:param exit_check: CheckTuple with signal and reason
:return: True if it succeeds False
"""
@@ -2101,7 +2101,7 @@ class FreqtradeBot(LoggingMixin):
order_type = ordertype or self.strategy.order_types[exit_type]
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
# Emergency sells (default to market!)
# Emergency exits (default to market!)
order_type = self.strategy.order_types.get("emergency_exit", "market")
amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
@@ -2130,7 +2130,7 @@ class FreqtradeBot(LoggingMixin):
return False
try:
# Execute sell and update trade record
# Execute exit and update trade record
order = self.exchange.create_order(
pair=trade.pair,
ordertype=order_type,
@@ -2157,7 +2157,7 @@ class FreqtradeBot(LoggingMixin):
trade.exit_reason = exit_reason
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
# In case of market sell orders the order can be closed immediately
# In case of market exit orders the order can be closed immediately
if order.get("status", "unknown") in ("closed", "expired"):
self.update_trade_state(trade, order_obj.order_id, order)
Trade.commit()

View File

@@ -374,7 +374,6 @@ 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,
)

View File

@@ -104,11 +104,8 @@ 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
@@ -135,10 +132,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, timeframe1, candle_type)
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe, candle_type)
if inf_dataframe.empty:
raise ValueError(
f"Informative dataframe for ({asset}, {timeframe1}, {candle_type}) is empty. "
f"Informative dataframe for ({asset}, {timeframe}, {candle_type}) is empty. "
"Can't populate informative indicators."
)
inf_dataframe = populate_indicators_fn(strategy, inf_dataframe, inf_metadata)
@@ -166,7 +163,7 @@ def _create_and_merge_informative_pair(
dataframe,
inf_dataframe,
strategy.timeframe,
timeframe1,
timeframe,
ffill=inf_data.ffill,
append_timeframe=False,
date_column=date_column,

View File

@@ -6,8 +6,8 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.14.6
mypy==1.18.2
ruff==0.14.7
mypy==1.19.0
pre-commit==4.5.0
pytest==9.0.1
pytest-asyncio==1.3.0
@@ -24,7 +24,7 @@ time-machine==3.1.0
nbconvert==7.16.6
# mypy types
scipy-stubs==1.16.3.1 # keep in sync with `scipy` in `requirements-hyperopt.txt`
scipy-stubs==1.16.3.2 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.2.0.20251022
types-filelock==3.2.7
types-requests==2.32.4.20250913

View File

@@ -7,7 +7,7 @@ ft-pandas-ta==0.3.16
ta-lib==0.6.8
technical==1.5.3
ccxt==4.5.24
ccxt==4.5.27
cryptography==46.0.3
aiohttp==3.13.2
SQLAlchemy==2.0.44
@@ -17,7 +17,7 @@ httpx>=0.24.1
humanize==4.14.0
cachetools==6.2.2
requests==2.32.5
urllib3==2.5.0
urllib3==2.6.0
certifi==2025.11.12
jsonschema==4.25.1
tabulate==0.9.0
@@ -37,7 +37,7 @@ orjson==3.11.4
sdnotify==0.3.2
# API Server
fastapi==0.122.0
fastapi==0.123.0
pydantic==2.12.5
uvicorn==0.38.0
pyjwt==2.10.1

View File

@@ -1801,10 +1801,10 @@ def test_start_list_data(testdatadir, capsys):
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 5 pair / timeframe combinations." in captured.out
assert "Found 6 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.* mark |\n", captured.out)
assert re.search(r"\n.* XRP/USDT:USDT .* 1h, 8h .* mark |\n", captured.out)
args = [
"list-data",

View File

@@ -126,7 +126,8 @@ 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", "1h", "funding_rate"),
("XRP/USDT:USDT", "8h", "mark"),
("XRP/USDT:USDT", "8h", "funding_rate"),
}
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT)

View File

@@ -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, log_has_re
from tests.conftest import EXMS, generate_test_data, get_patched_exchange
@pytest.mark.parametrize(
@@ -185,28 +185,6 @@ 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"]
@@ -658,21 +636,3 @@ 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()

View File

@@ -534,19 +534,18 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
@pytest.mark.parametrize(
"trademode,callcount, callcount_parallel",
"trademode,callcount",
[
("spot", 4, 2),
("margin", 4, 2),
("futures", 8, 4), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
("spot", 4),
("margin", 4),
("futures", 8), # 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, callcount_parallel
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount
):
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}
@@ -574,15 +573,14 @@ def test_refresh_backtest_ohlcv_data(
)
# Called once per timeframe (as we return an empty dataframe)
# called twice for spot/margin and 4 times for futures
assert parallel_mock.call_count == callcount_parallel
assert parallel_mock.call_count == 2
assert dl_mock.call_count == callcount
assert dl_mock.call_args[1]["timerange"].starttype == "date"
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 1h\.", caplog)
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 1h\.", caplog)
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)
# Test with only one pair - no parallel download should happen 1 pair/timeframe combination
# doesn't justify parallelization
@@ -601,24 +599,6 @@ 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(

View File

@@ -2388,7 +2388,6 @@ 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)
@@ -2439,7 +2438,6 @@ 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)
@@ -2690,7 +2688,6 @@ 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)
@@ -2840,29 +2837,6 @@ 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 = [
@@ -3927,37 +3901,29 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
default_conf["dry_run"] = False
mock_prefix = "freqtrade.exchange.gate.Gate"
if exchange_name == "okx":
mock_prefix = "freqtrade.exchange.okx.Okx"
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value={"for": 123})
mocker.patch(f"{mock_prefix}.fetch_stoploss_order", return_value={"for": 123})
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
mocker.patch.object(exchange, "fetch_stoploss_order", return_value={"for": 123})
res = {"fee": {}, "status": "canceled", "amount": 1234}
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=res)
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", return_value=res)
mocker.patch.object(exchange, "cancel_stoploss_order", return_value=res)
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
assert co == res
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value="canceled")
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", return_value="canceled")
mocker.patch.object(exchange, "cancel_stoploss_order", return_value="canceled")
# Fall back to fetch_stoploss_order
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
assert co == {"for": 123}
exc = InvalidOrderException("")
mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=exc)
mocker.patch(f"{mock_prefix}.fetch_stoploss_order", side_effect=exc)
mocker.patch.object(exchange, "fetch_stoploss_order", side_effect=exc)
co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555)
assert co["amount"] == 555
assert co == {"id": "_", "fee": {}, "status": "canceled", "amount": 555, "info": {}}
with pytest.raises(InvalidOrderException):
exc = InvalidOrderException("Did not find order")
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=exc)
mocker.patch(f"{mock_prefix}.cancel_stoploss_order", side_effect=exc)
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
mocker.patch.object(exchange, "cancel_stoploss_order", side_effect=exc)
exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=123)
@@ -4142,7 +4108,7 @@ def test_fetch_order_or_stoploss_order(default_conf, mocker):
fetch_order_mock = MagicMock()
fetch_stoploss_order_mock = MagicMock()
mocker.patch.multiple(
EXMS,
exchange,
fetch_order=fetch_order_mock,
fetch_stoploss_order=fetch_stoploss_order_mock,
)
@@ -5376,12 +5342,11 @@ 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) == 2
assert len(df) == 3
assert df.iloc[0]["open_fund"] == funding_rate
# 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]
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]
else:
assert len(df) == 2
assert df["date"].to_list() == [prior2_date, trade_date]
@@ -5475,13 +5440,8 @@ 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={
"fetchFundingRateHistory": True,
"fetchMarkOHLCV": True,
"fetchOHLCV": True,
}
)
type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True})
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True})
ex = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange)
mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["1h", "4h", "8h"]))
@@ -5525,13 +5485,8 @@ 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={
"fetchFundingRateHistory": True,
"fetchMarkOHLCV": True,
"fetchOHLCV": True,
}
)
type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True})
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": 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")
@@ -6618,51 +6573,3 @@ 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)
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)

View File

@@ -270,14 +270,11 @@ 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: str, timeframe: str, candle_type: CandleType, factor: float = 0.9
):
def _ccxt__async_get_candle_history(self, exchange, pair, timeframe, candle_type, factor=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)
for offset in (360, 120, 30, 10, 5, 2):
since = now - timedelta(days=offset)
since_ms = int(since.timestamp() * 1000)
res = exchange.loop.run_until_complete(
@@ -292,15 +289,8 @@ 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
# 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=}"
assert len(candles) >= min(candle_count, candle_count1), (
f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}"
)
# Check if first-timeframe is either the start, or start + 1
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
@@ -319,8 +309,6 @@ class TestCCXTExchange:
[
CandleType.FUTURES,
CandleType.FUNDING_RATE,
CandleType.INDEX,
CandleType.PREMIUMINDEX,
CandleType.MARK,
],
)
@@ -334,10 +322,6 @@ 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,
@@ -353,7 +337,6 @@ 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(
@@ -367,26 +350,14 @@ 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))
# 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
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"]
# Test For last 4 hours
# Avoids random test-failure when funding-fees are 0 for a few hours.
assert (
row0["open"] != 0.0 or row1["open"] != 0.0 or row2["open"] != 0.0 or row3["open"] != 0.0
)
assert val0 != 0.0 or val1 != 0.0 or val2 != 0.0 or val3 != 0.0
# We expect funding rates to be different from 0.0 - or moving around.
assert (
rate["open"].max() != 0.0
@@ -398,10 +369,7 @@ 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)
candle_type = CandleType.from_string(
exchange.get_option("mark_ohlcv_price", default=CandleType.MARK)
)
pair_tf = (pair, "1h", candle_type)
pair_tf = (pair, "1h", CandleType.MARK)
mark_ohlcv = exchange.refresh_latest_ohlcv([pair_tf], since_ms=since, drop_incomplete=False)

View File

@@ -60,13 +60,10 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
cancel_order_mock = MagicMock(side_effect=patch_stoploss)
mocker.patch.multiple(
EXMS,
create_stoploss=stoploss,
fetch_ticker=ticker,
get_fee=fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
fetch_stoploss_order=stoploss_order_mock,
cancel_stoploss_order_with_result=cancel_order_mock,
)
mocker.patch.multiple(
@@ -80,6 +77,12 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
mocker.patch("freqtrade.wallets.Wallets.check_exit_amount", return_value=True)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch.multiple(
freqtrade.exchange,
create_stoploss=stoploss,
fetch_stoploss_order=stoploss_order_mock,
cancel_stoploss_order_with_result=cancel_order_mock,
)
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
# Switch ordertype to market to close trade immediately
freqtrade.strategy.order_types["exit"] = "market"

View File

@@ -103,7 +103,7 @@ def test_handle_stoploss_on_exchange(
trade.is_open = True
hanging_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "open"})
mocker.patch(f"{EXMS}.fetch_stoploss_order", hanging_stoploss_order)
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", hanging_stoploss_order)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
hanging_stoploss_order.assert_called_once_with("13434334", trade.pair)
@@ -116,7 +116,7 @@ def test_handle_stoploss_on_exchange(
trade.is_open = True
canceled_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "canceled"})
mocker.patch(f"{EXMS}.fetch_stoploss_order", canceled_stoploss_order)
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", canceled_stoploss_order)
stoploss.reset_mock()
amount_before = trade.amount
@@ -149,7 +149,7 @@ def test_handle_stoploss_on_exchange(
"amount": enter_order["amount"],
}
)
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_order_hit)
freqtrade.strategy.order_filled = MagicMock(return_value=None)
assert freqtrade.handle_stoploss_on_exchange(trade) is True
assert log_has_re(r"STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.", caplog)
@@ -158,7 +158,7 @@ def test_handle_stoploss_on_exchange(
assert freqtrade.strategy.order_filled.call_count == 1
caplog.clear()
mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError())
mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError())
trade.is_open = True
freqtrade.handle_stoploss_on_exchange(trade)
assert log_has("Unable to place a stoploss order on exchange.", caplog)
@@ -168,8 +168,13 @@ def test_handle_stoploss_on_exchange(
# It should try to add stoploss order
stop_order_dict.update({"id": "105"})
stoploss.reset_mock()
mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=InvalidOrderException())
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=MagicMock(
side_effect=InvalidOrderException(),
),
create_stoploss=stoploss,
)
freqtrade.handle_stoploss_on_exchange(trade)
assert len(trade.open_sl_orders) == 1
assert stoploss.call_count == 1
@@ -179,8 +184,7 @@ def test_handle_stoploss_on_exchange(
trade.is_open = False
trade.open_sl_orders[-1].ft_is_open = False
stoploss.reset_mock()
mocker.patch(f"{EXMS}.fetch_order")
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
mocker.patch.multiple(freqtrade.exchange, fetch_order=MagicMock(), create_stoploss=stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.has_open_sl_orders is False
assert stoploss.call_count == 0
@@ -252,9 +256,12 @@ def test_handle_stoploss_on_exchange_emergency(
stoploss = MagicMock(side_effect=InvalidOrderException())
assert trade.has_open_sl_orders is True
Trade.commit()
mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", side_effect=InvalidOrderException())
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_cancelled)
mocker.patch(f"{EXMS}.create_stoploss", stoploss)
mocker.patch.multiple(
freqtrade.exchange,
cancel_stoploss_order_with_result=MagicMock(side_effect=InvalidOrderException()),
fetch_stoploss_order=stoploss_order_cancelled,
create_stoploss=stoploss,
)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.has_open_sl_orders is False
assert trade.is_open is False
@@ -311,7 +318,7 @@ def test_handle_stoploss_on_exchange_partial(
"amount": enter_order["amount"],
}
)
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
mocker.patch.multiple(freqtrade.exchange, fetch_stoploss_order=stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# Stoploss filled partially ...
assert trade.amount == 15
@@ -383,8 +390,11 @@ def test_handle_stoploss_on_exchange_partial_cancel_here(
"amount": enter_order["amount"],
}
)
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit)
mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", stoploss_order_cancel)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=stoploss_order_hit,
cancel_stoploss_order_with_result=stoploss_order_cancel,
)
time_machine.shift(timedelta(minutes=15))
assert freqtrade.handle_stoploss_on_exchange(trade) is False
@@ -408,20 +418,20 @@ def test_handle_sle_cancel_cant_recreate(
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf_usdt)
mocker.patch.multiple(
freqtrade.exchange,
create_order=MagicMock(
side_effect=[
enter_order,
exit_order,
]
),
get_fee=fee,
)
mocker.patch.multiple(
EXMS,
fetch_stoploss_order=MagicMock(return_value={"status": "canceled", "id": "100"}),
create_stoploss=MagicMock(side_effect=ExchangeError()),
)
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
freqtrade.enter_positions()
@@ -644,8 +654,11 @@ def test_handle_stoploss_on_exchange_trailing(
stoploss_order_cancel = deepcopy(stoploss_order_hanging)
stoploss_order_cancel["status"] = "canceled"
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging)
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=stoploss_order_cancel)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=MagicMock(return_value=stoploss_order_hanging),
cancel_stoploss_order=MagicMock(return_value=stoploss_order_cancel),
)
# stoploss initially at 5%
assert freqtrade.handle_trade(trade) is False
@@ -671,9 +684,12 @@ def test_handle_stoploss_on_exchange_trailing(
return_value={"id": "13434334", "status": "canceled", "fee": {}, "amount": trade.amount}
)
stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"})
mocker.patch(f"{EXMS}.fetch_stoploss_order")
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=MagicMock(),
cancel_stoploss_order=cancel_order_mock,
create_stoploss=stoploss_order_mock,
)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@@ -711,8 +727,9 @@ def test_handle_stoploss_on_exchange_trailing(
}
),
)
mocker.patch(
f"{EXMS}.cancel_stoploss_order_with_result",
mocker.patch.object(
freqtrade.exchange,
"cancel_stoploss_order_with_result",
return_value={"id": "so1", "status": "canceled"},
)
assert len(trade.open_sl_orders) == 1
@@ -786,8 +803,12 @@ def test_handle_stoploss_on_exchange_trailing_error(
order_date=dt_now(),
)
)
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException())
mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging)
mocker.patch.object(
freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException()
)
mocker.patch.object(
freqtrade.exchange, "fetch_stoploss_order", return_value=stoploss_order_hanging
)
time_machine.shift(timedelta(minutes=50))
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog)
@@ -799,8 +820,8 @@ def test_handle_stoploss_on_exchange_trailing_error(
# Fail creating stoploss order
caplog.clear()
cancel_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order")
mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError())
cancel_mock = mocker.patch.object(freqtrade.exchange, "cancel_stoploss_order")
mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError())
time_machine.shift(timedelta(minutes=50))
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 2
@@ -846,20 +867,9 @@ def test_handle_stoploss_on_exchange_custom_stop(
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
create_order=MagicMock(
side_effect=[
enter_order,
exit_order,
]
),
get_fee=fee,
is_cancel_order_result_suitable=MagicMock(return_value=True),
)
mocker.patch.multiple(
EXMS,
create_stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
# enabling TSL
default_conf_usdt["use_custom_stoploss"] = True
@@ -868,6 +878,17 @@ def test_handle_stoploss_on_exchange_custom_stop(
default_conf_usdt["minimal_roi"]["0"] = 999999999
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple(
freqtrade.exchange,
create_order=MagicMock(
side_effect=[
enter_order,
exit_order,
]
),
create_stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
# enabling stoploss on exchange
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
@@ -912,8 +933,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
x["id"] = order_id
return x
mocker.patch(f"{EXMS}.fetch_stoploss_order", MagicMock(fetch_stoploss_order_mock))
mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=slo_canceled)
mocker.patch.multiple(
freqtrade.exchange,
fetch_stoploss_order=MagicMock(fetch_stoploss_order_mock),
cancel_stoploss_order=MagicMock(return_value=slo_canceled),
)
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
@@ -932,8 +956,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"})
mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock)
mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock)
mocker.patch.multiple(
freqtrade.exchange,
cancel_stoploss_order=cancel_order_mock,
create_stoploss=stoploss_order_mock,
)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@@ -1054,7 +1081,9 @@ def test_execute_trade_exit_sloe_cancel_exception(
mocker, default_conf_usdt, ticker_usdt, fee, caplog
) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException())
mocker.patch.object(
freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException()
)
mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=300))
create_order_mock = MagicMock(
side_effect=[
@@ -1114,12 +1143,15 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
get_fee=fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
)
freqtrade = FreqtradeBot(default_conf_usdt)
mocker.patch.multiple(
freqtrade.exchange,
create_stoploss=stoploss,
cancel_stoploss_order=cancel_order,
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
)
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.order_types["stoploss_on_exchange"] = True
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
@@ -1208,7 +1240,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
"trades": None,
}
)
mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_executed)
mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_executed)
freqtrade.exit_positions(trades)
assert trade.has_open_sl_orders is False

View File

@@ -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.0180457882, 15),
(False, -0.0178000543, 12),
(True, -0.018054162, 10),
(False, -0.01780296, 6),
],
)
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, 78, -1.17988972),
(False, 6, 360, 34, -0.14673681),
(True, 50, 3000, 55, -1.18038144),
(False, 6, 360, 11, -0.14679994),
],
)
def test_backtest_one_detail_futures_funding_fees(
@@ -2382,12 +2382,13 @@ 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, 1h, data starts at 2021-11-18 00:00:00",
"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",
f"Running backtesting for Strategy {CURRENT_TEST_STRATEGY}",
]
for line in exists:
assert log_has(line, caplog), line
assert log_has(line, caplog)
captured = capsys.readouterr()
assert "BACKTESTING REPORT" in captured.out

View File

@@ -386,11 +386,14 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
mocker.patch.multiple(
freqtradebot.exchange,
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
freqtradebot.strategy.order_types["stoploss_on_exchange"] = True
create_mock_trades(fee, is_short)
rpc = RPC(freqtradebot)
@@ -426,13 +429,17 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
assert stoploss_mock.call_count == 1
assert res["cancel_order_count"] == 1
stoploss_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException)
stoploss_mock = mocker.patch.object(
freqtradebot.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException
)
res = rpc._rpc_delete("3")
assert stoploss_mock.call_count == 1
stoploss_mock.reset_mock()
cancel_mock = mocker.patch(f"{EXMS}.cancel_order", side_effect=InvalidOrderException)
cancel_mock = mocker.patch.object(
freqtradebot.exchange, "cancel_order", side_effect=InvalidOrderException
)
res = rpc._rpc_delete("4")
assert cancel_mock.call_count == 1

View File

@@ -1034,8 +1034,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
stoploss_mock = MagicMock()
cancel_mock = MagicMock()
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
ftbot.exchange,
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)

Binary file not shown.

View File

@@ -20,8 +20,8 @@ def test_binance_mig_data_conversion(default_conf_usdt, tmp_path, testdatadir):
files = [
"-1h-mark.feather",
"-1h-futures.feather",
"-1h-funding_rate.feather",
"-1h-mark.feather",
"-8h-funding_rate.feather",
"-8h-mark.feather",
]
# Copy files to tmpdir and rename to old naming

View File

@@ -5,13 +5,13 @@ 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_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_4h = tmp_path / "futures" / "XRP_USDT_USDT-4h-funding_rate.feather"
file_8h = tmp_path / "futures" / "XRP_USDT_USDT-8h-funding_rate.feather"
file_1h = tmp_path / "futures" / "XRP_USDT_USDT-1h-futures.feather"
file_1h_fr.rename(file_30m)
file_8h.rename(file_4h)
assert file_1h.exists()
assert file_30m.exists()
assert not file_1h_fr.exists()
assert file_4h.exists()
assert not file_8h.exists()
default_conf_usdt["datadir"] = tmp_path
@@ -22,7 +22,7 @@ def test_migrate_funding_rate_timeframe(default_conf_usdt, tmp_path, testdatadir
migrate_funding_fee_timeframe(default_conf_usdt, None)
assert not file_30m.exists()
assert file_1h_fr.exists()
assert not file_4h.exists()
assert file_8h.exists()
# futures files is untouched.
assert file_1h.exists()