Compare commits

...

340 Commits

Author SHA1 Message Date
Matthias dc2abe32a3 Merge pull request #13003 from freqtrade/new_release
New release 2026.3
2026-03-30 19:11:38 +02:00
Matthias 43bb0f7cb1 chore: bump version to 2026.3 2026-03-30 06:38:56 +02:00
Matthias 9ee87cb6ba Merge branch 'stable' into new_release 2026-03-30 06:38:42 +02:00
Matthias 0ed613a574 fix: further improvements to exit codes 2026-03-29 13:52:51 +02:00
Matthias c7dfd54eb3 test: stabilize time-dependant test after summer time switch 2026-03-29 13:33:56 +02:00
Matthias 697814ed6a fix: improved sysexit handling
closes #12988
2026-03-29 13:30:26 +02:00
Matthias d5417d40ae Merge pull request #12986 from freqtrade/dependabot/pip/cryptography-46.0.6
chore(deps): bump cryptography from 46.0.5 to 46.0.6
2026-03-29 12:55:25 +02:00
dependabot[bot] e386334130 chore(deps): bump cryptography from 46.0.5 to 46.0.6
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.5 to 46.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.5...46.0.6)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-28 12:50:32 +00:00
Matthias 57e489b9aa Merge pull request #12985 from freqtrade/maint/pin_build
Pin individual installs in CI
2026-03-28 13:49:39 +01:00
Matthias e0d55bf5d3 chore(ci): install pinned ccxt and orjson versions 2026-03-28 13:04:42 +01:00
Matthias f169f99a5f chore(ci): pre-commit-update pin pre-commit 2026-03-28 13:03:24 +01:00
Matthias 0982b26d6b chore(ci): pin pyyaml explicitly 2026-03-28 13:02:03 +01:00
Matthias 626e719942 chore(ci): don't upgrade wheel
it'll be auto-installed in the build environment
2026-03-28 12:56:24 +01:00
Matthias d82cac3895 chore(ci): pin "build" and use the pinned version in CI 2026-03-28 12:55:02 +01:00
Matthias 3e40944b4e test: skip pygments vulnerability for now 2026-03-26 07:15:21 +01:00
Matthias 05081df48b chore(ci): revert build pinning 2026-03-26 07:12:36 +01:00
Matthias 6e6082fa2b test: update test wording for stoploss price type 2026-03-26 07:08:44 +01:00
Matthias 3c43e3dae3 feat: improve stop price type not supported message 2026-03-26 07:08:40 +01:00
Matthias 515a754dc4 docs: clarify stop price type validation logic for spot markets
(it's not validated in that case).
2026-03-26 07:08:40 +01:00
Matthias 1c98c2feeb feat(bitget): add stoploss price type support
closes #12981
2026-03-26 07:08:40 +01:00
Matthias 697059a4a2 Merge pull request #12980 from freqtrade/dependabot/pip/requests-2.33.0
chore(deps): bump requests from 2.32.5 to 2.33.0
2026-03-26 07:08:07 +01:00
Matthias 1b1508d8f2 Merge pull request #12982 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2026-03-26 07:02:25 +01:00
Freqtrade Bot 48905e55e5 chore: update pre-commit hooks 2026-03-26 04:18:28 +00:00
Matthias a203f4129a Merge pull request #12952 from Briarion/feat/pytorch-early-stopping
feat: add early stopping support to PyTorchModelTrainer
2026-03-25 19:54:48 +01:00
dependabot[bot] 0b3f3a94b5 chore(deps): bump requests from 2.32.5 to 2.33.0
Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.5...v2.33.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.33.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 18:12:12 +00:00
Briarion daf9918bb5 docs: add early_stopping_patience to parameter table
Document the new early_stopping_patience trainer_kwargs parameter
in the FreqAI parameter table, including description, datatype,
default value, and usage notes.
2026-03-25 15:29:15 +03:00
Briarion 3bde73740c feat: add early stopping support to PyTorchModelTrainer
Add optional early stopping to prevent overfitting in PyTorch-based
FreqAI models. When `early_stopping_patience` is set in
model_training_parameters, training will stop if validation loss
does not improve for the specified number of epochs.

Changes:
- Add `early_stopping_patience` parameter (default 0 = disabled)
- `estimate_loss()` now returns average loss (float | None) instead
  of None, enabling downstream use for schedulers and early stopping
- Track best validation loss and patience counter across epochs

Usage in config:
```json
{
  "model_training_parameters": {
    "n_epochs": 100,
    "early_stopping_patience": 10
  }
}
```

The change is fully backward compatible - early stopping is disabled
by default, and the return value of estimate_loss() can be safely
ignored by existing subclasses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:24:58 +03:00
Matthias 3f9eaba1ef Merge pull request #12979 from freqtrade/fix/freqai_torch
Fix freqai torch model loading
2026-03-25 12:45:41 +01:00
Matthias d557b4b298 Merge branch 'develop' into fix/freqai_torch 2026-03-25 12:31:28 +01:00
Matthias fd858b79af test: remove "ignore-vuln" from pip-audit tests
the vulnerability was fixed.
2026-03-25 10:58:39 +01:00
Matthias e96c67e140 fix: pin build to 1.4.0 for now 2026-03-25 10:58:02 +01:00
Matthias faf28180a5 docs: fix minor documentation bug 2026-03-24 20:57:17 +01:00
Matthias e98959f3f0 fix: set weights_only=False when loading from pytorch
closes #12964
2026-03-24 20:31:35 +01:00
Matthias 0a2c3379c6 docs: add security warning message to pytorch docs 2026-03-24 20:29:45 +01:00
Matthias b8f5df7cf6 chore: remove unnecessary child method
(it does the same thing than the parent).
2026-03-24 20:15:09 +01:00
Matthias 04a4a97efb Merge pull request #12974 from freqtrade/maint/migrate_add_event_handler
migrate fastapi add event handler
2026-03-24 07:05:58 +01:00
Matthias 143d9a90c6 chore: import sorting 2026-03-24 06:33:57 +01:00
Matthias c112fa6632 Merge pull request #12978 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2026-03-24 06:29:43 +01:00
Freqtrade Bot cb46f72109 chore: update pre-commit hooks 2026-03-24 03:47:28 +00:00
Matthias 2eb5efed64 Merge pull request #12975 from hallonstedt/docs-krakenfutures-exchange-features
docs: add Kraken futures row to exchange features
2026-03-23 10:51:56 +01:00
matstedt 7657e37f67 docs: add Kraken futures row to exchange features 2026-03-23 09:36:43 +01:00
Matthias ec763e684f Merge pull request #12967 from freqtrade/dependabot/pip/develop/scipy-7cfe552463
chore(deps-dev): bump scipy-stubs from 1.17.1.1 to 1.17.1.2 in the scipy group
2026-03-23 08:00:57 +01:00
Matthias d3847080e8 Merge pull request #12966 from freqtrade/dependabot/pip/develop/mkdocs-d673e4145a
chore(deps): bump mkdocs-material from 9.7.4 to 9.7.5 in the mkdocs group
2026-03-23 07:54:00 +01:00
Matthias 97305221ac Merge pull request #12968 from freqtrade/dependabot/github_actions/develop/actions-1fa769d870
chore(deps): bump actions/download-artifact from 8.0.0 to 8.0.1 in the actions group
2026-03-23 07:53:08 +01:00
Matthias b65aa8671a Merge pull request #12970 from freqtrade/dependabot/pip/develop/cachetools-7.0.5
chore(deps): bump cachetools from 7.0.4 to 7.0.5
2026-03-23 07:50:47 +01:00
Matthias d35bc15562 Merge pull request #12971 from freqtrade/dependabot/pip/develop/numpy-2.4.3
chore(deps): bump numpy from 2.4.2 to 2.4.3
2026-03-23 07:50:31 +01:00
Matthias a9cabe1162 Merge pull request #12969 from freqtrade/dependabot/pip/develop/pyjwt-2.12.1
chore(deps): bump pyjwt from 2.12.0 to 2.12.1
2026-03-23 07:33:02 +01:00
Matthias bb6f4eb60d Merge pull request #12972 from freqtrade/dependabot/pip/develop/filelock-3.25.2
chore(deps): bump filelock from 3.25.0 to 3.25.2
2026-03-23 07:21:10 +01:00
Matthias cea1700eaa chore: drop starlette upper version bound 2026-03-23 07:20:08 +01:00
Matthias bb2f4ac37e fix: migrate from add_event_handler to lifespan
add_event_handler was removed in starlette 1.0.0
2026-03-23 07:19:53 +01:00
Matthias 996123ea52 Merge pull request #12973 from freqtrade/dependabot/pip/develop/ruff-0.15.6
chore(deps-dev): bump ruff from 0.15.5 to 0.15.6
2026-03-23 07:19:43 +01:00
Matthias 252b7b5e68 chore: bump scipy-stubs in pre-commit config 2026-03-23 07:06:29 +01:00
dependabot[bot] fefba87922 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.7.4 to 9.7.5
- [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.7.4...9.7.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 06:04:51 +00:00
dependabot[bot] 434911c1cf chore(deps-dev): bump scipy-stubs in the scipy group
Bumps the scipy group with 1 update: [scipy-stubs](https://github.com/scipy/scipy-stubs).


Updates `scipy-stubs` from 1.17.1.1 to 1.17.1.2
- [Release notes](https://github.com/scipy/scipy-stubs/releases)
- [Commits](https://github.com/scipy/scipy-stubs/compare/v1.17.1.1...v1.17.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 06:04:20 +00:00
dependabot[bot] 1d947ba21a chore(deps): bump actions/download-artifact in the actions group
Bumps the actions group with 1 update: [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/download-artifact` from 8.0.0 to 8.0.1
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 05:56:41 +00:00
dependabot[bot] 55a6591f10 chore(deps): bump cachetools from 7.0.4 to 7.0.5
Bumps [cachetools](https://github.com/tkem/cachetools) from 7.0.4 to 7.0.5.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v7.0.4...v7.0.5)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-version: 7.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 05:55:39 +00:00
dependabot[bot] 266fe5c6cb chore(deps): bump numpy from 2.4.2 to 2.4.3
Bumps [numpy](https://github.com/numpy/numpy) from 2.4.2 to 2.4.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.4.2...v2.4.3)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 05:51:35 +00:00
dependabot[bot] 3d4395ebe6 chore(deps): bump filelock from 3.25.0 to 3.25.2
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.25.0 to 3.25.2.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.25.0...3.25.2)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.25.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 05:44:30 +00:00
dependabot[bot] 463f0050ed chore(deps-dev): bump ruff from 0.15.5 to 0.15.6
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.5 to 0.15.6.
- [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.15.5...0.15.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 05:42:49 +00:00
dependabot[bot] b0b9c8d43a chore(deps): bump pyjwt from 2.12.0 to 2.12.1
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.12.0 to 2.12.1.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.12.0...2.12.1)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 05:41:35 +00:00
Matthias 94d0080208 chore: pin starlette to < 1.0.0 2026-03-23 06:37:34 +01:00
Matthias 250156f1a3 Merge pull request #12706 from hallonstedt/feature-krakenfutures
Feature krakenfutures
2026-03-22 19:22:43 +01:00
Matthias 724318c43e test(krakenfutures): Add canceled order test 2026-03-22 19:07:19 +01:00
Matthias 37dbaae8aa feat(krakenfutures): exchange_override fetchOrders to false
to force fallback to individual orders
2026-03-22 19:07:19 +01:00
Matthias 1bd0404503 fix: workaround for missing "filled" parsing 2026-03-22 19:07:19 +01:00
matstedt eba9c24510 refactor(krakenfutures): drop resolved trigger price workaround 2026-03-22 19:07:19 +01:00
Matthias 6bf1901f9b chore: bump ccxt to 4.5.44 2026-03-22 19:07:19 +01:00
Matthias bd9697e6be test: fix krakenfutures tests 2026-03-22 19:07:19 +01:00
Matthias 200b9da2a2 test: remove unnecessary test
ccxt limit is tested as part of test_ohlcv_limit_futures in compat testing
2026-03-22 19:07:19 +01:00
Matthias 32fecd7b15 test(krakenfutures): improve _ft_has test 2026-03-22 19:07:19 +01:00
Matthias f8b99c6e66 test(krakenfutures): update stop strip test 2026-03-22 19:07:19 +01:00
Matthias 0fda77a39c chore: filter open orders by pair 2026-03-22 19:07:19 +01:00
Matthias 57c7edfe26 test: update krakenfutures live test 2026-03-22 19:07:19 +01:00
Matthias 437d0a264c test: Improve test sequence (simplifies analysis) 2026-03-22 19:07:19 +01:00
Matthias 3a28c91635 test: add regular market order 2026-03-22 19:07:19 +01:00
Matthias 1f4c188dbf test: add futures market-parsing test 2026-03-22 19:07:19 +01:00
matstedt 7f9b1a9f90 krakenfutures: correct ccxt issue link to 27996 2026-03-22 19:07:19 +01:00
matstedt 5096ec8a50 krakenfutures: defer order-level fee aggregation 2026-03-22 19:07:19 +01:00
matstedt 35d812a271 krakenfutures: run order adjustments via _order_contracts_to_amount 2026-03-22 19:07:19 +01:00
matstedt 1e79c1961b krakenfutures: aggregate order fees in _adjust_krakenfutures_order 2026-03-22 19:07:19 +01:00
matstedt bfd758d353 krakenfutures: recompute average and cost from fills for terminal orders 2026-03-22 19:07:19 +01:00
matstedt b1ef0bf58d krakenfutures: use safe_value_nested for trigger price extraction 2026-03-22 19:07:19 +01:00
Matthias d25b2ea10a fix: crash on comparison of filled=None 2026-03-22 19:07:19 +01:00
matstedt c533e48310 refactor: remove funding_fee_candle_limit override
Kraken Futures now provides 1 year of hourly funding rate history.
The previous limit of 700 candles (29 days) was a workaround for
limited API retention. Freqtrade paginates funding rate fetches
automatically, so the CCXT default (2000) works correctly.
2026-03-22 19:07:19 +01:00
matstedt a1e9b6bea8 fix: compute average price from trades and enrich fees for Kraken Futures
Kraken Futures' /orders/status returns limitPrice (not fill price) and
/fills omits fee amounts (only fillType). This adds:

- _adjust_krakenfutures_order: fetches trades for closed/filled orders
  with average=None and computes VWAP average price.
- get_trades_for_order override: enriches trades with calculated fees
  from the market's maker/taker fee schedule.

Tests: 7 new tests covering VWAP computation, fee enrichment with
maker/taker rates, and preservation of existing values.
2026-03-22 19:07:19 +01:00
matstedt 0dfd7324bd fix: extract triggerPrice from priceTriggerOptions for stoploss orders
CCXT's krakenfutures parse_order misses triggerPrice when the
/orders/status endpoint nests it inside priceTriggerOptions.
Override _order_contracts_to_amount to populate triggerPrice and
stopPrice from info.order.priceTriggerOptions.triggerPrice.
2026-03-22 19:07:19 +01:00
Matthias 3c4b07ce46 test: update krakenfutures_tests for usage of __name__ 2026-03-22 19:07:19 +01:00
Matthias d6e6c9d038 chore: log fetch_order responses for better tracability 2026-03-22 19:07:19 +01:00
Matthias 027ac3d66c test: simplify longrun tests 2026-03-22 19:07:19 +01:00
matstedt 8c1674b415 fix(tests): use CandleType.FUTURES in test_ohlcv_limit_futures
The futures_only ternary incorrectly used CandleType.SPOT for exchanges
that support both spot and futures (binance, gate), causing candle limit
mismatch against candle_count_futures.
2026-03-22 19:07:19 +01:00
matstedt eaa35d7b47 style: ruff format 2026-03-22 19:07:19 +01:00
matstedt 0dc26584cc krakenfutures: add funding_fee_candle_limit for limited history
Kraken Futures retains only ~29 days of hourly funding rate history.
Set funding_fee_candle_limit to 700 so ohlcv_candle_limit returns the
correct cap for CandleType.FUNDING_RATE instead of the general OHLCV
limit.
2026-03-22 19:07:19 +01:00
matstedt cb2e50abcf fix(tests): restore exchange_params return in get_futures_exchange 2026-03-22 19:07:19 +01:00
matstedt f0aedc8d23 test(krakenfutures): align tests with simplified stoploss handling
- Add test for stoploss_query_requires_stop_flag in _ft_has.
- Replace _fix_trigger_order_id tests with base class integration tests
  (verify stop=True is passed by fetch_stoploss_order/cancel_stoploss_order).
- Add tests for stop param flow: passed to history endpoints, stripped
  from open orders query.
- Remove obsolete cancel_stoploss_order/fetch_stoploss_order override tests.
2026-03-22 19:07:19 +01:00
matstedt 086e1d8bad refactor(krakenfutures): simplify stoploss handling after ccxt fix
- Add stoploss_query_requires_stop_flag to _ft_has so the base class
  handles fetch_stoploss_order and cancel_stoploss_order (adds stop=True
  which CCXT maps to trigger history endpoints).
- Remove _fix_trigger_order_id — ccxt 4.5.38 now parses order IDs
  correctly in all response formats.
- Remove cancel_stoploss_order override — base class handles it.
- Remove fetch_stoploss_order override — base class handles it.
- Simplify _fetch_order_fallback — stop=True flows from base class,
  no need to auto-retry with trigger=True.

Addresses review comments from freqtrade/freqtrade#12706.
2026-03-22 19:07:19 +01:00
Matthias 83c2ca5a90 test: krakenfutures has no websockets at the moment 2026-03-22 19:07:19 +01:00
Matthias 68c8dd4e18 chore: krakenfutures tickers don't have volume 2026-03-22 19:07:19 +01:00
Matthias b25defb8b9 refactor: invert balance logic to simplify function 2026-03-22 19:07:19 +01:00
Matthias b190079aa8 test: Simplify live test setup 2026-03-22 19:07:19 +01:00
Matthias a831a6ee34 docs: align krakenfutures link in Readme.md 2026-03-22 19:07:19 +01:00
matstedt bfbae5d040 fix(krakenfutures): re-introduce improved stoploss lookup for trigger orders 2026-03-22 19:07:19 +01:00
matstedt bb2f19deaa krakenfutures: replace assert with typed None-guard for ruff S101 2026-03-22 19:07:19 +01:00
matstedt cfa4924b8e krakenfutures: satisfy mypy narrowing in flex USD used calculation 2026-03-22 19:07:19 +01:00
matstedt 4f8291384d docs: align Kraken Futures config and collateral guidance 2026-03-22 19:07:05 +01:00
matstedt f81e335b27 krakenfutures: simplify order/balance handling and harden error mapping 2026-03-22 19:07:05 +01:00
matstedt 756f178ec3 exchange_utils: use ccxt exchange.has directly in capability checks 2026-03-22 19:07:05 +01:00
Matthias c68c6896d7 chore: revert odd, unnecessary modifications 2026-03-22 19:07:05 +01:00
Matthias 712fd8ac14 chore: import formatting 2026-03-22 19:07:05 +01:00
Matthias cb6a1267b6 chore: remove pointless comments 2026-03-22 19:07:05 +01:00
matstedt 1c77bd6f5d krakenfutures: improve fetch_order fallback 2026-03-22 19:07:05 +01:00
Matthias 1d0bcbe234 test: remove no longer necessary test 2026-03-22 19:07:05 +01:00
Matthias 445a22222a chore: remove leverage tier fixing code 2026-03-22 19:07:04 +01:00
matstedt ba80329eda krakenfutures: guard maxNotional for non‑contract tiers 2026-03-22 19:07:04 +01:00
matstedt a8d7a72cec tests: drop unused krakenfutures spot leverage flag 2026-03-22 19:07:04 +01:00
matstedt 6ed470884b krakenfutures: backfill leverage tier notionals 2026-03-22 19:07:04 +01:00
matstedt 0d4dadd63f docs: add Kraken to the list of supported Futures Exchanges 2026-03-22 19:07:04 +01:00
matstedt 0cab0e8ac5 fix(tests): use myokx for non-futures list-exchanges check 2026-03-22 19:07:04 +01:00
matstedt 9ddf464c03 fix(exchange): drop unused get_ft_has override plumbing 2026-03-22 19:07:04 +01:00
matstedt 06abc95b2e test: fix contradictory sorting by ruff and isort 7 2026-03-22 19:07:04 +01:00
matstedt b4daabe491 test: re-organize imports 2026-03-22 19:07:04 +01:00
matstedt e5a23f5afd test: fix import order in exchange utils test 2026-03-22 19:07:04 +01:00
matstedt 7ee4338b1f tests: increase coverage - krakenfutures edge paths and ft_has overrides 2026-03-22 19:07:04 +01:00
matstedt 3f6e7ca4c1 tests: cover krakenfutures currency sum and funding fees 2026-03-22 19:07:04 +01:00
matstedt 1345cfdbee exchange: fallback to canceled orders for krakenfutures fetch_order; add tests 2026-03-22 19:07:04 +01:00
matstedt f2780c7c68 exchange: drop ccxt workarounds; keep closed-order fallback 2026-03-22 19:07:04 +01:00
matstedt 1254bd5bab tests: align krakenfutures ohlcv limit check 2026-03-22 19:07:04 +01:00
matstedt cef63725aa exchange: drop krakenfutures ohlcv limit override 2026-03-22 19:07:04 +01:00
matstedt 5af4147eb3 tests: adjust list-exchanges futures assertions for krakenfutures 2026-03-22 19:07:04 +01:00
matstedt ca9d4ed026 tests: fix _exchange_has_helper input in exchange_utils test 2026-03-22 19:07:04 +01:00
matstedt 15359f57a8 krakenfutures: avoid mypy optional assignment in fetch_order 2026-03-22 19:07:04 +01:00
matstedt 4b23e9ccce krakenfutures: strip history-only params from open/closed order calls 2026-03-22 19:07:04 +01:00
matstedt 35706eb9ec krakenfutures: restore get_ft_has so exchange validation sees fetchOrder/createMarketOrder overrides until CCXT is updated 2026-03-22 19:07:04 +01:00
matstedt 272771af38 docs(exchanges): remove redundant text 2026-03-22 19:07:04 +01:00
matstedt 2ed3db330a krakenfutures: use fetch_order_emulated with history fallback 2026-03-22 19:07:04 +01:00
matstedt 38d197b3bf krakenfutures: rely on ft_has stoploss params, update tests/docs 2026-03-22 19:07:04 +01:00
Mattias Hellerstedt 6c588cce65 docs: clarify flex account handling in Kraken Futures balances 2026-03-22 19:07:04 +01:00
matstedt 5752e52b0f tests: update krakenfutures tests for get_ft_has removal and InvalidOrderException 2026-03-22 19:07:04 +01:00
matstedt 2077f34a78 krakenfutures: use Bybit/Hyperliquid funding fee calculation 2026-03-22 19:07:04 +01:00
matstedt 7718e8e1e9 krakenfutures: fix fetch_order to raise exception instead of returning pseudo order 2026-03-22 19:07:04 +01:00
matstedt 06e2b8ae0d style: ruff format 2026-03-22 19:07:04 +01:00
matstedt 42361bc046 krakenfutures: remove redundant get_ft_has method 2026-03-22 19:07:04 +01:00
matstedt 507ea87e9f krakenfutures: convert _filter_params_for_open_closed to instance method 2026-03-22 19:07:04 +01:00
matstedt 60f71bbc2b tests: re-enable hyperliquid spot online tests 2026-03-22 19:07:04 +01:00
matstedt ea5ae9627d style: ruff format 2026-03-22 19:07:04 +01:00
matstedt 6fdeab2747 krakenfutures: align class name with resolver titlecase and remove unnecessary alias 2026-03-22 19:07:04 +01:00
matstedt 04ec24e737 krakenfutures: move ohlcv cap to _ft_has and support futures-only online tests 2026-03-22 19:07:04 +01:00
matstedt d9629c4d67 docs(exchange): add Kraken Futures notes 2026-03-22 19:07:04 +01:00
matstedt 2f7e77a41e test(exchange): add Kraken Futures exchange tests 2026-03-22 19:07:04 +01:00
matstedt 46e96e20a9 feat(exchange): add Kraken Futures exchange class 2026-03-22 19:07:04 +01:00
matstedt 1a5f4127e0 feat(exchange): register Kraken Futures exchange 2026-03-22 19:07:04 +01:00
matstedt 51588c2ef1 feat(exchange): apply ft_has overrides in check_exchange 2026-03-22 19:07:04 +01:00
matstedt 271ee6d36e feat(exchange): honor subclass has overrides in exchange validation 2026-03-22 19:07:04 +01:00
matstedt 72995835ea feat(exchange): enable krakenfutures in supported list 2026-03-22 19:07:04 +01:00
Matthias 1922f15735 chore(ci): remove reference to non-existing step 2026-03-22 09:12:25 +01:00
Matthias 633ff8c98c chore(ci): remove outdated action
it's not needed as the new discord action has nofail set by default
2026-03-22 09:10:15 +01:00
Matthias 10b2a27342 Merge pull request #12962 from freqtrade/maint/ci-discord-notify
Update discord notify action
2026-03-22 09:04:01 +01:00
Matthias 6ed329bac3 chore(ci): replace discord-webhook-notify
it's buggy since months without update / fix - we need a maintained action.
2026-03-22 08:45:07 +01:00
Matthias cebbb40c99 test: remove testing for time_in_ratio 2026-03-21 14:09:49 +01:00
Matthias adec491b3b chore: remove unused argument time_in_ratio
It was there in preparation for kraken, but kraken
doesn't seem to have this anymore.
2026-03-21 14:09:42 +01:00
Matthias cbeae975f0 test: simplify funding fee test 2026-03-21 14:08:20 +01:00
Matthias a2b1c3710f chore: remove pointless futures stuff from kraken class
(kraken futures can only be supported via krakenfutures class)
2026-03-21 14:00:57 +01:00
Matthias bbb1fecb02 test: enhanced fetch_order_fallback test 2026-03-21 09:32:28 +01:00
Matthias b7c89a72fa feat: enhance orders_emulate to include canceled orders 2026-03-21 09:19:27 +01:00
Matthias 0a7fe730f0 fix: trade recovery improvement 2026-03-21 08:51:15 +01:00
Matthias cadb269b58 Merge pull request #12957 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2026-03-19 07:40:47 +01:00
Matthias 168962f847 Merge pull request #12953 from Briarion/fix/data-kitchen-fragmentation
fix: avoid DataFrame fragmentation in get_predictions_to_append
2026-03-19 07:24:45 +01:00
Freqtrade Bot e70e56df96 chore: update pre-commit hooks 2026-03-19 07:22:10 +01:00
Matthias 3242be2e29 fix: add dynamic timeframe_floor freq
fixes problems where funding-rates may be on the 1st second of the hour
(observed on gate from time to time)
2026-03-19 07:19:07 +01:00
Matthias 7fc6463900 refactor: align naming for clean_ohlcv_dataframe 2026-03-19 07:04:49 +01:00
Briarion 9560ba2144 fix: move do_predict and DI_values into dict before DataFrame construction
Address review feedback: do_predict and DI_values were still assigned
after DataFrame creation, partially defeating the fragmentation fix.
Now all columns are collected in the dict before the single DataFrame()
call, ensuring zero post-construction column assignments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 08:05:22 +03:00
Matthias 257e26f2c0 Merge pull request #12955 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2026-03-17 06:29:38 +01:00
Freqtrade Bot 27a650cf1c chore: update pre-commit hooks 2026-03-17 03:47:29 +00:00
Matthias 5dca913b9e Merge pull request #12936 from freqtrade/dependabot/pip/develop/types-9ab316b5d0
chore(deps-dev): bump the types group with 2 updates
2026-03-16 14:56:49 +01:00
Matthias 6492f3d29e chore: bump types in pre-commit-config 2026-03-16 14:43:22 +01:00
dependabot[bot] 21c5cc7994 chore(deps-dev): bump the types group with 2 updates
Bumps the types group with 2 updates: [types-tabulate](https://github.com/typeshed-internal/stub_uploader) and [types-python-dateutil](https://github.com/typeshed-internal/stub_uploader).


Updates `types-tabulate` from 0.9.0.20241207 to 0.10.0.20260308
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

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

---
updated-dependencies:
- dependency-name: types-tabulate
  dependency-version: 0.10.0.20260308
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: types
- dependency-name: types-python-dateutil
  dependency-version: 2.9.0.20260305
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: types
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 10:14:00 +00:00
Matthias 056bb213f8 Merge pull request #12941 from freqtrade/dependabot/pip/develop/sqlalchemy-2.0.48
chore(deps): bump sqlalchemy from 2.0.47 to 2.0.48
2026-03-16 11:10:02 +01:00
Matthias 5d3c65f7da chore: bump sqlalchemy in pre-commit config 2026-03-16 09:00:57 +01:00
dependabot[bot] 5061554ada chore(deps): bump sqlalchemy from 2.0.47 to 2.0.48
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.47 to 2.0.48.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

---
updated-dependencies:
- dependency-name: sqlalchemy
  dependency-version: 2.0.48
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 07:56:21 +00:00
Matthias 14e97e3f22 Merge pull request #12938 from freqtrade/dependabot/pip/develop/scipy-1e08eb4daa
chore(deps-dev): bump scipy-stubs from 1.17.1.0 to 1.17.1.1 in the scipy group
2026-03-16 07:36:56 +01:00
Matthias 0f816ddfb7 Merge pull request #12937 from freqtrade/dependabot/pip/develop/mkdocs-4d03651020
chore(deps): bump mkdocs-material from 9.7.3 to 9.7.4 in the mkdocs group
2026-03-16 07:29:06 +01:00
dependabot[bot] 92dd17ab62 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.7.3 to 9.7.4
- [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.7.3...9.7.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 05:49:03 +00:00
Briarion a8c887a5b0 fix: avoid DataFrame fragmentation in get_predictions_to_append
Replace column-by-column DataFrame assignment with dict-based
construction. The previous approach triggered pandas
PerformanceWarning about DataFrame fragmentation when many
prediction columns and their corresponding mean/std columns
were added one at a time.

Also add defensive key checks (label in self.data["labels_mean"])
to prevent KeyError when custom models produce prediction columns
that don't have corresponding entries in labels_mean/labels_std.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 08:46:57 +03:00
Matthias 009fa747e3 chore: combine actions updates from docker/*
they release as one anyway
2026-03-16 06:45:47 +01:00
Matthias 2744449773 Merge pull request #12950 from stash86/main-stash
fix cagr on zero or negative values
2026-03-16 06:41:47 +01:00
Matthias 537426d77a Merge pull request #12948 from freqtrade/dependabot/pip/develop/tabulate-0.10.0
chore(deps): bump tabulate from 0.9.0 to 0.10.0
2026-03-16 06:38:45 +01:00
Matthias e3994d1add chore: bump scipy-stubs in pre-commit config 2026-03-16 06:37:24 +01:00
Matthias 720cd8b68c Merge pull request #12946 from freqtrade/dependabot/github_actions/develop/docker/setup-qemu-action-4.0.0
chore(deps): bump docker/setup-qemu-action from 3.7.0 to 4.0.0
2026-03-16 06:36:21 +01:00
Matthias 3846bd16f0 Merge pull request #12944 from freqtrade/dependabot/github_actions/develop/docker/login-action-4.0.0
chore(deps): bump docker/login-action from 3.7.0 to 4.0.0
2026-03-16 06:36:08 +01:00
Matthias 7863d4b854 Merge pull request #12945 from freqtrade/dependabot/pip/develop/pytz-2026.1.post1
chore(deps): bump pytz from 2025.2 to 2026.1.post1
2026-03-16 06:34:44 +01:00
Matthias 7e8da54ad1 Merge pull request #12943 from freqtrade/dependabot/pip/develop/plotly-6.6.0
chore(deps): bump plotly from 6.5.2 to 6.6.0
2026-03-16 06:34:30 +01:00
Matthias ae06f90ca6 Merge pull request #12942 from freqtrade/dependabot/pip/develop/cachetools-7.0.4
chore(deps): bump cachetools from 7.0.1 to 7.0.4
2026-03-16 06:34:06 +01:00
Matthias 8a7b235b6e Merge pull request #12940 from freqtrade/dependabot/github_actions/develop/zizmorcore/zizmor-action-0.5.2
chore(deps): bump zizmorcore/zizmor-action from 0.5.0 to 0.5.2
2026-03-16 06:33:53 +01:00
Matthias daa0361c02 Merge pull request #12939 from freqtrade/dependabot/github_actions/develop/docker/setup-buildx-action-4.0.0
chore(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0
2026-03-16 06:33:33 +01:00
Matthias 9136877bc7 Merge pull request #12949 from freqtrade/dependabot/pip/develop/mike-2.1.4
chore(deps): bump mike from 2.1.3 to 2.1.4
2026-03-16 06:33:03 +01:00
Matthias 2df3890917 Merge pull request #12947 from freqtrade/dependabot/pip/develop/ruff-0.15.5
chore(deps-dev): bump ruff from 0.15.4 to 0.15.5
2026-03-16 06:32:50 +01:00
Stefano f2b930079b fix cagr on zero or negative values 2026-03-16 12:38:39 +09:00
dependabot[bot] f0a83c9e59 chore(deps): bump mike from 2.1.3 to 2.1.4
Bumps [mike](https://github.com/jimporter/mike) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/jimporter/mike/releases)
- [Changelog](https://github.com/jimporter/mike/blob/master/CHANGES.md)
- [Commits](https://github.com/jimporter/mike/compare/v2.1.3...v2.1.4)

---
updated-dependencies:
- dependency-name: mike
  dependency-version: 2.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:04:36 +00:00
dependabot[bot] b12ea975e0 chore(deps): bump tabulate from 0.9.0 to 0.10.0
Bumps [tabulate](https://github.com/astanin/python-tabulate) from 0.9.0 to 0.10.0.
- [Changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG)
- [Commits](https://github.com/astanin/python-tabulate/compare/v0.9.0...v0.10.0)

---
updated-dependencies:
- dependency-name: tabulate
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:04:30 +00:00
dependabot[bot] ab74dd785c chore(deps-dev): bump ruff from 0.15.4 to 0.15.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.4 to 0.15.5.
- [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.15.4...0.15.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:04:25 +00:00
dependabot[bot] 39affd30f9 chore(deps): bump docker/setup-qemu-action from 3.7.0 to 4.0.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/c7c53464625b32c7a7e944ae62b3e17d2b600130...ce360397dd3f832beb865e1373c09c0e9f86d70a)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:04:20 +00:00
dependabot[bot] ff6c96022e chore(deps): bump pytz from 2025.2 to 2026.1.post1
Bumps [pytz](https://github.com/stub42/pytz) from 2025.2 to 2026.1.post1.
- [Release notes](https://github.com/stub42/pytz/releases)
- [Commits](https://github.com/stub42/pytz/compare/release_2025.2...release_2026.1.post1)

---
updated-dependencies:
- dependency-name: pytz
  dependency-version: 2026.1.post1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:04:06 +00:00
dependabot[bot] fc3b8f37b0 chore(deps): bump docker/login-action from 3.7.0 to 4.0.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...b45d80f862d83dbcd57f89517bcf500b2ab88fb2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:03:54 +00:00
dependabot[bot] 26615dbd55 chore(deps): bump plotly from 6.5.2 to 6.6.0
Bumps [plotly](https://github.com/plotly/plotly.py) from 6.5.2 to 6.6.0.
- [Release notes](https://github.com/plotly/plotly.py/releases)
- [Changelog](https://github.com/plotly/plotly.py/blob/main/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.py/compare/v6.5.2...v6.6.0)

---
updated-dependencies:
- dependency-name: plotly
  dependency-version: 6.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:03:45 +00:00
dependabot[bot] 6411a76f03 chore(deps): bump cachetools from 7.0.1 to 7.0.4
Bumps [cachetools](https://github.com/tkem/cachetools) from 7.0.1 to 7.0.4.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v7.0.1...v7.0.4)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-version: 7.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:03:40 +00:00
dependabot[bot] ce1734b176 chore(deps): bump zizmorcore/zizmor-action from 0.5.0 to 0.5.2
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.0 to 0.5.2.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d...71321a20a9ded102f6e9ce5718a2fcec2c4f70d8)

---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:03:32 +00:00
dependabot[bot] 2b3e3a5c46 chore(deps): bump docker/setup-buildx-action from 3.12.0 to 4.0.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.12.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/8d2750c68a42422c14e847fe6c8ac0403b4cbd6f...4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:03:28 +00:00
dependabot[bot] 0eddda2ee1 chore(deps-dev): bump scipy-stubs in the scipy group
Bumps the scipy group with 1 update: [scipy-stubs](https://github.com/scipy/scipy-stubs).


Updates `scipy-stubs` from 1.17.1.0 to 1.17.1.1
- [Release notes](https://github.com/scipy/scipy-stubs/releases)
- [Commits](https://github.com/scipy/scipy-stubs/compare/v1.17.1.0...v1.17.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 03:03:26 +00:00
Matthias 05ffc2dba0 chore: migrate pytest ini_options to pytest section 2026-03-15 09:11:03 +01:00
Matthias ec1aee1f85 chore: reorder pyproject.toml dependencies 2026-03-15 08:58:27 +01:00
Matthias 9524fc0f81 chore: remove isort - ruff covers this already 2026-03-15 08:57:22 +01:00
Matthias 3d56701e20 chore: remove unused black configuration 2026-03-15 08:54:08 +01:00
Matthias d8d86ce861 chore: remove flake8 pre-commit check 2026-03-15 08:52:43 +01:00
Matthias 503dfb7313 docs: update recommendation from black to ruff format 2026-03-15 08:52:33 +01:00
Matthias 16717df1a9 docs: improve sql cheatsheet example 2026-03-15 08:44:51 +01:00
Matthias 6713cda109 docs: Update timeout docs to point to adjust_order_price
part of #12928
2026-03-15 08:41:56 +01:00
Matthias 0652339356 docs: fix parameters table formatting 2026-03-15 08:39:12 +01:00
Matthias 59a81dc07a fix: round exit-prices to proper precision
this avoids odd visualization in freqUI where prices had 16 decimals
out of backtesting
2026-03-14 17:00:46 +01:00
Matthias fcecbe780b test: Re-write test-data with proper decimals 2026-03-14 16:40:04 +01:00
Matthias be00f2c26f test: update mock for price_to_precision 2026-03-14 15:54:18 +01:00
Matthias 10ba9e3105 Merge pull request #12930 from freqtrade/dependabot/pip/pyjwt-2.12.0
chore(deps): bump pyjwt from 2.11.0 to 2.12.0
2026-03-14 08:20:16 +01:00
dependabot[bot] 1498860530 chore(deps): bump pyjwt from 2.11.0 to 2.12.0
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.11.0 to 2.12.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.11.0...2.12.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 06:58:35 +00:00
Matthias 902bc75bf3 Merge pull request #12931 from stash86/main-stash
Fix short desc error on RemotePairlist when number_assets is None
2026-03-14 07:53:22 +01:00
Matthias bd6f891b07 chore(ci): rename online action name 2026-03-14 07:52:28 +01:00
Stefano 36e259b347 undo minor typo fix 2026-03-14 12:11:52 +09:00
Stefano f1aaf99317 fix desc 2026-03-14 12:02:34 +09:00
Stefano 459a0decbd forgotten change 2026-03-14 11:59:36 +09:00
Matthias 71598294a1 chore(ci): Update python setup 2026-03-13 21:08:43 +01:00
Matthias e28eedaf1c chore(ci): re-add python step for pre-commit 2026-03-13 20:21:51 +01:00
Matthias 3cd8b2e841 chore(ci): remove unnecessary action step, bump doc build to 3.12 2026-03-13 20:14:28 +01:00
Matthias 9925c4ac14 fix: docstring typos 2026-03-13 20:11:55 +01:00
Matthias 4170c943e7 chore: update remotePairlist num_assets for webserver mode 2026-03-13 06:35:20 +01:00
Matthias 70fe211a8b Merge pull request #12927 from stash86/main-stash
number_assets in RemotePairlist becomes optional
2026-03-13 06:31:43 +01:00
Matthias 23a17859b1 fix: fastAPI user warning due to unsupported use of api_route 2026-03-13 06:29:14 +01:00
Matthias c1fa259ad7 feat: Improved docstrings for /sysinfo endpoint 2026-03-13 06:29:14 +01:00
Matthias 4a9a630765 test: add test for new sysinfo behavior 2026-03-13 06:29:14 +01:00
Matthias a1cbfce316 feat: enhance sysinfo endpoint with additional features
closes #12918
2026-03-13 06:29:14 +01:00
Stefano 5edfa675ef update docs 2026-03-13 12:01:30 +09:00
Stefano 3158adad03 one change left 2026-03-13 11:53:28 +09:00
Stefano ebf39b35b8 Merge branch 'main-stash' of https://github.com/stash86/freqtrade into main-stash 2026-03-13 11:52:45 +09:00
Stefano 756b956a77 number_assets in RemotePairlist become optional 2026-03-13 11:52:40 +09:00
Matthias b46ff76d58 Merge pull request #12925 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2026-03-12 06:52:46 +01:00
Matthias 6f039fa27c Merge pull request #12923 from stash86/main-stash
use default needstickers for rangestabilityfilter
2026-03-12 06:32:15 +01:00
Freqtrade Bot acf3559205 chore: update pre-commit hooks 2026-03-12 04:12:41 +00:00
Stefano c05fe50b47 use default needstickers 2026-03-12 08:41:46 +09:00
Matthias a51ba0a2b4 fix(hyperliquid): dex balance requests are only necessary in non-unified mode 2026-03-11 21:40:19 +01:00
Matthias 9da9250865 chore: bump ccxt to 4.5.43
closes #12848
2026-03-11 19:57:02 +01:00
Matthias 881edddb6e test: explicitly convert warnings to string 2026-03-10 19:01:39 +01:00
Matthias 0bd5086a0f Merge pull request #12914 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2026-03-10 12:31:33 +01:00
Matthias e563e07d40 Merge branch 'develop' into update/pre-commit-hooks 2026-03-10 12:16:42 +01:00
Matthias f34ad5fc42 chore(ci): pass dockerhub secrets from parent
though they only exist in the environment
2026-03-10 07:15:31 +01:00
Matthias 6b6dcd8c52 chore(ci): re-add secrets as docker build parameters 2026-03-10 06:47:48 +01:00
Matthias d5fa99696d fix(ci): allow certain secrets at repo level 2026-03-10 06:46:15 +01:00
Freqtrade Bot 2d75f002f9 chore: update pre-commit hooks 2026-03-10 03:45:37 +00:00
Matthias c31894be72 chore: enable docker description dispatch 2026-03-09 20:10:13 +01:00
Matthias 4d254952e3 fix(ci): move Docker environment vars to environment 2026-03-09 20:09:50 +01:00
Matthias 24c57b5356 fix(ci): move REPO_SCOPED_TOKEN to be an environment token 2026-03-09 19:55:36 +01:00
Matthias 186e547ce1 chore(ci): fix wrong version in upload-artifact 2026-03-09 19:45:37 +01:00
Matthias 555f653440 Merge pull request #12906 from freqtrade/dependabot/pip/develop/sqlalchemy-2.0.47
chore(deps): bump sqlalchemy from 2.0.46 to 2.0.47
2026-03-09 07:23:35 +01:00
Matthias 8ef5f54e61 chore: bump sqlalchemy in pre-commit config 2026-03-09 07:07:27 +01:00
dependabot[bot] 6576528aad chore(deps): bump sqlalchemy from 2.0.46 to 2.0.47
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.46 to 2.0.47.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

---
updated-dependencies:
- dependency-name: sqlalchemy
  dependency-version: 2.0.47
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 06:01:07 +00:00
Matthias 052e6f7865 Merge pull request #12904 from freqtrade/dependabot/pip/develop/scipy-9cda224771
chore(deps-dev): bump scipy-stubs from 1.17.0.2 to 1.17.1.0 in the scipy group
2026-03-09 06:57:11 +01:00
Matthias 560cb49d66 chore: bump scipy-stubs in pre-commit config 2026-03-09 06:33:30 +01:00
Matthias c93ca48d17 Merge pull request #12907 from freqtrade/dependabot/pip/develop/ccxt-4.5.42
chore(deps): bump ccxt from 4.5.40 to 4.5.42
2026-03-09 06:28:36 +01:00
Matthias c2d682a5ed Merge pull request #12908 from freqtrade/dependabot/github_actions/develop/astral-sh/setup-uv-7.3.1
chore(deps): bump astral-sh/setup-uv from 7.3.0 to 7.3.1
2026-03-09 06:28:10 +01:00
Matthias 1e7c9cc285 Merge pull request #12905 from freqtrade/dependabot/github_actions/develop/actions-1a3042a095
chore(deps): bump the actions group with 2 updates
2026-03-09 06:27:57 +01:00
Matthias f0a9a6a7c0 Merge pull request #12913 from freqtrade/dependabot/pip/develop/filelock-3.25.0
chore(deps): bump filelock from 3.24.3 to 3.25.0
2026-03-09 06:27:09 +01:00
Matthias 47433ebe67 Merge pull request #12912 from freqtrade/dependabot/pip/develop/isort-8.0.1
chore(deps-dev): bump isort from 8.0.0 to 8.0.1
2026-03-09 06:26:56 +01:00
Matthias e2034ed286 Merge pull request #12911 from freqtrade/dependabot/pip/develop/certifi-2026.2.25
chore(deps): bump certifi from 2026.1.4 to 2026.2.25
2026-03-09 06:26:46 +01:00
Matthias cd5b9a0d56 Merge pull request #12910 from freqtrade/dependabot/pip/develop/ruff-0.15.4
chore(deps-dev): bump ruff from 0.15.2 to 0.15.4
2026-03-09 06:26:31 +01:00
Matthias 8dd3fdafa9 Merge pull request #12903 from freqtrade/dependabot/pip/develop/mkdocs-007c418a98
chore(deps): bump mkdocs-material from 9.7.2 to 9.7.3 in the mkdocs group
2026-03-09 06:25:54 +01:00
Matthias 6d6a9619a3 Merge pull request #12909 from freqtrade/dependabot/pip/develop/fastapi-0.135.1
chore(deps): bump fastapi from 0.131.0 to 0.135.1
2026-03-09 06:24:29 +01:00
dependabot[bot] a097727e32 chore(deps): bump filelock from 3.24.3 to 3.25.0
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.24.3 to 3.25.0.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.24.3...3.25.0)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:04:50 +00:00
dependabot[bot] 14f3d7c819 chore(deps-dev): bump isort from 8.0.0 to 8.0.1
Bumps [isort](https://github.com/PyCQA/isort) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/8.0.0...8.0.1)

---
updated-dependencies:
- dependency-name: isort
  dependency-version: 8.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:04:44 +00:00
dependabot[bot] 7492f6d34e chore(deps): bump certifi from 2026.1.4 to 2026.2.25
Bumps [certifi](https://github.com/certifi/python-certifi) from 2026.1.4 to 2026.2.25.
- [Commits](https://github.com/certifi/python-certifi/compare/2026.01.04...2026.02.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:04:40 +00:00
dependabot[bot] 7a69e85cd6 chore(deps-dev): bump ruff from 0.15.2 to 0.15.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.2 to 0.15.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.15.2...0.15.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:04:28 +00:00
dependabot[bot] 27a39a4ec6 chore(deps): bump fastapi from 0.131.0 to 0.135.1
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.131.0 to 0.135.1.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.131.0...0.135.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:04:06 +00:00
dependabot[bot] 5e520136be chore(deps): bump ccxt from 4.5.40 to 4.5.42
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.40 to 4.5.42.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.40...v4.5.42)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:03:56 +00:00
dependabot[bot] c6c30135c3 chore(deps): bump astral-sh/setup-uv from 7.3.0 to 7.3.1
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.3.0 to 7.3.1.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/eac588ad8def6316056a12d4907a9d4d84ff7a3b...5a095e7a2014a4212f075830d4f7277575a9d098)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:03:56 +00:00
dependabot[bot] 6f06569a11 chore(deps): bump the actions group with 2 updates
Bumps the actions group with 2 updates: [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/upload-artifact` from 6.0.0 to 7.0.0
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

Updates `actions/download-artifact` from 7.0.0 to 8.0.0
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/37930b1c2abaa49bbe596cd826c3c89aef350131...70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:03:35 +00:00
dependabot[bot] f3f6c7b73c chore(deps-dev): bump scipy-stubs in the scipy group
Bumps the scipy group with 1 update: [scipy-stubs](https://github.com/scipy/scipy-stubs).


Updates `scipy-stubs` from 1.17.0.2 to 1.17.1.0
- [Release notes](https://github.com/scipy/scipy-stubs/releases)
- [Commits](https://github.com/scipy/scipy-stubs/compare/v1.17.0.2...v1.17.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:03:29 +00:00
dependabot[bot] 958a9307fc 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.7.2 to 9.7.3
- [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.7.2...9.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 03:03:21 +00:00
Matthias c49dc36a23 docs: hyperliquid unified accounts documentation 2026-03-08 15:45:10 +01:00
Matthias 6cf73786e3 feat: Be verbose on init 2026-03-08 15:17:19 +01:00
Matthias 71f03ffbc6 feat: support hyperliquid unified account 2026-03-08 15:15:46 +01:00
Matthias b57a7f207e test: simplify ccxt_order_parse test (but improve it at the same time) 2026-03-06 21:07:59 +01:00
Matthias b283cabbcd refactor: rely on parent class "needstickers" property 2026-03-06 18:32:14 +01:00
Matthias 38e64e06d1 refactor: use default "needstickers" function
most pairlists use False - so we can use this default and omit tons of lines.
2026-03-06 18:31:40 +01:00
Matthias b35082e45e docs: update brightness of discord badge 2026-03-06 06:38:58 +01:00
Matthias 02475ebcce Merge pull request #12899 from stash86/main-stash
fix typo
2026-03-05 09:54:47 +01:00
Stefano d584544a6a fix typo 2026-03-05 17:36:22 +09:00
Matthias 428d1397a7 Merge pull request #12874 from stash86/main-stash
Add CrossMarketPairList
2026-03-05 06:57:31 +01:00
Matthias aabda8d4f1 test: simplify test 2026-03-05 06:40:04 +01:00
Matthias 9cee5cc4d0 chore: fix pairlist docstring 2026-03-05 06:39:51 +01:00
Matthias 9dd2a9e5f5 Merge pull request #12898 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2026-03-05 06:33:51 +01:00
Freqtrade Bot 2832c6b858 chore: update pre-commit hooks 2026-03-05 04:12:28 +00:00
Matthias bc0aeb367a Merge pull request #12876 from freqtrade/fix/chinese_char_pairs
Allow non-ascii characters in pair names
2026-03-04 07:11:01 +01:00
Matthias 1e33ea0059 Merge pull request #12890 from hamadbinghalib/fix/download-data-convert-log-format
fix(configuration): avoid format-string IndexError for --convert in download-data
2026-03-04 07:01:38 +01:00
Stefano 0ca49bf1d3 Merge branch 'freqtrade:develop' into main-stash 2026-03-04 10:03:14 +09:00
Stefano c1c2d24bdc fix docs 2026-03-04 10:02:52 +09:00
Stefano bbba9579dc switch "mode" into "pairs_exist_on" 2026-03-04 09:57:11 +09:00
Matthias f92c66db56 Merge pull request #12891 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2026-03-03 09:54:36 +01:00
Freqtrade Bot f08874d9b8 chore: update pre-commit hooks 2026-03-03 03:46:52 +00:00
hamadbinghalib da0e3912c6 fix(configuration): avoid IndexError with --convert in download-data 2026-03-03 07:08:16 +04:00
Matthias e15cd81383 Merge pull request #12885 from freqtrade/dependabot/pip/develop/filelock-3.24.3
chore(deps): bump filelock from 3.24.2 to 3.24.3
2026-03-02 10:51:11 +01:00
Matthias d1a17a2508 Merge pull request #12886 from freqtrade/dependabot/pip/develop/pyarrow-23.0.1
chore(deps): bump pyarrow from 23.0.0 to 23.0.1
2026-03-02 09:22:56 +01:00
Matthias da3c44137a Merge pull request #12887 from freqtrade/dependabot/pip/develop/ast-comments-1.3.0
chore(deps): bump ast-comments from 1.2.3 to 1.3.0
2026-03-02 09:03:35 +01:00
Matthias 192bd754df chore: add armhf wheel for pyarrow 23.0.1 2026-03-02 06:53:13 +01:00
dependabot[bot] ec46299ec0 chore(deps): bump pyarrow from 23.0.0 to 23.0.1
Bumps [pyarrow](https://github.com/apache/arrow) from 23.0.0 to 23.0.1.
- [Release notes](https://github.com/apache/arrow/releases)
- [Commits](https://github.com/apache/arrow/compare/apache-arrow-23.0.0...apache-arrow-23.0.1)

---
updated-dependencies:
- dependency-name: pyarrow
  dependency-version: 23.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 05:39:01 +00:00
Matthias f1bc324eb3 Merge pull request #12884 from freqtrade/dependabot/pip/develop/uvicorn-0.41.0
chore(deps): bump uvicorn from 0.40.0 to 0.41.0
2026-03-02 06:37:54 +01:00
Matthias f5189f8846 Merge pull request #12888 from freqtrade/dependabot/pip/develop/rich-14.3.3
chore(deps): bump rich from 14.3.2 to 14.3.3
2026-03-02 06:34:52 +01:00
Matthias 29d24c7a9d Merge pull request #12883 from freqtrade/dependabot/pip/develop/ccxt-4.5.40
chore(deps): bump ccxt from 4.5.39 to 4.5.40
2026-03-02 06:34:22 +01:00
Matthias c360f8fe04 Merge pull request #12882 from freqtrade/dependabot/pip/develop/isort-8.0.0
chore(deps-dev): bump isort from 7.0.0 to 8.0.0
2026-03-02 06:34:02 +01:00
Matthias 539b7a3a22 Merge pull request #12880 from freqtrade/dependabot/pip/develop/fastapi-0.131.0
chore(deps): bump fastapi from 0.129.0 to 0.131.0
2026-03-02 06:33:38 +01:00
dependabot[bot] 8a67eee960 chore(deps): bump filelock from 3.24.2 to 3.24.3
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.24.2 to 3.24.3.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.24.2...3.24.3)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.24.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 05:32:46 +00:00
Matthias de0c275832 Merge pull request #12879 from freqtrade/dependabot/pip/develop/ruff-0.15.2
chore(deps-dev): bump ruff from 0.15.1 to 0.15.2
2026-03-02 06:31:48 +01:00
Matthias 97062ef4b8 Merge pull request #12878 from freqtrade/dependabot/pip/develop/scipy-3a2038e1d7
chore(deps): bump scipy from 1.17.0 to 1.17.1 in the scipy group
2026-03-02 06:30:49 +01:00
Matthias ac5491bf6d Merge pull request #12877 from freqtrade/dependabot/pip/develop/mkdocs-ec1788883e
chore(deps): bump mkdocs-material from 9.7.1 to 9.7.2 in the mkdocs group
2026-03-02 06:30:22 +01:00
dependabot[bot] d49075b4e2 chore(deps): bump rich from 14.3.2 to 14.3.3
Bumps [rich](https://github.com/Textualize/rich) from 14.3.2 to 14.3.3.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.3.2...v14.3.3)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 14.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:04:49 +00:00
dependabot[bot] e9f3543040 chore(deps): bump ast-comments from 1.2.3 to 1.3.0
Bumps [ast-comments](https://github.com/t3rn0/ast-comments) from 1.2.3 to 1.3.0.
- [Release notes](https://github.com/t3rn0/ast-comments/releases)
- [Commits](https://github.com/t3rn0/ast-comments/compare/1.2.3...1.3.0)

---
updated-dependencies:
- dependency-name: ast-comments
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:04:45 +00:00
dependabot[bot] 022134a652 chore(deps): bump uvicorn from 0.40.0 to 0.41.0
Bumps [uvicorn](https://github.com/Kludex/uvicorn) from 0.40.0 to 0.41.0.
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/uvicorn/compare/0.40.0...0.41.0)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:04:14 +00:00
dependabot[bot] e551cb0bfe chore(deps): bump ccxt from 4.5.39 to 4.5.40
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.39 to 4.5.40.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.39...v4.5.40)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:04:05 +00:00
dependabot[bot] 2731decf60 chore(deps-dev): bump isort from 7.0.0 to 8.0.0
Bumps [isort](https://github.com/PyCQA/isort) from 7.0.0 to 8.0.0.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/7.0.0...8.0.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-version: 8.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:03:54 +00:00
dependabot[bot] 5dad681ab6 chore(deps): bump fastapi from 0.129.0 to 0.131.0
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.129.0 to 0.131.0.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.129.0...0.131.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:03:44 +00:00
dependabot[bot] 96f7c1e37b chore(deps-dev): bump ruff from 0.15.1 to 0.15.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.1 to 0.15.2.
- [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.15.1...0.15.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:03:35 +00:00
dependabot[bot] bab787efb1 chore(deps): bump scipy from 1.17.0 to 1.17.1 in the scipy group
Bumps the scipy group with 1 update: [scipy](https://github.com/scipy/scipy).


Updates `scipy` from 1.17.0 to 1.17.1
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](https://github.com/scipy/scipy/compare/v1.17.0...v1.17.1)

---
updated-dependencies:
- dependency-name: scipy
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: scipy
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:03:24 +00:00
dependabot[bot] 2601be0777 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.7.1 to 9.7.2
- [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.7.1...9.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 03:03:12 +00:00
Stefano e487b76d1e Merge branch 'main-stash' of https://github.com/stash86/freqtrade into main-stash 2026-03-02 10:24:52 +09:00
Stefano 5788a75acc add tests to satisfy codecov 2026-03-02 10:24:45 +09:00
Stefano 64f87fe187 Update freqtrade/plugins/pairlist/CrossMarketPairList.py
Co-authored-by: Matthias <xmatthias@outlook.com>
2026-03-01 08:28:56 +09:00
Matthias 84a680d6c3 fix: improve non-matching pairs filter 2026-02-28 13:32:01 +01:00
Matthias 56d73e0d74 test: update tests for new, simplified scenarios 2026-02-28 13:03:48 +01:00
Matthias b8220a0a46 fix: Support non-utf8 characters in pair filenames 2026-02-28 13:03:19 +01:00
Matthias 31bec6b65e fix: allow chinese names as pair names 2026-02-28 13:03:19 +01:00
Matthias a73028796c chore: bump version to 2026.3-dev 2026-02-28 08:07:35 +01:00
Stefano 3b59616cbe add to checma 2026-02-28 11:03:24 +09:00
Stefano 818162b630 fix letters' case 2026-02-28 11:01:45 +09:00
Stefano 991a1b1ab7 add tests 2026-02-28 10:58:34 +09:00
Stefano b94b0926a7 Merge branch 'freqtrade:develop' into main-stash 2026-02-27 16:53:10 +09:00
Stefano 8fb21e8f50 change into pairlist plugin 2026-02-25 17:17:26 +09:00
Stefano 9719f28795 add crossmarketfilter 2026-02-25 15:51:56 +09:00
90 changed files with 7661 additions and 5636 deletions
+3 -1
View File
@@ -61,5 +61,7 @@ updates:
groups:
actions:
patterns:
# Combine updates for github provided actions
- "actions/*"
docker:
patterns:
- "docker/*"
@@ -29,14 +29,14 @@ jobs:
python-version: "3.14"
- name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
with:
activate-environment: true
enable-cache: false
python-version: "3.14"
- name: Install ccxt
run: uv pip install ccxt orjson
run: uv pip install $(grep -E "^ccxt==" requirements.txt) $(grep -E "^orjson==" requirements.txt)
- name: Run leverage tier update
env:
@@ -55,7 +55,7 @@ jobs:
Dependencies
branch: update/binance-leverage-tiers
title: Update Binance Leverage Tiers
commit-message: "chore: update pre-commit hooks"
commit-message: "chore: update binance leverage tiers"
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
body: Update binance leverage tiers.
+63 -54
View File
@@ -32,13 +32,13 @@ jobs:
with:
persist-credentials: false
- name: Set up Python
- name: Set up Python 🐍
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
with:
activate-environment: true
enable-cache: true
@@ -55,7 +55,6 @@ jobs:
- name: Installation (python)
run: |
uv pip install --upgrade wheel
uv pip install -r requirements-dev.txt
uv pip install -e ft_client/
uv pip install -e .
@@ -78,7 +77,7 @@ jobs:
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
- name: Cleanup codecov dirty state files
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
@@ -137,10 +136,6 @@ jobs:
freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
- name: Sort imports (isort)
run: |
isort --check .
- name: Run Ruff
run: |
ruff check --output-format=github
@@ -167,12 +162,12 @@ jobs:
if ($Error.Length -gt 0) {exit 1}
- name: Discord notification
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
if: ${{ failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
with:
severity: error
details: Freqtrade CI failed on ${{ matrix.os }} with Python ${{ matrix.python-version }}!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
color: '#FF0000' # red
title: Freqtrade CI failed on ${{ matrix.os }} with Python ${{ matrix.python-version }}!
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
mypy-version-check:
name: "Mypy Version Check"
@@ -182,14 +177,20 @@ jobs:
with:
persist-credentials: false
- name: Set up Python
- name: Set up Python 🐍
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0
with:
python-version: "3.12"
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
with:
activate-environment: true
python-version: "3.13"
- name: pre-commit dependencies
run: |
pip install pyaml
uv pip install $(grep -E "^pyyaml==" requirements-dev.txt)
python build_helpers/pre_commit_update.py
pre-commit:
@@ -202,7 +203,8 @@ jobs:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
python-version: "3.13"
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
docs-check:
@@ -217,51 +219,59 @@ jobs:
run: |
./tests/test_docs.sh
- name: Set up Python
- name: Set up Python 🐍
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
with:
activate-environment: true
python-version: "3.13"
- name: Documentation build
run: |
pip install -r docs/requirements-docs.txt
uv pip install -r docs/requirements-docs.txt
mkdocs build
- name: Discord notification
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
severity: error
details: Freqtrade doc test failed!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
color: '#FF0000' # red
title: Freqtrade doc test failed!
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
build-linux-online:
# Run pytest with "live" checks
name: "Tests and Linting - Online tests"
name: "Online / live tests"
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python
- name: Set up Python 🐍
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
with:
activate-environment: true
enable-cache: true
python-version: "3.12"
python-version: "${{ matrix.python-version }}"
cache-dependency-glob: "requirements**.txt"
cache-suffix: "3.12"
- name: Installation - *nix
run: |
uv pip install --upgrade wheel
uv pip install -r requirements-dev.txt
uv pip install -e ft_client/
uv pip install -e .
@@ -285,22 +295,13 @@ jobs:
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
steps:
- name: Check user permission
id: check
continue-on-error: true
uses: prince-chrismc/check-actor-permissions-action@d504e74ba31658f4cdf4fcfeb509d4c09736d88e # v3.0.2
with:
permission: "write"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Discord notification
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
if: steps.check.outputs.permitted == 'true' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
with:
severity: info
details: Test Completed!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
color: '#00FF00' # green
title: Test Completed!
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
build:
if: always()
@@ -312,6 +313,9 @@ jobs:
pre-commit,
]
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.13"]
steps:
@@ -324,18 +328,24 @@ jobs:
with:
persist-credentials: false
- name: Set up Python
- name: Set up Python 🐍
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
with:
activate-environment: true
python-version: "${{ matrix.python-version }}"
- name: Build distribution
run: |
pip install -U build
uv pip install $(grep -E "^build==" requirements-dev.txt)
python -m build --sdist --wheel
- name: Upload artifacts 📦
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: freqtrade-build
path: |
@@ -344,11 +354,10 @@ jobs:
- name: Build Client distribution
run: |
pip install -U build
python -m build --sdist --wheel ft_client
- name: Upload artifacts 📦
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: freqtrade-client-build
path: |
@@ -372,7 +381,7 @@ jobs:
persist-credentials: false
- name: Download artifact 📦
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: freqtrade*-build
path: dist
@@ -401,7 +410,7 @@ jobs:
persist-credentials: false
- name: Download artifact 📦
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: freqtrade*-build
path: dist
@@ -422,9 +431,9 @@ jobs:
packages: write # Needed to push package versions
contents: read
secrets:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
packages-cleanup:
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
with:
persist-credentials: false
- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+18 -15
View File
@@ -3,9 +3,9 @@ name: Docker Build and Deploy
on:
workflow_call:
secrets:
DOCKER_PASSWORD:
DOCKERHUB_USERNAME:
required: true
DOCKER_USERNAME:
DOCKERHUB_TOKEN:
required: true
DISCORD_WEBHOOK:
required: false
@@ -35,6 +35,8 @@ jobs:
name: "Deploy Docker x64 and armv7l"
runs-on: ubuntu-22.04
if: github.repository == 'freqtrade/freqtrade'
environment:
name: docker
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -57,19 +59,19 @@ jobs:
uses: ./.github/actions/docker-tags
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
with:
cache-image: false
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0
- name: Available platforms
run: echo ${PLATFORMS}
@@ -168,6 +170,8 @@ jobs:
# Only run on 64bit machines
runs-on: [self-hosted, linux, ARM64]
if: github.repository == 'freqtrade/freqtrade'
environment:
name: docker
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -179,13 +183,13 @@ jobs:
uses: ./.github/actions/docker-tags
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to github
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -306,9 +310,8 @@ jobs:
docker image prune -a --force --filter "until=24h"
- name: Discord notification
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
with:
severity: info
details: Deploy Succeeded!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
title: Deploy Succeeded!
webhook: ${{ secrets.DISCORD_WEBHOOK }}
+5 -2
View File
@@ -3,6 +3,7 @@ on:
push:
branches:
- stable
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}
@@ -15,6 +16,8 @@ jobs:
dockerHubDescription:
name: "Update Docker Hub Description"
runs-on: ubuntu-latest
environment:
name: docker
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -23,6 +26,6 @@ jobs:
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: freqtradeorg/freqtrade
+10 -2
View File
@@ -17,6 +17,9 @@ jobs:
auto-update:
name: Auto-update pre-commit hooks
runs-on: ubuntu-latest
environment:
name: develop
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -24,11 +27,16 @@ jobs:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
with:
activate-environment: true
python-version: "3.13"
- name: Install pre-commit
run: pip install pre-commit
run: uv pip install $(grep -E "^pre-commit==" requirements-dev.txt)
- name: Run auto-update
run: pre-commit autoupdate
+1 -1
View File
@@ -31,4 +31,4 @@ jobs:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
+7 -20
View File
@@ -13,12 +13,6 @@ repos:
pass_filenames: false
additional_dependencies: ["python-rapidjson", "jsonschema"]
- repo: https://github.com/pycqa/flake8
rev: "7.3.0"
hooks:
- id: flake8
additional_dependencies: [Flake8-pyproject]
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.19.1"
@@ -29,22 +23,15 @@ repos:
- types-cachetools==6.2.0.20251022
- types-filelock==3.2.7
- types-requests==2.32.4.20260107
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20260124
- scipy-stubs==1.17.0.2
- SQLAlchemy==2.0.46
# stages: [push]
- repo: https://github.com/pycqa/isort
rev: "8.0.0"
hooks:
- id: isort
name: isort (python)
- types-tabulate==0.10.0.20260308
- types-python-dateutil==2.9.0.20260305
- scipy-stubs==1.17.1.2
- SQLAlchemy==2.0.48
# stages: [push]
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.15.2'
rev: 'v0.15.7'
hooks:
- id: ruff
- id: ruff-format
@@ -75,7 +62,7 @@ repos:
- id: strip-exif
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
rev: v2.4.2
hooks:
- id: codespell
additional_dependencies:
@@ -83,6 +70,6 @@ repos:
# Ensure github actions remain safe
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.22.0
rev: v1.23.1
hooks:
- id: zizmor
+2 -1
View File
@@ -4,7 +4,7 @@
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![codecov](https://codecov.io/gh/freqtrade/freqtrade/branch/develop/graph/badge.svg?token=AD5BG3ATKI)](https://codecov.io/gh/freqtrade/freqtrade)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Discord Server](https://img.shields.io/badge/Freqtrade_Discord-18181B?logo=discord)](https://discord.gg/p7nuUNVfP7)
[![Discord Server](https://img.shields.io/badge/Freqtrade_Discord-4E4E4E?logo=discord)](https://discord.gg/p7nuUNVfP7)
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
@@ -50,6 +50,7 @@ Please read the [exchange-specific notes](https://www.freqtrade.io/en/stable/exc
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [OKX](https://okx.com/)
- [X] [Bybit](https://bybit.com/)
- [X] [Kraken](https://www.kraken.com/features/futures)
Please make sure to read the [exchange specific notes](https://www.freqtrade.io/en/stable/exchanges/), as well as the [trading with leverage](https://www.freqtrade.io/en/stable/leverage/) documentation before diving in.
+1
View File
@@ -649,6 +649,7 @@
"ProducerPairList",
"RemotePairList",
"MarketCapPairList",
"CrossMarketPairList",
"AgeFilter",
"DelistFilter",
"FullTradesFilter",
+5 -5
View File
@@ -191,7 +191,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| | **Unfilled timeout**
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set `unfilledtimeout.unit` to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
| | **Pricing**
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
@@ -229,7 +229,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`*<br> **Datatype:** float
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.only_from_ccxt` | Prevent data-download from data.binance.vision. Leaving this as false can greatly speed up downloads, but may be problematic if the site is not available.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
@@ -240,7 +240,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `true`.<br> **Datatype:** boolean
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `true`.*<br> **Datatype:** boolean
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
| | **Webhook**
@@ -280,8 +280,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `feather`*. <br> **Datatype:** String
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `feather`*. <br> **Datatype:** String
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing backtesting/hyperopt and in FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
| `log_config` | Dictionary containing the log config for python logging. [more info](advanced-setup.md#advanced-logging) <br> **Datatype:** dict. <br> Default: `FtRichHandler`
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing backtesting/hyperopt and in FreqAI). <br> Default: `False`. <br> **Datatype:** Boolean.
| `log_config` | Dictionary containing the log config for python logging. [more info](advanced-setup.md#advanced-logging) <br> Default: `FtRichHandler` <br> **Datatype:** dict.
### Parameters in the strategy
+2
View File
@@ -269,6 +269,8 @@ If `--convert` is also provided, the resample step will happen automatically and
!!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
Kraken Futures uses standard OHLCV downloads and does not require `--dl-trades`.
Example call:
```bash
+32
View File
@@ -217,6 +217,32 @@ freqtrade download-data --exchange kraken --dl-trades -p BTC/EUR BCH/EUR
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests/sec rate.
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
## Kraken Futures
Kraken Futures uses the exchange id `krakenfutures` and supports isolated futures mode.
```jsonc
"exchange": {
"name": "krakenfutures",
"key": "your_exchange_key",
"secret": "your_exchange_secret"
},
"trading_mode": "futures",
"margin_mode": "isolated",
"stake_currency": "USD"
```
!!! Tip "Stoploss on Exchange"
Kraken Futures supports `stoploss_on_exchange` with both `limit` and `market` stop orders.
Use `order_types.stoploss_price_type` to select the trigger price source (`mark`, `last`, or `index`).
!!! Note "Collateral"
Kraken Futures is USD-settled. Use USD as your stake currency.
!!! Note "Flex (Multi-collateral) Accounts"
Kraken Futures flex accounts allow collateral in multiple currencies, while trading remains USD-settled.
Freqtrade derives the `USD` balance from Kraken margin fields, so keep `stake_currency` set to `USD`.
## Kucoin
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
@@ -368,6 +394,11 @@ On startup, freqtrade will set the position mode to "One-way Mode" for the whole
!!! Tip "Stoploss on Exchange"
Hyperliquid supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it.
!!! Warning "Unified accounts"
Hyperliquid unified accounts are supported - though this relies freqtrade's assumption of "owning" the account, and being the only one trading on it (in this case, extended to both spot and futures).
We hence recommend the usage of subaccounts where possible, and to avoid manual trading on the same account while the bot is running.
Freqtrade will attempt to detect the account type on startup - changing the account type mid-trading is not supported and may lead to exceptions and errors.
Hyperliquid is a Decentralized Exchange (DEX). Decentralized exchanges work a bit different compared to normal exchanges. Instead of authenticating private API calls using an API key, private API calls need to be signed with the private key of your wallet (We recommend using an api Wallet for this, generated either on Hyperliquid or in your wallet of choice).
This needs to be configured like this:
@@ -423,6 +454,7 @@ Your balance and trades will now be used from your vault / subaccount - and no l
!!! Note
You can only use either a vault or a subaccount - not both at the same time.
### Historic Hyperliquid data
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
+4
View File
@@ -260,6 +260,10 @@ freqtrade trade --config config_examples/config_freqai.example.json --strategy F
PyTorch dropped support for macOS x64 (intel based Apple devices) in version 2.3. Subsequently, freqtrade also dropped support for PyTorch on this platform.
!!! Danger "Security notice"
Loading saved models from disk can cause security issues if using remote model files (files you downloaded from the internet or received from an untrusted source) due to having the necessity to have `weights_only=False`, which can cause security problems.
As long as you only load models that you have trained yourself, there is no risk.
### Structure
#### Model
+1
View File
@@ -106,6 +106,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset. Overrides `n_steps`. Either `n_epochs` or `n_steps` must be set. <br><br> **Datatype:** int. optional. <br> Default: `10`.
| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. Ignored if `n_epochs` is set. A simplified version of the function: <br><br> n_epochs = n_steps / (n_obs / batch_size) <br><br> The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points. <br> <br> **Datatype:** int. optional. <br> Default: `None`.
| `batch_size` | The size of the batches to use during training. <br><br> **Datatype:** int. <br> Default: `64`.
| `early_stopping_patience` | Number of epochs with no improvement in validation loss before training is stopped early. This helps prevent overfitting by halting training when the model stops improving. Set to `0` to disable early stopping. Requires a test/validation split (`test_size > 0`). <br><br> **Datatype:** int. <br> Default: `0` (disabled).
### Additional parameters
+4
View File
@@ -87,6 +87,10 @@ To save the models generated during a particular backtest so that you can start
To ensure that the model can be reused, freqAI will call your strategy with a dataframe of length 1.
If your strategy requires more data than this to generate the same features, you can't reuse backtest predictions for live deployment and need to update your `identifier` for each new backtest.
!!! Danger "Security notice"
Loading saved models from disk can cause security issues if using remote model files (files you downloaded from the internet or received from an untrusted source) due to having the necessity to have `weights_only=False`, which can cause security problems.
As long as you only load models that you have trained yourself, there is no risk.
### Backtest live collected predictions
FreqAI allow you to reuse live historic predictions through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse predictions generated in dry/run for comparison or other study.
+1
View File
@@ -15,6 +15,7 @@
| [Hyperliquid](exchanges.md#hyperliquid) | spot | | ❌ (not supported) |
| [Hyperliquid](exchanges.md#hyperliquid) | futures | isolated, cross | limit |
| [Kraken](exchanges.md#kraken) | spot | | market, limit |
| [Kraken](exchanges.md#kraken-futures) | futures | isolated | market, limit |
| [OKX](exchanges.md#okx) | spot | | limit |
| [OKX](exchanges.md#okx) | futures | isolated | limit |
| [Bitvavo](exchanges.md#bitvavo) | spot | | ❌ (not supported) |
+11 -2
View File
@@ -2,11 +2,11 @@
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list), [`CrossMarketPairList`](#crossmarketpairlist), [`MarketCapPairlist`](#marketcappairlist) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
Additionally, [`AgeFilter`](#agefilter), [`DelistFilter`](#delistfilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList` or `PercentChangePairList` as the starting Pairlist Handler.
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList`, `PercentChangePairList` or `CrossMarketPairList` as the starting Pairlist Handler.
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
@@ -26,6 +26,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
* [`ProducerPairList`](#producerpairlist)
* [`RemotePairList`](#remotepairlist)
* [`MarketCapPairList`](#marketcappairlist)
* [`CrossMarketPairList`](#crossmarketpairlist)
* [`AgeFilter`](#agefilter)
* [`DelistFilter`](#delistfilter)
* [`FullTradesFilter`](#fulltradesfilter)
@@ -303,6 +304,8 @@ The optional `mode` option specifies if the pairlist should be used as a `blackl
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter".
The optional `number_assets` option in the RemotePairList configuration determines how many pairs will be returned if used in whitelist `mode`. By default, all pairs will be returned. In blacklist `mode`, this option will be ignored.
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering.
@@ -402,6 +405,12 @@ Coins like 1000PEPE/USDT or KPEPE/USDT:USDT are detected on a best effort basis,
!!! Danger "Duplicate symbols in coingecko"
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
#### CrossMarketPairList
Generate or filter pairs based of their availability on the opposite market.
The `pairs_exist_on` setting defines whether the pairs should exists on both spot and futures market (`both_markets`) or only exist on the specified trading mode (`current_market_only`). By default, the plugin will be in `both_markets` setting, which means whitelisted pairs have to exists on both spot and futures markets.
#### AgeFilter
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
+2 -1
View File
@@ -4,7 +4,7 @@
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
[![codecov](https://codecov.io/gh/freqtrade/freqtrade/branch/develop/graph/badge.svg?token=AD5BG3ATKI)](https://codecov.io/gh/freqtrade/freqtrade)
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
[![Discord Server](https://img.shields.io/badge/Freqtrade_Discord-18181B?logo=discord)](https://discord.gg/p7nuUNVfP7)
[![Discord Server](https://img.shields.io/badge/Freqtrade_Discord-4E4E4E?logo=discord)](https://discord.gg/p7nuUNVfP7)
<!-- GitHub action buttons -->
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
@@ -62,6 +62,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [OKX](https://okx.com/)
- [X] [Kraken](https://www.kraken.com/features/futures)
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
+2 -2
View File
@@ -1,7 +1,7 @@
markdown==3.10.2
mkdocs==1.6.1
mkdocs-material==9.7.1
mkdocs-material==9.7.5
mdx_truly_sane_lists==1.3
pymdown-extensions==10.21
jinja2==3.1.6
mike==2.1.3
mike==2.1.4
+7 -2
View File
@@ -104,7 +104,7 @@ WHERE id=31;
### Remove trade from the database
!!! Tip "Use RPC Methods to delete trades"
Consider using `/delete <tradeid>` via telegram or rest API. That's the recommended way to deleting trades.
Consider using `/delete <tradeid>` via telegram or rest API. That's the recommended way to deleting trades, as it will also remove the corresponding orders and custom data, and it will also trigger the necessary events in the bot to keep everything in sync.
If you'd still like to remove a trade from the database directly, you can use the below query.
@@ -113,9 +113,14 @@ If you'd still like to remove a trade from the database directly, you can use th
```sql
DELETE FROM trades WHERE id = <tradeid>;
DELETE FROM orders WHERE ft_trade_id = <tradeid>;
DELETE FROM trade_custom_data WHERE ft_trade_id = <tradeid>;
DELETE FROM trades WHERE id = 31;
DELETE FROM orders WHERE ft_trade_id = 31;
DELETE FROM trade_custom_data WHERE ft_trade_id = 31;
```
!!! Warning
This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause.
This will remove the specified trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause.
+3 -2
View File
@@ -66,9 +66,10 @@ This same logic will reapply a stoploss order on the exchange should you cancel
### stoploss_price_type
!!! Warning "Only applies to futures"
`stoploss_price_type` only applies to futures markets (on exchanges where it's available).
`stoploss_price_type` only applies to futures markets (on exchanges where it's available).
Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected.
Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports.
Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports.
In spot markets, this setting is ignored and not validated, as most exchanges only support one price type for stoploss orders on spot markets.
Stoploss on exchange on futures markets can trigger on different price types.
The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index".
+1 -1
View File
@@ -33,7 +33,7 @@ class AwesomeStrategy(IStrategy):
trade_entry_type = trade.get_custom_data(key='entry_type')
if trade_entry_type is None:
trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip'
elif fills > 1:
elif len(fills) > 1:
trade_entry_type = 'buy_up'
trade.set_custom_data(key='entry_type', value=trade_entry_type)
return super().bot_loop_start(**kwargs)
+3
View File
@@ -696,6 +696,9 @@ However, freqtrade also offers a custom callback for both order types, which all
Backtesting fills orders if their price falls within the candle's low/high range.
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
!!! Tip "Replacing orders"
If you'd like to replace an order with a different price instead of just cancelling it, you might want to look at [`adjust_order_price()`](#adjust-order-price) instead, which will allow you to both cancel the order, as well as replace it with a new price.
### Custom order timeout example
Called for every open order until that order is either filled or cancelled.
+1 -1
View File
@@ -416,6 +416,6 @@ Your original strategy will remain available in the `user_data/strategies_orig_u
!!! Warning "Conversion results"
Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion.
We also recommend to run a python formatter (e.g. `black`) to format results in a sane manner.
We also recommend to run a python formatter (e.g. `ruff format`) to format results in a sane manner.
--8<-- "commands/strategy-updater.md"
+1 -1
View File
@@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2026.2"
__version__ = "2026.3"
if "dev" in __version__:
from pathlib import Path
+1 -1
View File
@@ -410,7 +410,7 @@ class Configuration:
("include_inactive", "Detected --include-inactive-pairs: {}"),
("no_parallel_download", "Detected --no-parallel-download: {}"),
("download_trades", "Detected --dl-trades: {}"),
("convert_trades", "Detected --convert: {} - Converting Trade data to OHCV {}"),
("convert_trades", "Detected --convert: {} - Converting trade data to OHLCV."),
("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
("dataformat_trades", 'Using "{}" to store trades data.'),
("show_timerange", "Detected --show-timerange"),
+1
View File
@@ -61,6 +61,7 @@ AVAILABLE_PAIRLISTS = [
"ProducerPairList",
"RemotePairList",
"MarketCapPairList",
"CrossMarketPairList",
"AgeFilter",
"DelistFilter",
"FullTradesFilter",
+11 -7
View File
@@ -39,7 +39,11 @@ def ohlcv_to_dataframe(
df = DataFrame(ohlcv, columns=cols)
# Floor date to seconds to account for exchange imprecisions
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s")
from freqtrade.exchange import timeframe_to_floor_freq
resample_interval = timeframe_to_floor_freq(timeframe)
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor(resample_interval)
# Some exchanges return int values for Volume and even for OHLC.
# Convert them since TA-LIB indicators used in the strategy assume floats
@@ -59,14 +63,14 @@ def ohlcv_to_dataframe(
def clean_ohlcv_dataframe(
data: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
dataframe: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
) -> DataFrame:
"""
Cleanse a OHLCV dataframe by
* Grouping it by date (removes duplicate tics)
* dropping last candles if requested
* Filling up missing data (if requested)
:param data: DataFrame containing candle (OHLCV) data.
:param dataframe: DataFrame containing candle (OHLCV) data.
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
:param pair: Pair this data is for (used to warn if fillup was necessary)
:param fill_missing: fill up missing candles with 0 candles
@@ -75,7 +79,7 @@ def clean_ohlcv_dataframe(
:return: DataFrame
"""
# group by index and aggregate results to eliminate duplicate ticks
data = data.groupby(by="date", as_index=False, sort=True).agg(
dataframe = dataframe.groupby(by="date", as_index=False, sort=True).agg(
{
"open": "first",
"high": "max",
@@ -86,13 +90,13 @@ def clean_ohlcv_dataframe(
)
# eliminate partial candle
if drop_incomplete:
data.drop(data.tail(1).index, inplace=True)
dataframe.drop(dataframe.tail(1).index, inplace=True)
logger.debug("Dropping last candle")
if fill_missing:
return ohlcv_fill_up_missing_data(data, timeframe, pair)
return ohlcv_fill_up_missing_data(dataframe, timeframe, pair)
else:
return data
return dataframe
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
@@ -31,8 +31,8 @@ logger = logging.getLogger(__name__)
class IDataHandler(ABC):
_OHLCV_REGEX = r"^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
_TRADES_REGEX = r"^([a-zA-Z_\d-]+)\-(trades)?(?=\.)"
_OHLCV_REGEX = r"^([\w-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
_TRADES_REGEX = r"^([\w-]+)\-(trades)?(?=\.)"
def __init__(self, datadir: Path) -> None:
self._datadir = datadir
@@ -336,11 +336,10 @@ class IDataHandler(ABC):
def rebuild_pair_from_filename(pair: str) -> str:
"""
Rebuild pair name from filename
Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names.
Replaces the first '_' with '/' and the second '_' (if present) with ':'.
e.g. BTC_USDT -> BTC/USDT, BTC_USDT_USDT -> BTC/USDT:USDT
"""
res = re.sub(r"^(([A-Za-z\d]{1,10})|^([A-Za-z\-]{1,6}))(_)", r"\g<1>/", pair, count=1)
res = re.sub("_", ":", res, count=1)
return res
return pair.replace("_", "/", 1).replace("_", ":", 1)
def ohlcv_load(
self,
+1 -1
View File
@@ -296,7 +296,7 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
:param final_balance: Final balance to calculate CAGR against
:return: CAGR
"""
if final_balance < 0:
if (final_balance < 0) or (starting_balance <= 0) or (days_passed <= 0):
# With leveraged trades, final_balance can become negative.
return 0
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
+2
View File
@@ -30,6 +30,7 @@ from freqtrade.exchange.exchange_utils import (
validate_exchange,
)
from freqtrade.exchange.exchange_utils_timeframe import (
timeframe_to_floor_freq,
timeframe_to_minutes,
timeframe_to_msecs,
timeframe_to_next_date,
@@ -43,6 +44,7 @@ from freqtrade.exchange.htx import Htx
from freqtrade.exchange.hyperliquid import Hyperliquid
from freqtrade.exchange.idex import Idex
from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.krakenfutures import Krakenfutures
from freqtrade.exchange.kucoin import Kucoin
from freqtrade.exchange.lbank import Lbank
from freqtrade.exchange.luno import Luno
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import (
DDosProtection,
OperationalException,
@@ -34,6 +34,11 @@ class Bitget(Exchange):
"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"],
"stop_price_type_field": "triggerType",
"stop_price_type_value_mapping": {
PriceType.LAST: "fill_price",
PriceType.MARK: "mark_price",
},
}
_ft_has_futures: FtHas = {
"funding_fee_candle_limit": 100,
+1 -1
View File
@@ -39,7 +39,6 @@ BAD_EXCHANGES = {
"bitmex": "Various reasons",
"probit": "Requires additional, regular calls to `signIn()`",
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders",
"krakenfutures": "Unsupported futures exchange",
"kucoinfutures": "Unsupported futures exchange",
"poloniexfutures": "Unsupported futures exchange",
"binancecoinm": "Unsupported futures exchange",
@@ -63,6 +62,7 @@ SUPPORTED_EXCHANGES = [
"htx",
"hyperliquid",
"kraken",
"krakenfutures",
"okx",
"myokx",
]
+8 -6
View File
@@ -825,7 +825,8 @@ class Exchange:
and order_types["stoploss_price_type"] not in price_mapping
):
raise ConfigurationError(
f"On exchange stoploss price type is not supported for {self.name}."
f"On exchange stoploss price type '{order_types['stoploss_price_type']}' "
f"is not supported for {self.name}."
)
def validate_pricing(self, pricing: dict) -> None:
@@ -1896,9 +1897,12 @@ class Exchange:
orders = []
if self.exchange_has("fetchClosedOrders"):
orders = self._api.fetch_closed_orders(pair, since=since_ms)
if self.exchange_has("fetchOpenOrders"):
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
orders.extend(orders_open)
if self.exchange_has("fetchCanceledOrders"):
orders_canceled = self._api.fetch_canceled_orders(pair, since=since_ms)
orders.extend(orders_canceled)
if self.exchange_has("fetchOpenOrders"):
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
orders.extend(orders_open)
return orders
@retrier(retries=0)
@@ -3931,7 +3935,6 @@ class Exchange:
is_short: bool,
open_date: datetime,
close_date: datetime,
time_in_ratio: float | None = None,
) -> float:
"""
calculates the sum of all funding fees that occurred for a pair during a futures trade
@@ -3941,7 +3944,6 @@ class Exchange:
:param is_short: trade direction
:param open_date: The date and time that the trade started
:param close_date: The date and time that the trade ended
:param time_in_ratio: Not used by most exchange classes
"""
fees: float = 0
@@ -29,6 +29,21 @@ def timeframe_to_msecs(timeframe: str) -> int:
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
def timeframe_to_floor_freq(timeframe: str) -> str:
"""
Translates the timeframe interval value written in the human readable
form ('1m', '5m', '1h', '1d', '1w', etc.) to the desired floor frequency used by pandas
("1m", "5m", "1h", "1d", "1w", etc.).
Will use minute for most higher timeframes.
"""
timeframe_seconds = timeframe_to_seconds(timeframe)
timeframe_minutes = timeframe_seconds // 60
if timeframe_minutes <= 1:
return "1s"
else:
return "1min"
def timeframe_to_resample_freq(timeframe: str) -> str:
"""
Translates the timeframe interval value written in the human readable
+67 -19
View File
@@ -5,11 +5,20 @@ from copy import deepcopy
from datetime import datetime
from typing import Any
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.runmode import NON_UTIL_MODES
from freqtrade.exceptions import ConfigurationError, ExchangeError, OperationalException
from freqtrade.exceptions import (
ConfigurationError,
DDosProtection,
ExchangeError,
OperationalException,
TemporaryError,
)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, CcxtPosition, FtHas
from freqtrade.util.datetime_helpers import dt_from_ts
@@ -22,6 +31,8 @@ class Hyperliquid(Exchange):
Contains adjustments needed for Freqtrade to work with this exchange.
"""
unified_account = False
_ft_has: FtHas = {
"ohlcv_has_history": False,
"l2_limit_range": [20],
@@ -58,6 +69,38 @@ class Hyperliquid(Exchange):
config.update(super()._ccxt_config)
return config
@retrier
def additional_exchange_init(self) -> None:
"""
Additional exchange initialization logic.
.api will be available at this point.
Query User account Account Type to determine unified account status
https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#query-a-users-abstraction-state
"""
try:
if self.trading_mode == TradingMode.FUTURES and not self._config["dry_run"]:
# Determine account status
# Unified accounts must use the spot endpoint for balances
request = {
"type": "userAbstraction",
"user": self._api.walletAddress,
}
response = self._api.publicPostInfo(request)
self.unified_account = response in ('"unifiedAccount"', '"portfolioMargin"')
if self.unified_account:
logger.info("Unified Hyperliquid account detected.")
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def _get_configured_hip3_dexes(self) -> list[str]:
"""Get list of configured HIP-3 DEXes."""
return self._config.get("exchange", {}).get("hip3_dexes", [])
@@ -122,28 +165,33 @@ class Hyperliquid(Exchange):
This override is not absolutely necessary and is only there for correct used / total values
which are however not used by Freqtrade in futures mode at the moment.
"""
balances = super().get_balances()
dexes = self._get_configured_hip3_dexes()
for dex in dexes:
try:
dex_balance = super().get_balances(params={"dex": dex})
params = params or {}
if self.unified_account:
params["type"] = "spot"
balances = super().get_balances(params)
if not self.unified_account:
# In unified accounts, the balance already includes all DEXes
dexes = self._get_configured_hip3_dexes()
for dex in dexes:
try:
dex_balance = super().get_balances(params={"dex": dex})
for currency, amount_info in dex_balance.items():
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
continue
for currency, amount_info in dex_balance.items():
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
continue
if currency not in balances:
balances[currency] = amount_info
else:
balances[currency]["free"] += amount_info["free"]
balances[currency]["used"] += amount_info["used"]
balances[currency]["total"] += amount_info["total"]
if currency not in balances:
balances[currency] = amount_info
else:
balances[currency]["free"] += amount_info["free"]
balances[currency]["used"] += amount_info["used"]
balances[currency]["total"] += amount_info["total"]
except Exception as e:
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
except Exception as e:
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
if dexes:
self._log_exchange_response("fetch_balance", balances, add_info="combined")
if dexes:
self._log_exchange_response("fetch_balance", balances, add_info="combined")
return balances
def fetch_positions(
-50
View File
@@ -1,11 +1,9 @@
"""Kraken exchange subclass"""
import logging
from datetime import datetime
from typing import Any
import ccxt
from pandas import DataFrame
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
@@ -40,7 +38,6 @@ class Kraken(Exchange):
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS)
]
def market_is_tradable(self, market: dict[str, Any]) -> bool:
@@ -114,18 +111,6 @@ class Kraken(Exchange):
except ccxt.BaseError as e:
raise OperationalException(e) from e
def _set_leverage(
self,
leverage: float,
pair: str | None = None,
accept_fail: bool = False,
):
"""
Kraken set's the leverage as an option in the order object, so we need to
add it to params
"""
return
def _get_params(
self,
side: BuySell,
@@ -148,41 +133,6 @@ class Kraken(Exchange):
params["postOnly"] = True
return params
def calculate_funding_fees(
self,
df: DataFrame,
amount: float,
is_short: bool,
open_date: datetime,
close_date: datetime,
time_in_ratio: float | None = None,
) -> float:
"""
# ! This method will always error when run by Freqtrade because time_in_ratio is never
# ! passed to _get_funding_fee. For kraken futures to work in dry run and backtesting
# ! functionality must be added that passes the parameter time_in_ratio to
# ! _get_funding_fee when using Kraken
calculates the sum of all funding fees that occurred for a pair during a futures trade
:param df: Dataframe containing combined funding and mark rates
as `open_fund` and `open_mark`.
:param amount: The quantity of the trade
:param is_short: trade direction
:param open_date: The date and time that the trade started
:param close_date: The date and time that the trade ended
:param time_in_ratio: Not used by most exchange classes
"""
if not time_in_ratio:
raise OperationalException(
f"time_in_ratio is required for {self.name}._get_funding_fee"
)
fees: float = 0
if not df.empty:
df = df[(df["date"] >= open_date) & (df["date"] <= close_date)]
fees = sum(df["open_fund"] * df["open_mark"] * amount * time_in_ratio)
return fees if is_short else -fees
def _get_trade_pagination_next_value(self, trades: list[dict]):
"""
Extract pagination id for the next "from_id" value
+300
View File
@@ -0,0 +1,300 @@
"""Kraken Futures exchange subclass"""
import logging
from datetime import datetime
from typing import Any
import ccxt
from freqtrade.enums import MarginMode, PriceType, TradingMode
from freqtrade.exceptions import (
DDosProtection,
ExchangeError,
InvalidOrderException,
OperationalException,
TemporaryError,
)
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
from freqtrade.exchange.exchange import Exchange
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, FtHas
from freqtrade.misc import safe_value_nested
from freqtrade.util.datetime_helpers import dt_from_ts
logger = logging.getLogger(__name__)
class Krakenfutures(Exchange):
"""Kraken Futures exchange class.
Contains adjustments needed for Freqtrade to work with this exchange.
Key differences from spot Kraken:
- Stop orders use triggerPrice/triggerSignal instead of stopPrice
- Flex (multi-collateral) accounts need USD balance synthesis
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
_ft_has: FtHas = {
"tickers_have_quoteVolume": False,
"stoploss_on_exchange": True,
"stoploss_order_types": {
"limit": "limit",
"market": "market",
},
"stoploss_query_requires_stop_flag": True,
"stop_price_param": "triggerPrice",
"stop_price_prop": "stopPrice",
"stop_price_type_field": "triggerSignal",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "mark",
PriceType.INDEX: "index",
},
"exchange_has_overrides": {"fetchOrders": False},
}
@retrier
def get_balances(self, params: dict | None = None) -> CcxtBalances:
"""
Fetch balances with USD synthesis for flex (multi-collateral) accounts.
Kraken Futures flex accounts hold multiple currencies as collateral.
CCXT returns per-currency balances but doesn't expose margin values
as a USD balance. This override synthesizes a USD entry from flex account data
when stake_currency is USD.
Field mapping (margin-centric for internal consistency):
- free: availableMargin (margin available for new positions)
- total: marginEquity (haircut-adjusted collateral + unrealized P&L)
- used: total - free (margin currently in use)
Fallback chain for total: marginEquity -> portfolioValue -> balanceValue
"""
try:
balances = self._api.fetch_balance(params or {})
# Only synthesize USD if stake_currency is USD
stake = str(self._config.get("stake_currency", "")).upper()
if stake == "USD":
# Only synthesize if USD stake - flex only applies for these currencies.
# For flex accounts, synthesize USD balance from margin values
info = balances.get("info", {})
accounts = info.get("accounts", {}) if isinstance(info, dict) else {}
flex = accounts.get("flex", {}) if isinstance(accounts, dict) else {}
if flex:
usd_free = self._safe_float(flex.get("availableMargin"))
# Prefer marginEquity for consistency (same basis as availableMargin)
raw_total = (
flex.get("marginEquity")
or flex.get("portfolioValue")
or flex.get("balanceValue")
)
usd_total = self._safe_float(raw_total)
if usd_free is not None or usd_total is not None:
# Use available value for both if only one is present
usd_free_value = usd_free if usd_free is not None else usd_total
usd_total_value = usd_total if usd_total is not None else usd_free
if usd_free_value is not None and usd_total_value is not None:
usd_used = max(0.0, usd_total_value - usd_free_value)
balances["USD"] = {
"free": usd_free_value,
"used": usd_used,
"total": usd_total_value,
}
# Remove additional info from ccxt results (same as base class)
balances.pop("info", None)
balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
self._log_exchange_response("fetch_balance", balances, add_info=params)
return balances
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not get balance due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@staticmethod
def _safe_float(value: Any) -> float | None:
"""Convert value to float, returning None if conversion fails."""
if value is None:
return None
try:
return float(value)
except (ValueError, TypeError):
return None
def _order_contracts_to_amount(self, order: CcxtOrder) -> CcxtOrder:
"""Normalize order and apply Kraken Futures-specific order corrections."""
order = super()._order_contracts_to_amount(order)
return self._adjust_krakenfutures_order(order)
def _adjust_krakenfutures_order(self, order: CcxtOrder) -> CcxtOrder:
"""Apply Kraken Futures-specific order corrections.
For filled terminal orders, always fetch trades and compute VWAP because
CCXT's average is still unreliable.
See: https://github.com/ccxt/ccxt/issues/27996
"""
if order.get("status") == "canceled" and order.get("filled") is None:
# Workaround for missing filled parsing - https://github.com/ccxt/ccxt/issues/28210
order["filled"] = safe_value_nested(order, "info.order.filled", default_value=None)
filled = self._safe_float(order.get("filled")) or 0.0
if order.get("status") in ("canceled", "closed") and filled > 0:
# Compute VWAP and cost for filled orders.
trades = self.get_trades_for_order(
order["id"], order["symbol"], since=dt_from_ts(order["timestamp"])
)
if trades:
total_amount = sum(t["amount"] for t in trades)
if total_amount:
# Compute VWAP
order["average"] = sum(t["price"] * t["amount"] for t in trades) / total_amount
trade_costs = [t["cost"] for t in trades if t.get("cost") is not None]
if trade_costs:
order["cost"] = sum(trade_costs)
return order
def get_trades_for_order(
self, order_id: str, pair: str, since: datetime, params: dict | None = None
) -> list:
"""Fetch trades and enrich with calculated fees.
Kraken Futures' /fills endpoint does not include fee amounts — only
fillType (maker/taker). This enriches each trade with a calculated fee
using the market's fee schedule so Freqtrade's fee detection works.
"""
trades = super().get_trades_for_order(order_id, pair, since, params)
for trade in trades:
if trade.get("fee") is None or trade["fee"].get("cost") is None:
taker_or_maker = trade.get("takerOrMaker", "taker")
symbol = trade.get("symbol", pair)
market = self.markets.get(symbol, {})
fee_rate = market.get(taker_or_maker, market.get("taker", 0.0005))
cost = trade.get("cost")
if cost is not None and fee_rate is not None:
trade["fee"] = {
"cost": cost * fee_rate,
"currency": market.get("quote", "USD"),
"rate": fee_rate,
}
return trades
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
def fetch_order(
self, order_id: str, pair: str, params: dict[str, Any] | None = None
) -> CcxtOrder:
"""Fetch order with direct CCXT call and fallback to history endpoints."""
if self._config.get("dry_run"):
return self.fetch_dry_run_order(order_id)
params = params or {}
status_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")}
try:
order = self._api.fetch_order(order_id, pair, params=status_params)
self._log_exchange_response("fetch_order", order)
return self._order_contracts_to_amount(order)
except ccxt.OrderNotFound:
# Expected for older Kraken Futures orders not visible in orders/status.
pass
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except ccxt.InvalidOrder as e:
msg = f"Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}"
raise InvalidOrderException(msg) from e
except (ccxt.OperationFailed, ccxt.ExchangeError):
# Fallback to history endpoints for temporary/status endpoint gaps.
pass
except ccxt.BaseError as e:
raise OperationalException(e) from e
order = self._fetch_order_fallback(order_id, pair, params)
if order is not None:
return order
# Order not in status, open, closed, or canceled endpoints - genuinely gone.
# Raise non-retrying InvalidOrderException (Kraken has limited history retention).
raise InvalidOrderException(
f"Order not found in any endpoint (pair: {pair} id: {order_id})"
)
def _fetch_order_fallback(
self, order_id: str, pair: str, params: dict[str, Any]
) -> CcxtOrder | None:
"""Search open, closed, and canceled order endpoints for order_id.
Kraken Futures' orders/status endpoint only returns currently open orders.
Older orders require querying history endpoints (closed/canceled).
For stoploss (trigger) orders, the caller should pass stop=True in params
(handled automatically via stoploss_query_requires_stop_flag in _ft_has)
so that closed/canceled queries hit the trigger history endpoint.
"""
order_id_str = str(order_id)
# Open orders include triggers by default. Avoid passing trigger/stop flags
# to prevent endpoint/filter mismatches.
open_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")}
order = self._find_order_in_list(
self._api.fetch_open_orders, pair, open_params, order_id_str
)
if order is not None:
return order
# Closed/canceled: pass params through (including stop=True for stoploss orders,
# which CCXT maps to the trigger history endpoint).
for fetch_fn in (self._api.fetch_closed_orders, self._api.fetch_canceled_orders):
order = self._find_order_in_list(fetch_fn, pair, params, order_id_str)
if order is not None:
return order
return None
def _find_order_in_list(
self,
fetch_fn,
symbol: str | None,
params: dict[str, Any],
order_id_str: str,
) -> CcxtOrder | None:
"""Fetch orders and return matching order_id, or None."""
try:
orders = fetch_fn(symbol, params=params) or []
self._log_exchange_response(fetch_fn.__name__, orders)
for order in orders:
if str(order.get("id")) == order_id_str:
self._log_exchange_response("fetch_order_fallback", order)
return self._order_contracts_to_amount(order)
except (ccxt.OrderNotFound, ccxt.InvalidOrder) as e:
logger.debug(f"{fetch_fn.__name__} failed: {e}")
return None
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
return None
def get_funding_fees(self, pair: str, amount: float, is_short: bool, open_date) -> float:
"""Fetch funding fees, returning 0.0 if retrieval fails."""
if self.trading_mode == TradingMode.FUTURES:
try:
return self._fetch_and_calculate_funding_fees(pair, amount, is_short, open_date)
except ExchangeError:
logger.warning(f"Could not update funding fees for {pair}.")
return 0.0
+7 -3
View File
@@ -614,9 +614,13 @@ class FreqaiDataDrawer:
elif self.model_type == "pytorch":
import torch
zipfile = torch.load(dk.data_path / f"{dk.model_filename}_model.zip")
model = zipfile["pytrainer"]
model = model.load_from_checkpoint(zipfile)
zipfile = torch.load(
dk.data_path / f"{dk.model_filename}_model.zip",
weights_only=False,
)
# weights_only is necessary due to pytrainer being a serialized python object.
_trainer = zipfile["pytrainer"]
model = _trainer.load_from_checkpoint(zipfile)
if not model:
raise OperationalException(
+16 -10
View File
@@ -428,22 +428,28 @@ class FreqaiDataKitchen:
Get backtest prediction from current backtest period
"""
append_df = DataFrame()
# Build dict first and construct DataFrame once to avoid
# column-by-column assignment which causes DataFrame fragmentation
# and PerformanceWarning on large prediction sets.
append_dict: dict[str, Any] = {}
for label in predictions.columns:
append_df[label] = predictions[label]
if append_df[label].dtype == object:
append_dict[label] = predictions[label]
if predictions[label].dtype == object:
continue
if "labels_mean" in self.data:
append_df[f"{label}_mean"] = self.data["labels_mean"][label]
if "labels_std" in self.data:
append_df[f"{label}_std"] = self.data["labels_std"][label]
if "labels_mean" in self.data and label in self.data["labels_mean"]:
append_dict[f"{label}_mean"] = self.data["labels_mean"][label]
if "labels_std" in self.data and label in self.data["labels_std"]:
append_dict[f"{label}_std"] = self.data["labels_std"][label]
for extra_col in self.data["extra_returns_per_train"]:
append_df[f"{extra_col}"] = self.data["extra_returns_per_train"][extra_col]
append_dict[f"{extra_col}"] = self.data["extra_returns_per_train"][extra_col]
append_df["do_predict"] = do_predict
append_dict["do_predict"] = do_predict
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
append_df["DI_values"] = self.DI_values
append_dict["DI_values"] = self.DI_values
append_df = DataFrame(append_dict)
user_cols = [col for col in dataframe_backtest.columns if col.startswith("%%")]
cols = ["date"]
+39 -7
View File
@@ -63,6 +63,11 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
self.tb_logger = tb_logger
self.test_batch_counter = 0
# Early stopping parameters
self.early_stopping_patience: int = kwargs.get("early_stopping_patience", 0)
self.best_val_loss: float = float("inf")
self.patience_counter: int = 0
def fit(self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]):
"""
:param data_dictionary: the dictionary constructed by DataHandler to hold
@@ -99,15 +104,40 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
# evaluation
if "test" in splits:
self.estimate_loss(data_loaders_dictionary, "test")
val_loss = self.estimate_loss(data_loaders_dictionary, "test")
# Early stopping check
if self.early_stopping_patience > 0 and val_loss is not None:
if val_loss < self.best_val_loss:
self.best_val_loss = val_loss
self.patience_counter = 0
else:
self.patience_counter += 1
if self.patience_counter >= self.early_stopping_patience:
logger.info(
f"Early stopping triggered after {self.patience_counter} "
f"epochs without improvement. "
f"Best val_loss: {self.best_val_loss:.6f}"
)
break
@torch.no_grad()
def estimate_loss(
self,
data_loader_dictionary: dict[str, DataLoader],
split: str,
) -> None:
) -> float | None:
"""
Estimate loss on a data split.
:param data_loader_dictionary: dictionary of data loaders.
:param split: split to estimate loss on (e.g. "test").
:return: average loss over all batches, or None if no batches.
"""
self.model.eval()
total_loss = 0.0
num_batches = 0
for _, batch_data in enumerate(data_loader_dictionary[split]):
xb, yb = batch_data
xb = xb.to(self.device)
@@ -115,11 +145,17 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
yb_pred = self.model(xb)
loss = self.criterion(yb_pred, yb)
total_loss += loss.item()
num_batches += 1
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), self.test_batch_counter)
self.test_batch_counter += 1
self.model.train()
if num_batches > 0:
return total_loss / num_batches
return None
def create_data_loaders_dictionary(
self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]
) -> dict[str, DataLoader]:
@@ -179,16 +215,12 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
path,
)
def load(self, path: Path):
checkpoint = torch.load(path)
return self.load_from_checkpoint(checkpoint)
def load_from_checkpoint(self, checkpoint: dict):
"""
when using continual_learning, DataDrawer will load the dictionary
(containing state dicts and model_meta_data) by calling torch.load(path).
you can access this dict from any class that inherits IFreqaiModel by calling
get_init_model method.
the get_init_model method.
"""
self.model.load_state_dict(checkpoint["model_state_dict"])
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
+1 -1
View File
@@ -555,7 +555,7 @@ class FreqtradeBot(LoggingMixin):
if trade.base_currency
else 0
)
if total < trade.amount:
if total < trade.amount or (total == 0 and trade.amount == 0):
if trade.fully_canceled_entry_order_count == len(trade.orders):
logger.warning(
f"Trade only had fully canceled entry orders. "
+3 -5
View File
@@ -6,7 +6,6 @@ Read the documentation to know what cli arguments you need.
import logging
import sys
from typing import Any
# check min. python version
@@ -35,7 +34,7 @@ def main(sysargv: list[str] | None = None) -> None:
:return: None
"""
return_code: Any = 1
return_code: int | None = None
try:
setup_logging_pre()
asyncio_setup()
@@ -62,11 +61,9 @@ def main(sysargv: list[str] | None = None) -> None:
"`freqtrade --help` or `freqtrade <command> --help`."
)
except SystemExit as e: # pragma: no cover
return_code = e
except KeyboardInterrupt:
logger.info("SIGINT received, aborting ...")
return_code = 0
return_code = 130
except ConfigurationError as e:
logger.error(
f"Configuration error: {e}\n"
@@ -77,6 +74,7 @@ def main(sysargv: list[str] | None = None) -> None:
return_code = 2
except Exception:
logger.exception("Fatal exception!")
return_code = 1
finally:
sys.exit(return_code)
+4 -3
View File
@@ -848,9 +848,7 @@ class Backtesting:
exit_tag=exit_reason,
)
if rate is not None and rate != close_rate:
close_rate = price_to_precision(
rate, trade.price_precision, trade.precision_mode_price
)
close_rate = rate
# We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately
if trade.is_short:
@@ -892,6 +890,9 @@ class Backtesting:
self.order_id_counter += 1
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
order_type = self.strategy.order_types["exit"]
close_rate = price_to_precision(
close_rate, trade.price_precision, trade.precision_mode_price
)
# amount = amount or trade.amount
amount = amount_to_contract_precision(
amount or trade.amount, trade.amount_precision, self.precision_mode, trade.contract_size
-9
View File
@@ -51,15 +51,6 @@ class AgeFilter(IPairList):
f"({candle_limit})"
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
@@ -0,0 +1,133 @@
"""Cross Market pair list filter"""
import logging
from freqtrade.constants import PairPrefixes
from freqtrade.exchange.exchange_types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
from freqtrade.util import FtTTLCache
logger = logging.getLogger(__name__)
class CrossMarketPairList(IPairList):
is_pairlist_generator = True
supports_backtesting = SupportsBacktesting.BIASED
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._pairs_exist_on: str = self._pairlistconfig.get("pairs_exist_on", "both_markets")
self._stake_currency: str = self._config["stake_currency"]
self._target_mode = "spot" if self._config["trading_mode"] == "futures" else "futures"
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
pairs_exist_on = self._pairs_exist_on
msg = f"{self.name} - Pairs that exists on {pairs_exist_on.capitalize()}."
return msg
@staticmethod
def description() -> str:
return "Filter pairs if they exist or not on another market."
@staticmethod
def available_parameters() -> dict[str, PairlistParameter]:
return {
"pairs_exist_on": {
"type": "option",
"default": "both_markets",
"options": ["current_market_only", "both_markets"],
"description": "Mode of operation",
"help": "Mode of operation (current_market_only/both_markets)",
},
**IPairList.refresh_period_parameter(),
}
def get_base_list(self) -> list[str]:
target_mode = self._target_mode
spot_only = True if target_mode == "spot" else False
futures_only = True if target_mode == "futures" else False
bases = [
v.get("base", "")
for _, v in self._exchange.get_markets(
quote_currencies=[self._stake_currency],
tradable_only=False,
active_only=True,
spot_only=spot_only,
futures_only=futures_only,
).items()
]
return bases
def gen_pairlist(self, tickers: Tickers) -> list[str]:
"""
Generate the pairlist
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: List of pairs
"""
# Generate dynamic whitelist
# Must always run if this pairlist is the first in the list.
pairlist = self._pair_cache.get("pairlist")
if pairlist:
# Item found - no refresh necessary
return pairlist.copy()
else:
# Use fresh pairlist
# Check if pair quote currency equals to the stake currency.
_pairlist = [
k
for k in self._exchange.get_markets(
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
).keys()
]
_pairlist = self.verify_blacklist(_pairlist, logger.info)
pairlist = self.filter_pairlist(_pairlist, tickers)
self._pair_cache["pairlist"] = pairlist.copy()
return pairlist
def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
bases = self.get_base_list()
pairs_exist_on = self._pairs_exist_on
is_whitelist_mode = pairs_exist_on == "both_markets"
whitelisted_pairlist: list[str] = []
filtered_pairlist = pairlist.copy()
for pair in pairlist:
base = self._exchange.get_pair_base_currency(pair)
if not base:
self.log_once(
f"Unable to get base currency for pair {pair}, skipping it.", logger.warning
)
filtered_pairlist.remove(pair)
continue
found_in_bases = base in bases
if not found_in_bases:
for prefix in PairPrefixes:
# Check in case of PEPE needs to be changed into 1000PEPE for example
test_prefix = f"{prefix}{base}"
found_in_bases = test_prefix in bases
if found_in_bases:
break
# Avoid false positive since there are KAVA and AVA pairs, which aren't related
if prefix != "K":
# Check in case of 1000PEPE needs to be changed into PEPE for example
if base.startswith(prefix):
temp_base = base.removeprefix(prefix)
found_in_bases = temp_base in bases
if found_in_bases:
break
if found_in_bases:
whitelisted_pairlist.append(pair)
filtered_pairlist.remove(pair)
return whitelisted_pairlist if is_whitelist_mode else filtered_pairlist
@@ -28,15 +28,6 @@ class DelistFilter(IPairList):
"DelistFilter doesn't support this exchange and trading mode combination.",
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
@@ -15,15 +15,6 @@ logger = logging.getLogger(__name__)
class FullTradesFilter(IPairList):
supports_backtesting = SupportsBacktesting.NO_ACTION
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short allowlist method description - used for startup-messages
-1
View File
@@ -107,7 +107,6 @@ class IPairList(LoggingMixin, ABC):
return self.__class__.__name__
@property
@abstractmethod
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
@@ -65,15 +65,6 @@ class MarketCapPairList(IPairList):
"Please ensure this value is necessary for your use case.",
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
@@ -24,15 +24,6 @@ class OffsetFilter(IPairList):
if self._offset < 0:
raise OperationalException("OffsetFilter requires offset to be >= 0")
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
@@ -25,15 +25,6 @@ class PerformanceFilter(IPairList):
self._minutes = self._pairlistconfig.get("minutes", 0)
self._min_profit = self._pairlistconfig.get("min_profit")
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short allowlist method description - used for startup-messages
@@ -42,15 +42,6 @@ class ProducerPairList(IPairList):
"ProducerPairList requires external_message_consumer to be enabled."
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
+7 -20
View File
@@ -31,12 +31,6 @@ class RemotePairList(IPairList):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if "number_assets" not in self._pairlistconfig:
raise OperationalException(
"`number_assets` not specified. Please check your configuration "
'for "pairlist.config.number_assets"'
)
if "pairlist_url" not in self._pairlistconfig:
raise OperationalException(
"`pairlist_url` not specified. Please check your configuration "
@@ -45,7 +39,7 @@ class RemotePairList(IPairList):
self._mode = self._pairlistconfig.get("mode", "whitelist")
self._processing_mode = self._pairlistconfig.get("processing_mode", "filter")
self._number_pairs = self._pairlistconfig["number_assets"]
self._number_pairs: int | None = self._pairlistconfig.get("number_assets", None)
self._refresh_period: int = self._pairlistconfig.get("refresh_period", 1800)
self._keep_pairlist_on_failure = self._pairlistconfig.get("keep_pairlist_on_failure", True)
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
@@ -72,20 +66,11 @@ class RemotePairList(IPairList):
"position of your pairlist."
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - {self._pairlistconfig['number_assets']} pairs from RemotePairlist."
return f"{self.name} - {self._number_pairs or 'all'} pairs from RemotePairlist."
@staticmethod
def description() -> str:
@@ -102,7 +87,7 @@ class RemotePairList(IPairList):
},
"number_assets": {
"type": "number",
"default": 30,
"default": None,
"description": "Number of assets",
"help": "Number of assets to use from the pairlist.",
},
@@ -257,7 +242,8 @@ class RemotePairList(IPairList):
pairlist = expand_pairlist(pairlist, list(self._exchange.get_markets().keys()))
pairlist = self._whitelist_for_active_markets(pairlist)
pairlist = pairlist[: self._number_pairs]
if self._number_pairs and (self._mode == "whitelist"):
pairlist = pairlist[: self._number_pairs]
if pairlist:
self._pair_cache["pairlist"] = pairlist.copy()
@@ -314,5 +300,6 @@ class RemotePairList(IPairList):
if filtered:
self.log_once(f"Blacklist - Filtered out pairs: {filtered}", logger.info)
merged_list = merged_list[: self._number_pairs]
if self._number_pairs and (self._mode == "whitelist"):
merged_list = merged_list[: self._number_pairs]
return merged_list
@@ -39,15 +39,6 @@ class ShuffleFilter(IPairList):
maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"])
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
@@ -28,15 +28,6 @@ class StaticPairList(IPairList):
# Pair cache - only used for optimize modes
self._bt_pair_cache: LRUCache = LRUCache(maxsize=1)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
@@ -53,15 +53,6 @@ class VolatilityFilter(IPairList):
"either None (undefined), 'asc' or 'desc'"
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
@@ -28,7 +28,11 @@ def expand_pairlist(
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
# Remove wildcard pairs that didn't have a match.
result = [element for element in result if re.fullmatch(r"^[A-Za-z0-9:/-]+$", element)]
result = [
element
for element in result
if re.fullmatch(r"^[\w:/-]+$", element) and "_" not in element
]
else:
for pair_wc in wildcardpl:
@@ -47,15 +47,6 @@ class RangeStabilityFilter(IPairList):
"either None (undefined), 'asc' or 'desc'"
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
+18 -2
View File
@@ -697,9 +697,25 @@ class MarketResponse(BaseModel):
exchange_id: str
class CpuInfo(BaseModel):
cpu: int
pct: float
class SysInfo(BaseModel):
cpu_pct: list[float]
ram_pct: float
"""Information about the system running the bot based on psutil output/measurements
Note: cpu_pct is deprecated and may be removed in a future release. Use cpu_load instead.
"""
cpu_pct: list[float] = Field(
default=[], deprecated=True, description="Use cpu_load object instead"
)
cpu_load: list[CpuInfo]
cpu_load_avg: dict[str, float]
cpu_count: int = Field(description="Number of logical CPUs as provided by psutil")
cpu_avg: float = Field(description="Average CPU load across all cores as provided by psutil")
ram_pct: float = Field(description="RAM usage percentage as provided by psutil")
class Health(BaseModel):
+6 -2
View File
@@ -77,9 +77,13 @@ router_public = APIRouter()
router = APIRouter()
@router_public.api_route("/ping", methods=["GET", "HEAD"], response_model=Ping, tags=["Info"])
@router_public.get("/ping", response_model=Ping, tags=["Info"])
@router_public.head("/ping", response_model=Ping, tags=["Info"])
def ping():
"""simple ping"""
"""simple ping to check if API is responsive
Performs no internal checks, just returns pong.
"""
return {"status": "pong"}
+15 -18
View File
@@ -1,4 +1,5 @@
import logging
from contextlib import asynccontextmanager
from ipaddress import ip_address
from typing import Any
@@ -100,6 +101,19 @@ class FTJSONResponse(JSONResponse):
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup logic
if not ApiServer._message_stream:
# Creates the MessageStream class on startup so it has access to the same event loop
# as uvicorn
ApiServer._message_stream = MessageStream()
yield
# Shutdown logic
if ApiServer._message_stream:
ApiServer._message_stream = None
class ApiServer(RPCHandler):
__instance = None
__initialized = False
@@ -137,6 +151,7 @@ class ApiServer(RPCHandler):
redoc_url=None,
default_response_class=FTJSONResponse,
openapi_tags=_OPENAPI_TAGS,
lifespan=lifespan,
)
self.configure_app(self.app, self._config)
self.start_api()
@@ -261,24 +276,6 @@ class ApiServer(RPCHandler):
)
app.add_exception_handler(RPCException, self.handle_rpc_exception)
app.add_event_handler(event_type="startup", func=self._api_startup_event)
app.add_event_handler(event_type="shutdown", func=self._api_shutdown_event)
async def _api_startup_event(self):
"""
Creates the MessageStream class on startup
so it has access to the same event loop
as uvicorn
"""
if not ApiServer._message_stream:
ApiServer._message_stream = MessageStream()
async def _api_shutdown_event(self):
"""
Removes the MessageStream class on shutdown
"""
if ApiServer._message_stream:
ApiServer._message_stream = None
def start_api(self):
"""
+17 -1
View File
@@ -1736,8 +1736,24 @@ class RPC:
@staticmethod
def _rpc_sysinfo() -> dict[str, Any]:
cpu_pct = psutil.cpu_percent(interval=0.1, percpu=True)
load_avg = psutil.getloadavg()
return {
"cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
"cpu_pct": cpu_pct, # Deprecated, use cpu_load instead
"cpu_load": [
{
"cpu": idx,
"pct": pct,
}
for idx, pct in enumerate(cpu_pct)
],
"cpu_avg": sum(cpu_pct) / len(cpu_pct) if cpu_pct else 0.0,
"cpu_load_avg": {
"1m": load_avg[0],
"5m": load_avg[1],
"15m": load_avg[2],
},
"cpu_count": len(cpu_pct),
"ram_pct": psutil.virtual_memory().percent,
}
+1 -1
View File
@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2026.2"
__version__ = "2026.3"
if "dev" in __version__:
from pathlib import Path
+1 -1
View File
@@ -1,3 +1,3 @@
# Requirements for freqtrade client library
requests==2.32.5
requests==2.33.0
python-rapidjson==1.23
+12 -43
View File
@@ -77,29 +77,28 @@ dependencies = [
# Requirements used for submodules
plot = ["plotly>=4.0"]
hyperopt = [
"scipy",
"scikit-learn",
"optuna > 4.0.0",
"cmaes",
"filelock",
"optuna > 4.0.0",
"scikit-learn",
"scipy",
]
freqai = [
"scikit-learn",
"datasieve>=0.1.5",
"joblib",
"lightgbm",
"xgboost",
"scikit-learn",
"tensorboard",
"datasieve>=0.1.5",
"xgboost",
]
freqai_rl = [
"torch; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"gymnasium",
"stable-baselines3; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"sb3-contrib; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"stable-baselines3; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"torch; sys_platform != 'darwin' or platform_machine != 'x86_64'",
"tqdm",
]
develop = [
"isort",
"mypy",
"pre-commit",
"pytest-asyncio",
@@ -119,10 +118,10 @@ develop = [
"types-tabulate",
]
jupyter = [
"jupyter",
"nbstripout",
"ipykernel",
"jupyter",
"nbconvert",
"nbstripout",
]
all = [
"freqtrade[plot,hyperopt,freqai,freqai_rl,jupyter]",
@@ -153,43 +152,13 @@ namespaces = true
[tool.setuptools.dynamic]
version = {attr = "freqtrade.__version__"}
[tool.black]
line-length = 100
exclude = '''
(
/(
\.eggs # exclude a few common directories in the
| \.git # root of the project
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
# Exclude vendor directory
| vendor
)
'''
[tool.isort]
line_length = 100
profile = "black"
# multi_line_output=3
lines_after_imports=2
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
[tool.pytest]
log_format = "%(asctime)s %(levelname)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S"
addopts = ["--dist", "loadscope"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
addopts = "--dist loadscope"
[tool.mypy]
ignore_missing_imports = true
+8 -5
View File
@@ -6,7 +6,7 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.15.1
ruff==0.15.6
mypy==1.19.1
pre-commit==4.5.1
pytest==9.0.2
@@ -16,7 +16,6 @@ pytest-mock==3.15.1
pytest-random-order==1.2.0
pytest-timeout==2.4.0
pytest-xdist==3.8.0
isort==7.0.0
# For datetime mocking
time-machine==3.2.0
@@ -24,10 +23,14 @@ time-machine==3.2.0
nbconvert==7.17.0
# mypy types
scipy-stubs==1.17.0.2 # keep in sync with `scipy` in `requirements-hyperopt.txt`
scipy-stubs==1.17.1.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.20260107
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20260124
types-tabulate==0.10.0.20260308
types-python-dateutil==2.9.0.20260305
pip-audit==2.10.0
# For build step in CI
build==1.4.2
# For pre-commit-update check
pyyaml==6.0.3
+2 -2
View File
@@ -2,8 +2,8 @@
-r requirements.txt
# Required for hyperopt
scipy==1.17.0
scipy==1.17.1
scikit-learn==1.8.0
filelock==3.24.2
filelock==3.25.2
optuna==4.7.0
cmaes==0.12.0
+1 -1
View File
@@ -1,4 +1,4 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==6.5.2
plotly==6.6.0
+15 -15
View File
@@ -1,4 +1,4 @@
numpy==2.4.2
numpy==2.4.3
pandas==2.3.3
bottleneck==1.6.0
numexpr==2.14.1
@@ -7,25 +7,25 @@ ft-pandas-ta==0.3.16
ta-lib==0.6.8
technical==1.5.4
ccxt==4.5.39
cryptography==46.0.5
ccxt==4.5.44
cryptography==46.0.6
aiohttp==3.13.3
SQLAlchemy==2.0.46
SQLAlchemy==2.0.48
python-telegram-bot==22.6
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.15.0
cachetools==7.0.1
requests==2.32.5
cachetools==7.0.5
requests==2.33.0
urllib3==2.6.3
certifi==2026.1.4
certifi==2026.2.25
jsonschema==4.26.0
tabulate==0.9.0
tabulate==0.10.0
pycoingecko==3.2.0
jinja2==3.1.6
joblib==1.5.3
rich==14.3.2
pyarrow==23.0.0; platform_machine != 'armv7l'
rich==14.3.3
pyarrow==23.0.1; platform_machine != 'armv7l'
# Load ticker files 30% faster
@@ -37,10 +37,10 @@ orjson==3.11.7
sdnotify==0.3.2
# API Server
fastapi==0.129.0
fastapi==0.135.1
pydantic==2.12.5
uvicorn==0.40.0
pyjwt==2.11.0
uvicorn==0.41.0
pyjwt==2.12.1
aiofiles==25.1.0
psutil==7.2.2
@@ -49,7 +49,7 @@ questionary==2.1.1
prompt-toolkit==3.0.52
# Extensions to datetime library
python-dateutil==2.9.0.post0
pytz==2025.2
pytz==2026.1.post1
#Futures
schedule==1.2.2
@@ -58,5 +58,5 @@ schedule==1.2.2
websockets==16.0
janus==2.0.0
ast-comments==1.2.3
ast-comments==1.3.0
packaging==26.0
+26 -1
View File
@@ -83,6 +83,30 @@ def test_setup_utils_configuration():
assert config["dry_run"] is False
def test_setup_utils_configuration_download_convert_flag():
args = [
"download-data",
"--exchange",
"kraken",
"--pairs",
"ETH/USDT",
"--dl-trades",
"--convert",
"-t",
"1m",
"--timerange",
"20260101-",
"--config",
"tests/testdata/testconfigs/main_test_config.json",
]
config = setup_utils_configuration(get_args(args), RunMode.UTIL_EXCHANGE)
assert config["download_trades"] is True
assert config["convert_trades"] is True
assert config["timeframes"] == ["1m"]
def test_start_trading_fail(mocker, caplog):
mocker.patch("freqtrade.worker.Worker.run", MagicMock(side_effect=OperationalException))
@@ -186,7 +210,8 @@ def test_list_exchanges(capsys):
captured = capsys.readouterr()
assert re.search(r"Exchanges available for Freqtrade.*", captured.out)
assert re.search(r".*binance.*", captured.out)
assert not re.search(r".*kraken.*", captured.out)
assert re.search(r"\bkrakenfutures\b", captured.out)
assert not re.search(r"\bmyokx\b", captured.out)
def test_list_timeframes(mocker, capsys):
+2
View File
@@ -516,6 +516,8 @@ def test_calculate_sqn_cases(profits, starting_balance, expected_sqn, descriptio
(1000, 1500, 365, 0.5),
(1000, 1500, 100, 3.3927), # sub year
(0.01000000, 0.01762792, 120, 4.6087), # sub year BTC values
(1000, 1010, 0, 0.0), # zero days
(-100, 100, 365, 0.0), # negative starting balance
],
)
def test_calculate_cagr(start, end, days, expected):
+2 -2
View File
@@ -56,9 +56,9 @@ def test_datahandler_ohlcv_regex(filename, pair, timeframe, candletype):
("USDT_BUSD", "USDT/BUSD"),
("BTC_USDT_USDT", "BTC/USDT:USDT"), # Futures
("XRP_USDT_USDT", "XRP/USDT:USDT"), # futures
("BTC-PERP", "BTC-PERP"),
("BTC-PERP_USDT", "BTC-PERP:USDT"),
("XYZ-XRP_USDT_USDT", "XYZ-XRP/USDT:USDT"), # hip3 futures
("UNITTEST_USDT", "UNITTEST/USDT"),
("币安人生_USDT_USDT", "币安人生/USDT:USDT"), # futures
],
)
def test_rebuild_pair_from_filename(pair, expected):
+77 -35
View File
@@ -960,7 +960,8 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
ExchangeResolver.load_exchange(default_conf)
else:
with pytest.raises(
OperationalException, match=r"On exchange stoploss price type is not supported for .*"
OperationalException,
match=r"On exchange stoploss price type '.*' is not supported for .*",
):
ExchangeResolver.load_exchange(default_conf)
@@ -1968,6 +1969,7 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
api_mock.fetch_orders = MagicMock(side_effect=return_value)
api_mock.fetch_open_orders = MagicMock(return_value=[limit_order["buy"]])
api_mock.fetch_canceled_orders = MagicMock(return_value=[])
api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order["buy"]])
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
@@ -2000,6 +2002,8 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
return True
if endpoint == "fetchOpenOrders":
return True
if endpoint == "fetchCanceledOrders":
return True
if exchange_name == "okx":
# Special OKX case is tested separately
@@ -2012,6 +2016,7 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
assert api_mock.fetch_orders.call_count == 0
assert api_mock.fetch_open_orders.call_count == expected
assert api_mock.fetch_closed_orders.call_count == expected
assert api_mock.fetch_canceled_orders.call_count == expected
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
@@ -2031,12 +2036,77 @@ def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
api_mock.fetch_orders = MagicMock(side_effect=ccxt.NotSupported())
api_mock.fetch_open_orders.reset_mock()
api_mock.fetch_closed_orders.reset_mock()
api_mock.fetch_canceled_orders.reset_mock()
exchange.fetch_orders("mocked", start_time)
assert api_mock.fetch_orders.call_count == expected
assert api_mock.fetch_open_orders.call_count == expected
assert api_mock.fetch_closed_orders.call_count == expected
assert api_mock.fetch_canceled_orders.call_count == expected
@pytest.mark.parametrize("exchange_name", [ex for ex in EXCHANGES if ex != "bybit"])
@pytest.mark.parametrize(
"call_config, expected",
[
# call_config: (fetch_orders, fetch_open, fetch_closed, fetch_canceled)
# expected: (fetch_orders_calls, fetch_open_calls, fetch_closed_calls, fetch_canceled_calls)
((True, False, False, False), (1, 0, 0, 0)),
((False, True, True, False), (0, 1, 1, 0)),
((False, True, False, True), (0, 1, 0, 1)),
((False, True, True, True), (0, 1, 1, 1)),
],
)
def test_fetch_orders_multi(
default_conf, mocker, exchange_name, limit_order, call_config, expected
):
default_conf["dry_run"] = False
api_mock = MagicMock()
call_count = 1
def return_value(*args, **kwargs):
nonlocal call_count
call_count += 2
return [
{**limit_order["buy"], "id": call_count},
{**limit_order["sell"], "id": call_count + 1},
]
api_mock.fetch_orders = MagicMock(side_effect=return_value)
api_mock.fetch_open_orders = MagicMock(return_value=[limit_order["buy"]])
api_mock.fetch_canceled_orders = MagicMock(return_value=[limit_order["sell"]])
api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order["buy"]])
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
start_time = datetime.now(UTC) - timedelta(days=20)
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name)
def has_resp(_, endpoint):
if endpoint == "fetchOrders":
return call_config[0]
if endpoint == "fetchClosedOrders":
return call_config[2]
if endpoint == "fetchOpenOrders":
return call_config[1]
if endpoint == "fetchCanceledOrders":
return call_config[3]
if exchange_name == "okx":
# Special OKX case is tested separately
return
mocker.patch(f"{EXMS}.exchange_has", has_resp)
#
resp = exchange.fetch_orders("mocked", start_time)
assert api_mock.fetch_orders.call_count == expected[0]
assert api_mock.fetch_open_orders.call_count == expected[1]
assert api_mock.fetch_closed_orders.call_count == expected[2]
assert api_mock.fetch_canceled_orders.call_count == expected[3]
assert len(resp) == 2 * expected[0] + expected[1] + expected[2] + expected[3]
def test_fetch_trading_fees(default_conf, mocker):
@@ -5274,19 +5344,16 @@ def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value,
@pytest.mark.parametrize(
"size,funding_rate,mark_price,time_in_ratio,funding_fee,kraken_fee",
"size,funding_rate,mark_price,funding_fee",
[
(10, 0.0001, 2.0, 1.0, 0.002, 0.002),
(10, 0.0002, 2.0, 0.01, 0.004, 0.00004),
(10, 0.0002, 2.5, None, 0.005, None),
(10, 0.0002, nan, None, 0.0, None),
(10, 0.0001, 2.0, 0.002),
(10, 0.0002, 2.0, 0.004),
(10, 0.0002, 2.5, 0.005),
(10, 0.0002, nan, 0.0),
],
)
def test_calculate_funding_fees(
default_conf, mocker, size, funding_rate, mark_price, funding_fee, kraken_fee, time_in_ratio
):
def test_calculate_funding_fees(default_conf, mocker, size, funding_rate, mark_price, funding_fee):
exchange = get_patched_exchange(mocker, default_conf)
kraken = get_patched_exchange(mocker, default_conf, exchange="kraken")
prior_date = timeframe_to_prev_date("1h", datetime.now(UTC) - timedelta(hours=1))
trade_date = timeframe_to_prev_date("1h", datetime.now(UTC))
funding_rates = DataFrame(
@@ -5310,35 +5377,10 @@ def test_calculate_funding_fees(
is_short=True,
open_date=trade_date,
close_date=trade_date,
time_in_ratio=time_in_ratio,
)
== funding_fee
)
if kraken_fee is None:
with pytest.raises(OperationalException):
kraken.calculate_funding_fees(
df,
amount=size,
is_short=True,
open_date=trade_date,
close_date=trade_date,
time_in_ratio=time_in_ratio,
)
else:
assert (
kraken.calculate_funding_fees(
df,
amount=size,
is_short=True,
open_date=trade_date,
close_date=trade_date,
time_in_ratio=time_in_ratio,
)
== kraken_fee
)
@pytest.mark.parametrize(
"mark_price,funding_rate,futures_funding_rate",
+910
View File
@@ -0,0 +1,910 @@
"""Tests for Kraken Futures exchange class"""
from copy import deepcopy
from datetime import UTC, datetime
from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import (
DDosProtection,
ExchangeError,
InvalidOrderException,
OperationalException,
TemporaryError,
)
from freqtrade.exchange.krakenfutures import Krakenfutures
from tests.conftest import EXMS, get_patched_exchange
ExchangeBase = Krakenfutures.__mro__[1] # freqtrade.exchange.exchange.Exchange
# --- _ft_has and OHLCV tests ---
def test_krakenfutures_ft_has_overrides():
"""Test that _ft_has contains Kraken Futures stoploss settings."""
ft_has = Krakenfutures._ft_has
assert ft_has["stoploss_on_exchange"] is True
assert ft_has["stoploss_order_types"] == {"limit": "limit", "market": "market"}
assert ft_has["stoploss_query_requires_stop_flag"] is True
assert ft_has["stop_price_prop"] == "stopPrice"
assert ft_has["stop_price_param"] == "triggerPrice"
assert ft_has["stop_price_type_field"] == "triggerSignal"
# --- _adjust_krakenfutures_order average price tests ---
def test_krakenfutures_adjust_order_computes_average_from_trades(mocker, default_conf):
"""Compute VWAP average price from trades when CCXT returns None."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "closed",
"filled": 0.0004,
"average": None,
"timestamp": 1771354195241,
}
trades = [
{
"amount": 0.0002,
"price": 67800.0,
"cost": 13.56,
"takerOrMaker": "taker",
"symbol": "BTC/USD:USD",
"fee": None,
},
{
"amount": 0.0002,
"price": 67900.0,
"cost": 13.58,
"takerOrMaker": "taker",
"symbol": "BTC/USD:USD",
"fee": None,
},
]
mocker.patch.object(ex, "get_trades_for_order", return_value=trades)
result = ex._adjust_krakenfutures_order(order)
assert result["average"] == pytest.approx(67850.0)
assert result["cost"] == pytest.approx(27.14)
def test_krakenfutures_adjust_order_skips_open_orders(mocker, default_conf):
"""Don't fetch trades for open orders."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "open",
"filled": 0,
"average": None,
"timestamp": 1771354195241,
}
trades_mock = mocker.patch.object(ex, "get_trades_for_order")
result = ex._adjust_krakenfutures_order(order)
assert result["average"] is None
trades_mock.assert_not_called()
def test_krakenfutures_adjust_order_handles_none_filled(mocker, default_conf):
"""Don't crash or fetch trades when filled is None."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "closed",
"filled": None,
"average": None,
"timestamp": 1771354195241,
}
trades_mock = mocker.patch.object(ex, "get_trades_for_order")
result = ex._adjust_krakenfutures_order(order)
assert result["average"] is None
trades_mock.assert_not_called()
def test_krakenfutures_adjust_order_recomputes_existing_average(mocker, default_conf):
"""Recompute average from fills even when CCXT already provided one."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "closed",
"filled": 0.0004,
"average": 67843.0,
"timestamp": 1771354195241,
}
trades = [
{
"amount": 0.0002,
"price": 67800.0,
"cost": 13.56,
"takerOrMaker": "taker",
"symbol": "BTC/USD:USD",
"fee": None,
},
{
"amount": 0.0002,
"price": 67900.0,
"cost": 13.58,
"takerOrMaker": "taker",
"symbol": "BTC/USD:USD",
"fee": None,
},
]
trades_mock = mocker.patch.object(ex, "get_trades_for_order", return_value=trades)
result = ex._adjust_krakenfutures_order(order)
assert result["average"] == pytest.approx(67850.0)
assert result["cost"] == pytest.approx(27.14)
trades_mock.assert_called_once()
def test_krakenfutures_adjust_order_no_trades_found(mocker, default_conf):
"""Leave average as None when no trades are found."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "closed",
"filled": 0.0004,
"average": None,
"timestamp": 1771354195241,
}
mocker.patch.object(ex, "get_trades_for_order", return_value=[])
result = ex._adjust_krakenfutures_order(order)
assert result["average"] is None
# --- get_trades_for_order fee enrichment tests ---
def test_krakenfutures_get_trades_enriches_fees(mocker, default_conf):
"""Calculate fees from market fee schedule when CCXT returns fee: None."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
raw_trades = [
{
"amount": 0.0004,
"price": 67843.0,
"cost": 27.14,
"order": "abc",
"symbol": "BTC/USD:USD",
"takerOrMaker": "taker",
"fee": {"cost": None, "currency": None},
},
]
mocker.patch.object(
ExchangeBase,
"get_trades_for_order",
return_value=raw_trades,
)
# Re-patch markets property with fee rates for BTC/USD:USD
kf_markets = {"BTC/USD:USD": {"taker": 0.0005, "maker": 0.0002, "quote": "USD"}}
mocker.patch.object(type(ex), "markets", PropertyMock(return_value=kf_markets))
result = ex.get_trades_for_order("abc", "BTC/USD:USD", since=MagicMock())
assert len(result) == 1
assert result[0]["fee"]["cost"] == pytest.approx(27.14 * 0.0005)
assert result[0]["fee"]["currency"] == "USD"
assert result[0]["fee"]["rate"] == 0.0005
def test_krakenfutures_get_trades_uses_maker_rate(mocker, default_conf):
"""Use maker fee rate when fillType is maker."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
raw_trades = [
{
"amount": 0.0004,
"price": 67843.0,
"cost": 27.14,
"order": "abc",
"symbol": "BTC/USD:USD",
"takerOrMaker": "maker",
"fee": None,
},
]
mocker.patch.object(
ExchangeBase,
"get_trades_for_order",
return_value=raw_trades,
)
kf_markets = {"BTC/USD:USD": {"taker": 0.0005, "maker": 0.0002, "quote": "USD"}}
mocker.patch.object(type(ex), "markets", PropertyMock(return_value=kf_markets))
result = ex.get_trades_for_order("abc", "BTC/USD:USD", since=MagicMock())
assert result[0]["fee"]["cost"] == pytest.approx(27.14 * 0.0002)
assert result[0]["fee"]["rate"] == 0.0002
def test_krakenfutures_get_trades_preserves_existing_fees(mocker, default_conf):
"""Don't overwrite fees if CCXT already provided them."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
existing_fee = {"cost": 0.01, "currency": "USD", "rate": 0.0005}
raw_trades = [
{
"amount": 0.0004,
"price": 67843.0,
"cost": 27.14,
"order": "abc",
"symbol": "BTC/USD:USD",
"takerOrMaker": "taker",
"fee": existing_fee,
},
]
mocker.patch.object(
ExchangeBase,
"get_trades_for_order",
return_value=raw_trades,
)
kf_markets = {"BTC/USD:USD": {"taker": 0.0005, "maker": 0.0002, "quote": "USD"}}
mocker.patch.object(type(ex), "markets", PropertyMock(return_value=kf_markets))
result = ex.get_trades_for_order("abc", "BTC/USD:USD", since=MagicMock())
# Should keep existing fee, not recalculate
assert result[0]["fee"] == existing_fee
# --- fetch_order fallback tests ---
def test_krakenfutures_fetch_order_falls_back_to_closed_orders(mocker, default_conf):
"""Fallback to fetch_closed_orders when fetch_order can't find the order."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found"))
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(
ex._api,
"fetch_closed_orders",
return_value=[{"id": "abc", "symbol": "BTC/USD:USD", "status": "closed"}],
create=True,
)
closed_fetch.__name__ = "fetch_closed_orders"
res = ex.fetch_order("abc", "BTC/USD:USD")
assert res["id"] == "abc"
def test_krakenfutures_fetch_order_falls_back_to_canceled_orders(mocker, default_conf):
"""Fallback to fetch_canceled_orders when closed orders don't contain the order."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.ExchangeError("UUID too large"))
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True)
closed_fetch.__name__ = "fetch_closed_orders"
canceled_fetch = mocker.patch.object(
ex._api,
"fetch_canceled_orders",
return_value=[{"id": "def", "symbol": "BTC/USD:USD", "status": "canceled"}],
create=True,
)
canceled_fetch.__name__ = "fetch_canceled_orders"
res = ex.fetch_order("def", "BTC/USD:USD")
assert res["id"] == "def"
def test_krakenfutures_fetch_order_returns_direct_ccxt_result(mocker, default_conf):
"""Use direct CCXT fetch_order result when available."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
ccxt_order = {"id": "live-123", "symbol": "BTC/USD:USD", "status": "open"}
converted = {"id": "live-123", "status": "open"}
mocker.patch.object(ex._api, "fetch_order", return_value=ccxt_order)
converter = mocker.patch.object(ex, "_order_contracts_to_amount", return_value=converted)
fallback = mocker.patch.object(ex, "_fetch_order_fallback")
res = ex.fetch_order("live-123", "BTC/USD:USD")
assert res == converted
converter.assert_called_once_with(ccxt_order)
fallback.assert_not_called()
def test_krakenfutures_fetch_order_strips_stop_from_status_query(mocker, default_conf):
"""Direct CCXT fetch_order status lookup should not receive stop/trigger params."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
ccxt_order = {"id": "order-1", "symbol": "BTC/USD:USD", "status": "open"}
fetch_order = mocker.patch.object(ex._api, "fetch_order", return_value=ccxt_order)
# Simulate call from base fetch_stoploss_order which adds stop=True
ex.fetch_order("order-1", "BTC/USD:USD", params={"stop": True})
# stop should be stripped from the direct CCXT status call
fetch_order.assert_called_once_with("order-1", "BTC/USD:USD", params={})
def test_krakenfutures_fetch_order_raises_invalid_when_not_found(mocker, default_conf):
"""Raise InvalidOrderException (non-retrying) when order is not in any endpoint."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found"))
mocker.patch.object(ex, "_fetch_order_fallback", return_value=None)
with pytest.raises(InvalidOrderException, match="Order not found in any endpoint"):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_invalid_order_maps_exception(mocker, default_conf):
"""Map ccxt.InvalidOrder to InvalidOrderException."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.InvalidOrder("bad order"))
with pytest.raises(InvalidOrderException, match="bad order"):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_ddos_maps_exception(mocker, default_conf):
"""Map ccxt.DDoSProtection to DDosProtection."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.DDoSProtection("ratelimit"))
with pytest.raises(DDosProtection):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_baseerror_maps_exception(mocker, default_conf):
"""Map generic ccxt.BaseError to OperationalException."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.BaseError("unexpected"))
with pytest.raises(OperationalException):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_fallback_returns_none(mocker, default_conf):
"""Return None when order is not found in any endpoint."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True)
closed_fetch.__name__ = "fetch_closed_orders"
canceled_fetch = mocker.patch.object(
ex._api, "fetch_canceled_orders", return_value=[], create=True
)
canceled_fetch.__name__ = "fetch_canceled_orders"
res = ex._fetch_order_fallback("abc", "BTC/USD:USD", {})
assert res is None
def test_krakenfutures_fetch_order_fallback_returns_open_order_first(mocker, default_conf):
"""Return immediately when order is found in open orders."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
open_fetch = mocker.patch.object(
ex._api,
"fetch_open_orders",
return_value=[{"id": "abc", "symbol": "BTC/USD:USD", "status": "open"}],
create=True,
)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True)
closed_fetch.__name__ = "fetch_closed_orders"
canceled_fetch = mocker.patch.object(
ex._api, "fetch_canceled_orders", return_value=[], create=True
)
canceled_fetch.__name__ = "fetch_canceled_orders"
res = ex._fetch_order_fallback("abc", "BTC/USD:USD", {})
assert res is not None
assert res["id"] == "abc"
open_fetch.assert_called_once()
closed_fetch.assert_not_called()
canceled_fetch.assert_not_called()
def test_krakenfutures_fetch_order_dry_run(mocker, default_conf):
"""Test fetch_order uses dry_run order in dry_run mode."""
conf = dict(default_conf)
conf["dry_run"] = True
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
dry_order = {"id": "dry-123", "status": "open"}
mocker.patch.object(ex, "fetch_dry_run_order", return_value=dry_order)
res = ex.fetch_order("dry-123", "BTC/USD:USD")
assert res["id"] == "dry-123"
def test_krakenfutures_fetch_order_finds_stoploss_via_stop_param(mocker, default_conf):
"""Test fetch_order finds stoploss orders via closed orders fallback with stop=True."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found"))
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
# With stop=True, CCXT queries trigger history endpoint
closed_fetch = mocker.patch.object(
ex._api,
"fetch_closed_orders",
return_value=[
{"id": "trigger-123", "symbol": "BTC/USD:USD", "status": "closed"},
],
create=True,
)
closed_fetch.__name__ = "fetch_closed_orders"
# Simulate what base class fetch_stoploss_order does (adds stop=True)
res = ex.fetch_order("trigger-123", "BTC/USD:USD", params={"stop": True})
assert res["id"] == "trigger-123"
def test_krakenfutures_fetch_order_fallback_passes_stop_to_history(mocker, default_conf):
"""Stoploss query (stop=True) should pass through to closed/canceled endpoints."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_order = {"id": "sl-123", "symbol": "BTC/USD:USD", "status": "closed"}
closed_fetch = mocker.patch.object(
ex._api,
"fetch_closed_orders",
return_value=[closed_order],
create=True,
)
closed_fetch.__name__ = "fetch_closed_orders"
res = ex._fetch_order_fallback("sl-123", "BTC/USD:USD", {"stop": True})
assert res is not None
assert res["id"] == "sl-123"
# Verify stop=True was passed to closed orders (CCXT maps stop→trigger)
closed_fetch.assert_called_once_with("BTC/USD:USD", params={"stop": True})
def test_krakenfutures_fetch_order_fallback_strips_stop_from_open_orders(mocker, default_conf):
"""Open orders query should not receive stop/trigger flags."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True)
closed_fetch.__name__ = "fetch_closed_orders"
canceled_fetch = mocker.patch.object(
ex._api, "fetch_canceled_orders", return_value=[], create=True
)
canceled_fetch.__name__ = "fetch_canceled_orders"
ex._fetch_order_fallback("abc", "BTC/USD:USD", {"stop": True})
# stop should be stripped from open orders call
open_fetch.assert_called_once_with("BTC/USD:USD", params={})
def test_krakenfutures_fetch_order_propagates_exchange_errors_from_fallback(mocker, default_conf):
"""Fallback list fetch should not hide exchange-level failures."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found"))
open_fetch = mocker.patch.object(
ex._api, "fetch_open_orders", side_effect=ccxt.ExchangeError("service unavailable")
)
open_fetch.__name__ = "fetch_open_orders"
with pytest.raises(TemporaryError):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_exchangeerror_uses_fallback(mocker, default_conf):
"""ExchangeError from fetch_order should trigger fallback lookup."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
fallback_order = {"id": "abc", "symbol": "BTC/USD:USD", "status": "closed"}
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.ExchangeError("temporary"))
fallback = mocker.patch.object(ex, "_fetch_order_fallback", return_value=fallback_order)
result = ex.fetch_order("abc", "BTC/USD:USD", count=0)
assert result == fallback_order
fallback.assert_called_once_with("abc", "BTC/USD:USD", {})
def test_krakenfutures_find_order_in_list_handles_ordernotfound(mocker, default_conf):
"""OrderNotFound in list fetch is treated as a missing order."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
def raise_order_not_found(_symbol, params=None):
raise ccxt.OrderNotFound("missing")
assert ex._find_order_in_list(raise_order_not_found, "BTC/USD:USD", {}, "abc") is None
def test_krakenfutures_find_order_in_list_maps_ddos(mocker, default_conf):
"""DDoS errors from list fetch are mapped to DDosProtection."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
def raise_ddos(_symbol, params=None):
raise ccxt.DDoSProtection("ratelimit")
with pytest.raises(DDosProtection):
ex._find_order_in_list(raise_ddos, "BTC/USD:USD", {}, "abc")
def test_krakenfutures_find_order_in_list_maps_temporary(mocker, default_conf):
"""OperationFailed/ExchangeError from list fetch map to TemporaryError."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
def raise_temp(_symbol, params=None):
raise ccxt.OperationFailed("temporary")
with pytest.raises(TemporaryError):
ex._find_order_in_list(raise_temp, "BTC/USD:USD", {}, "abc")
def test_krakenfutures_find_order_in_list_maps_operational(mocker, default_conf):
"""Unexpected BaseError from list fetch maps to OperationalException."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
def raise_base(_symbol, params=None):
raise ccxt.BaseError("unexpected")
with pytest.raises(OperationalException):
ex._find_order_in_list(raise_base, "BTC/USD:USD", {}, "abc")
# --- Stoploss tests ---
def test_krakenfutures_create_stoploss_uses_trigger_price_type(mocker, default_conf):
"""Test create_stoploss uses triggerPrice, triggerSignal, and reduceOnly."""
api_mock = MagicMock()
api_mock.create_order = MagicMock(return_value={"id": "order-id", "info": {"foo": "bar"}})
conf = deepcopy(default_conf)
conf["dry_run"] = False
conf["trading_mode"] = TradingMode.FUTURES
conf["margin_mode"] = MarginMode.ISOLATED
mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
ex = get_patched_exchange(mocker, conf, api_mock, exchange="krakenfutures")
ex.create_stoploss(
pair="ETH/BTC",
amount=1,
stop_price=90000.0,
side="sell",
order_types={"stoploss": "market", "stoploss_price_type": "mark"},
leverage=1.0,
)
call_args = api_mock.create_order.call_args
params = call_args[1].get("params") if call_args[1] else call_args[0][5]
assert params["triggerPrice"] == 90000.0
assert params["triggerSignal"] == "mark"
assert params["reduceOnly"] is True
# --- Funding fees tests ---
def test_krakenfutures_get_funding_fees_futures_success(mocker, default_conf):
"""Use funding fee helper in futures mode."""
conf = dict(default_conf)
conf["trading_mode"] = TradingMode.FUTURES
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
helper = mocker.patch.object(ex, "_fetch_and_calculate_funding_fees", return_value=1.23)
open_date = datetime.now(UTC)
assert ex.get_funding_fees("BTC/USD:USD", 0.1, False, open_date) == 1.23
helper.assert_called_once_with("BTC/USD:USD", 0.1, False, open_date)
def test_krakenfutures_get_funding_fees_futures_exchange_error(mocker, default_conf):
"""Return 0.0 when funding fee retrieval fails."""
conf = dict(default_conf)
conf["trading_mode"] = TradingMode.FUTURES
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex, "_fetch_and_calculate_funding_fees", side_effect=ExchangeError("fail"))
assert ex.get_funding_fees("BTC/USD:USD", 0.1, False, None) == 0.0
def test_krakenfutures_get_funding_fees_spot_returns_zero(mocker, default_conf):
"""Return 0.0 outside futures mode without calling the helper."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
helper = mocker.patch.object(ex, "_fetch_and_calculate_funding_fees")
assert ex.get_funding_fees("BTC/USD:USD", 0.1, False, None) == 0.0
helper.assert_not_called()
# --- Balance tests (flex account USD synthesis) ---
def test_krakenfutures_get_balances_flex_account_synthesizes_usd(mocker, default_conf):
"""Test that flex account availableMargin/portfolioValue are synthesized as USD balance."""
default_conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
flex_response = {
"EUR": {"free": 100.0, "used": 0.0, "total": 100.0},
"info": {
"accounts": {
"flex": {
"availableMargin": "950.50",
"marginEquity": "1000.00",
"portfolioValue": "1050.00", # Should be ignored, marginEquity preferred
"currencies": {"EUR": {"quantity": "100", "value": "105.00"}},
}
}
},
"free": {"EUR": 100.0},
"used": {"EUR": 0.0},
"total": {"EUR": 100.0},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
# USD should be synthesized from flex account
assert "USD" in balances
assert balances["USD"]["free"] == 950.50
assert balances["USD"]["total"] == 1000.00
# used = total - free = 1000.00 - 950.50 = 49.50
assert balances["USD"]["used"] == 49.50
# EUR should still be present
assert "EUR" in balances
# info, free, total, used dicts should be removed
assert "info" not in balances
assert "free" not in balances
assert "total" not in balances
assert "used" not in balances
def test_krakenfutures_get_balances_no_flex_account(mocker, default_conf):
"""Test that non-flex accounts work without USD synthesis."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
standard_response = {
"USD": {"free": 500.0, "used": 100.0, "total": 600.0},
"info": {"type": "cashAccount"},
"free": {"USD": 500.0},
"used": {"USD": 100.0},
"total": {"USD": 600.0},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=standard_response)
balances = ex.get_balances()
# USD should be preserved as-is
assert balances["USD"]["free"] == 500.0
assert balances["USD"]["total"] == 600.0
# info, free, total, used dicts should be removed
assert "info" not in balances
def test_krakenfutures_get_balances_flex_fallback_chain(mocker, default_conf):
"""Test fallback chain: marginEquity -> portfolioValue -> balanceValue."""
default_conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
# Test fallback to balanceValue (no marginEquity or portfolioValue)
flex_response = {
"info": {
"accounts": {
"flex": {
"availableMargin": "800.00",
"balanceValue": "850.00",
}
}
},
"free": {},
"used": {},
"total": {},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
assert balances["USD"]["free"] == 800.00
assert balances["USD"]["total"] == 850.00
# used = total - free = 850.00 - 800.00 = 50.00
assert balances["USD"]["used"] == 50.00
def test_krakenfutures_get_balances_flex_zero_free_calculates_used(mocker, default_conf):
"""Test used margin is correct when availableMargin is 0.0."""
default_conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
flex_response = {
"info": {
"accounts": {
"flex": {
"availableMargin": "0.00",
"marginEquity": "125.00",
}
}
},
"free": {},
"used": {},
"total": {},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
assert balances["USD"]["free"] == 0.00
assert balances["USD"]["total"] == 125.00
assert balances["USD"]["used"] == 125.00
def test_krakenfutures_get_balances_flex_missing_free_uses_total(mocker, default_conf):
"""When availableMargin is missing, free falls back to total and used is 0.0."""
default_conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
flex_response = {
"info": {
"accounts": {
"flex": {
"marginEquity": "250.00",
}
}
},
"free": {},
"used": {},
"total": {},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
assert balances["USD"]["free"] == 250.00
assert balances["USD"]["total"] == 250.00
assert balances["USD"]["used"] == 0.00
def test_krakenfutures_get_balances_skips_synthesis_for_non_usd_stake(mocker, default_conf):
"""Test that USD synthesis is skipped when stake_currency is not USD."""
default_conf["stake_currency"] = "EUR"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
flex_response = {
"EUR": {"free": 100.0, "used": 0.0, "total": 100.0},
"info": {
"accounts": {
"flex": {
"availableMargin": "950.50",
"portfolioValue": "1000.00",
}
}
},
"free": {"EUR": 100.0},
"used": {"EUR": 0.0},
"total": {"EUR": 100.0},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
# USD should NOT be synthesized since stake_currency is EUR
assert "USD" not in balances
# EUR should still be present
assert "EUR" in balances
assert balances["EUR"]["free"] == 100.0
def test_krakenfutures_get_balances_maps_ddos(mocker, default_conf):
"""Map ccxt.DDoSProtection from fetch_balance to DDosProtection."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_balance", side_effect=ccxt.DDoSProtection("ratelimit"))
with pytest.raises(DDosProtection):
ex.get_balances(count=0)
def test_krakenfutures_get_balances_maps_temporary(mocker, default_conf):
"""Map ccxt.OperationFailed/ExchangeError from fetch_balance to TemporaryError."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_balance", side_effect=ccxt.OperationFailed("temporary"))
with pytest.raises(TemporaryError):
ex.get_balances(count=0)
def test_krakenfutures_get_balances_maps_operational(mocker, default_conf):
"""Map unexpected ccxt.BaseError from fetch_balance to OperationalException."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_balance", side_effect=ccxt.BaseError("unexpected"))
with pytest.raises(OperationalException):
ex.get_balances(count=0)
def test_krakenfutures_safe_float():
"""Test _safe_float handles various input types."""
assert Krakenfutures._safe_float("123.45") == 123.45
assert Krakenfutures._safe_float(100) == 100.0
assert Krakenfutures._safe_float(None) is None
assert Krakenfutures._safe_float("invalid") is None
assert Krakenfutures._safe_float({}) is None
# --- Stoploss via base class (stoploss_query_requires_stop_flag) ---
def test_krakenfutures_fetch_stoploss_order_uses_base_class(mocker, default_conf):
"""Base class fetch_stoploss_order should add stop=True and delegate to fetch_order."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
expected_order = {"id": "sl-order-1", "status": "open", "info": {}}
fetch_order = mocker.patch.object(ex, "fetch_order", return_value=expected_order)
result = ex.fetch_stoploss_order("sl-order-1", "BTC/USD:USD")
assert result["id"] == "sl-order-1"
# Base class should pass stop=True
fetch_order.assert_called_once()
call_params = fetch_order.call_args[0][2] if len(fetch_order.call_args[0]) > 2 else {}
assert call_params.get("stop") is True
def test_krakenfutures_cancel_stoploss_order_uses_base_class(mocker, default_conf):
"""Base class cancel_stoploss_order should add stop=True and delegate to cancel_order."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
expected_order = {"id": "sl-cancel-1", "status": "canceled"}
cancel_order = mocker.patch.object(ex, "cancel_order", return_value=expected_order)
result = ex.cancel_stoploss_order("sl-cancel-1", "BTC/USD:USD")
assert result["id"] == "sl-cancel-1"
# Base class should pass stop=True
cancel_order.assert_called_once()
call_params = cancel_order.call_args[0][2] if len(cancel_order.call_args[0]) > 2 else {}
assert call_params.get("stop") is True
+190 -51
View File
@@ -18,6 +18,7 @@ class TestExchangeOnlineSetup(TypedDict):
timeframe: str
candle_count: int
futures: bool
futures_only: bool | None
futures_pair: str | None
candle_count_futures: int | None
hasQuoteVolumeFutures: bool | None
@@ -26,6 +27,7 @@ class TestExchangeOnlineSetup(TypedDict):
trades_lookback_hours: int
private_methods: list[str] | None
sample_order: list[dict[str, Any]] | None
sample_order_futures: list[dict[str, Any]] | None
sample_my_trades: list[dict[str, Any]] | None
skip_ws_tests: bool | None
@@ -77,11 +79,12 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "3551312894",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"status": "open",
"side": "buy",
"amount": 1.1,
},
},
@@ -107,11 +110,12 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "3551312894",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"status": "open",
"side": "buy",
"status": "closed",
"amount": 1.1,
},
},
@@ -148,9 +152,9 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "3551312894",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"status": "open",
"amount": 1.1,
@@ -203,12 +207,12 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"price": 15.5,
"status": "open",
"amount": 1.1,
"id": "63d6742d0adc5570001d2bbf7",
"timestamp": None,
"datetime": None,
"price": None,
"status": None,
"amount": None,
},
}, # create order
{
@@ -247,11 +251,11 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "63d6742d0adc5570001d2bbf7",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"status": "open",
"status": "closed",
"amount": 1.1,
},
},
@@ -304,11 +308,12 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "276266139423",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"status": "open",
"average": 15.5,
"status": "closed",
"amount": 1.1,
},
},
@@ -347,11 +352,11 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "276401180529",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"status": "open",
"status": "canceled",
"amount": 1.1,
},
},
@@ -420,10 +425,11 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "1274754916287346280",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"side": "buy",
"status": "open",
"amount": 1.1,
},
@@ -499,18 +505,19 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "1762393630149869568",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"status": "open",
"average": 15.5,
"status": "closed",
"amount": 1.1,
},
},
{
"exchange_response": {
"symbol": "SOL-USDT",
"orderId": "1762393630149869568",
"orderId": "1762393630149869567",
"transactTime": "1674493798550",
"price": "15.5",
"stopPrice": "0",
@@ -525,11 +532,12 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
"pair": "SOL/USDT",
"expected": {
"symbol": "SOL/USDT",
"orderId": "3551312894",
"id": "1762393630149869567",
"timestamp": 1674493798550,
"datetime": "2023-03-25T15:49:58.550Z",
"datetime": "2023-01-23T17:09:58.550Z",
"price": 15.5,
"status": "open",
"average": 15.5,
"status": "closed",
"amount": 1.1,
},
},
@@ -552,9 +560,137 @@ EXCHANGES: dict[str, TestExchangeOnlineSetup] = {
# TODO: re-enable hyperliquid websocket tests
"skip_ws_tests": True,
},
"krakenfutures": {
"pair": "BTC/USD:USD",
"stake_currency": "USD",
"hasQuoteVolume": False,
"skip_ws_tests": True,
"timeframe": "1h",
"futures": True,
"futures_only": True,
"candle_count": 2000,
"futures_pair": "BTC/USD:USD",
"hasQuoteVolumeFutures": False,
"leverage_tiers_public": True,
"sample_order_futures": [
{
# Regular market order
"exchange_response": {
"uid": "a11a8dc2-0440-4fe1-5212-1bx15c8f1c8e",
"accountUid": "cabdb242-5111-4dac-bac-76f33395d76d",
"tradeable": "PF_XBTUSD",
"direction": "Sell",
"quantity": "0",
"filled": "0.0004",
"timestamp": 1771354195241,
"limitPrice": "67164.00",
"orderType": "IoC",
"clientId": "",
"reduceOnly": False,
"lastUpdateTimestamp": 1771354195241,
"regulatoryExternalUid": "ae198dd6-6be0-4014-8af-ebd472190648",
"status": "closed",
},
"pair": "BTC/USD:USD",
"expected": {
"symbol": "BTC/USD:USD",
"id": "a11a8dc2-0440-4fe1-5212-1bx15c8f1c8e",
"timestamp": 1771354195241,
"datetime": "2026-02-17T18:49:55.241Z",
"price": None,
# Average should be None (it's not correct for market orders)
"average": None,
"status": "closed",
"type": "market",
"amount": 0.0004,
"side": "sell",
"triggerPrice": None,
"stopPrice": None,
"stopLossPrice": None,
},
},
{
# Trigger order
"exchange_response": {
"order": {
"type": "TRIGGER_ORDER",
"orderId": "a11a8ff3-17f3-5112-8caa-9cbbacfa1c8e",
"cliOrdId": None,
"symbol": "PF_XBTUSD",
"side": "buy",
"quantity": 0.0004,
"limitPrice": 71712,
"reduceOnly": True,
"timestamp": "2026-02-17T16:26:02.918Z",
"lastUpdateTimestamp": "2026-02-17T16:26:02.918Z",
"priceTriggerOptions": {
"triggerPrice": 71641,
"triggerSignal": "LAST_PRICE",
"triggerSide": "TRIGGER_ABOVE",
"limitPriceOffsetValue": None,
"limitPriceOffsetUnit": None,
},
},
"status": "TRIGGER_PLACED",
"updateReason": None,
"error": None,
},
"pair": "BTC/USD:USD",
"expected": {
"symbol": "BTC/USD:USD",
"id": "a11a8ff3-17f3-5112-8caa-9cbbacfa1c8e",
"timestamp": 1771345562918,
"datetime": "2026-02-17T16:26:02.918Z",
# TODO: re-verify this ...
# "price": 71712.0,
"price": None,
"status": "open",
"amount": 0.0004,
"side": "buy",
"triggerPrice": 71641.0,
"stopPrice": 71641.0,
# krakenfutures uses stopPrice - so this is fine.
"stopLossPrice": None,
},
},
{
# Canceled order
"exchange_response": {
"order": {
"type": "ORDER",
"orderId": "a159faef-6a0f-4651-bb78-xxfa4c71ac7e",
"cliOrdId": None,
"symbol": "PF_XBTUSD",
"side": "buy",
"quantity": 0.0022,
"filled": 0,
"limitPrice": 68000,
"reduceOnly": False,
"timestamp": "2026-03-21T07:32:21.555Z",
"lastUpdateTimestamp": "2026-03-21T07:32:21.555Z",
},
"status": "CANCELLED",
"updateReason": "CANCELLED_BY_USER",
"error": None,
},
"pair": "BTC/USD:USD",
"expected": {
"symbol": "BTC/USD:USD",
"id": "a159faef-6a0f-4651-bb78-xxfa4c71ac7e",
"timestamp": 1774078341555,
"datetime": "2026-03-21T07:32:21.555Z",
"price": None,
"status": "canceled",
# TODO: filled should be 0, not None.
"filled": None,
},
},
],
},
}
EXCHANGES_FUTURES = [exch for exch, params in EXCHANGES.items() if params.get("futures")]
EXCHANGES_SPOT = [exch for exch, params in EXCHANGES.items() if not params.get("futures_only")]
@pytest.fixture(scope="class")
@@ -584,11 +720,12 @@ def set_test_proxy(config: Config, use_proxy: bool) -> Config:
return config
def get_exchange(exchange_name, exchange_conf):
def get_exchange(exchange_name, exchange_conf, class_mocker):
exchange_params = EXCHANGES[exchange_name]
exchange_conf = set_test_proxy(exchange_conf, exchange_params.get("use_ci_proxy", False))
exchange_conf["exchange"]["name"] = exchange_name
exchange_conf["stake_currency"] = exchange_params["stake_currency"]
class_mocker.patch(f"{EXMS}.ft_additional_exchange_init")
exchange = ExchangeResolver.load_exchange(
exchange_conf, validate=True, load_leverage_tiers=True
)
@@ -601,25 +738,28 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker):
if exchange_params.get("futures") is not True:
pytest.skip(f"Exchange {exchange_name} does not support futures.")
else:
exchange_conf = deepcopy(exchange_conf)
exchange_conf = set_test_proxy(exchange_conf, exchange_params.get("use_ci_proxy", False))
exchange_conf["trading_mode"] = "futures"
exchange_conf["margin_mode"] = "isolated"
exchange_conf = deepcopy(exchange_conf)
exchange_conf = set_test_proxy(exchange_conf, exchange_params.get("use_ci_proxy", False))
exchange_conf["exchange"]["name"] = exchange_name
exchange_conf["stake_currency"] = exchange_params["stake_currency"]
exchange_conf["trading_mode"] = "futures"
exchange_conf["margin_mode"] = "isolated"
class_mocker.patch("freqtrade.exchange.binance.Binance.fill_leverage_tiers")
class_mocker.patch(f"{EXMS}.fetch_trading_fees")
class_mocker.patch(f"{EXMS}.ft_additional_exchange_init")
class_mocker.patch(f"{EXMS}.load_cached_leverage_tiers", return_value=None)
class_mocker.patch(f"{EXMS}.cache_leverage_tiers")
return get_exchange(exchange_name, exchange_conf)
@pytest.fixture(params=EXCHANGES, scope="class")
def exchange(request, exchange_conf, class_mocker):
class_mocker.patch("freqtrade.exchange.binance.Binance.fill_leverage_tiers")
class_mocker.patch(f"{EXMS}.fetch_trading_fees")
class_mocker.patch(f"{EXMS}.ft_additional_exchange_init")
exchange, name, exchange_params = get_exchange(request.param, exchange_conf)
class_mocker.patch(f"{EXMS}.load_cached_leverage_tiers", return_value=None)
class_mocker.patch(f"{EXMS}.cache_leverage_tiers")
exchange = ExchangeResolver.load_exchange(
exchange_conf, validate=True, load_leverage_tiers=True
)
return exchange, exchange_name, exchange_params
@pytest.fixture(params=EXCHANGES_SPOT, scope="class")
def exchange(request, exchange_conf, class_mocker):
exchange, name, exchange_params = get_exchange(request.param, exchange_conf, class_mocker)
yield exchange, name, exchange_params
exchange.close()
@@ -640,13 +780,12 @@ def exchange_mode(request):
@pytest.fixture(params=EXCHANGES, scope="class")
def exchange_ws(request, exchange_conf, exchange_mode, class_mocker):
class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init")
exchange_conf["exchange"]["enable_ws"] = True
exchange_param = EXCHANGES[request.param]
if exchange_param.get("skip_ws_tests"):
pytest.skip(f"{request.param} does not support websocket tests.")
if exchange_mode == "spot":
exchange, name, _ = get_exchange(request.param, exchange_conf)
exchange, name, _ = get_exchange(request.param, exchange_conf, class_mocker)
pair = exchange_param["pair"]
elif exchange_param.get("futures"):
exchange, name, _ = get_futures_exchange(
+32 -17
View File
@@ -90,23 +90,38 @@ class TestCCXTExchange:
expected = order["expected"]
assert isinstance(po["id"], str)
assert po["id"] is not None
if len(exchange_response.keys()) < 5:
# Kucoin case
assert po["status"] is None
continue
assert po["timestamp"] == expected["timestamp"]
assert isinstance(po["datetime"], str)
assert isinstance(po["timestamp"], int)
assert isinstance(po["price"], float)
assert po["price"] == expected["price"]
if po["status"] == "closed":
# Filled orders should have average assigned.
assert isinstance(po["average"], float)
assert po["average"] == 15.5
assert po["symbol"] == pair
assert isinstance(po["amount"], float)
assert po["amount"] == expected["amount"]
assert isinstance(po["status"], str)
# Generic comparison which works for all fields
for key, value in expected.items():
assert key in po, f"Expected key {key} not found in parsed order"
assert po[key] == value, f"Expected {key} to be {value}, got {po[key]}"
assert isinstance(po[key], type(value)), (
f"Expected {key} to be of type {type(value)}, got {type(po[key])}"
)
else:
pytest.skip(f"No sample order available for exchange {exchangename}")
def test_ccxt_order_parse_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exch, exchangename, exchange_params = exchange_futures
if orders := exchange_params.get("sample_order_futures"):
for order in orders:
pair = order["pair"]
exchange_response: dict = order["exchange_response"]
market = exch._api.markets[pair]
po = exch._api.parse_order(exchange_response, market)
expected = order["expected"]
assert isinstance(po["id"], str)
assert po["id"] is not None
# Generic comparison which works for all fields
for key, value in expected.items():
assert key in po, f"Expected key {key} not found in parsed order"
assert po[key] == value, f"Expected {key} to be {value}, got {po[key]}"
assert isinstance(po[key], type(value)), (
f"Expected {key} to be of type {type(value)}, got {type(po[key])}"
)
else:
pytest.skip(f"No sample order available for exchange {exchangename}")
+3 -1
View File
@@ -1888,7 +1888,9 @@ def test_backtest_multi_pair_long_short_switch(
if use_detail:
default_conf_usdt["timeframe_detail"] = "1m"
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
mocker.patch(
"freqtrade.optimize.backtesting.price_to_precision", lambda price, *args, **kwargs: price
)
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"))
mocker.patch(f"{EXMS}.get_fee", fee)
@@ -72,8 +72,8 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
"initial_stop_loss_ratio": [-0.1, -0.1],
"stop_loss_abs": [0.0940005, 0.092722365],
"stop_loss_ratio": [-0.1, -0.1],
"min_rate": [0.10370188, 0.10300000000000001],
"max_rate": [0.10481985, 0.10388887000000001],
"min_rate": [0.10370188, 0.103000000],
"max_rate": [0.10481985, 0.103888870],
"is_open": [False, False],
"enter_tag": ["", ""],
"leverage": [1.0, 1.0],
+201
View File
@@ -2584,6 +2584,207 @@ def test_MarketCapPairList_exceptions(mocker, default_conf_usdt, caplog):
PairListManager(exchange, default_conf_usdt)
@pytest.mark.parametrize(
"pairlists,trade_mode,result",
[
(
[
# Spot pairs that exist on both markets
{"method": "StaticPairList", "allow_inactive": True},
{"method": "CrossMarketPairList", "pairs_exist_on": "both_markets"},
],
"spot",
["ETH/USDT"],
),
(
[
# Spot pairs that exist only on spot market
{"method": "StaticPairList", "allow_inactive": True},
{"method": "CrossMarketPairList", "pairs_exist_on": "current_market_only"},
],
"spot",
["LTC/USDT", "XRP/USDT", "NEO/USDT", "TKN/USDT", "BTC/USDT"],
),
(
[
# Futures pairs that exist on both markets
{"method": "StaticPairList", "allow_inactive": True},
{"method": "CrossMarketPairList", "pairs_exist_on": "both_markets"},
],
"futures",
["ETH/USDT:USDT"],
),
(
[
# Futures pairs that exist only on futures market
{"method": "StaticPairList", "allow_inactive": True},
{"method": "CrossMarketPairList", "pairs_exist_on": "current_market_only"},
],
"futures",
["ADA/USDT:USDT"],
),
(
[
# CrossMarketPairList as generator, spot market, pairs that exist on both markets
{"method": "CrossMarketPairList", "pairs_exist_on": "both_markets"},
],
"spot",
["ETH/USDT"],
),
(
[
# CrossMarketPairList as generator, spot pairs that exist only on spot market
{"method": "CrossMarketPairList", "pairs_exist_on": "current_market_only"},
],
"spot",
["BTC/USDT", "XRP/USDT", "NEO/USDT", "TKN/USDT"],
),
(
[
# CrossMarketPairList as generator, futures pairs that exist on both markets
{"method": "CrossMarketPairList", "pairs_exist_on": "both_markets"},
],
"futures",
["ETH/USDT:USDT"],
),
(
[
# CrossMarketPairList as generator, futures pairs that exist only on futures market
{"method": "CrossMarketPairList", "pairs_exist_on": "current_market_only"},
],
"futures",
["ADA/USDT:USDT"],
),
],
)
def test_CrossMarketPairlist_filter(
mocker, default_conf_usdt, trade_mode, markets, pairlists, result
):
default_conf_usdt["trading_mode"] = trade_mode
if trade_mode == "spot":
default_conf_usdt["exchange"]["pair_whitelist"].extend(["BTC/USDT", "ETC/USDT", "ADA/USDT"])
else:
default_conf_usdt["exchange"]["pair_whitelist"] = [
"BTC/USDT:USDT",
"ETH/USDT:USDT",
"ETC/USDT:USDT",
"ADA/USDT:USDT",
]
default_conf_usdt["pairlists"] = pairlists
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
)
exchange = get_patched_exchange(mocker, default_conf_usdt)
pm = PairListManager(exchange, default_conf_usdt)
pm.refresh_pairlist()
assert pm.whitelist == result
def test_CrossMarketPairlist_gen_pairlist_uses_cache(mocker, default_conf_usdt, markets):
default_conf_usdt["trading_mode"] = "spot"
default_conf_usdt["pairlists"] = [
{"method": "CrossMarketPairList", "pairs_exist_on": "both_markets"}
]
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
)
exchange = get_patched_exchange(mocker, default_conf_usdt)
pm = PairListManager(exchange, default_conf_usdt)
pl = pm._pairlist_handlers[0]
pl._pair_cache["pairlist"] = ["ETH/USDT", "ADA/USDT"]
pl._exchange.get_markets = MagicMock(
side_effect=AssertionError("get_markets should not be called")
)
result = pl.gen_pairlist({})
assert result == ["ETH/USDT", "ADA/USDT"]
# Make sure the returned list is a copy, not the cached one
assert result is not pl._pair_cache["pairlist"]
result.append("BTC/USDT")
# Make sure the cache is not modified
assert pl._pair_cache["pairlist"] == ["ETH/USDT", "ADA/USDT"]
def test_CrossMarketPairList_breaks_prefix_loop_on_match(mocker, default_conf_usdt, markets):
default_conf_usdt["trading_mode"] = "spot"
default_conf_usdt["pairlists"] = [
{"method": "CrossMarketPairList", "pairs_exist_on": "both_markets"}
]
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
)
exchange = get_patched_exchange(mocker, default_conf_usdt)
pm = PairListManager(exchange, default_conf_usdt)
pl = pm._pairlist_handlers[0]
# Force base lookup path
mocker.patch.object(pl, "get_base_list", return_value=["1000PEPE"])
mocker.patch.object(pl._exchange, "get_pair_base_currency", return_value="PEPE")
def prefix_generator():
yield "1000" # first prefix => match via "1000PEPE"
raise AssertionError("Prefix loop did not break after match")
mocker.patch(
"freqtrade.plugins.pairlist.CrossMarketPairList.PairPrefixes",
new=prefix_generator(),
)
result = pl.filter_pairlist(["PEPE/USDT"], {})
assert result == ["PEPE/USDT"]
def test_CrossMarketPairList_breaks_prefix_loop_on_delayed_match(
mocker, default_conf_usdt, markets
):
default_conf_usdt["trading_mode"] = "spot"
default_conf_usdt["pairlists"] = [
{"method": "CrossMarketPairList", "pairs_exist_on": "both_markets"}
]
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
)
exchange = get_patched_exchange(mocker, default_conf_usdt)
pm = PairListManager(exchange, default_conf_usdt)
pl = pm._pairlist_handlers[0]
# Force second matching path: base startswith prefix and removeprefix() matches bases.
mocker.patch.object(pl, "get_base_list", return_value=["PEPE"])
mocker.patch.object(pl._exchange, "get_pair_base_currency", return_value="1000PEPE")
def prefix_generator():
yield "X" # no match, loop should continue
yield "1000" # second path should match via removeprefix -> PEPE and break
raise AssertionError("Prefix loop did not break after delayed match")
mocker.patch(
"freqtrade.plugins.pairlist.CrossMarketPairList.PairPrefixes",
new=prefix_generator(),
)
result = pl.filter_pairlist(["1000PEPE/USDT"], {})
assert result == ["1000PEPE/USDT"]
@pytest.mark.parametrize(
"pairlists,expected_error,expected_warning",
[
+57 -19
View File
@@ -137,25 +137,6 @@ def test_remote_pairlist_init_no_pairlist_url(mocker, rpl_config):
get_patched_freqtradebot(mocker, rpl_config)
def test_remote_pairlist_init_no_number_assets(mocker, rpl_config):
rpl_config["pairlists"] = [
{
"method": "RemotePairList",
"pairlist_url": "http://example.com/pairlist",
"keep_pairlist_on_failure": True,
}
]
get_patched_exchange(mocker, rpl_config)
with pytest.raises(
OperationalException,
match=r"`number_assets` not specified. "
'Please check your configuration for "pairlist.config.number_assets"',
):
get_patched_freqtradebot(mocker, rpl_config)
def test_fetch_pairlist_mock_response_valid(mocker, rpl_config):
rpl_config["pairlists"] = [
{
@@ -341,3 +322,60 @@ def test_remote_pairlist_whitelist(mocker, rpl_config, processing_mode, markets,
whitelist = remote_pairlist.filter_pairlist(rpl_config["exchange"]["pair_whitelist"], {})
assert whitelist == (["XRP/USDT"] if processing_mode == "filter" else ["ETH/USDT", "XRP/USDT"])
@pytest.mark.parametrize(
"number_assets, result",
[
(1, ["ETH/USDT"]),
(2, ["ETH/USDT", "XRP/USDT"]),
(500, ["ETH/USDT", "XRP/USDT"]),
(None, ["ETH/USDT", "XRP/USDT"]),
],
)
def test_remote_pairlist_whitelist_number_assets(
mocker, rpl_config, number_assets, result, markets, tickers
):
mock_response = MagicMock()
mock_response.json.return_value = {
"pairs": ["ETH/USDT", "XRP/USDT", "TKN/USDT"],
"refresh_period": 60,
}
mock_response.headers = {"content-type": "application/json"}
rpl_config["pairlists"] = [
{
"method": "RemotePairList",
"mode": "whitelist",
"pairlist_url": "http://example.com/pairlist",
"number_assets": number_assets,
},
]
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers,
)
mocker.patch(
"freqtrade.plugins.pairlist.RemotePairList.requests.get", return_value=mock_response
)
exchange = get_patched_exchange(mocker, rpl_config)
pairlistmanager = PairListManager(exchange, rpl_config)
remote_pairlist = RemotePairList(
exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0
)
pairs, _ = remote_pairlist.fetch_pairlist()
assert pairs == ["ETH/USDT", "XRP/USDT", "TKN/USDT"]
whitelist = remote_pairlist.filter_pairlist(rpl_config["exchange"]["pair_whitelist"], {})
assert whitelist == result
+9
View File
@@ -2967,6 +2967,15 @@ def test_sysinfo(botclient):
result = rc.json()
assert "cpu_pct" in result
assert "ram_pct" in result
assert "cpu_load" in result
assert "cpu_count" in result
assert "cpu_load_avg" in result
assert "1m" in result["cpu_load_avg"]
assert "5m" in result["cpu_load_avg"]
assert "15m" in result["cpu_load_avg"]
assert isinstance(result["cpu_load"], list)
assert isinstance(result["cpu_load"][0], dict)
def test_api_backtesting(botclient, mocker, fee, caplog, tmp_path):
+2 -2
View File
@@ -1047,7 +1047,7 @@ def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn):
# Fixed in 2.2.x
getattr(_STRATEGY, function)(df, {"pair": "ETH/BTC"})
else:
assert len(recwarn) == 0, f"warnings: {', '.join(recwarn.list)}"
assert len(recwarn) == 0, f"warnings: {', '.join(str(w) for w in recwarn.list)}"
getattr(_STRATEGY, function)(df, {"pair": "ETH/BTC"})
@@ -1055,4 +1055,4 @@ def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn):
def test_pandas_warning_through_analyze_pair(ohlcv_history, mocker, recwarn):
mocker.patch.object(_STRATEGY.dp, "ohlcv", return_value=ohlcv_history)
_STRATEGY.analyze_pair("ETH/BTC")
assert len(recwarn) == 0, f"warnings: {', '.join(recwarn.list)}"
assert len(recwarn) == 0, f"warnings: {', '.join(str(w) for w in recwarn.list)}"
+5 -3
View File
@@ -23,9 +23,11 @@ def test_pip_audit_no_vulnerabilities():
Run pip-audit to check for known security vulnerabilities.
This test will fail if any vulnerabilities are detected in the installed packages.
Note: Document ignores here if vulnerabilities are acceptable.
pygments: CVE-2026-4539 - https://github.com/pygments/pygments/issues/3065
not considered a security vulnerability by pygments.
Note: CVE-2025-53000 (nbconvert Windows vulnerability) is ignored as it only affects
Windows platforms and is a known acceptable risk for this project.
"""
# Get the project root directory
project_root = Path(__file__).parent.parent
@@ -36,7 +38,7 @@ def test_pip_audit_no_vulnerabilities():
# "--format=json",
"--progress-spinner=off",
"--ignore-vuln",
"CVE-2025-53000",
"CVE-2026-4539",
"--skip-editable",
]
Binary file not shown.
+9 -6
View File
@@ -78,12 +78,15 @@ def test_shorten_date() -> None:
def test_dt_humanize() -> None:
assert dt_humanize_delta(dt_now()) == "now"
assert dt_humanize_delta(dt_now() - timedelta(minutes=50)) == "50 minutes ago"
assert dt_humanize_delta(dt_now() - timedelta(hours=16)) == "16 hours ago"
assert dt_humanize_delta(dt_now() - timedelta(hours=16, minutes=30)) == "16 hours ago"
assert dt_humanize_delta(dt_now() - timedelta(days=16, hours=10, minutes=25)) == "a month ago"
assert dt_humanize_delta(dt_now() - timedelta(minutes=50)) == "50 minutes ago"
with time_machine.travel("2026-03-25 10:01:00 +00:00", tick=False):
assert dt_humanize_delta(dt_now()) == "now"
assert dt_humanize_delta(dt_now() - timedelta(minutes=50)) == "50 minutes ago"
assert dt_humanize_delta(dt_now() - timedelta(hours=16)) == "16 hours ago"
assert dt_humanize_delta(dt_now() - timedelta(hours=16, minutes=30)) == "16 hours ago"
assert (
dt_humanize_delta(dt_now() - timedelta(days=16, hours=10, minutes=25)) == "a month ago"
)
assert dt_humanize_delta(dt_now() - timedelta(minutes=50)) == "50 minutes ago"
def test_format_ms_time() -> None: