Merge branch 'freqtrade:develop' into feature/hyperliquid-hip3-support

This commit is contained in:
igi01
2025-12-21 07:30:20 +01:00
committed by GitHub
69 changed files with 5800 additions and 6249 deletions
@@ -15,7 +15,7 @@ jobs:
environment:
name: develop
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
+8 -8
View File
@@ -28,7 +28,7 @@ jobs:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -177,7 +177,7 @@ jobs:
name: "Mypy Version Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -195,7 +195,7 @@ jobs:
name: "Pre-commit checks"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -208,7 +208,7 @@ jobs:
name: "Documentation build"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -240,7 +240,7 @@ jobs:
name: "Tests and Linting - Online tests"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -320,7 +320,7 @@ jobs:
with:
jobs: ${{ toJSON(needs) }}
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -367,7 +367,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -396,7 +396,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
name: Deploy Docs through mike
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: true
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
- name: Login to GitHub Container Registry
+8 -2
View File
@@ -33,10 +33,13 @@ jobs:
if: github.repository == 'freqtrade/freqtrade'
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
- name: Visualize disk usage before build
run: df -h
- name: Set docker tag names
id: tags
uses: ./.github/actions/docker-tags
@@ -142,6 +145,9 @@ jobs:
run: |
docker images
- name: Visualize disk usage after build
run: df -h
deploy-arm:
name: "Deploy Docker ARM64"
permissions:
@@ -152,7 +158,7 @@ jobs:
if: github.repository == 'freqtrade/freqtrade'
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
auto-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.0
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
# actions: read # only needed for private repos
steps:
- name: Checkout repository
uses: actions/checkout@v6.0.0
uses: actions/checkout@v6.0.1
with:
persist-credentials: false
+3 -3
View File
@@ -21,7 +21,7 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.19.0"
rev: "v1.19.1"
hooks:
- id: mypy
exclude: build_helpers
@@ -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.9'
hooks:
- id: ruff
- id: ruff-format
+2 -2
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)
+6
View File
@@ -11,6 +11,7 @@ usage: freqtrade download-data [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--data-format-ohlcv {json,jsongz,feather,parquet}]
[--data-format-trades {json,jsongz,feather,parquet}]
[--trading-mode {spot,margin,futures}]
[--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]]
[--prepend]
options:
@@ -50,6 +51,11 @@ options:
`feather`).
--trading-mode, --tradingmode {spot,margin,futures}
Select Trading mode
--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]
Select candle type to download. Defaults to the
necessary candles for the selected trading mode (e.g.
'spot' or ('futures', 'funding_rate' and 'mark') for
futures).
--prepend Allow data prepending. (Data-appending is disabled)
Common arguments:
+1
View File
@@ -60,6 +60,7 @@ freqtrade download-data --exchange binance --pairs ".*/USDT"
* Given starting points are ignored if data is already available, downloading only missing data up to today.
* Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
* To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
* When downloading futures data (`--trading-mode futures` or a configuration specifying futures mode), freqtrade will automatically download the necessary candle types (e.g. `mark` and `funding_rate` candles) unless specified otherwise via `--candle-types`.
??? Note "Permission denied errors"
If your configuration directory `user_data` was made by docker, you may get the following error:
+37
View File
@@ -98,3 +98,40 @@ 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.
### Strategy
Most strategies should not need adjustments to continue to work as expected - however, strategies using `@informative("8h", candle_type="funding_rate")` or similar will have to switch the timeframe to 1h.
The same is true for `dp.get_pair_dataframe(metadata["pair"], "8h", candle_type="funding_rate")` - which will need to be switched to 1h.
freqtrade will auto-adjust the timeframe and return `funding_rates` despite the wrongly given timeframe. It'll issue a warning - and may still break your strategy.
### Selective data re-download
The script below should serve as an example - you may need to adjust the timeframe and exchange to your needs!
``` 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.
+1 -1
View File
@@ -2,6 +2,6 @@ markdown==3.10
mkdocs==1.6.1
mkdocs-material==9.7.0
mdx_truly_sane_lists==1.3
pymdown-extensions==10.17.2
pymdown-extensions==10.18
jinja2==3.1.6
mike==2.1.3
+6 -1
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.
+1 -1
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
+7 -1
View File
@@ -3,6 +3,7 @@ This module contains the argument manager class
"""
from argparse import ArgumentParser, Namespace, _ArgumentGroup
from copy import deepcopy
from functools import partial
from pathlib import Path
from typing import Any
@@ -174,6 +175,7 @@ ARGS_DOWNLOAD_DATA = [
"dataformat_ohlcv",
"dataformat_trades",
"trading_mode",
"candle_types",
"prepend_data",
]
@@ -348,7 +350,11 @@ class Arguments:
def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val]
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
options = deepcopy(opt.kwargs)
help_text = options.pop("help", None)
if opt.fthelp and isinstance(opt.fthelp, dict) and hasattr(parser, "prog"):
help_text = opt.fthelp.get(parser.prog, help_text)
parser.add_argument(*opt.cli, dest=val, help=help_text, **options)
def _build_subcommands(self) -> None:
"""
+15 -1
View File
@@ -38,8 +38,14 @@ def check_int_nonzero(value: str) -> int:
class Arg:
# Optional CLI arguments
def __init__(self, *args, **kwargs):
def __init__(self, *args, fthelp: dict[str, str] | None = None, **kwargs):
"""
CLI Arguments - used to build subcommand parsers consistently.
:param fthelp: dict - fthelp per command - should be "freqtrade <command>": help_text
If not provided or not found, 'help' from kwargs is used instead.
"""
self.cli = args
self.fthelp = fthelp
self.kwargs = kwargs
@@ -422,6 +428,14 @@ AVAILABLE_CLI_OPTIONS = {
),
"candle_types": Arg(
"--candle-types",
fthelp={
"freqtrade download-data": (
"Select candle type to download. "
"Defaults to the necessary candles for the selected trading mode "
"(e.g. 'spot' or ('futures', 'funding_rate' and 'mark') for futures)."
),
"_": "Select candle type to convert. Defaults to all available types.",
},
help="Select candle type to convert. Defaults to all available types.",
choices=[c.value for c in CandleType],
nargs="+",
+2 -1
View File
@@ -38,7 +38,8 @@ def ohlcv_to_dataframe(
cols = DEFAULT_DATAFRAME_COLUMNS
df = DataFrame(ohlcv, columns=cols)
df["date"] = to_datetime(df["date"], unit="ms", utc=True)
# Floor date to seconds to account for exchange imprecisions
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s")
# Some exchanges return int values for Volume and even for OHLC.
# Convert them since TA-LIB indicators used in the strategy assume floats
+26
View File
@@ -348,6 +348,22 @@ 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:
@@ -361,6 +377,7 @@ class DataProvider:
:return: Dataframe for this pair
:param candle_type: '', mark, index, premiumIndex, or funding_rate
"""
timeframe = self.__fix_funding_rate_timeframe(pair, timeframe, candle_type)
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
# Get live OHLCV data.
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
@@ -620,3 +637,12 @@ class DataProvider:
except ExchangeError:
logger.warning(f"Could not fetch market data for {pair}. Assuming no delisting.")
return None
def get_funding_rate_timeframe(self) -> str:
"""
Get the funding rate timeframe from exchange options
:return: Timeframe string
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
return self._exchange.get_option("funding_fee_timeframe")
@@ -397,6 +397,9 @@ class IDataHandler(ABC):
pairdf = self._ohlcv_load(
pair, timeframe, timerange=timerange_startup, candle_type=candle_type
)
if not pairdf.empty and candle_type == CandleType.FUNDING_RATE:
# Funding rate data is sometimes off by a couple of ms - floor to seconds
pairdf["date"] = pairdf["date"].dt.floor("s")
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
return pairdf
else:
@@ -508,8 +511,15 @@ class IDataHandler(ABC):
Applies to bybit and okx, where funding-fee and mark candles have different timeframes.
"""
paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES)
ff_timeframe_s = timeframe_to_seconds(ff_timeframe)
funding_rate_combs = [
f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe
f
for f in paircombs
if f[2] == CandleType.FUNDING_RATE
and f[1] != ff_timeframe
# Only allow smaller timeframes to move from smaller to larger timeframes
and timeframe_to_seconds(f[1]) < ff_timeframe_s
]
if funding_rate_combs:
+42 -32
View File
@@ -353,6 +353,7 @@ def _download_pair_history(
def refresh_backtest_ohlcv_data(
exchange: Exchange,
*,
pairs: list[str],
timeframes: list[str],
datadir: Path,
@@ -363,6 +364,7 @@ def refresh_backtest_ohlcv_data(
data_format: str | None = None,
prepend: bool = False,
progress_tracker: CustomProgress | None = None,
candle_types: list[CandleType] | None = None,
no_parallel_download: bool = False,
) -> list[str]:
"""
@@ -375,10 +377,44 @@ def refresh_backtest_ohlcv_data(
pairs_not_available = []
fast_candles: dict[PairWithTimeframe, DataFrame] = {}
data_handler = get_datahandler(datadir, data_format)
candle_type = CandleType.get_default(trading_mode)
def_candletype = CandleType.SPOT if trading_mode != "futures" else CandleType.FUTURES
if trading_mode != "futures":
# Ignore user passed candle types for non-futures trading
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
else:
# Filter out SPOT candle type for futures trading
candle_types = (
[ct for ct in candle_types if ct != CandleType.SPOT] if candle_types else None
)
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
if candle_types:
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)
)
with progress_tracker as progress:
tf_length = len(timeframes) if trading_mode != "futures" else len(timeframes) + 2
timeframe_task = progress.add_task("Timeframe", total=tf_length)
timeframe_task = progress.add_task("Timeframe", total=len(timeframes_with_candletype))
pair_task = progress.add_task("Downloading data...", total=len(pairs))
for pair in pairs:
@@ -389,7 +425,7 @@ def refresh_backtest_ohlcv_data(
pairs_not_available.append(f"{pair}: Pair not available on exchange.")
logger.info(f"Skipping pair {pair}...")
continue
for timeframe in timeframes:
for timeframe, candle_type in timeframes_with_candletype:
# Get fast candles via parallel method on first loop through per timeframe
# and candle type. Downloads all the pairs in the list and stores them.
# Also skips if only 1 pair/timeframe combination is scheduled for download.
@@ -416,7 +452,7 @@ def refresh_backtest_ohlcv_data(
# get the already downloaded pair candles if they exist
pair_candles = fast_candles.pop((pair, timeframe, candle_type), None)
progress.update(timeframe_task, description=f"Timeframe {timeframe}")
progress.update(timeframe_task, description=f"Timeframe {timeframe} {candle_type}")
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
_download_pair_history(
pair=pair,
@@ -432,33 +468,6 @@ def refresh_backtest_ohlcv_data(
pair_candles=pair_candles, # optional pass of dataframe of parallel candles
)
progress.update(timeframe_task, advance=1)
if trading_mode == "futures":
# Predefined candletype (and timeframe) depending on exchange
# Downloads what is necessary to backtest based on futures data.
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
# All exchanges need FundingRate for futures trading.
# The timeframe is aligned to the mark-price timeframe.
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
for candle_type_f, tf in combs:
logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.")
_download_pair_history(
pair=pair,
datadir=datadir,
exchange=exchange,
timerange=timerange,
data_handler=data_handler,
timeframe=str(tf),
new_pairs_days=new_pairs_days,
candle_type=candle_type_f,
erase=erase,
prepend=prepend,
)
progress.update(
timeframe_task, advance=1, description=f"Timeframe {candle_type_f}, {tf}"
)
progress.update(pair_task, advance=1)
progress.update(timeframe_task, description="Timeframe")
@@ -804,6 +813,7 @@ def download_data(
trading_mode=config.get("trading_mode", "spot"),
prepend=config.get("prepend_data", False),
progress_tracker=progress_tracker,
candle_types=config.get("candle_types"),
no_parallel_download=config.get("no_parallel_download", False),
)
finally:
+2 -1
View File
@@ -74,9 +74,10 @@ def combined_dataframes_with_rel_mean(
df_comb = combine_dataframes_by_column(data, column)
# Trim dataframes to the given timeframe
df_comb = df_comb.iloc[(df_comb.index >= fromdt) & (df_comb.index < todt)]
rel_mean = df_comb.pct_change().mean(axis=1).fillna(0).cumsum()
df_comb["count"] = df_comb.count(axis=1)
df_comb["mean"] = df_comb.mean(axis=1)
df_comb["rel_mean"] = df_comb["mean"].pct_change().fillna(0).cumsum()
df_comb["rel_mean"] = rel_mean
return df_comb[["mean", "rel_mean", "count"]]
+1 -1
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
+25
View File
@@ -51,6 +51,8 @@ class Binance(Exchange):
"funding_fee_candle_limit": 1000,
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
"stoploss_query_requires_stop_flag": True,
"stoploss_algo_order_info_id": "actualOrderId",
"tickers_have_price": False,
"floor_leverage": True,
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
@@ -544,3 +546,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
+1 -4
View File
@@ -31,11 +31,11 @@ class Bitget(Exchange):
"stop_price_prop": "stopPrice",
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
"stoploss_order_types": {"limit": "limit", "market": "market"},
"stoploss_query_requires_stop_flag": True,
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
}
_ft_has_futures: FtHas = {
"mark_ohlcv_timeframe": "4h",
"funding_fee_candle_limit": 100,
"has_delisting": True,
}
@@ -129,9 +129,6 @@ class Bitget(Exchange):
return self._fetch_stop_order_fallback(order_id, pair)
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
@retrier
def additional_exchange_init(self) -> None:
"""
-2
View File
@@ -38,8 +38,6 @@ 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"},
+5 -2
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,6 +97,9 @@ EXCHANGE_HAS_OPTIONAL = [
# 'fetchLeverageTiers', # Futures initialization
# 'fetchMarketLeverageTiers', # Futures initialization
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
# "fetchPremiumIndexOHLCV", # Futures additional data
# "fetchMarkOHLCV", # Futures additional data
# "fetchIndexOHLCV", # Futures additional data
# ccxt.pro
"watchOHLCV",
]
+92 -24
View File
@@ -132,6 +132,7 @@ class Exchange:
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
"stoploss_order_types": {},
"stoploss_blocks_assets": True, # By default stoploss orders block assets
"stoploss_query_requires_stop_flag": False, # Require "stop": True" to fetch stop orders
"order_time_in_force": ["GTC"],
"ohlcv_params": {},
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
@@ -153,8 +154,8 @@ class Exchange:
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"l2_limit_upper": None, # Upper limit for L2 limit
"mark_ohlcv_price": "mark",
"mark_ohlcv_timeframe": "8h",
"funding_fee_timeframe": "8h",
"mark_ohlcv_timeframe": "1h",
"funding_fee_timeframe": "1h",
"ccxt_futures_name": "swap",
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
"order_props_in_contracts": ["amount", "filled", "remaining"],
@@ -1405,8 +1406,9 @@ class Exchange:
amount: float,
rate: float,
leverage: float,
reduceOnly: bool = False,
time_in_force: str = "GTC",
reduceOnly: bool = False,
initial_order: bool = True,
) -> CcxtOrder:
if self._config["dry_run"]:
dry_order = self.create_dry_run_order(
@@ -1423,7 +1425,7 @@ class Exchange:
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
if not reduceOnly:
self._lev_prep(pair, leverage, side)
self._lev_prep(pair, leverage, side, accept_fail=not initial_order)
order = self._api.create_order(
pair,
@@ -1686,7 +1688,24 @@ class Exchange:
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
return self.fetch_order(order_id, pair, params)
if self.get_option("stoploss_query_requires_stop_flag"):
params = params or {}
params["stop"] = True
order = self.fetch_order(order_id, pair, params)
val = self.get_option("stoploss_algo_order_info_id")
if val and order.get("status", "open") == "closed":
if new_orderid := order.get("info", {}).get(val):
# Fetch real order, which was placed by the algo order.
actual_order = self.fetch_order(order_id=new_orderid, pair=pair, params=None)
actual_order["id_stop"] = actual_order["id"]
actual_order["id"] = order_id
actual_order["type"] = "stoploss"
actual_order["stopPrice"] = order.get("stopPrice")
actual_order["status_stop"] = "triggered"
return actual_order
return order
def fetch_order_or_stoploss_order(
self, order_id: str, pair: str, stoploss_order: bool = False
@@ -1740,6 +1759,9 @@ class Exchange:
raise OperationalException(e) from e
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
if self.get_option("stoploss_query_requires_stop_flag"):
params = params or {}
params["stop"] = True
return self.cancel_order(order_id, pair, params)
def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
@@ -1834,9 +1856,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,24 +2712,25 @@ class Exchange:
input_coroutines: list[Coroutine[Any, Any, OHLCVResponse]] = []
cached_pairs = []
for pair, timeframe, candle_type in set(pair_list):
invalid_funding = (
candle_type == CandleType.FUNDING_RATE
and timeframe != self.get_option("funding_fee_timeframe")
)
if candle_type == CandleType.FUNDING_RATE and timeframe != (
ff_tf := self.get_option("funding_fee_timeframe")
):
# TODO: does this message make sense? would docs be better?
# if any, this should be cached to avoid log spam!
logger.warning(
f"Wrong funding rate timeframe {timeframe} for pair {pair}, "
f"downloading {ff_tf} instead."
)
timeframe = ff_tf
invalid_timeframe = timeframe not in self.timeframes and candle_type in (
CandleType.SPOT,
CandleType.FUTURES,
)
if invalid_timeframe or invalid_funding:
timeframes_ = (
", ".join(self.timeframes)
if candle_type != CandleType.FUNDING_RATE
else self.get_option("funding_fee_timeframe")
)
if invalid_timeframe:
logger.warning(
f"Cannot download ({pair}, {timeframe}, {candle_type}) combination as this "
f"timeframe is not available on {self.name}. Available timeframes are "
f"{timeframes_}."
f"{', '.join(self.timeframes)}."
)
continue
@@ -2744,7 +2767,11 @@ class Exchange:
has_cache = cache and (pair, timeframe, c_type) in self._klines
# in case of existing cache, fill_missing happens after concatenation
ohlcv_df = ohlcv_to_dataframe(
ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
ticks,
timeframe,
pair=pair,
fill_missing=not has_cache and c_type != CandleType.FUNDING_RATE,
drop_incomplete=drop_incomplete,
)
# keeping parsed dataframe in cache
if cache:
@@ -2755,7 +2782,7 @@ class Exchange:
concat([old, ohlcv_df], axis=0),
timeframe,
pair,
fill_missing=True,
fill_missing=c_type != CandleType.FUNDING_RATE,
drop_incomplete=False,
)
candle_limit = self.ohlcv_candle_limit(timeframe, self._config["candle_type_def"])
@@ -2890,9 +2917,10 @@ class Exchange:
timeframe, candle_type=candle_type, since_ms=since_ms
)
if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES):
params.update({"price": 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
)
@@ -2957,6 +2985,38 @@ 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:
@@ -3808,8 +3868,16 @@ class Exchange:
combined = mark_rates.merge(
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
)
combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
return combined[relevant_cols]
# Fill only leading missing funding rates so gaps stay untouched
first_valid_idx = combined["open_fund"].first_valid_index()
if first_valid_idx is None:
combined["open_fund"] = futures_funding_rate
else:
is_leading_na = (combined.index <= first_valid_idx) & combined[
"open_fund"
].isna()
combined.loc[is_leading_na, "open_fund"] = futures_funding_rate
return combined[relevant_cols].dropna()
def calculate_funding_fees(
self,
+2
View File
@@ -19,6 +19,8 @@ class FtHas(TypedDict, total=False):
stop_price_type_value_mapping: dict
stoploss_order_types: dict[str, str]
stoploss_blocks_assets: bool
stoploss_query_requires_stop_flag: bool
stoploss_algo_order_info_id: str
# ohlcv
ohlcv_params: dict
ohlcv_candle_limit: int
+3 -22
View File
@@ -30,6 +30,8 @@ class Gate(Exchange):
"stoploss_order_types": {"limit": "limit"},
"stop_price_param": "stopPrice",
"stop_price_prop": "stopPrice",
"stoploss_query_requires_stop_flag": True,
"stoploss_algo_order_info_id": "fired_order_id",
"l2_limit_upper": 1000,
"marketOrderRequiresPrice": True,
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
@@ -42,6 +44,7 @@ class Gate(Exchange):
"stop_price_type_field": "price_type",
"l2_limit_upper": 300,
"stoploss_blocks_assets": False,
"stoploss_algo_order_info_id": "trade_id",
"stop_price_type_value_mapping": {
PriceType.LAST: 0,
PriceType.MARK: 1,
@@ -132,25 +135,3 @@ class Gate(Exchange):
def get_order_id_conditional(self, order: CcxtOrder) -> str:
return safe_value_fallback2(order, order, "id_stop", "id")
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True})
if order.get("status", "open") == "closed":
# Places a real order - which we need to fetch explicitly.
val = "trade_id" if self.trading_mode == TradingMode.FUTURES else "fired_order_id"
if new_orderid := order.get("info", {}).get(val):
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
order1["id_stop"] = order1["id"]
order1["id"] = order_id
order1["type"] = "stoploss"
order1["stopPrice"] = order.get("stopPrice")
order1["status_stop"] = "triggered"
return order1
return order
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
-1
View File
@@ -35,7 +35,6 @@ class Kraken(Exchange):
"trades_pagination_arg": "since",
"trades_pagination_overlap": False,
"trades_has_history": True,
"mark_ohlcv_timeframe": "4h",
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
+3 -1
View File
@@ -44,8 +44,9 @@ class Kucoin(Exchange):
amount: float,
rate: float,
leverage: float,
reduceOnly: bool = False,
time_in_force: str = "GTC",
reduceOnly: bool = False,
initial_order: bool = True,
) -> CcxtOrder:
res = super().create_order(
pair=pair,
@@ -56,6 +57,7 @@ class Kucoin(Exchange):
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
initial_order=initial_order,
)
# Kucoin returns only the order-id.
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
+3 -14
View File
@@ -29,10 +29,9 @@ 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,
"stoploss_query_requires_stop_flag": True,
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
"ws_enabled": True,
}
@@ -41,8 +40,8 @@ class Okx(Exchange):
"stop_price_type_field": "slTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "index",
PriceType.INDEX: "mark",
PriceType.MARK: "mark",
PriceType.INDEX: "index",
},
"stoploss_blocks_assets": False,
"ws_enabled": True,
@@ -265,16 +264,6 @@ class Okx(Exchange):
return safe_value_fallback2(order, order, "id_stop", "id")
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,
)
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
orders = []
+28 -21
View File
@@ -937,6 +937,7 @@ class FreqtradeBot(LoggingMixin):
reduceOnly=False,
time_in_force=time_in_force,
leverage=leverage,
initial_order=trade is None,
)
order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
order_obj.ft_order_tag = enter_tag
@@ -2011,14 +2012,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!
@@ -2054,11 +2055,12 @@ class FreqtradeBot(LoggingMixin):
exit_tag: str | None = None,
ordertype: str | None = None,
sub_trade_amt: float | None = None,
skip_custom_exit_price: bool = False,
) -> bool:
"""
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
"""
@@ -2080,30 +2082,34 @@ class FreqtradeBot(LoggingMixin):
):
exit_type = "stoploss"
order_type = (
(ordertype or self.strategy.order_types[exit_type])
if exit_check.exit_type != ExitType.EMERGENCY_EXIT
else self.strategy.order_types.get("emergency_exit", "market")
)
# set custom_exit_price if available
proposed_limit_rate = limit
custom_exit_price = limit
current_profit = trade.calc_profit_ratio(limit)
custom_exit_price = strategy_safe_wrapper(
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
)(
pair=trade.pair,
trade=trade,
current_time=datetime.now(UTC),
proposed_rate=proposed_limit_rate,
current_profit=current_profit,
exit_tag=exit_reason,
)
if order_type == "limit" and not skip_custom_exit_price:
custom_exit_price = strategy_safe_wrapper(
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
)(
pair=trade.pair,
trade=trade,
current_time=datetime.now(UTC),
proposed_rate=proposed_limit_rate,
current_profit=current_profit,
exit_tag=exit_reason,
)
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
# First cancelling stoploss on exchange ...
trade = self.cancel_stoploss_on_exchange(trade, allow_nonblocking=True)
order_type = ordertype or self.strategy.order_types[exit_type]
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
# Emergency sells (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)
time_in_force = self.strategy.order_time_in_force["exit"]
@@ -2130,7 +2136,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,
@@ -2140,6 +2146,7 @@ class FreqtradeBot(LoggingMixin):
leverage=trade.leverage,
reduceOnly=self.trading_mode == TradingMode.FUTURES,
time_in_force=time_in_force,
initial_order=False,
)
except InsufficientFundsError as e:
logger.warning(f"Unable to place order {e}.")
@@ -2157,7 +2164,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()
+1
View File
@@ -374,6 +374,7 @@ class Backtesting:
timerange=self.timerange,
startup_candles=0,
fail_without_data=True,
fill_up_missing=False,
data_format=self.config["dataformat_ohlcv"],
candle_type=CandleType.FUNDING_RATE,
)
@@ -63,6 +63,8 @@ def pairlists_evaluate(
config_loc["timeframes"] = payload.timeframes
config_loc["erase"] = payload.erase
config_loc["download_trades"] = payload.download_trades
if payload.candle_types is not None:
config_loc["candle_types"] = payload.candle_types
handleExchangePayload(payload, config_loc)
+2
View File
@@ -426,6 +426,7 @@ class ForceExitPayload(BaseModel):
tradeid: str | int
ordertype: OrderTypeValues | None = None
amount: float | None = None
price: float | None = None
class BlacklistPayload(BaseModel):
@@ -506,6 +507,7 @@ class DownloadDataPayload(ExchangeModePayloadMixin, BaseModel):
timerange: str | None = None
erase: bool = False
download_trades: bool = False
candle_types: list[str] | None = None
@model_validator(mode="before")
def check_mutually_exclusive(cls, values):
+6 -2
View File
@@ -91,7 +91,9 @@ logger = logging.getLogger(__name__)
# 2.41: Add download-data endpoint
# 2.42: Add /pair_history endpoint with live data
# 2.43: Add /profit_all endpoint
API_VERSION = 2.43
# 2.44: Add candle_types parameter to download-data endpoint
# 2.45: Add price to forceexit endpoint
API_VERSION = 2.45
# Public API, requires no auth.
router_public = APIRouter()
@@ -324,7 +326,9 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
@router.post("/forcesell", response_model=ResultMsg, tags=["trading"])
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_force_exit(str(payload.tradeid), ordertype, amount=payload.amount)
return rpc._rpc_force_exit(
str(payload.tradeid), ordertype, amount=payload.amount, price=payload.price
)
@router.get("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"])
+26 -7
View File
@@ -940,7 +940,11 @@ class RPC:
return {"status": "Reloaded from orders from exchange"}
def __exec_force_exit(
self, trade: Trade, ordertype: str | None, amount: float | None = None
self,
trade: Trade,
ordertype: str | None,
amount: float | None = None,
price: float | None = None,
) -> bool:
# Check if there is there are open orders
trade_entry_cancelation_registry = []
@@ -964,8 +968,13 @@ class RPC:
# Order cancellation failed, so we can't exit.
return False
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True
current_rate = (
self._freqtrade.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True
)
if ordertype == "market" or price is None
else price
)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
@@ -983,18 +992,28 @@ class RPC:
sub_amount = amount
self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type, sub_trade_amt=sub_amount
trade,
current_rate,
exit_check,
ordertype=order_type,
sub_trade_amt=sub_amount,
skip_custom_exit_price=price is not None and ordertype == "limit",
)
return True
return False
def _rpc_force_exit(
self, trade_id: str, ordertype: str | None = None, *, amount: float | None = None
self,
trade_id: str,
ordertype: str | None = None,
*,
amount: float | None = None,
price: float | None = None,
) -> dict[str, str]:
"""
Handler for forceexit <id>.
Sells the given trade at current price
exits the given trade. Uses current price if price is None.
"""
if self._freqtrade.state == State.STOPPED:
@@ -1024,7 +1043,7 @@ class RPC:
logger.warning("force_exit: Invalid argument received")
raise RPCException("invalid argument")
result = self.__exec_force_exit(trade, ordertype, amount)
result = self.__exec_force_exit(trade, ordertype, amount, price)
Trade.commit()
self._freqtrade.wallets.update()
if not result:
+6 -3
View File
@@ -104,8 +104,11 @@ def _create_and_merge_informative_pair(
):
asset = inf_data.asset or ""
timeframe = inf_data.timeframe
timeframe1 = inf_data.timeframe
fmt = inf_data.fmt
candle_type = inf_data.candle_type
if candle_type == CandleType.FUNDING_RATE:
timeframe1 = strategy.dp.get_funding_rate_timeframe()
config = strategy.config
@@ -132,10 +135,10 @@ def _create_and_merge_informative_pair(
fmt = "{base}_{quote}_" + fmt # Informatives of other pairs
inf_metadata = {"pair": asset, "timeframe": timeframe}
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe, candle_type)
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe1, candle_type)
if inf_dataframe.empty:
raise ValueError(
f"Informative dataframe for ({asset}, {timeframe}, {candle_type}) is empty. "
f"Informative dataframe for ({asset}, {timeframe1}, {candle_type}) is empty. "
"Can't populate informative indicators."
)
inf_dataframe = populate_indicators_fn(strategy, inf_dataframe, inf_metadata)
@@ -163,7 +166,7 @@ def _create_and_merge_informative_pair(
dataframe,
inf_dataframe,
strategy.timeframe,
timeframe,
timeframe1,
ffill=inf_data.ffill,
append_timeframe=False,
date_column=date_column,
+1 -1
View File
@@ -1718,7 +1718,7 @@ class IStrategy(ABC, HyperStrategyMixin):
timeout_unit = self.config.get("unfilledtimeout", {}).get("unit", "minutes")
timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = current_time + timedelta(**timeout_kwargs)
timedout = order.status == "open" and order.order_date_utc < timeout_threshold
timedout = order.status == "open" and order.order_date_utc <= timeout_threshold
if timedout:
return True
time_method = (
+1 -1
View File
@@ -1,3 +1,3 @@
# Requirements for freqtrade client library
requests==2.32.5
python-rapidjson==1.22
python-rapidjson==1.23
+4 -4
View File
@@ -6,10 +6,10 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.14.6
mypy==1.18.2
ruff==0.14.8
mypy==1.19.0
pre-commit==4.5.0
pytest==9.0.1
pytest==9.0.2
pytest-asyncio==1.3.0
pytest-cov==7.0.0
pytest-mock==3.15.1
@@ -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
+1 -1
View File
@@ -5,7 +5,7 @@
torch==2.9.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==1.2.2
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
stable_baselines3==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
sb3_contrib>=2.2.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
# Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.1
+1 -1
View File
@@ -4,6 +4,6 @@
# Required for hyperopt
scipy==1.16.3
scikit-learn==1.7.2
filelock==3.20.0
filelock==3.20.1
optuna==4.6.0
cmaes==0.12.0
+3 -3
View File
@@ -1767,7 +1767,7 @@ def test_start_list_data(testdatadir, capsys):
pargs["config"] = None
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 16 pair / timeframe combinations." in captured.out
assert "Found 18 pair / timeframe combinations." in captured.out
assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out)
assert re.search(r"\n.* UNITTEST/BTC .* 1m, 5m, 8m, 30m .* spot |\n", captured.out)
@@ -1801,10 +1801,10 @@ def test_start_list_data(testdatadir, capsys):
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 6 pair / timeframe combinations." in captured.out
assert "Found 5 pair / timeframe combinations." in captured.out
assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out)
assert re.search(r"\n.* XRP/USDT:USDT .* 5m, 1h .* futures |\n", captured.out)
assert re.search(r"\n.* XRP/USDT:USDT .* 1h, 8h .* mark |\n", captured.out)
assert re.search(r"\n.* XRP/USDT:USDT .* 1h.* mark |\n", captured.out)
args = [
"list-data",
+8 -5
View File
@@ -290,20 +290,23 @@ def test_combine_dataframes_with_mean(testdatadir):
def test_combined_dataframes_with_rel_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
pairs = ["BTC/USDT", "XRP/USDT"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
df = combined_dataframes_with_rel_mean(
data, datetime(2018, 1, 12, tzinfo=UTC), datetime(2018, 1, 28, tzinfo=UTC)
data,
fromdt=data["BTC/USDT"].at[0, "date"],
todt=data["BTC/USDT"].at[data["BTC/USDT"].index[-1], "date"],
)
assert isinstance(df, DataFrame)
assert "ETH/BTC" not in df.columns
assert "ADA/BTC" not in df.columns
assert "BTC/USDT" not in df.columns
assert "XRP/USDT" not in df.columns
assert "mean" in df.columns
assert "rel_mean" in df.columns
assert "count" in df.columns
assert df.iloc[0]["count"] == 2
assert df.iloc[-1]["count"] == 2
assert len(df) < len(data["ETH/BTC"])
assert len(df) < len(data["BTC/USDT"])
assert df["rel_mean"].between(-0.5, 0.5).all()
def test_combine_dataframes_with_mean_no_data(testdatadir):
+5 -2
View File
@@ -40,6 +40,8 @@ def test_datahandler_ohlcv_get_pairs(testdatadir):
"NXT/BTC",
"DASH/BTC",
"XRP/ETH",
"BTC/USDT",
"XRP/USDT",
}
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, "8m", candle_type=CandleType.SPOT)
@@ -111,6 +113,8 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
("DASH/BTC", "5m", CandleType.SPOT),
("XRP/ETH", "1m", CandleType.SPOT),
("XRP/ETH", "5m", CandleType.SPOT),
("BTC/USDT", "5m", CandleType.SPOT),
("XRP/USDT", "5m", CandleType.SPOT),
("UNITTEST/BTC", "30m", CandleType.SPOT),
("UNITTEST/BTC", "8m", CandleType.SPOT),
}
@@ -122,8 +126,7 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
("XRP/USDT:USDT", "5m", "futures"),
("XRP/USDT:USDT", "1h", "futures"),
("XRP/USDT:USDT", "1h", "mark"),
("XRP/USDT:USDT", "8h", "mark"),
("XRP/USDT:USDT", "8h", "funding_rate"),
("XRP/USDT:USDT", "1h", "funding_rate"),
}
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT)
+41 -1
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
from tests.conftest import EXMS, generate_test_data, get_patched_exchange, log_has_re
@pytest.mark.parametrize(
@@ -185,6 +185,28 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type):
assert len(df) == 2 # ohlcv_history is limited to 2 rows now
def test_get_pair_dataframe_funding_rate(mocker, default_conf, ohlcv_history, caplog):
default_conf["runmode"] = RunMode.DRY_RUN
timeframe = "1h"
exchange = get_patched_exchange(mocker, default_conf)
candletype = CandleType.FUNDING_RATE
exchange._klines[("XRP/BTC", timeframe, candletype)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", timeframe, candletype)] = ohlcv_history
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN
assert ohlcv_history.equals(
dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type="funding_rate")
)
msg = r".*funding rate timeframe not matching"
assert not log_has_re(msg, caplog)
assert ohlcv_history.equals(
dp.get_pair_dataframe("UNITTEST/BTC", "5h", candle_type="funding_rate")
)
assert log_has_re(msg, caplog)
def test_available_pairs(mocker, default_conf, ohlcv_history):
exchange = get_patched_exchange(mocker, default_conf)
timeframe = default_conf["timeframe"]
@@ -636,3 +658,21 @@ def test_check_delisting(mocker, default_conf_usdt):
assert res == dt_utc(2025, 10, 2)
assert delist_mock2.call_count == 1
def test_get_funding_rate_timeframe(mocker, default_conf_usdt):
default_conf_usdt["trading_mode"] = "futures"
default_conf_usdt["margin_mode"] = "isolated"
exchange = get_patched_exchange(mocker, default_conf_usdt)
mock_get_option = mocker.spy(exchange, "get_option")
dp = DataProvider(default_conf_usdt, exchange)
assert dp.get_funding_rate_timeframe() == "1h"
mock_get_option.assert_called_once_with("funding_fee_timeframe")
def test_get_funding_rate_timeframe_no_exchange(default_conf_usdt):
dp = DataProvider(default_conf_usdt, None)
with pytest.raises(OperationalException, match=r"Exchange is not available to DataProvider."):
dp.get_funding_rate_timeframe()
+28 -8
View File
@@ -534,18 +534,19 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
@pytest.mark.parametrize(
"trademode,callcount",
"trademode,callcount, callcount_parallel",
[
("spot", 4),
("margin", 4),
("futures", 8), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
("spot", 4, 2),
("margin", 4, 2),
("futures", 8, 4), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
],
)
def test_refresh_backtest_ohlcv_data(
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount, callcount_parallel
):
caplog.set_level(logging.DEBUG)
dl_mock = mocker.patch("freqtrade.data.history.history_utils._download_pair_history")
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}
@@ -573,14 +574,15 @@ def test_refresh_backtest_ohlcv_data(
)
# Called once per timeframe (as we return an empty dataframe)
assert parallel_mock.call_count == 2
# called twice for spot/margin and 4 times for futures
assert parallel_mock.call_count == callcount_parallel
assert dl_mock.call_count == callcount
assert dl_mock.call_args[1]["timerange"].starttype == "date"
assert log_has_re(r"Downloading pair ETH/BTC, .* interval 1m\.", caplog)
if trademode == "futures":
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 8h\.", caplog)
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 4h\.", caplog)
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 1h\.", caplog)
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 1h\.", caplog)
# Test with only one pair - no parallel download should happen 1 pair/timeframe combination
# doesn't justify parallelization
@@ -599,6 +601,24 @@ def test_refresh_backtest_ohlcv_data(
)
assert parallel_mock.call_count == 0
if trademode == "futures":
dl_mock.reset_mock()
refresh_backtest_ohlcv_data(
exchange=ex,
pairs=[
"ETH/BTC",
],
timeframes=["5m", "1h"],
datadir=testdatadir,
timerange=timerange,
erase=False,
trading_mode=trademode,
no_parallel_download=True,
candle_types=["premiumIndex", "funding_rate"],
)
assert parallel_mock.call_count == 0
assert dl_mock.call_count == 3 # 2 timeframes premiumIndex + 1x funding_rate
def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
dl_mock = mocker.patch(
+99 -22
View File
@@ -2389,6 +2389,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
]
]
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
mocker.patch.object(exchange, "verify_candle_type_support")
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
@@ -2439,6 +2440,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf_usdt, caplog, candle_type) ->
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf_usdt)
mocker.patch.object(exchange, "verify_candle_type_support")
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pairs = [("IOTA/USDT", "5m", candle_type), ("XRP/USDT", "5m", candle_type)]
@@ -2689,6 +2691,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach
time_machine.move_to(start + timedelta(hours=99, minutes=30))
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(exchange, "verify_candle_type_support")
exchange._set_startup_candle_count(default_conf)
mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100)
@@ -2837,6 +2840,29 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
assert ohlcv_mock.call_args_list[0][0][0] == pairs
def test_refresh_latest_ohlcv_funding_rate(mocker, default_conf_usdt, caplog) -> None:
ohlcv = generate_test_data_raw("1h", 24, "2025-01-02 12:00:00+00:00")
funding_data = [{"timestamp": x[0], "fundingRate": x[1]} for x in ohlcv]
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf_usdt)
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
exchange._api_async.fetch_funding_rate_history = get_mock_coro(funding_data)
pairs = [
("IOTA/USDT:USDT", "8h", CandleType.FUNDING_RATE),
("XRP/USDT:USDT", "1h", CandleType.FUNDING_RATE),
]
# empty dicts
assert not exchange._klines
res = exchange.refresh_latest_ohlcv(pairs, cache=False)
assert len(res) == len(pairs)
assert log_has_re(r"Wrong funding rate timeframe 8h for pair IOTA/USDT:USDT", caplog)
assert not log_has_re(r"Wrong funding rate timeframe 8h for pair XRP/USDT:USDT", caplog)
assert exchange._api_async.fetch_ohlcv.call_count == 0
@pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
ohlcv = [
@@ -3901,37 +3927,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)
@@ -4116,7 +4134,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,
)
@@ -5350,11 +5368,12 @@ def test_combine_funding_and_mark(
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
if futures_funding_rate is not None:
assert len(df) == 3
assert len(df) == 2
assert df.iloc[0]["open_fund"] == funding_rate
assert df.iloc[1]["open_fund"] == futures_funding_rate
assert df.iloc[2]["open_fund"] == funding_rate
assert df["date"].to_list() == [prior2_date, prior_date, trade_date]
# assert df.iloc[1]["open_fund"] == futures_funding_rate
assert df.iloc[-1]["open_fund"] == funding_rate
# Mid-candle is dropped ...
assert df["date"].to_list() == [prior2_date, trade_date]
else:
assert len(df) == 2
assert df["date"].to_list() == [prior2_date, trade_date]
@@ -5448,8 +5467,13 @@ def test__fetch_and_calculate_funding_fees(
api_mock = MagicMock()
api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history)
api_mock.fetch_ohlcv = get_mock_coro(return_value=mark_ohlcv)
type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True})
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True})
type(api_mock).has = PropertyMock(
return_value={
"fetchFundingRateHistory": True,
"fetchMarkOHLCV": True,
"fetchOHLCV": True,
}
)
ex = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange)
mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["1h", "4h", "8h"]))
@@ -5493,8 +5517,13 @@ def test__fetch_and_calculate_funding_fees_datetime_called(
api_mock.fetch_funding_rate_history = get_mock_coro(
return_value=funding_rate_history_octohourly
)
type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True})
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True})
type(api_mock).has = PropertyMock(
return_value={
"fetchFundingRateHistory": True,
"fetchMarkOHLCV": True,
"fetchOHLCV": True,
}
)
mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["4h", "8h"]))
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange)
d1 = datetime.strptime("2021-08-31 23:00:01 +0000", "%Y-%m-%d %H:%M:%S %z")
@@ -6581,3 +6610,51 @@ 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)
+8 -10
View File
@@ -16,9 +16,9 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert fetch_order_mock.call_count == 1
assert fetch_order_mock.call_args_list[0][1]["order_id"] == "1234"
assert fetch_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
assert fetch_order_mock.call_args_list[0][1]["params"] == {"stop": True}
assert fetch_order_mock.call_args_list[0][0][0] == "1234"
assert fetch_order_mock.call_args_list[0][0][1] == "ETH/BTC"
assert fetch_order_mock.call_args_list[0][0][2] == {"stop": True}
default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated"
@@ -36,21 +36,19 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert exchange.fetch_order.call_count == 2
assert exchange.fetch_order.call_args_list[0][1]["order_id"] == "1234"
assert exchange.fetch_order.call_args_list[0][0][0] == "1234"
assert exchange.fetch_order.call_args_list[1][1]["order_id"] == "222555"
def test_cancel_stoploss_order_gate(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, exchange="gate")
cancel_order_mock = MagicMock()
exchange.cancel_order = cancel_order_mock
cancel_order_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
exchange.cancel_stoploss_order("1234", "ETH/BTC")
assert cancel_order_mock.call_count == 1
assert cancel_order_mock.call_args_list[0][1]["order_id"] == "1234"
assert cancel_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
assert cancel_order_mock.call_args_list[0][1]["params"] == {"stop": True}
assert cancel_order_mock.call_args_list[0][0][0] == "1234"
assert cancel_order_mock.call_args_list[0][0][1] == "ETH/BTC"
assert cancel_order_mock.call_args_list[0][0][2] == {"stop": True}
@pytest.mark.parametrize(
+6 -6
View File
@@ -661,14 +661,14 @@ def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side):
def test_stoploss_cancel_okx(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, exchange="okx")
exchange.cancel_order = MagicMock()
co_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
exchange.cancel_stoploss_order("1234", "ETH/USDT")
assert exchange.cancel_order.call_count == 1
assert exchange.cancel_order.call_args_list[0][1]["order_id"] == "1234"
assert exchange.cancel_order.call_args_list[0][1]["pair"] == "ETH/USDT"
assert exchange.cancel_order.call_args_list[0][1]["params"] == {"stop": True}
assert co_mock.call_count == 1
args, _ = co_mock.call_args
assert args[0] == "1234"
assert args[1] == "ETH/USDT"
assert args[2] == {"stop": True}
def test__get_stop_params_okx(mocker, default_conf):
+2 -1
View File
@@ -515,7 +515,8 @@ EXCHANGES = {
],
},
"hyperliquid": {
"pair": "UBTC/USDC",
# TODO: Should be UBTC/USDC - probably needs a fix in ccxt
"pair": "BTC/USDC",
"stake_currency": "USDC",
"hasQuoteVolume": False,
"timeframe": "30m",
+43 -11
View File
@@ -270,11 +270,14 @@ class TestCCXTExchange:
assert exch.klines(pair_tf).iloc[-1]["date"] >= timeframe_to_prev_date(timeframe, now)
assert exch.klines(pair_tf)["date"].astype(int).iloc[0] // 1e6 == since_ms
def _ccxt__async_get_candle_history(self, exchange, pair, timeframe, candle_type, factor=0.9):
def _ccxt__async_get_candle_history(
self, exchange, pair: str, timeframe: str, candle_type: CandleType, factor: float = 0.9
):
timeframe_ms = timeframe_to_msecs(timeframe)
timeframe_ms_8h = timeframe_to_msecs("8h")
now = timeframe_to_prev_date(timeframe, datetime.now(UTC))
for offset in (360, 120, 30, 10, 5, 2):
since = now - timedelta(days=offset)
for offset_days in (360, 120, 30, 10, 5, 2):
since = now - timedelta(days=offset_days)
since_ms = int(since.timestamp() * 1000)
res = exchange.loop.run_until_complete(
@@ -289,8 +292,15 @@ class TestCCXTExchange:
candles = res[3]
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor
assert len(candles) >= min(candle_count, candle_count1), (
f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {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=}"
)
# Check if first-timeframe is either the start, or start + 1
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
@@ -309,6 +319,8 @@ class TestCCXTExchange:
[
CandleType.FUTURES,
CandleType.FUNDING_RATE,
CandleType.INDEX,
CandleType.PREMIUMINDEX,
CandleType.MARK,
],
)
@@ -322,6 +334,10 @@ class TestCCXTExchange:
timeframe = exchange._ft_has.get(
"funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"]
)
else:
# never skip funding rate!
if not exchange.check_candle_type_support(candle_type):
pytest.skip(f"Exchange does not support candle type {candle_type}")
self._ccxt__async_get_candle_history(
exchange,
pair=pair,
@@ -337,6 +353,7 @@ class TestCCXTExchange:
timeframe_ff = exchange._ft_has.get(
"funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"]
)
timeframe_ff_8h = "8h"
pair_tf = (pair, timeframe_ff, CandleType.FUNDING_RATE)
funding_ohlcv = exchange.refresh_latest_ohlcv(
@@ -350,14 +367,26 @@ class TestCCXTExchange:
hour1 = timeframe_to_prev_date(timeframe_ff, this_hour - timedelta(minutes=1))
hour2 = timeframe_to_prev_date(timeframe_ff, hour1 - timedelta(minutes=1))
hour3 = timeframe_to_prev_date(timeframe_ff, hour2 - timedelta(minutes=1))
val0 = rate[rate["date"] == this_hour].iloc[0]["open"]
val1 = rate[rate["date"] == hour1].iloc[0]["open"]
val2 = rate[rate["date"] == hour2].iloc[0]["open"]
val3 = rate[rate["date"] == hour3].iloc[0]["open"]
# Alternative 8h timeframe - funding fee timeframe is not stable.
h8_this_hour = timeframe_to_prev_date(timeframe_ff_8h)
h8_hour1 = timeframe_to_prev_date(timeframe_ff_8h, h8_this_hour - timedelta(minutes=1))
h8_hour2 = timeframe_to_prev_date(timeframe_ff_8h, h8_hour1 - timedelta(minutes=1))
h8_hour3 = timeframe_to_prev_date(timeframe_ff_8h, h8_hour2 - timedelta(minutes=1))
row0 = rate.iloc[-1]
row1 = rate.iloc[-2]
row2 = rate.iloc[-3]
row3 = rate.iloc[-4]
assert row0["date"] == this_hour or row0["date"] == h8_this_hour
assert row1["date"] == hour1 or row1["date"] == h8_hour1
assert row2["date"] == hour2 or row2["date"] == h8_hour2
assert row3["date"] == hour3 or row3["date"] == h8_hour3
# Test For last 4 hours
# Avoids random test-failure when funding-fees are 0 for a few hours.
assert val0 != 0.0 or val1 != 0.0 or val2 != 0.0 or val3 != 0.0
assert (
row0["open"] != 0.0 or row1["open"] != 0.0 or row2["open"] != 0.0 or row3["open"] != 0.0
)
# We expect funding rates to be different from 0.0 - or moving around.
assert (
rate["open"].max() != 0.0
@@ -369,7 +398,10 @@ class TestCCXTExchange:
exchange, exchangename = exchange_futures
pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"])
since = int((datetime.now(UTC) - timedelta(days=5)).timestamp() * 1000)
pair_tf = (pair, "1h", CandleType.MARK)
candle_type = CandleType.from_string(
exchange.get_option("mark_ohlcv_price", default=CandleType.MARK)
)
pair_tf = (pair, "1h", candle_type)
mark_ohlcv = exchange.refresh_latest_ohlcv([pair_tf], since_ms=since, drop_incomplete=False)
+1 -1
View File
@@ -3092,7 +3092,7 @@ def test_execute_trade_exit_custom_exit_price(
"exit_reason": "foo",
"open_date": ANY,
"close_date": ANY,
"close_rate": ANY,
"close_rate": 2.25, # the custom exit price
"sub_trade": False,
"cumulative_profit": 0.0,
"stake_amount": pytest.approx(60),
+6 -3
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"
+80 -48
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
+12 -9
View File
@@ -879,6 +879,10 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
patch_exchange(mocker)
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
default_conf_usdt["unfilledtimeout"] = {
"entry": 11,
"exit": 30,
}
if use_detail:
default_conf_usdt["timeframe_detail"] = "1m"
@@ -916,7 +920,7 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
)
results = result["results"]
assert not results.empty
# Timeout settings from default_conf = entry: 10, exit: 30
# Timeout settings from = entry: 11, exit: 30
assert len(results) == (2 if use_detail else 3)
assert "orders" in results.columns
@@ -966,8 +970,8 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
@pytest.mark.parametrize(
"use_detail,exp_funding_fee, exp_ff_updates",
[
(True, -0.018054162, 10),
(False, -0.01780296, 6),
(True, -0.0180457882, 15),
(False, -0.0178000543, 12),
],
)
def test_backtest_one_detail_futures(
@@ -1077,8 +1081,8 @@ def test_backtest_one_detail_futures(
@pytest.mark.parametrize(
"use_detail,entries,max_stake,ff_updates,expected_ff",
[
(True, 50, 3000, 55, -1.18038144),
(False, 6, 360, 11, -0.14679994),
(True, 50, 3000, 78, -1.17988972),
(False, 6, 360, 34, -0.14673681),
],
)
def test_backtest_one_detail_futures_funding_fees(
@@ -1800,7 +1804,7 @@ def test_backtest_multi_pair_detail_simplified(
if use_detail:
# Backtest loop is called once per candle per pair
# Exact numbers depend on trade state - but should be around 2_600
assert bl_spy.call_count > 2_170
assert bl_spy.call_count > 2_159
assert bl_spy.call_count < 2_800
assert len(evaluate_result_multi(results["results"], "1h", 3)) > 0
else:
@@ -2378,13 +2382,12 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, caplog, testda
f"Using data directory: {testdatadir} ...",
"Loading data from 2021-11-17 01:00:00 up to 2021-11-21 04:00:00 (4 days).",
"Backtesting with data from 2021-11-17 21:00:00 up to 2021-11-21 04:00:00 (3 days).",
"XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00",
"XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00",
"XRP/USDT:USDT, funding_rate, 1h, data starts at 2021-11-18 00:00:00",
f"Running backtesting for Strategy {CURRENT_TEST_STRATEGY}",
]
for line in exists:
assert log_has(line, caplog)
assert log_has(line, caplog), line
captured = capsys.readouterr()
assert "BACKTESTING REPORT" in captured.out
+11 -4
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
+30 -4
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,
)
@@ -1853,9 +1852,35 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
last_order = trade.orders[-1]
assert last_order.side == "sell"
assert last_order.status == "closed"
assert last_order.order_type == "market"
assert last_order.amount == 23
assert pytest.approx(trade.amount) == 100
assert trade.is_open is True
# Test with explicit price
rc = client_post(
client,
f"{BASE_URI}/forceexit",
data={"tradeid": "5", "ordertype": "limit", "amount": 25, "price": 0.12345},
)
assert_response(rc)
assert rc.json() == {"result": "Created exit order for trade 5."}
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
last_order = trade.orders[-1]
assert last_order.status == "closed"
assert last_order.order_type == "limit"
assert pytest.approx(last_order.safe_price) == 0.12345
assert pytest.approx(last_order.amount) == 25
assert pytest.approx(trade.amount) == 75
assert trade.is_open is True
rc = client_post(client, f"{BASE_URI}/forceexit", data={"tradeid": "5"})
assert_response(rc)
assert rc.json() == {"result": "Created exit order for trade 5."}
@@ -2758,12 +2783,12 @@ def test_list_available_pairs(botclient):
rc = client_get(client, f"{BASE_URI}/available_pairs")
assert_response(rc)
assert rc.json()["length"] == 12
assert rc.json()["length"] == 14
assert isinstance(rc.json()["pairs"], list)
rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m")
assert_response(rc)
assert rc.json()["length"] == 12
assert rc.json()["length"] == 14
rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH")
assert_response(rc)
@@ -3251,6 +3276,7 @@ def test_api_download_data(botclient, mocker, tmp_path):
body = {
"pairs": ["ETH/BTC", "XRP/BTC"],
"timeframes": ["5m"],
"candle_types": ["spot"],
}
# Fail, already running
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
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",
"-8h-funding_rate.feather",
"-8h-mark.feather",
"-1h-funding_rate.feather",
"-1h-mark.feather",
]
# Copy files to tmpdir and rename to old naming
+7 -7
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_4h = tmp_path / "futures" / "XRP_USDT_USDT-4h-funding_rate.feather"
file_8h = tmp_path / "futures" / "XRP_USDT_USDT-8h-funding_rate.feather"
file_30m = tmp_path / "futures" / "XRP_USDT_USDT-30m-funding_rate.feather"
file_1h_fr = tmp_path / "futures" / "XRP_USDT_USDT-1h-funding_rate.feather"
file_1h = tmp_path / "futures" / "XRP_USDT_USDT-1h-futures.feather"
file_8h.rename(file_4h)
file_1h_fr.rename(file_30m)
assert file_1h.exists()
assert file_4h.exists()
assert not file_8h.exists()
assert file_30m.exists()
assert not file_1h_fr.exists()
default_conf_usdt["datadir"] = tmp_path
@@ -22,7 +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_4h.exists()
assert file_8h.exists()
assert not file_30m.exists()
assert file_1h_fr.exists()
# futures files is untouched.
assert file_1h.exists()