Compare commits

..

71 Commits

Author SHA1 Message Date
Matthias
1feb11bac0 refactor: use fstring for telegram sending 2025-11-22 13:32:38 +01:00
Matthias
93936c9946 refactor: use fstrings for /order telegram message 2025-11-22 13:29:27 +01:00
Matthias
ce78039ea8 refactor: don't do delayed formatting on status message 2025-11-22 13:26:50 +01:00
Matthias
1db871e42d Merge pull request #12536 from stash86/add-liq-price
Add liq price info on telegram message
2025-11-22 13:05:01 +01:00
Matthias
1513ba9af9 chore: Move liquidation up a line 2025-11-22 12:48:51 +01:00
Stefano
22b88249ff Merge branch 'add-liq-price' of https://github.com/stash86/freqtrade into add-liq-price 2025-11-22 16:52:47 +09:00
Stefano
18f73af6e6 use get to return None on trade with no liq price 2025-11-22 16:52:43 +09:00
Stefano
b8c835e24e Merge branch 'freqtrade:develop' into add-liq-price 2025-11-22 16:40:25 +09:00
Stefano
2f392b483c add liq line 2025-11-22 16:39:35 +09:00
Matthias
220480327c test: add explicit test for dry_order_filled 2025-11-21 07:11:05 +01:00
Matthias
2c6ff3f018 docs: update supported exchanges for Delist Filter 2025-11-20 20:30:43 +01:00
Matthias
56a8fb4aae Merge pull request #12532 from stash86/bitget-delist
add delisting check for bitget futures
2025-11-20 19:51:05 +01:00
Matthias
3f782fc482 Merge pull request #12531 from stash86/bybit-delist
add delisting check for bybit futures
2025-11-20 19:48:11 +01:00
Matthias
d02e5f2b90 chore: simplify imports 2025-11-20 19:34:51 +01:00
Matthias
92fd9411e5 chore: simplify import 2025-11-20 19:31:28 +01:00
Stefano
650cdf5eb3 change to exclude optimize mode 2025-11-20 15:16:11 +09:00
Stefano
060a1543e9 change to exclude optimize mode 2025-11-20 15:13:56 +09:00
Matthias
50402c5cdc test: add explicit tests for price_crossed 2025-11-20 06:54:22 +01:00
Matthias
0835414f24 Merge pull request #12533 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-11-20 06:30:14 +01:00
Freqtrade Bot
6449074658 chore: update pre-commit hooks 2025-11-20 03:20:59 +00:00
Stefano
b8d558a455 remove unnecessary check because it will be catch anyway 2025-11-20 09:41:57 +09:00
Stefano
4017b010f4 add test 2025-11-20 09:36:17 +09:00
Stefano
7e178cb032 add test 2025-11-20 09:26:16 +09:00
Stefano
e17936c407 use dt_ts 2025-11-19 14:55:31 +09:00
Stefano
f437d4a55b use dt_ts 2025-11-19 14:53:57 +09:00
Stefano
a731b73457 add delisting check for bitget futures 2025-11-19 12:36:34 +09:00
Stefano
b5be462dd1 add delisting check for bybit futures 2025-11-19 12:25:35 +09:00
Matthias
b55e7bcf4e chore: Improve fetch_dry_run_order 2025-11-18 19:45:43 +01:00
Matthias
d1e71544af test: imrpove testcase 2025-11-18 19:45:32 +01:00
Matthias
6e1367fa94 test: enable gate futures test 2025-11-18 19:16:32 +01:00
Matthias
58452ebc7c chore: bump ccxt to 4.5.20
closes #12516
2025-11-18 19:15:50 +01:00
Matthias
7b1dd61ae4 Merge pull request #12530 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-11-18 06:28:09 +01:00
Freqtrade Bot
1321991fca chore: update pre-commit hooks 2025-11-18 03:22:24 +00:00
Matthias
8ae8b615b2 Merge pull request #12518 from freqtrade/dependabot/pip/develop/pytest-9513a616b5
chore(deps-dev): bump the pytest group with 2 updates
2025-11-17 19:23:24 +01:00
Matthias
1c96dfd58f chore: add TODO comment for pytest migration 2025-11-17 18:24:10 +01:00
Matthias
e67c2eefff test: live test "get_fee" for futures exchange 2025-11-17 07:20:44 +01:00
Matthias
2ea19f2ab0 Merge pull request #12517 from freqtrade/dependabot/pip/develop/types-ee32193104
chore(deps-dev): bump types-python-dateutil from 2.9.0.20251008 to 2.9.0.20251108 in the types group
2025-11-17 06:57:29 +01:00
Matthias
44a6d2ead7 Merge pull request #12520 from freqtrade/dependabot/pip/develop/fastapi-0.121.1
chore(deps): bump fastapi from 0.121.0 to 0.121.1
2025-11-17 06:57:00 +01:00
Matthias
5fa314e4a8 Merge pull request #12522 from freqtrade/dependabot/pip/develop/certifi-2025.11.12
chore(deps): bump certifi from 2025.10.5 to 2025.11.12
2025-11-17 06:52:58 +01:00
dependabot[bot]
89a8adcbcf chore(deps-dev): bump the pytest group with 2 updates
Bumps the pytest group with 2 updates: [pytest](https://github.com/pytest-dev/pytest) and [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio).


Updates `pytest` from 8.4.2 to 9.0.1
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.1)

Updates `pytest-asyncio` from 1.2.0 to 1.3.0
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: pytest
- dependency-name: pytest-asyncio
  dependency-version: 1.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: pytest
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 05:39:17 +00:00
Matthias
57f216e9c7 chore: bump dateutil in pre-commit config 2025-11-17 06:32:18 +01:00
Matthias
927a771f2e Merge pull request #12526 from freqtrade/dependabot/pip/develop/optuna-4.6.0
chore(deps): bump optuna from 4.5.0 to 4.6.0
2025-11-17 06:30:19 +01:00
Matthias
3a1fffeeb7 Merge pull request #12521 from freqtrade/dependabot/pip/develop/torch-2.9.1
chore(deps): bump torch from 2.9.0 to 2.9.1
2025-11-17 06:30:00 +01:00
Matthias
9a5622fe33 Merge pull request #12527 from freqtrade/dependabot/pip/develop/ccxt-4.5.19
chore(deps): bump ccxt from 4.5.17 to 4.5.19
2025-11-17 06:29:09 +01:00
Matthias
ec51820074 Merge pull request #12519 from freqtrade/dependabot/pip/develop/mkdocs-a50e39aa08
chore(deps): bump mkdocs-material from 9.6.23 to 9.7.0 in the mkdocs group
2025-11-17 06:28:14 +01:00
Matthias
4425e0cd40 Merge pull request #12525 from freqtrade/dependabot/pip/develop/pymdown-extensions-10.17.1
chore(deps): bump pymdown-extensions from 10.16.1 to 10.17.1
2025-11-17 06:27:34 +01:00
Matthias
97a830b4f0 Merge pull request #12523 from freqtrade/dependabot/pip/develop/pre-commit-4.4.0
chore(deps-dev): bump pre-commit from 4.3.0 to 4.4.0
2025-11-17 06:27:20 +01:00
Matthias
cb28498890 Merge pull request #12524 from freqtrade/dependabot/pip/develop/ruff-0.14.4
chore(deps-dev): bump ruff from 0.14.3 to 0.14.4
2025-11-17 06:27:09 +01:00
dependabot[bot]
1747114b57 chore(deps): bump ccxt from 4.5.17 to 4.5.19
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.17 to 4.5.19.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.17...v4.5.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:02:58 +00:00
dependabot[bot]
1056b2562e chore(deps): bump optuna from 4.5.0 to 4.6.0
Bumps [optuna](https://github.com/optuna/optuna) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/optuna/optuna/releases)
- [Commits](https://github.com/optuna/optuna/compare/v4.5.0...v4.6.0)

---
updated-dependencies:
- dependency-name: optuna
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:02:48 +00:00
dependabot[bot]
64f680ed86 chore(deps): bump pymdown-extensions from 10.16.1 to 10.17.1
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.16.1 to 10.17.1.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.16.1...10.17.1)

---
updated-dependencies:
- dependency-name: pymdown-extensions
  dependency-version: 10.17.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:02:44 +00:00
dependabot[bot]
028c60ce5a chore(deps-dev): bump ruff from 0.14.3 to 0.14.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.3 to 0.14.4.
- [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.3...0.14.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:02:36 +00:00
dependabot[bot]
d27ee227cb chore(deps-dev): bump pre-commit from 4.3.0 to 4.4.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.3.0...v4.4.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:02:21 +00:00
dependabot[bot]
81c49136d1 chore(deps): bump certifi from 2025.10.5 to 2025.11.12
Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.10.5 to 2025.11.12.
- [Commits](https://github.com/certifi/python-certifi/compare/2025.10.05...2025.11.12)

---
updated-dependencies:
- dependency-name: certifi
  dependency-version: 2025.11.12
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:02:18 +00:00
dependabot[bot]
4297207226 chore(deps): bump torch from 2.9.0 to 2.9.1
Bumps [torch](https://github.com/pytorch/pytorch) from 2.9.0 to 2.9.1.
- [Release notes](https://github.com/pytorch/pytorch/releases)
- [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md)
- [Commits](https://github.com/pytorch/pytorch/compare/v2.9.0...v2.9.1)

---
updated-dependencies:
- dependency-name: torch
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:02:14 +00:00
dependabot[bot]
3877fb46de chore(deps): bump fastapi from 0.121.0 to 0.121.1
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.121.0 to 0.121.1.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.121.0...0.121.1)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.121.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:01:56 +00:00
dependabot[bot]
22707b8664 chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


Updates `mkdocs-material` from 9.6.23 to 9.7.0
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.23...9.7.0)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-version: 9.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: mkdocs
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:01:46 +00:00
dependabot[bot]
d2c380c83b chore(deps-dev): bump types-python-dateutil in the types group
Bumps the types group with 1 update: [types-python-dateutil](https://github.com/typeshed-internal/stub_uploader).


Updates `types-python-dateutil` from 2.9.0.20251008 to 2.9.0.20251108
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-python-dateutil
  dependency-version: 2.9.0.20251108
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: types
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 03:01:12 +00:00
Matthias
bc516db3f8 refactor: add generic "migrate" method 2025-11-16 09:11:02 +01:00
Matthias
5c0cf8f228 chore: prevent execution of command partials on python <3.13 2025-11-16 08:41:14 +01:00
Matthias
b753231d3c Merge pull request #12512 from mrpabloyeah/fix-high_value-calculation-in-calculate_max_drawdown
Fix high_value calculation in calculate_max_drawdown()
2025-11-15 16:03:35 +01:00
Matthias
af728f8224 chore: update command partial 2025-11-15 15:50:58 +01:00
Matthias
5000927939 test: add tests for new list-timeframes behavior 2025-11-15 15:49:45 +01:00
Matthias
89274bfcdb test: update tests for new timeframe approach 2025-11-15 15:46:09 +01:00
Matthias
ef86b4113d feat: add trading-mode to list-timeframes
some exchanges provide different timeframe configurations depending on the market type
2025-11-15 13:11:10 +01:00
Matthias
38ff755533 fix: start drawdown series with a 0 row
This will account for edge-cases with no winning / losing trades
2025-11-15 09:52:24 +01:00
Matthias
70ec376657 test: further expand drawdown test 2025-11-15 09:51:27 +01:00
Matthias
1a506dc4b4 test: add tests for high/low logic 2025-11-14 19:41:52 +01:00
mrpabloyeah
47451dd989 Fix high_value calculation in calculate_max_drawdown() 2025-11-13 20:14:39 +01:00
Matthias
705849db3d Merge pull request #12511 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-11-13 06:55:06 +01:00
Freqtrade Bot
14d3096a22 chore: update pre-commit hooks 2025-11-13 03:22:53 +00:00
31 changed files with 4709 additions and 2971 deletions

View File

@@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
os: [ "ubuntu-22.04", "ubuntu-24.04", "macos-14", "macos-15" , "windows-2022", "windows-2025" ]
python-version: ["3.11", "3.12", "3.13", "3.14"]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v5
@@ -108,7 +108,7 @@ jobs:
fi
- name: Check for repository changes - Windows
if: ${{ runner.os == 'Windows' }}
if: ${{ runner.os == 'Windows' && (matrix.python-version != '3.13') }}
run: |
if (git status --porcelain) {
Write-Host "Repository is dirty, changes detected:"

View File

@@ -30,7 +30,7 @@ repos:
- types-filelock==3.2.7
- types-requests==2.32.4.20250913
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20251008
- types-python-dateutil==2.9.0.20251108
- scipy-stubs==1.16.3.0
- SQLAlchemy==2.0.44
# stages: [push]
@@ -44,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.14.4'
rev: 'v0.14.5'
hooks:
- id: ruff
- id: ruff-format

View File

@@ -1,4 +1,5 @@
import subprocess # noqa: S404, RUF100
import sys
from pathlib import Path
@@ -62,4 +63,9 @@ def extract_command_partials():
if __name__ == "__main__":
if sys.version_info < (3, 13): # pragma: no cover
sys.exit(
"argparse output changed in Python 3.13+. "
"To keep command partials up to date, please run this script with Python 3.13+."
)
extract_command_partials()

View File

@@ -2,11 +2,14 @@
usage: freqtrade list-timeframes [-h] [-v] [--no-color] [--logfile FILE] [-V]
[-c PATH] [-d PATH] [--userdir PATH]
[--exchange EXCHANGE] [-1]
[--trading-mode {spot,margin,futures}]
options:
-h, --help show this help message and exit
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
-1, --one-column Print output in one column.
--trading-mode, --tradingmode {spot,margin,futures}
Select Trading mode
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@@ -417,7 +417,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at
Removes pairs that will be delisted on the exchange maximum `max_days_from_now` days from now (defaults to `0` which remove all future delisted pairs no matter how far from now). Currently this filter only supports following exchanges:
!!! Note "Available exchanges"
Delist filter is only available on Binance, where Binance Futures will work for both dry and live modes, while Binance Spot is limited to live mode (for technical reasons).
Delist filter is available on Bybit Futures, Bitget Futures and Binance, where Binance Futures will work for both dry and live modes, while Binance Spot is limited to live mode (for technical reasons).
!!! Warning "Backtesting"
`DelistFilter` does not support backtesting mode.

View File

@@ -1,7 +1,7 @@
markdown==3.10
mkdocs==1.6.1
mkdocs-material==9.6.23
mkdocs-material==9.7.0
mdx_truly_sane_lists==1.3
pymdown-extensions==10.16.1
pymdown-extensions==10.17.1
jinja2==3.1.6
mike==2.1.3

View File

@@ -104,7 +104,7 @@ ARGS_BACKTEST_SHOW = [
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column", "trading_mode"]
ARGS_LIST_PAIRS = [
"exchange",

View File

@@ -143,6 +143,20 @@ def _calc_drawdown_series(
max_drawdown_df["drawdown_relative"] = (
max_drawdown_df["high_value"] - max_drawdown_df["cumulative"]
) / max_drawdown_df["high_value"]
# Add zero row at start to account for edge-cases with no winning / losing trades - so high/low
# will be 0.0 in such cases.
zero_row = pd.DataFrame(
{
"cumulative": [0.0],
"high_value": [0.0],
"drawdown": [0.0],
"drawdown_relative": [0.0],
"date": [profit_results.loc[0, date_col]],
}
)
max_drawdown_df = pd.concat([zero_row, max_drawdown_df], ignore_index=True)
return max_drawdown_df
@@ -215,6 +229,7 @@ def calculate_max_drawdown(
max_drawdown_df = _calc_drawdown_series(
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
)
# max_drawdown_df has an extra zero row at the start
# Calculate maximum drawdown
idxmin = (
@@ -223,15 +238,15 @@ def calculate_max_drawdown(
else max_drawdown_df["drawdown"].idxmin()
)
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
high_date = profit_results.loc[high_idx, date_col]
low_date = profit_results.loc[idxmin, date_col]
high_val = max_drawdown_df.loc[high_idx, "cumulative"]
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
high_date = profit_results.at[max(high_idx - 1, 0), date_col]
low_date = profit_results.at[max(idxmin - 1, 0), date_col]
high_val = max_drawdown_df.at[high_idx, "cumulative"]
low_val = max_drawdown_df.at[idxmin, "cumulative"]
max_drawdown_rel = max_drawdown_df.at[idxmin, "drawdown_relative"]
# Calculate current drawdown
current_high_idx = max_drawdown_df["high_value"].iloc[:-1].idxmax()
current_high_date = profit_results.loc[current_high_idx, date_col]
current_high_date = profit_results.at[max(current_high_idx - 1, 0), date_col]
current_high_value = max_drawdown_df.iloc[-1]["high_value"]
current_cumulative = max_drawdown_df.iloc[-1]["cumulative"]
current_drawdown_abs = current_high_value - current_cumulative

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
import logging
from datetime import timedelta
from datetime import datetime, timedelta
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (
DDosProtection,
OperationalException,
@@ -14,7 +14,7 @@ from freqtrade.exceptions import (
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.util.datetime_helpers import dt_now, dt_ts
from freqtrade.util import dt_from_ts, dt_now, dt_ts
logger = logging.getLogger(__name__)
@@ -37,6 +37,7 @@ class Bitget(Exchange):
_ft_has_futures: FtHas = {
"mark_ohlcv_timeframe": "4h",
"funding_fee_candle_limit": 100,
"has_delisting": True,
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
@@ -236,3 +237,35 @@ class Bitget(Exchange):
raise OperationalException(
"Freqtrade currently only supports isolated futures for bitget"
)
def check_delisting_time(self, pair: str) -> datetime | None:
"""
Check if the pair gonna be delisted.
By default, it returns None.
:param pair: Market symbol
:return: Datetime if the pair gonna be delisted, None otherwise
"""
if self._config["runmode"] in OPTIMIZE_MODES:
return None
if self.trading_mode == TradingMode.FUTURES:
return self._check_delisting_futures(pair)
return None
def _check_delisting_futures(self, pair: str) -> datetime | None:
delivery_time = self.markets.get(pair, {}).get("info", {}).get("limitOpenTime", None)
if delivery_time:
if isinstance(delivery_time, str) and (delivery_time != ""):
delivery_time = int(delivery_time)
if not isinstance(delivery_time, int) or delivery_time <= 0:
return None
max_delivery = dt_ts() + (
14 * 24 * 60 * 60 * 1000
) # Assume exchange don't announce delisting more than 14 days in advance
if delivery_time < max_delivery:
return dt_from_ts(delivery_time)
return None

View File

@@ -4,12 +4,13 @@ from datetime import datetime, timedelta
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, PriceType, TradingMode
from freqtrade.enums import OPTIMIZE_MODES, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.misc import deep_merge_dicts
from freqtrade.util import dt_from_ts, dt_ts
logger = logging.getLogger(__name__)
@@ -54,6 +55,7 @@ class Bybit(Exchange):
"exchange_has_overrides": {
"fetchOrder": True,
},
"has_delisting": True,
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
@@ -294,3 +296,35 @@ class Bybit(Exchange):
self.cache_leverage_tiers(tiers, self._config["stake_currency"])
return tiers
def check_delisting_time(self, pair: str) -> datetime | None:
"""
Check if the pair gonna be delisted.
By default, it returns None.
:param pair: Market symbol
:return: Datetime if the pair gonna be delisted, None otherwise
"""
if self._config["runmode"] in OPTIMIZE_MODES:
return None
if self.trading_mode == TradingMode.FUTURES:
return self._check_delisting_futures(pair)
return None
def _check_delisting_futures(self, pair: str) -> datetime | None:
delivery_time = self.markets.get(pair, {}).get("info", {}).get("deliveryTime", 0)
if delivery_time:
if isinstance(delivery_time, str) and (delivery_time != ""):
delivery_time = int(delivery_time)
if not isinstance(delivery_time, int) or delivery_time <= 0:
return None
max_delivery = dt_ts() + (
14 * 24 * 60 * 60 * 1000
) # Assume exchange don't announce delisting more than 14 days in advance
if delivery_time < max_delivery:
return dt_from_ts(delivery_time)
return None

View File

@@ -430,7 +430,15 @@ class Exchange:
@property
def timeframes(self) -> list[str]:
return list((self._api.timeframes or {}).keys())
market_type = (
"spot"
if self.trading_mode != TradingMode.FUTURES
else self._ft_has["ccxt_futures_name"]
)
timeframes = self._api.options.get("timeframes", {}).get(market_type)
if timeframes is None:
timeframes = self._api.timeframes
return list((timeframes or {}).keys())
@property
def markets(self) -> dict[str, Any]:
@@ -1295,7 +1303,7 @@ class Exchange:
return order
def fetch_dry_run_order(self, order_id) -> CcxtOrder:
def fetch_dry_run_order(self, order_id: str) -> CcxtOrder:
"""
Return dry-run order
Only call if running in dry-run mode.
@@ -1307,11 +1315,12 @@ class Exchange:
except KeyError as e:
from freqtrade.persistence import Order
order = Order.order_by_id(order_id)
if order:
ccxt_order = order.to_ccxt_object(self._ft_has["stop_price_prop"])
self._dry_run_open_orders[order_id] = ccxt_order
return ccxt_order
order_obj = Order.order_by_id(order_id)
if order_obj:
order = order_obj.to_ccxt_object(self._ft_has["stop_price_prop"])
order = self.check_dry_limit_order_filled(order)
self._dry_run_open_orders[order_id] = order
return order
# Gracefully handle errors with dry-run orders.
raise InvalidOrderException(
f"Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}"

View File

@@ -63,7 +63,7 @@ from freqtrade.rpc.rpc_types import (
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise, MeasureTime, PeriodicCache, dt_from_ts, dt_now
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names
from freqtrade.util.migrations import migrate_live_content
from freqtrade.wallets import Wallets
@@ -229,7 +229,7 @@ class FreqtradeBot(LoggingMixin):
Called on startup and after reloading the bot - triggers notifications and
performs startup tasks
"""
migrate_binance_futures_names(self.config)
migrate_live_content(self.config, self.exchange)
set_startup_time()
self.rpc.startup_messages(self.config, self.pairlists, self.protections)

View File

@@ -670,7 +670,7 @@ class Telegram(RPCHandler):
# TODO: This calculation ignores fees.
price_to_1st_entry = (cur_entry_average - first_avg) / first_avg
if is_open:
lines.append("({})".format(dt_humanize_delta(order["order_filled_date"])))
lines.append(f"({dt_humanize_delta(order['order_filled_date'])})")
lines.append(
f"*Amount:* {round_value(cur_entry_amount, 8)} "
f"({fmt_coin(order['cost'], quote_currency)})"
@@ -701,7 +701,7 @@ class Telegram(RPCHandler):
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
for r in results:
lines = ["*Order List for Trade #*`{trade_id}`"]
lines = [f"*Order List for Trade #*`{r['trade_id']}`"]
lines_detail = self._prepare_order_details(
r["orders"], r["quote_currency"], r["is_open"]
@@ -720,10 +720,10 @@ class Telegram(RPCHandler):
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
msg += line + "\n"
else:
await self._send_msg(msg.format(**r))
msg = "*Order List for Trade #*`{trade_id}` - continued\n" + line + "\n"
await self._send_msg(msg)
msg = f"*Order List for Trade #*`{r['trade_id']}` - continued\n" + line + "\n"
await self._send_msg(msg.format(**r))
await self._send_msg(msg)
@authorized_only
async def _status(self, update: Update, context: CallbackContext) -> None:
@@ -774,26 +774,25 @@ class Telegram(RPCHandler):
r["realized_profit_r"] = fmt_coin(r["realized_profit"], r["quote_currency"])
r["total_profit_abs_r"] = fmt_coin(r["total_profit_abs"], r["quote_currency"])
lines = [
"*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r["is_open"] else ""),
"*Current Pair:* {pair}",
f"*Trade ID:* `{r['trade_id']}`"
+ (f" `(since {r['open_date_hum']})`" if r["is_open"] else ""),
f"*Current Pair:* {r['pair']}",
(
f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}"
+ " ` ({leverage}x)`"
if r.get("leverage")
else ""
+ (f" ` ({r['leverage']}x)`" if r.get("leverage") else "")
),
"*Amount:* `{amount} ({stake_amount_r})`",
"*Total invested:* `{max_stake_amount_r}`" if position_adjust else "",
"*Enter Tag:* `{enter_tag}`" if r["enter_tag"] else "",
"*Exit Reason:* `{exit_reason}`" if r["exit_reason"] else "",
f"*Amount:* `{r['amount']} ({r['stake_amount_r']})`",
f"*Total invested:* `{r['max_stake_amount_r']}`" if position_adjust else "",
f"*Enter Tag:* `{r['enter_tag']}`" if r["enter_tag"] else "",
f"*Exit Reason:* `{r['exit_reason']}`" if r["exit_reason"] else "",
]
if position_adjust:
max_buy_str = f"/{max_entries + 1}" if (max_entries > 0) else ""
lines.extend(
[
"*Number of Entries:* `{num_entries}" + max_buy_str + "`",
"*Number of Exits:* `{num_exits}`",
f"*Number of Entries:* `{r['num_entries']}{max_buy_str}`",
f"*Number of Exits:* `{r['num_exits']}`",
]
)
@@ -801,15 +800,15 @@ class Telegram(RPCHandler):
[
f"*Open Rate:* `{round_value(r['open_rate'], 8)}`",
f"*Close Rate:* `{round_value(r['close_rate'], 8)}`" if r["close_rate"] else "",
"*Open Date:* `{open_date}`",
"*Close Date:* `{close_date}`" if r["close_date"] else "",
f"*Open Date:* `{r['open_date']}`",
f"*Close Date:* `{r['close_date']}`" if r["close_date"] else "",
(
f" \n*Current Rate:* `{round_value(r['current_rate'], 8)}`"
if r["is_open"]
else ""
),
("*Unrealized Profit:* " if r["is_open"] else "*Close Profit: *")
+ "`{profit_ratio:.2%}` `({profit_abs_r})`",
+ f"`{r['profit_ratio']:.2%}` `({r['profit_abs_r']})`",
]
)
@@ -817,37 +816,44 @@ class Telegram(RPCHandler):
if r.get("realized_profit"):
lines.extend(
[
"*Realized Profit:* `{realized_profit_ratio:.2%} "
"({realized_profit_r})`",
"*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`",
f"*Realized Profit:* `{r['realized_profit_ratio']:.2%} "
f"({r['realized_profit_r']})`",
(
f"*Total Profit:* `{r['total_profit_ratio']:.2%} "
f"({r['total_profit_abs_r']})`",
),
]
)
# Append empty line to improve readability
lines.append(" ")
# Adding liquidation only if it is not None
if liquidation := r.get("liquidation_price"):
lines.append(f"*Liquidation:* `{round_value(liquidation, 8)}`")
if (
r["stop_loss_abs"] != r["initial_stop_loss_abs"]
and r["initial_stop_loss_ratio"] is not None
):
# Adding initial stoploss only if it is different from stoploss
lines.append(
"*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
"`({initial_stop_loss_ratio:.2%})`"
f"*Initial Stoploss:* `{r['initial_stop_loss_abs']:.8f}` "
f"`({r['initial_stop_loss_ratio']:.2%})`"
)
# Adding stoploss and stoploss percentage only if it is not None
lines.append(
f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` "
+ ("`({stop_loss_ratio:.2%})`" if r["stop_loss_ratio"] else "")
+ (f"`({r['stop_loss_ratio']:.2%})`" if r["stop_loss_ratio"] else "")
)
lines.append(
f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` "
"`({stoploss_current_dist_ratio:.2%})`"
f"`({r['stoploss_current_dist_ratio']:.2%})`"
)
if r.get("open_orders"):
if open_orders := r.get("open_orders"):
lines.append(
"*Open Order:* `{open_orders}`"
+ ("- `{exit_order_status}`" if r["exit_order_status"] else "")
f"*Open Order:* `{open_orders}`"
+ (f"- `{r['exit_order_status']}`" if r["exit_order_status"] else "")
)
await self.__send_status_msg(lines, r)
@@ -863,10 +869,10 @@ class Telegram(RPCHandler):
if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH:
msg += line + "\n"
else:
await self._send_msg(msg.format(**r))
msg = "*Trade ID:* `{trade_id}` - continued\n" + line + "\n"
await self._send_msg(msg)
msg = f"*Trade ID:* `{r['trade_id']}` - continued\n" + line + "\n"
await self._send_msg(msg.format(**r))
await self._send_msg(msg)
@authorized_only
async def _status_table(self, update: Update, context: CallbackContext) -> None:

View File

@@ -1,12 +1,23 @@
from freqtrade.exchange import Exchange
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_data
from freqtrade.util.migrations.binance_mig import (
migrate_binance_futures_data,
migrate_binance_futures_names,
)
from freqtrade.util.migrations.funding_rate_mig import migrate_funding_fee_timeframe
def migrate_data(config, exchange: Exchange | None = None):
def migrate_data(config, exchange: Exchange | None = None) -> None:
"""
Migrate persisted data from old formats to new formats
"""
migrate_binance_futures_data(config)
migrate_funding_fee_timeframe(config, exchange)
def migrate_live_content(config, exchange: Exchange | None = None) -> None:
"""
Migrate database content from old formats to new formats
Used for dry/live mode.
"""
migrate_binance_futures_names(config)

View File

@@ -14,6 +14,10 @@ logger = logging.getLogger(__name__)
def migrate_binance_futures_names(config: Config):
"""
Migrate binance futures names in both database and data files.
This is needed because ccxt naming changed from "BTC/USDT" to "BTC/USDT:USDT"
"""
if not (
config.get("trading_mode", TradingMode.SPOT) == TradingMode.FUTURES
and config["exchange"]["name"] == "binance"

View File

@@ -183,6 +183,7 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
known_first_party = ["freqtrade_client"]
[tool.pytest.ini_options]
# TODO: should be migrated to [tool.pytest] as support for this was added in 9.0
log_format = "%(asctime)s %(levelname)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S"

View File

@@ -6,11 +6,11 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.14.3
ruff==0.14.4
mypy==1.18.2
pre-commit==4.3.0
pytest==8.4.2
pytest-asyncio==1.2.0
pre-commit==4.4.0
pytest==9.0.1
pytest-asyncio==1.3.0
pytest-cov==7.0.0
pytest-mock==3.15.1
pytest-random-order==1.2.0
@@ -29,4 +29,4 @@ types-cachetools==6.2.0.20251022
types-filelock==3.2.7
types-requests==2.32.4.20250913
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20251008
types-python-dateutil==2.9.0.20251108

View File

@@ -2,7 +2,7 @@
-r requirements-freqai.txt
# Required for freqai-rl
torch==2.9.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
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'

View File

@@ -5,7 +5,7 @@
# Required for freqai
scikit-learn==1.7.2
joblib==1.5.2
catboost==1.2.8; 'arm' not in platform_machine and python_version < '3.14'
catboost==1.2.8; 'arm' not in platform_machine
lightgbm==4.6.0
xgboost==3.1.1
tensorboard==2.20.0

View File

@@ -5,5 +5,5 @@
scipy==1.16.3
scikit-learn==1.7.2
filelock==3.20.0
optuna==4.5.0
optuna==4.6.0
cmaes==0.12.0

View File

@@ -7,7 +7,7 @@ ft-pandas-ta==0.3.16
ta-lib==0.6.8
technical==1.5.3
ccxt==4.5.17
ccxt==4.5.20
cryptography==46.0.3
aiohttp==3.13.2
SQLAlchemy==2.0.44
@@ -18,7 +18,7 @@ humanize==4.14.0
cachetools==6.2.1
requests==2.32.5
urllib3==2.5.0
certifi==2025.10.5
certifi==2025.11.12
jsonschema==4.25.1
tabulate==0.9.0
pycoingecko==3.2.0
@@ -37,7 +37,7 @@ orjson==3.11.4
sdnotify==0.3.2
# API Server
fastapi==0.121.0
fastapi==0.121.1
pydantic==2.12.4
uvicorn==0.38.0
pyjwt==2.10.1

View File

@@ -198,6 +198,8 @@ def test_list_timeframes(mocker, capsys):
"1h": "hour",
"1d": "day",
}
api_mock.options = {}
patch_exchange(mocker, api_mock=api_mock, exchange="bybit")
args = [
"list-timeframes",
@@ -286,6 +288,52 @@ def test_list_timeframes(mocker, capsys):
assert re.search(r"^1h$", captured.out, re.MULTILINE)
assert re.search(r"^1d$", captured.out, re.MULTILINE)
api_mock.options = {
"timeframes": {
"spot": {"1m": "1m", "5m": "5m", "15m": "15m"},
"swap": {"1m": "1m", "15m": "15m", "1h": "1h"},
}
}
args = [
"list-timeframes",
"--exchange",
"binance",
]
start_list_timeframes(get_args(args))
captured = capsys.readouterr()
assert re.match(
"Timeframes available for the exchange `Binance`: 1m, 5m, 15m",
captured.out,
)
args = [
"list-timeframes",
"--exchange",
"binance",
"--trading-mode",
"spot",
]
start_list_timeframes(get_args(args))
captured = capsys.readouterr()
assert re.match(
"Timeframes available for the exchange `Binance`: 1m, 5m, 15m",
captured.out,
)
args = [
"list-timeframes",
"--exchange",
"binance",
"--trading-mode",
"futures",
]
start_list_timeframes(get_args(args))
captured = capsys.readouterr()
assert re.match(
"Timeframes available for the exchange `Binance`: 1m, 15m, 1h",
captured.out,
)
def test_list_markets(mocker, markets_static, capsys):
api_mock = MagicMock()

View File

@@ -303,6 +303,7 @@ def mock_order_usdt_6(is_short: bool):
"side": entry_side(is_short),
"type": "limit",
"price": 10.0,
"cost": 20.0,
"amount": 2.0,
"filled": 2.0,
"remaining": 0.0,
@@ -317,6 +318,7 @@ def mock_order_usdt_6_exit(is_short: bool):
"side": exit_side(is_short),
"type": "limit",
"price": 12.0,
"cost": 24.0,
"amount": 2.0,
"filled": 0.0,
"remaining": 2.0,

View File

@@ -575,12 +575,18 @@ def test_calculate_max_drawdown2():
# No losing trade ...
drawdown = calculate_max_drawdown(df, date_col="open_date", value_col="profit")
assert drawdown.drawdown_abs == 0.0
assert drawdown.low_value == 0.0
assert drawdown.current_high_value >= 0.0
assert drawdown.current_drawdown_abs == 0.0
df1 = DataFrame(zip(values[:5], dates[:5], strict=False), columns=["profit", "open_date"])
df1.loc[:, "profit"] = df1["profit"] * -1
# No winning trade ...
drawdown = calculate_max_drawdown(df1, date_col="open_date", value_col="profit")
assert drawdown.drawdown_abs == 0.055545
assert drawdown.high_value == 0.0
assert drawdown.current_high_value == 0.0
assert drawdown.current_drawdown_abs == 0.055545
@pytest.mark.parametrize(

View File

@@ -1,12 +1,13 @@
from copy import deepcopy
from datetime import timedelta
from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode
from freqtrade.exceptions import OperationalException, RetryableOrderError
from freqtrade.exchange.common import API_RETRY_COUNT
from freqtrade.util import dt_now, dt_ts
from freqtrade.util import dt_now, dt_ts, dt_utc
from tests.conftest import EXMS, get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -193,3 +194,43 @@ def test__lev_prep_bitget(default_conf, mocker):
assert api_mock.set_margin_mode.call_count == 0
assert api_mock.set_leverage.call_count == 1
api_mock.set_leverage.assert_called_with(symbol="BTC/USDC:USDC", leverage=19.99)
def test_check_delisting_time_bitget(default_conf_usdt, mocker):
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget")
exchange._config["runmode"] = RunMode.BACKTEST
delist_fut_mock = MagicMock(return_value=None)
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
# Invalid run mode
resp = exchange.check_delisting_time("BTC/USDT")
assert resp is None
assert delist_fut_mock.call_count == 0
# Delist spot called
exchange._config["runmode"] = RunMode.DRY_RUN
resp1 = exchange.check_delisting_time("BTC/USDT")
assert resp1 is None
assert delist_fut_mock.call_count == 0
# Delist futures called
exchange.trading_mode = TradingMode.FUTURES
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
assert resp1 is None
assert delist_fut_mock.call_count == 1
def test__check_delisting_futures_bitget(default_conf_usdt, mocker, markets):
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
markets["BTC/USDT:USDT"]["info"]["limitOpenTime"] = "-1"
markets["SOL/BUSD:BUSD"]["info"]["limitOpenTime"] = "-1"
markets["ADA/USDT:USDT"]["info"]["limitOpenTime"] = "1760745600000" # 2025-10-18
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bitget")
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
# No delisting date
assert resp_sol is None
# Has a delisting date
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
assert resp_ada == dt_utc(2025, 10, 18)

View File

@@ -1,10 +1,11 @@
from copy import deepcopy
from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock
from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade.enums.marginmode import MarginMode
from freqtrade.enums.tradingmode import TradingMode
from freqtrade.enums import MarginMode, RunMode, TradingMode
from freqtrade.util import dt_utc
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -214,3 +215,43 @@ def test_bybit__order_needs_price(
exchange.unified_account = uta
assert exchange._order_needs_price(side, order_type) == expected
def test_check_delisting_time_bybit(default_conf_usdt, mocker):
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
exchange._config["runmode"] = RunMode.BACKTEST
delist_fut_mock = MagicMock(return_value=None)
mocker.patch.object(exchange, "_check_delisting_futures", delist_fut_mock)
# Invalid run mode
resp = exchange.check_delisting_time("BTC/USDT:USDT")
assert resp is None
assert delist_fut_mock.call_count == 0
# Delist spot called
exchange._config["runmode"] = RunMode.DRY_RUN
resp1 = exchange.check_delisting_time("BTC/USDT")
assert resp1 is None
assert delist_fut_mock.call_count == 0
# Delist futures called
exchange.trading_mode = TradingMode.FUTURES
resp1 = exchange.check_delisting_time("BTC/USDT:USDT")
assert resp1 is None
assert delist_fut_mock.call_count == 1
def test__check_delisting_futures_bybit(default_conf_usdt, mocker, markets):
markets["BTC/USDT:USDT"] = deepcopy(markets["SOL/BUSD:BUSD"])
markets["BTC/USDT:USDT"]["info"]["deliveryTime"] = "0"
markets["SOL/BUSD:BUSD"]["info"]["deliveryTime"] = "0"
markets["ADA/USDT:USDT"]["info"]["deliveryTime"] = "1760745600000" # 2025-10-18
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange="bybit")
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets))
resp_sol = exchange._check_delisting_futures("SOL/BUSD:BUSD")
# SOL has no delisting date
assert resp_sol is None
# Actually has a delisting date
resp_ada = exchange._check_delisting_futures("ADA/USDT:USDT")
assert resp_ada == dt_utc(2025, 10, 18)

View File

@@ -742,10 +742,11 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
def test_validate_timeframes(default_conf, mocker, timeframe):
default_conf["timeframe"] = timeframe
api_mock = MagicMock()
id_mock = PropertyMock(return_value="test_exchange")
type(api_mock).id = id_mock
timeframes = PropertyMock(return_value={"1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"})
type(api_mock).timeframes = timeframes
id_mock = MagicMock(return_value="test_exchange")
api_mock.id = id_mock
api_mock.options = {}
timeframes = {"1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
api_mock.timeframes = timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
@@ -757,12 +758,11 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
def test_validate_timeframes_failed(default_conf, mocker):
default_conf["timeframe"] = "3m"
api_mock = MagicMock()
id_mock = PropertyMock(return_value="test_exchange")
type(api_mock).id = id_mock
timeframes = PropertyMock(
return_value={"15s": "15s", "1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
)
type(api_mock).timeframes = timeframes
id_mock = MagicMock(return_value="test_exchange")
api_mock.id = id_mock
timeframes = {"15s": "15s", "1m": "1m", "5m": "5m", "15m": "15m", "1h": "1h"}
api_mock.timeframes = timeframes
api_mock.options = {}
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets")
@@ -1110,6 +1110,116 @@ def test_create_dry_run_order_fees(
assert order1["fee"]["rate"] == fee
@pytest.mark.parametrize(
"side,limit,offset,expected",
[
("buy", 46.0, 0.0, True),
("buy", 26.0, 0.0, True),
("buy", 25.55, 0.0, False),
("buy", 1, 0.0, False), # Very far away
("sell", 25.5, 0.0, True),
("sell", 50, 0.0, False), # Very far away
("sell", 25.58, 0.0, False),
("sell", 25.563, 0.01, False),
("sell", 5.563, 0.01, True),
],
)
def test__dry_is_price_crossed_with_orderbook(
default_conf, mocker, order_book_l2_usd, side, limit, offset, expected
):
# Best bid 25.563
# Best ask 25.566
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
exchange.fetch_l2_order_book = order_book_l2_usd
orderbook = order_book_l2_usd.return_value
result = exchange._dry_is_price_crossed(
"LTC/USDT", side, limit, orderbook=orderbook, offset=offset
)
assert result is expected
assert order_book_l2_usd.call_count == 0
# Test without passing orderbook
order_book_l2_usd.reset_mock()
result = exchange._dry_is_price_crossed("LTC/USDT", side, limit, offset=offset)
assert result is expected
def test__dry_is_price_crossed_empty_orderbook(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
empty_book = {"asks": [], "bids": []}
assert not exchange._dry_is_price_crossed("LTC/USDT", "buy", 100.0, orderbook=empty_book)
def test__dry_is_price_crossed_fetches_orderbook(default_conf, mocker, order_book_l2_usd):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
exchange.fetch_l2_order_book = order_book_l2_usd
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 26.0)
assert order_book_l2_usd.call_count == 1
def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf)
exchange.fetch_l2_order_book = MagicMock()
mocker.patch(f"{EXMS}.exchange_has", return_value=False)
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0)
assert exchange.fetch_l2_order_book.call_count == 0
@pytest.mark.parametrize(
"crossed,immediate,side,amount,expected_status,expected_fee_rate,expected_calls,taker_or_maker",
[
(True, True, "buy", 2.0, "closed", 0.005, 1, "taker"),
(True, False, "sell", 1.5, "closed", 0.005, 1, "maker"),
(False, False, "sell", 1.0, "open", None, 0, None),
],
)
def test_check_dry_limit_order_filled_parametrized(
default_conf,
mocker,
crossed,
immediate,
side,
amount,
expected_status,
expected_fee_rate,
expected_calls,
taker_or_maker,
):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=crossed)
fee_mock = mocker.patch(f"{EXMS}.get_fee", return_value=0.005)
order = {
"symbol": "LTC/USDT",
"status": "open",
"type": "limit",
"side": side,
"price": 25.0,
"amount": amount,
"filled": 0.0,
"remaining": amount,
"cost": 25.0 * amount,
"fee": None,
}
result = exchange.check_dry_limit_order_filled(order, immediate=immediate)
assert result["status"] == expected_status
if crossed:
assert result["filled"] == amount
assert result["remaining"] == 0.0
assert result["fee"]["rate"] == expected_fee_rate
fee_mock.assert_called_once_with("LTC/USDT", taker_or_maker=taker_or_maker)
else:
assert result["filled"] == 0.0
assert result["remaining"] == amount
assert result["fee"] is None
assert fee_mock.call_count == expected_calls
@pytest.mark.parametrize(
"side,price,filled,converted",
[

View File

@@ -11,7 +11,7 @@ import pytest
from freqtrade.enums import CandleType
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.exchange.exchange import timeframe_to_msecs
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
from freqtrade.util import dt_floor_day, dt_now, dt_ts
from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES
@@ -422,15 +422,23 @@ class TestCCXTExchange:
trades_orig = nvspy.call_args_list[2][0][0]
assert len(trades_orig[-1].get("info")) > len(trades_orig[-2].get("info"))
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]["pair"]
def _ccxt_get_fee(self, exch: Exchange, pair: str):
threshold = 0.01
assert 0 < exch.get_fee(pair, "limit", "buy") < threshold
assert 0 < exch.get_fee(pair, "limit", "sell") < threshold
assert 0 < exch.get_fee(pair, "market", "buy") < threshold
assert 0 < exch.get_fee(pair, "market", "sell") < threshold
def test_ccxt_get_fee_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]["pair"]
self._ccxt_get_fee(exch, pair)
def test_ccxt_get_fee_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange_futures
pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"])
self._ccxt_get_fee(exch, pair)
def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
spot, spot_name = exchange
if spot:

View File

@@ -1,6 +1,5 @@
import logging
import shutil
import sys
from pathlib import Path
from unittest.mock import MagicMock
@@ -35,9 +34,6 @@ def can_run_model(model: str) -> None:
if is_arm() and "Catboost" in model:
pytest.skip("CatBoost is not supported on ARM.")
if "Catboost" in model and sys.version_info >= (3, 14):
pytest.skip("CatBoost is not supported on Python 3.14+.")
if is_pytorch_model and is_mac():
pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.")

View File

@@ -4525,6 +4525,7 @@ def test_check_for_open_trades(mocker, default_conf_usdt, fee, is_short):
def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_short):
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
create_mock_trades(fee, is_short=is_short)
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
freqtrade.startup_update_open_orders()
assert not log_has_re(r"Error updating Order .*", caplog)