Compare commits

...

299 Commits

Author SHA1 Message Date
Matthias 9f00a1d0d2 Merge pull request #12673 from freqtrade/new_release
New release 2025.12
2025-12-30 08:19:19 +01:00
Matthias 9a37d7bfbb chore: bump version to 2025.12 2025-12-29 13:19:31 +01:00
Matthias c9c08906e5 Merge branch 'stable' into new_release 2025-12-29 13:17:12 +01:00
Matthias 5c02ce934d test: remove outdated TODO 2025-12-27 09:53:46 +01:00
Matthias 260512d678 chore: remove outdated todo, replaced with comment 2025-12-27 09:53:46 +01:00
Matthias 8b1b16cb08 test: cleanup unnecessary test fixture 2025-12-27 09:53:46 +01:00
Matthias cacd52db1c chore: remove unnecessary todo 2025-12-27 09:53:42 +01:00
Matthias 4e809947c7 chore: improve create_command_partials runtime by not calling subprocess several times 2025-12-26 20:02:24 +01:00
Matthias 89c4aa23f3 docs: update docs with new --eps helpstring 2025-12-26 14:03:30 +01:00
Matthias 1bd043a912 chore: improve cli docstring to clarify "--eps" 2025-12-26 13:37:52 +01:00
Matthias 3ce23f99de Merge pull request #12655 from freqtrade/dependabot/pip/develop/time-machine-3.2.0
chore(deps-dev): bump time-machine from 3.1.0 to 3.2.0
2025-12-25 10:00:00 +01:00
Matthias c5fa5c5c69 chore: be more specific in dependabot schedule 2025-12-25 08:52:43 +01:00
Matthias 21d0b60168 Merge pull request #12660 from freqtrade/dependabot/pip/develop/cachetools-6.2.4
chore(deps): bump cachetools from 6.2.3 to 6.2.4
2025-12-25 08:20:22 +01:00
Matthias ba3925cdcd Merge pull request #12657 from freqtrade/dependabot/pip/develop/ccxt-4.5.29
chore(deps): bump ccxt from 4.5.28 to 4.5.29
2025-12-25 08:20:02 +01:00
Matthias 9cf6d47a43 Merge pull request #12656 from freqtrade/dependabot/github_actions/develop/docker/setup-buildx-action-3.12.0
chore(deps): bump docker/setup-buildx-action from 3.11.1 to 3.12.0
2025-12-25 08:19:29 +01:00
Matthias e01993b9c1 Merge pull request #12659 from freqtrade/dependabot/pip/develop/pre-commit-4.5.1
chore(deps-dev): bump pre-commit from 4.5.0 to 4.5.1
2025-12-25 08:19:15 +01:00
Matthias c354a76ae2 Merge pull request #12658 from freqtrade/dependabot/pip/develop/fastapi-0.125.0
chore(deps): bump fastapi from 0.124.4 to 0.125.0
2025-12-25 08:18:56 +01:00
Matthias f296213088 Merge pull request #12661 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-12-25 08:18:24 +01:00
Freqtrade Bot d80dbc1644 chore: update pre-commit hooks 2025-12-25 03:32:57 +00:00
dependabot[bot] 91ee8652ef chore(deps): bump cachetools from 6.2.3 to 6.2.4
Bumps [cachetools](https://github.com/tkem/cachetools) from 6.2.3 to 6.2.4.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v6.2.3...v6.2.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-25 03:02:10 +00:00
dependabot[bot] 75dfc95221 chore(deps-dev): bump pre-commit from 4.5.0 to 4.5.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.5.0...v4.5.1)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.5.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-25 03:02:07 +00:00
dependabot[bot] 05fe35ce57 chore(deps): bump fastapi from 0.124.4 to 0.125.0
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.124.4 to 0.125.0.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.124.4...0.125.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-25 03:02:01 +00:00
dependabot[bot] 95e36013db chore(deps): bump ccxt from 4.5.28 to 4.5.29
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.28 to 4.5.29.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.28...v4.5.29)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-25 03:01:56 +00:00
dependabot[bot] c5c96d9610 chore(deps): bump docker/setup-buildx-action from 3.11.1 to 3.12.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.11.1 to 3.12.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/e468171a9de216ec08956ac3ada2f0791b6bd435...8d2750c68a42422c14e847fe6c8ac0403b4cbd6f)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-25 03:01:43 +00:00
dependabot[bot] ad9a7279be chore(deps-dev): bump time-machine from 3.1.0 to 3.2.0
Bumps [time-machine](https://github.com/adamchainz/time-machine) from 3.1.0 to 3.2.0.
- [Changelog](https://github.com/adamchainz/time-machine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/adamchainz/time-machine/compare/3.1.0...3.2.0)

---
updated-dependencies:
- dependency-name: time-machine
  dependency-version: 3.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-25 03:01:41 +00:00
Matthias dafb2fda7b Merge pull request #12505 from freqtrade/maint/python_3.14
CI against python 3.14
2025-12-24 15:36:55 +01:00
Matthias 04fea4ac29 Merge branch 'develop' into maint/python_3.14 2025-12-24 13:30:22 +01:00
Matthias 896250fe88 chore: add debugging output to debug psgallery error 2025-12-24 13:29:01 +01:00
Matthias 8485abc9b8 Merge pull request #12652 from freqtrade/maint/pip-audit
Add pip-audit
2025-12-24 13:17:43 +01:00
Matthias 2013a2edda chore: add recognition to the original tests author 2025-12-24 12:04:30 +01:00
Matthias 11eb0c0802 fix: re-add latest tag for docker hub - which aligns to the :devleop tag 2025-12-24 11:58:44 +01:00
Matthias 78709a9d76 test: properly wrap line 2025-12-24 11:39:00 +01:00
Matthias f55a90d98c chore: add pip-audit as dependency and test 2025-12-24 11:35:25 +01:00
Matthias 4b3ff02d49 chore: Add pip-audit action 2025-12-24 11:06:49 +01:00
Matthias ea52deb98e Merge pull request #12648 from hippocritical/develop
adjusted the error message of "more than 5 queries needed for exchange"
2025-12-23 13:22:58 +01:00
hippocritical 7b33242aa0 fixed tests according to failed tests on github 2025-12-23 10:57:47 +01:00
Matthias 46ee092119 Merge pull request #12638 from freqtrade/dependabot/pip/develop/scikit-learn-1.8.0
chore(deps): bump scikit-learn from 1.7.2 to 1.8.0
2025-12-23 07:05:37 +01:00
Matthias 931cd2e7ba chore: bump Dockerfile to latest python release 2025-12-23 07:01:50 +01:00
Matthias b6899264a8 chore: update classifiers to 3.14 2025-12-23 06:59:50 +01:00
dependabot[bot] ca47882fa9 chore(deps): bump scikit-learn from 1.7.2 to 1.8.0
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.7.2 to 1.8.0.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.7.2...1.8.0)

---
updated-dependencies:
- dependency-name: scikit-learn
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 05:42:22 +00:00
Matthias c21ce5ef94 Merge branch 'develop' into maint/python_3.14 2025-12-23 06:38:25 +01:00
Matthias 9baa6eebbe Merge pull request #12649 from freqtrade/maint/remove_catboost
Remove catboost dependency
2025-12-23 06:32:30 +01:00
Matthias 0feec257e3 Merge pull request #12650 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-12-23 06:31:49 +01:00
Freqtrade Bot 802d42cc00 chore: update pre-commit hooks 2025-12-23 03:30:45 +00:00
hippocritical f546146d40 changed the wording in
if required_candle_call_count > 5:
as per matthias suggestion.

adjusted the elif part too, since this would have to be worded similarly.

If a native speaker thinks there is a better wording, be our guest.
2025-12-23 00:44:45 +01:00
Matthias d2c4bd1b50 docs: update formatting of link 2025-12-22 20:00:50 +01:00
Matthias 5e5495e050 docs: Add deprecation notice for Catboost models 2025-12-22 19:56:41 +01:00
Matthias 51f3f0d65a test: Remove catboost tests 2025-12-22 19:53:02 +01:00
hippocritical 0019867da8 didn't make a space in the end ... you can book that under typo ...
added a . at the end of another warning since all other messages do have the same structure.
2025-12-22 12:53:01 +01:00
hippocritical 335e632bbc adjusted the error message of "more than 5 queries needed for exchange" to be more verbose to the user.
Now they don't have to guess what the limit actually is to then work towards, helping to better understand the error message.
2025-12-22 12:45:31 +01:00
Matthias d0ac462dd3 Merge pull request #12647 from freqtrade/dependabot/pip/develop/mypy-1.19.1
chore(deps-dev): bump mypy from 1.19.0 to 1.19.1
2025-12-22 07:54:03 +01:00
dependabot[bot] fc113f7bbf chore(deps-dev): bump mypy from 1.19.0 to 1.19.1
Bumps [mypy](https://github.com/python/mypy) from 1.19.0 to 1.19.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.19.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 06:32:34 +00:00
Matthias 4f6b1b6fd0 Merge pull request #12633 from freqtrade/dependabot/pip/develop/scipy-e8974a44ab
chore(deps-dev): bump scipy-stubs from 1.16.3.2 to 1.16.3.3 in the scipy group
2025-12-22 07:31:21 +01:00
Matthias 006d2305af chore: update samples and comments to use lightgbm / xgboost
instead of catboost
2025-12-22 07:16:24 +01:00
Matthias 5c9f140c5d feat: remove catboost models and dependency 2025-12-22 07:11:11 +01:00
Matthias c6f3b00819 chore: combine updates on actions from the actions/* org 2025-12-22 07:02:01 +01:00
Matthias 1780d2db23 chore: bump scipy-stubs in pre-commit-config 2025-12-22 06:59:35 +01:00
dependabot[bot] 78d602f7b4 chore(deps-dev): bump scipy-stubs in the scipy group
Bumps the scipy group with 1 update: [scipy-stubs](https://github.com/scipy/scipy-stubs).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 05:58:11 +00:00
Matthias 4823f66c4e Merge pull request #12635 from freqtrade/dependabot/pip/develop/sqlalchemy-2.0.45
chore(deps): bump sqlalchemy from 2.0.44 to 2.0.45
2025-12-22 06:56:47 +01:00
Matthias a3c19d10f7 chore: bump sqlalchemy in pre-commit-config 2025-12-22 06:38:34 +01:00
Matthias ebb318a0f0 Merge pull request #12645 from freqtrade/dependabot/pip/develop/ccxt-4.5.28
chore(deps): bump ccxt from 4.5.27 to 4.5.28
2025-12-22 06:37:37 +01:00
Matthias 35d6e6e832 Merge pull request #12643 from freqtrade/dependabot/pip/develop/ruff-0.14.9
chore(deps-dev): bump ruff from 0.14.8 to 0.14.9
2025-12-22 06:37:05 +01:00
Matthias 37bc93e0d3 Merge pull request #12642 from freqtrade/dependabot/github_actions/develop/codecov/codecov-action-5.5.2
chore(deps): bump codecov/codecov-action from 5.5.1 to 5.5.2
2025-12-22 06:36:50 +01:00
Matthias 2cd30b0604 Merge pull request #12636 from freqtrade/dependabot/github_actions/develop/peter-evans/create-pull-request-8.0.0
chore(deps): bump peter-evans/create-pull-request from 7.0.9 to 8.0.0
2025-12-22 06:35:48 +01:00
Matthias 2f30cda9d7 Merge pull request #12640 from freqtrade/dependabot/github_actions/develop/actions/upload-artifact-6
chore(deps): bump actions/upload-artifact from 5 to 6
2025-12-22 06:34:41 +01:00
Matthias 9e1c760222 Merge pull request #12641 from freqtrade/dependabot/pip/develop/urllib3-2.6.2
chore(deps): bump urllib3 from 2.6.0 to 2.6.2
2025-12-22 06:33:53 +01:00
Matthias 53884cae2f Merge pull request #12637 from freqtrade/dependabot/pip/develop/fastapi-0.124.4
chore(deps): bump fastapi from 0.124.0 to 0.124.4
2025-12-22 06:33:35 +01:00
Matthias 1b16b450b7 Merge pull request #12634 from freqtrade/dependabot/pip/develop/cachetools-6.2.3
chore(deps): bump cachetools from 6.2.2 to 6.2.3
2025-12-22 06:32:15 +01:00
Matthias 18ba3241ce Merge pull request #12639 from freqtrade/dependabot/github_actions/develop/actions/download-artifact-7
chore(deps): bump actions/download-artifact from 6 to 7
2025-12-22 06:31:56 +01:00
Matthias 6f8ad595b7 Merge pull request #12646 from freqtrade/dependabot/pip/develop/pymdown-extensions-10.19.1
chore(deps): bump pymdown-extensions from 10.18 to 10.19.1
2025-12-22 06:31:23 +01:00
Matthias 5479f204b5 Merge pull request #12644 from freqtrade/dependabot/github_actions/develop/astral-sh/setup-uv-7.1.6
chore(deps): bump astral-sh/setup-uv from 7.1.4 to 7.1.6
2025-12-22 06:31:09 +01:00
dependabot[bot] c54b94b5a4 chore(deps): bump pymdown-extensions from 10.18 to 10.19.1
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.18 to 10.19.1.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.18...10.19.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:02:39 +00:00
dependabot[bot] 5ed590d862 chore(deps): bump ccxt from 4.5.27 to 4.5.28
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.27 to 4.5.28.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.27...v4.5.28)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:02:35 +00:00
dependabot[bot] 3f5ed399b5 chore(deps): bump astral-sh/setup-uv from 7.1.4 to 7.1.6
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.4 to 7.1.6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/1e862dfacbd1d6d858c55d9b792c756523627244...681c641aba71e4a1c380be3ab5e12ad51f415867)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:02:26 +00:00
dependabot[bot] a9a686c64e chore(deps-dev): bump ruff from 0.14.8 to 0.14.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.8 to 0.14.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.8...0.14.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:02:24 +00:00
dependabot[bot] 978ca8f852 chore(deps): bump codecov/codecov-action from 5.5.1 to 5.5.2
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.1 to 5.5.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/5a1091511ad55cbe89839c7260b706298ca349f7...671740ac38dd9b0130fbe1cec585b89eea48d3de)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:02:16 +00:00
dependabot[bot] ea30f906f9 chore(deps): bump urllib3 from 2.6.0 to 2.6.2
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.0 to 2.6.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.0...2.6.2)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:02:10 +00:00
dependabot[bot] 811112f0ce chore(deps): bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:02:08 +00:00
dependabot[bot] 5ebffb27aa chore(deps): bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:02:03 +00:00
dependabot[bot] 0cb676a677 chore(deps): bump fastapi from 0.124.0 to 0.124.4
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.124.0 to 0.124.4.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.124.0...0.124.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:01:57 +00:00
dependabot[bot] 6a6dce323d chore(deps): bump peter-evans/create-pull-request from 7.0.9 to 8.0.0
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.9 to 8.0.0.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/84ae59a2cdc2258d6fa0732dd66352dddae2a412...98357b18bf14b5342f975ff684046ec3b2a07725)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 8.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:01:51 +00:00
dependabot[bot] 0d62fab367 chore(deps): bump sqlalchemy from 2.0.44 to 2.0.45
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.44 to 2.0.45.
- [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.45
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:01:42 +00:00
dependabot[bot] a1b4a4dbfb chore(deps): bump cachetools from 6.2.2 to 6.2.3
Bumps [cachetools](https://github.com/tkem/cachetools) from 6.2.2 to 6.2.3.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v6.2.2...v6.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 03:01:32 +00:00
Matthias 092da6aee5 chore(ci): Try to cleanup docker build host 2025-12-21 13:43:16 +01:00
Matthias 13c63c0bf5 fix: exception when backtesting in webserver mode
Running a futures backtest more than once without cache caused
the process to crash due to detail data not being loaded.
2025-12-21 13:31:10 +01:00
Matthias aa38a36b56 docs: add override_exchange_check to documentation 2025-12-21 12:58:13 +01:00
Matthias e9847d9549 Merge pull request #12627 from freqtrade/unify/algo_stop_orders
refactor stoploss methods for exchanges with algo orders
2025-12-19 06:27:12 +01:00
Matthias 0beb76ce48 fix: function signature mismatch 2025-12-18 07:11:45 +01:00
Matthias 21d723b35d chore(ci): visualize disk sizes for docker build 2025-12-18 07:06:34 +01:00
Matthias f002ce67b1 chore: bump version to 2025.11.2 2025-12-18 07:02:17 +01:00
Matthias 5fdc8acbe9 test: switch to BTC/USDC in tests for now 2025-12-18 07:02:17 +01:00
Matthias 1811f9581f fix: allow set-leverage failures on followup orders 2025-12-18 06:56:07 +01:00
Matthias 66235f3198 fix: improve binance stoploss "triggered" behavior 2025-12-18 06:55:47 +01:00
Matthias 6c8b8f642a Merge pull request #12629 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-12-18 06:29:05 +01:00
Freqtrade Bot 6905b8059d chore: update pre-commit hooks 2025-12-18 03:28:09 +00:00
Matthias 1143aba671 test: simplify test call 2025-12-17 07:18:19 +01:00
Matthias 278bd0e97d chore: improve variable naming 2025-12-17 07:18:19 +01:00
Matthias d5b296c75c test: fix gate stoploss test 2025-12-17 07:18:19 +01:00
Matthias c63fba2e57 chore: rename flag to better match what it does 2025-12-17 07:18:19 +01:00
Matthias c8d74baaa1 refactor: unify fetch_stoploss_order 2025-12-17 07:18:19 +01:00
Matthias a9ebff3965 Merge pull request #12628 from freqtrade/dependabot/pip/filelock-3.20.1
chore(deps): bump filelock from 3.20.0 to 3.20.1
2025-12-17 06:31:43 +01:00
dependabot[bot] 8859b9009a chore(deps): bump filelock from 3.20.0 to 3.20.1
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.20.0 to 3.20.1.
- [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.20.0...3.20.1)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.20.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 22:56:31 +00:00
Matthias d4aee7e433 test: add autospec=True to just changed tests 2025-12-16 20:48:15 +01:00
Matthias f0908a1043 test: adapt test to new call logic 2025-12-16 20:32:26 +01:00
Matthias cf6bf1b7b5 refactor: reduce code duplication for cancel_stop_orders 2025-12-16 20:32:26 +01:00
Matthias 84e9251fcd test: use object patching instead of direct assignment 2025-12-16 20:32:26 +01:00
Matthias 5800002d42 feat: add stoploss_fetch_requires_stop_param property 2025-12-16 20:32:26 +01:00
Matthias 6a98c19dab fix: improve binance stoploss "triggered" behavior 2025-12-16 20:12:34 +01:00
Matthias 025707d9ad chore: bump joblib to 1.5.3 2025-12-16 17:44:15 +01:00
Matthias 4540e2ceea Merge branch 'develop' into maint/python_3.14 2025-12-16 17:39:55 +01:00
Matthias eb3e8b82f9 Merge pull request #12626 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-12-16 06:28:58 +01:00
Freqtrade Bot 98da54ae82 chore: update pre-commit hooks 2025-12-16 03:30:35 +00:00
Matthias fa5210aa4b fix: function signature mismatch 2025-12-15 20:35:05 +01:00
Matthias 289e53ed5a fix: allow set-leverage failures on followup orders 2025-12-15 20:23:11 +01:00
Matthias 8c62816972 Merge pull request #12619 from freqtrade/dependabot/pip/develop/python-rapidjson-1.23
chore(deps): bump python-rapidjson from 1.22 to 1.23
2025-12-15 07:11:04 +01:00
dependabot[bot] c0d40a03aa chore(deps): bump python-rapidjson from 1.22 to 1.23
Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.22 to 1.23.
- [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.22...v1.23)

---
updated-dependencies:
- dependency-name: python-rapidjson
  dependency-version: '1.23'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 05:50:08 +00:00
Matthias 551831c901 Merge pull request #12620 from freqtrade/dependabot/pip/develop/fastapi-0.124.0
chore(deps): bump fastapi from 0.123.0 to 0.124.0
2025-12-15 06:49:45 +01:00
Matthias 67850fd2a2 Merge pull request #12623 from freqtrade/dependabot/pip/develop/orjson-3.11.5
chore(deps): bump orjson from 3.11.4 to 3.11.5
2025-12-15 06:49:00 +01:00
Matthias 715ef21da0 Merge pull request #12621 from freqtrade/dependabot/github_actions/develop/actions/checkout-6.0.1
chore(deps): bump actions/checkout from 6.0.0 to 6.0.1
2025-12-15 06:43:44 +01:00
Matthias b8defa5d45 Merge pull request #12622 from freqtrade/dependabot/pip/develop/stable-baselines3-2.7.1
chore(deps): bump stable-baselines3 from 2.7.0 to 2.7.1
2025-12-15 06:27:06 +01:00
Matthias 6d3dac828a Merge pull request #12618 from freqtrade/dependabot/pip/develop/pytest-80f5f76e78
chore(deps-dev): bump pytest from 9.0.1 to 9.0.2 in the pytest group
2025-12-15 06:25:26 +01:00
Matthias 968a9a3750 Merge pull request #12625 from freqtrade/dependabot/pip/develop/ruff-0.14.8
chore(deps-dev): bump ruff from 0.14.7 to 0.14.8
2025-12-15 06:24:26 +01:00
Matthias 03b5cdb240 Merge pull request #12624 from freqtrade/dependabot/pip/develop/pymdown-extensions-10.18
chore(deps): bump pymdown-extensions from 10.17.2 to 10.18
2025-12-15 06:24:14 +01:00
dependabot[bot] 7dc0d5d146 chore(deps-dev): bump ruff from 0.14.7 to 0.14.8
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.7 to 0.14.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.7...0.14.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 03:02:48 +00:00
dependabot[bot] c20f511418 chore(deps): bump pymdown-extensions from 10.17.2 to 10.18
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.17.2 to 10.18.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.17.2...10.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 03:02:20 +00:00
dependabot[bot] 484b3ff2f3 chore(deps): bump orjson from 3.11.4 to 3.11.5
Bumps [orjson](https://github.com/ijl/orjson) from 3.11.4 to 3.11.5.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.11.4...3.11.5)

---
updated-dependencies:
- dependency-name: orjson
  dependency-version: 3.11.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 03:02:15 +00:00
dependabot[bot] 5d25adfc42 chore(deps): bump stable-baselines3 from 2.7.0 to 2.7.1
Bumps [stable-baselines3](https://github.com/DLR-RM/stable-baselines3) from 2.7.0 to 2.7.1.
- [Release notes](https://github.com/DLR-RM/stable-baselines3/releases)
- [Commits](https://github.com/DLR-RM/stable-baselines3/compare/v2.7.0...v2.7.1)

---
updated-dependencies:
- dependency-name: stable-baselines3
  dependency-version: 2.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 03:02:07 +00:00
dependabot[bot] 1099fd2525 chore(deps): bump actions/checkout from 6.0.0 to 6.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.0.0...v6.0.1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 03:01:59 +00:00
dependabot[bot] 57d1e24275 chore(deps-dev): bump pytest from 9.0.1 to 9.0.2 in the pytest group
Bumps the pytest group with 1 update: [pytest](https://github.com/pytest-dev/pytest).


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

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: pytest
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 03:01:39 +00:00
Matthias e52b5aef45 Merge pull request #12617 from freqtrade/feat/exit_price
Add price parameter to force-exit API
2025-12-14 19:34:25 +01:00
Matthias a16d2a1ef9 chore: fix typo 2025-12-14 18:29:43 +01:00
Matthias 13c86452e9 refactor: only assign order_type once 2025-12-14 18:29:37 +01:00
Matthias fe18731057 test: slightly improve custom_exit_rate test 2025-12-14 17:58:11 +01:00
Matthias e26529b695 feat: Don't run custom_exit_price callback when exiting with price 2025-12-14 17:58:11 +01:00
Matthias bac6219cc1 feat: add price to force-exit 2025-12-14 17:58:11 +01:00
Matthias 0ed3bdc747 test: add test for force exit API logic 2025-12-14 17:57:52 +01:00
Matthias 8af0631ff0 Merge pull request #12599 from freqtrade/fix/dynamic_funding_fees
Adjust to dynamic funding fees
2025-12-14 17:56:34 +01:00
Matthias 2e3d276304 docs: Add strategy docs to migrate funding fees 2025-12-14 17:13:03 +01:00
Matthias bd5630a104 test: simplify test mock 2025-12-14 16:19:37 +01:00
Matthias b406219515 test: add candle_types test 2025-12-14 15:51:06 +01:00
Matthias 31d3a19836 feat: support candle_type parameter via API download 2025-12-14 13:49:39 +01:00
Matthias 83b372a32d docs: add "Funding fee adjustment" to deprecated docs 2025-12-13 19:30:21 +01:00
Matthias 51e0b204b6 docs: improve download data docs 2025-12-13 18:30:30 +01:00
Matthias 6848f9197e Merge pull request #12613 from freqtrade/fix/binance
Fix binance futures stoploss Order handling
2025-12-13 08:28:21 +01:00
Matthias 6d2c30abca chore: bump ccxt to 4.5.27 2025-12-12 06:54:29 +01:00
Matthias 93bde7dc46 fix: support binance algo orders
closes #12610
2025-12-12 06:52:11 +01:00
Matthias 97e2e0a405 chore: remove hard-pin of pycares 2025-12-11 18:23:58 +01:00
Matthias 12206f028b chore: comment wording improvements 2025-12-11 07:16:36 +01:00
Matthias 4c7944ac77 chore: update comment wording 2025-12-11 07:09:23 +01:00
Matthias d4ced7b416 docs: improve doc wording 2025-12-11 07:05:58 +01:00
Matthias 451eef5c99 test: further test simplifications 2025-12-11 06:49:56 +01:00
Matthias ae8f059de0 Merge pull request #12611 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-12-11 06:49:18 +01:00
Matthias 878bd7cbc7 chore: pin pycares for now 2025-12-11 06:35:27 +01:00
Freqtrade Bot 6d017c9a6c chore: update pre-commit hooks 2025-12-11 03:29:24 +00:00
Matthias 4c3d9b8c70 test: simplify some stoploss test setups 2025-12-10 19:28:30 +01:00
Matthias 23a4260859 chore: simplify okx cancel stoploss method 2025-12-10 17:23:10 +01:00
Matthias d15d08a2d5 test: Improve refresh_backtest test 2025-12-09 20:18:12 +01:00
Matthias 6aeab16ce4 test: improve candle type verification test 2025-12-09 20:13:10 +01:00
Matthias 46538d9a5b fix: verify prog actually exists before using it 2025-12-09 19:34:58 +01:00
Matthias f0f48395c5 chore: update download-data help text 2025-12-09 18:25:54 +01:00
Matthias b3a1442a69 feat: allow varying help texts for different subcommands 2025-12-09 18:25:11 +01:00
Matthias bbafb1dabd fix: deduplicate list before downloading
This avoids duplicate downloads, for example on hyperliquid, which uses "futures" as mark candle type.
2025-12-09 13:54:05 +01:00
Matthias 645a9159e4 chore: hyperliquid doesn't have mark candles
it uses regular futures candles as multiplicator for funding fees.
2025-12-09 13:43:49 +01:00
Matthias 2d3ff2f8ca test: mark-test should use the candle's defined mark price attribute 2025-12-09 13:42:59 +01:00
Matthias 5919736904 Merge pull request #12607 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-12-09 07:23:50 +01:00
Freqtrade Bot 732610e200 chore: update pre-commit hooks 2025-12-09 03:27:07 +00:00
Matthias 309985503d test: fix funding_rate_history online test 2025-12-08 20:17:10 +01:00
Matthias 072ed705fc test: fix funding_fee online tests 2025-12-08 19:21:48 +01:00
Matthias 3689b52309 docs: add Section about very loose stoploss on exchange
closes #12598
2025-12-08 17:21:51 +01:00
Matthias 01fbf31405 chore: don't suggest binance.us supports futures
it doesn't.
2025-12-08 17:15:41 +01:00
Matthias 00f687f3f5 test: test futures data with online exchanges 2025-12-08 17:07:26 +01:00
Matthias 96849fcafe refactor: provide a non-failing check_candle_support method 2025-12-08 17:01:36 +01:00
Matthias 80d5b6e24c test: minor refactor in online tests 2025-12-08 16:23:43 +01:00
Matthias f33fd98c83 test: Add test for candle type verification 2025-12-08 14:43:12 +01:00
Matthias c7636734de feat: validate supported candle types when downloading data 2025-12-08 14:29:16 +01:00
Matthias 994e61fe42 feat: add (commented) validation for fetch_*_ohlcv methods 2025-12-08 13:47:26 +01:00
Matthias 359eba462b feat: add candle_types argument to download-data 2025-12-08 12:45:41 +01:00
Matthias 38e48c0c5e test: update refresh ohlcv data test 2025-12-08 11:55:58 +01:00
Matthias 1e187e0945 Merge pull request #12604 from freqtrade/dependabot/pip/develop/ccxt-4.5.25
chore(deps): bump ccxt from 4.5.24 to 4.5.26
2025-12-08 11:52:15 +01:00
dependabot[bot] e4fc5df1cf chore(deps): bump ccxt from 4.5.24 to 4.5.25
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.24 to 4.5.25.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.24...v4.5.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 09:25:30 +00:00
Matthias cde886b884 chore: use str for safe usage of candle_type 2025-12-08 10:20:00 +01:00
Matthias 9f4e167455 chore: force keyword usage on refresh_backtest_ohlcv 2025-12-08 09:27:48 +01:00
Matthias f5e6504e22 test: add test for funding rate exchange fix 2025-12-08 09:11:49 +01:00
Matthias 0ec1066b34 test: add test for funding_rate fix 2025-12-08 08:48:37 +01:00
Matthias 62d4da3b94 test: add test for get_funding_rate_timeframe 2025-12-08 08:43:50 +01:00
Matthias 253950deb6 Merge pull request #12600 from freqtrade/dependabot/pip/develop/scipy-ea2b5522bf
chore(deps-dev): bump scipy-stubs from 1.16.3.1 to 1.16.3.2 in the scipy group
2025-12-08 08:38:40 +01:00
Matthias ed92d6beb9 Merge pull request #12601 from freqtrade/dependabot/pip/develop/ruff-0.14.7
chore(deps-dev): bump ruff from 0.14.6 to 0.14.7
2025-12-08 08:27:20 +01:00
Matthias ebb362d9fa chore: bump scipy-stubs in pre-commit config 2025-12-08 08:16:30 +01:00
dependabot[bot] f23fad420e chore(deps-dev): bump ruff from 0.14.6 to 0.14.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.6 to 0.14.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.14.6...0.14.7)

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

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

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

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

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

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


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 03:01:42 +00:00
Matthias e6030b7f59 chore: minor adjustments for clarity 2025-12-07 20:01:08 +01:00
Matthias c1c968666e chore: some minor cleanups 2025-12-07 18:19:22 +01:00
Matthias f8d6363d19 test: update further tests 2025-12-07 16:11:47 +01:00
Matthias acc69e0d2e test: fix a couple more tests 2025-12-07 16:02:30 +01:00
Matthias 5110d0bdde test: update a couple of tests for new behavior 2025-12-07 15:12:42 +01:00
Matthias 597cc0592b test: update funding_rate_migration test 2025-12-06 20:14:22 +01:00
Matthias 01b0a8fa42 fix: 1h should be the default for funding/mark candles 2025-12-06 16:15:32 +01:00
Matthias cf6b7a847b fix: bitget's minimal funding fee interval is 1h 2025-12-06 16:01:05 +01:00
Matthias 4897080827 fix: bybit's minimal funding fee interval to 1h 2025-12-06 15:53:27 +01:00
Matthias 3ca8e0fb5c feat: auto-adjust funding rate timeframe in dataprovider 2025-12-06 15:42:40 +01:00
Matthias 40f4ff04c2 feat: auto-fix invalid funding rate timeframe in informative decorator 2025-12-06 14:16:27 +01:00
Matthias 3bd911982f feat: add get_funding_rate_timeframe to dataprovider 2025-12-06 14:13:53 +01:00
Matthias 730383ab18 feat: auto-download correct funding rate timeframe 2025-12-06 14:13:41 +01:00
Matthias b70f10dca6 chore: simplify warning formatting 2025-12-06 14:06:17 +01:00
Matthias 17009ac59f chore: allow non-matching funding timeframe - as timeframe doesn't actually matter any longer. 2025-12-06 13:53:18 +01:00
Matthias 07fbf2b467 feat: support dynamic funding fees in dry/live mode 2025-12-06 13:46:27 +01:00
Matthias 3f0be5e41f fix: floor timestamp to seconds
no candle has more than second precision.
2025-12-06 13:42:16 +01:00
Matthias 2845568f61 feat: limit funding_fee renaming to rename from low to high. 2025-12-05 20:32:56 +01:00
Matthias e3229935f6 Merge pull request #12594 from stremblayiOS/fix-hyperliquid-fetch-positions
Fix IndexError in fetch_positions for Hyperliquid when no pair specified
2025-12-05 20:16:48 +01:00
Matthias b1ee115b77 Merge pull request #12593 from arawrdn/develop
Update README.md
2025-12-05 19:53:33 +01:00
Matthias d6060f04bc Merge pull request #12595 from freqtrade/dependabot/pip/urllib3-2.6.0
chore(deps): bump urllib3 from 2.5.0 to 2.6.0
2025-12-05 19:43:22 +01:00
dependabot[bot] 1ae5310d2f chore(deps): bump urllib3 from 2.5.0 to 2.6.0
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)

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

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

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

## Quick changelog

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

## What's new?

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

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

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

This change does not use AI-generated code.
2025-12-05 18:33:40 +01:00
Matthias 41a82eff21 fix: don't fill up funding fee data Data
Timeframes are arbitrary and may vary between pairs or
time ranges
2025-12-05 18:20:14 +01:00
Matthias d41acc77f7 fix: floor funding-rate to seconds to account for slight time offset 2025-12-05 18:20:14 +01:00
Matthias 271fc6b585 feat: don't fill up missing funding-fees after merge 2025-12-05 18:20:14 +01:00
Matthias 262f4ffa4c test: update bt tests for new timeout behavior 2025-12-05 07:10:17 +01:00
Matthias 338c1c5424 fix: compare timeout with <= instead of <
closes #12590

Backtesting assumes round dates, so a timeout at "candle length" needs
to timeout at the hour - not after the hour.
otherwise the timeout becomes either double (60 instead of 30) -
or longer by one "timeframe detail" (31 instead of 30).
2025-12-05 07:10:04 +01:00
0xward 8ca25b1757 Update README.md
docs: fix minor typo and inconsistency in README disclaimer (Dry-run to dry-run)
2025-12-05 09:50:16 +07:00
Matthias 68e54248fd fix: calculate meaningful price change properly weighting each pair
closes #12588
2025-12-04 21:28:31 +01:00
Matthias e792bafe21 test: update test to ensure rel_mean is reasonable 2025-12-04 21:28:31 +01:00
Matthias fe95581acd test: update rel_mean test to use USDT Pairs
This will allow highlighting a problem pointed out in #12588
2025-12-04 21:28:31 +01:00
Matthias 7f831f8a9c test: update tests for new test pairs 2025-12-04 21:28:31 +01:00
Matthias 3fb7382cc4 test: update test data to include USDT pairs 2025-12-04 21:28:31 +01:00
Matthias 83f70b3c27 Merge pull request #12585 from LMessi2000/fix/okx-price-type-mapping
fix(okx): correct PriceType.MARK and PriceType.INDEX mapping
2025-12-04 07:24:31 +01:00
Matthias e6e14ab67f Merge branch 'develop' into fix/okx-price-type-mapping 2025-12-04 07:02:27 +01:00
Matthias 37792e7d86 Merge pull request #12589 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-12-04 07:01:44 +01:00
Matthias a63ea6adb8 test: switch to BTC/USDC in tests for now 2025-12-04 06:56:14 +01:00
Matthias 06a8f3d097 chore: bump ccxt to 4.5.24 2025-12-04 06:36:13 +01:00
Freqtrade Bot e76b9bb64a chore: update pre-commit hooks 2025-12-04 03:27:18 +00:00
LMessi2000 606dbe5db1 Revert "test(okx): add unit test for stop_price_type_value_mapping"
This reverts commit 81e4e0d301.
2025-12-03 22:17:59 +08:00
LMessi2000 81e4e0d301 test(okx): add unit test for stop_price_type_value_mapping 2025-12-03 20:58:12 +08:00
LMessi2000 a3e97527d1 fix(okx): correct PriceType.MARK and PriceType.INDEX mapping 2025-12-03 20:41:01 +08:00
Matthias 7137461891 feat: improved download-data logging 2025-12-02 21:16:37 +01:00
Matthias 10e7d279f0 test: improve funding rate combine test 2025-12-02 20:39:34 +01:00
Matthias 971cb94ce4 feat: limit returned columns for merged mark/funding calc to used columns 2025-12-02 20:24:52 +01:00
Matthias 19526f1df2 test: update tests to align to improved formatting 2025-12-02 19:12:24 +01:00
Matthias 9c73c8671e feat: improve format_date with fallback value 2025-12-02 19:12:24 +01:00
Matthias 0f5427f4a0 feat: round_value should support None 2025-12-02 19:12:24 +01:00
Matthias a013793b2f feat: improved trade repr format (used for logs) 2025-12-02 19:12:24 +01:00
Matthias 1a77f1b203 Merge pull request #12581 from freqtrade/fix/sloe_handling
Improve stoploss cancellation logic
2025-12-02 16:18:52 +01:00
Matthias faf552837a test: add test for stoploss cancel skips 2025-12-02 06:48:53 +01:00
Matthias 4b2d099258 fix: ensure stoploss on exchange is canceled once the trade closes. 2025-12-02 06:48:53 +01:00
Matthias 4e6ea1d2ba feat: don't cancel stoploss early if it's not necessary 2025-12-02 06:48:53 +01:00
Matthias 197623839d Merge pull request #12579 from freqtrade/dependabot/github_actions/develop/actions/checkout-6.0.0
chore(deps): bump actions/checkout from 5.0.1 to 6.0.0
2025-12-02 06:47:47 +01:00
Matthias 6d8fa76708 Merge pull request #12580 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-12-02 06:40:25 +01:00
Matthias d7eb49d5d0 chore: align usage of actions/checkout version pin 2025-12-02 06:31:32 +01:00
Freqtrade Bot 659509e085 chore: update pre-commit hooks 2025-12-02 03:26:42 +00:00
Matthias 2b729c2527 test: improve integration test to have proper return value 2025-12-01 20:11:03 +01:00
dependabot[bot] 38151423b8 chore(deps): bump actions/checkout from 5.0.1 to 6.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.1 to 6.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v5.0.1...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 18:48:43 +00:00
Matthias 2f990b773d chore: increase dependabot cooldown to 7 days 2025-12-01 19:08:50 +01:00
Matthias 20d253dd41 Merge pull request #12574 from freqtrade/dependabot/pip/develop/pre-commit-4.5.0
chore(deps-dev): bump pre-commit from 4.4.0 to 4.5.0
2025-12-01 08:16:40 +01:00
Matthias 6e2165b5e5 Merge pull request #12572 from freqtrade/dependabot/pip/develop/fastapi-0.122.0
chore(deps): bump fastapi from 0.121.3 to 0.122.0
2025-12-01 07:25:23 +01:00
Matthias 6860904bb9 Merge pull request #12573 from freqtrade/dependabot/pip/develop/ccxt-4.5.22
chore(deps): bump ccxt from 4.5.20 to 4.5.22
2025-12-01 07:16:12 +01:00
Matthias 450dc23374 Merge pull request #12571 from freqtrade/dependabot/pip/develop/pymdown-extensions-10.17.2
chore(deps): bump pymdown-extensions from 10.17.1 to 10.17.2
2025-12-01 07:10:42 +01:00
Matthias d781f09b46 Merge pull request #12566 from freqtrade/dependabot/pip/develop/scipy-e647f76feb
chore(deps-dev): bump scipy-stubs from 1.16.3.0 to 1.16.3.1 in the scipy group
2025-12-01 07:10:13 +01:00
dependabot[bot] dbde8332f7 chore(deps): bump fastapi from 0.121.3 to 0.122.0
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.121.3 to 0.122.0.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.121.3...0.122.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 06:00:27 +00:00
Matthias f5f1785b17 Merge pull request #12576 from freqtrade/dependabot/github_actions/develop/zizmorcore/zizmor-action-0.3.0
chore(deps): bump zizmorcore/zizmor-action from 0.2.0 to 0.3.0
2025-12-01 06:59:33 +01:00
Matthias 7d01928df0 Merge pull request #12567 from freqtrade/dependabot/pip/develop/pydantic-2.12.5
chore(deps): bump pydantic from 2.12.4 to 2.12.5
2025-12-01 06:58:53 +01:00
Matthias 31f712749b Merge pull request #12568 from freqtrade/dependabot/pip/develop/time-machine-3.1.0
chore(deps-dev): bump time-machine from 3.0.0 to 3.1.0
2025-12-01 06:58:36 +01:00
Matthias bcdf83100f Merge pull request #12577 from freqtrade/dependabot/github_actions/develop/actions/checkout-6.0.0
chore(deps): bump actions/checkout from 5.0.0 to 6.0.0
2025-12-01 06:58:02 +01:00
dependabot[bot] dc88ea0ed9 chore(deps-dev): bump pre-commit from 4.4.0 to 4.5.0
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.4.0...v4.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 05:57:42 +00:00
Matthias 543457a1ef Merge pull request #12575 from freqtrade/dependabot/github_actions/develop/peter-evans/create-pull-request-7.0.9
chore(deps): bump peter-evans/create-pull-request from 7.0.8 to 7.0.9
2025-12-01 06:57:05 +01:00
Matthias 5d781d0114 Merge pull request #12570 from freqtrade/dependabot/pip/develop/xgboost-3.1.2
chore(deps): bump xgboost from 3.1.1 to 3.1.2
2025-12-01 06:51:39 +01:00
Matthias 45b1dabe0a chore: bump scipy-stubs in pre-commit config 2025-12-01 06:44:00 +01:00
Matthias ffea373f86 Merge pull request #12578 from freqtrade/dependabot/github_actions/develop/astral-sh/setup-uv-7.1.4
chore(deps): bump astral-sh/setup-uv from 7.1.2 to 7.1.4
2025-12-01 06:41:44 +01:00
Matthias ba650de562 Merge pull request #12569 from freqtrade/dependabot/pip/develop/ruff-0.14.6
chore(deps-dev): bump ruff from 0.14.5 to 0.14.6
2025-12-01 06:41:20 +01:00
dependabot[bot] 39233ff240 chore(deps): bump astral-sh/setup-uv from 7.1.2 to 7.1.4
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.2 to 7.1.4.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41...1e862dfacbd1d6d858c55d9b792c756523627244)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:30:36 +00:00
dependabot[bot] 8950423418 chore(deps): bump actions/checkout from 5.0.0 to 6.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:30:21 +00:00
dependabot[bot] 760a2b9ff7 chore(deps): bump zizmorcore/zizmor-action from 0.2.0 to 0.3.0
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/e673c3917a1aef3c65c972347ed84ccd013ecda4...e639db99335bc9038abc0e066dfcd72e23d26fb4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:30:05 +00:00
dependabot[bot] a13f488b84 chore(deps): bump peter-evans/create-pull-request from 7.0.8 to 7.0.9
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.8 to 7.0.9.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/271a8d0340265f705b14b6d32b9829c1cb33d45e...84ae59a2cdc2258d6fa0732dd66352dddae2a412)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 7.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:29:53 +00:00
dependabot[bot] 09b1582704 chore(deps): bump ccxt from 4.5.20 to 4.5.22
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.5.20 to 4.5.22.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.5.20...v4.5.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:06:04 +00:00
dependabot[bot] 61ab49a124 chore(deps): bump pymdown-extensions from 10.17.1 to 10.17.2
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.17.1 to 10.17.2.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.17.1...10.17.2)

---
updated-dependencies:
- dependency-name: pymdown-extensions
  dependency-version: 10.17.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:05:36 +00:00
dependabot[bot] 1a95b04d79 chore(deps): bump xgboost from 3.1.1 to 3.1.2
Bumps [xgboost](https://github.com/dmlc/xgboost) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/dmlc/xgboost/releases)
- [Changelog](https://github.com/dmlc/xgboost/blob/master/NEWS.md)
- [Commits](https://github.com/dmlc/xgboost/compare/v3.1.1...v3.1.2)

---
updated-dependencies:
- dependency-name: xgboost
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:05:23 +00:00
dependabot[bot] 691a6b0a72 chore(deps-dev): bump ruff from 0.14.5 to 0.14.6
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.5 to 0.14.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.14.5...0.14.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:05:16 +00:00
dependabot[bot] 1d0a902251 chore(deps-dev): bump time-machine from 3.0.0 to 3.1.0
Bumps [time-machine](https://github.com/adamchainz/time-machine) from 3.0.0 to 3.1.0.
- [Changelog](https://github.com/adamchainz/time-machine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/adamchainz/time-machine/compare/3.0.0...3.1.0)

---
updated-dependencies:
- dependency-name: time-machine
  dependency-version: 3.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:04:53 +00:00
dependabot[bot] 1de9f5a2fb chore(deps): bump pydantic from 2.12.4 to 2.12.5
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.12.4 to 2.12.5.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.12.4...v2.12.5)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-version: 2.12.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:04:45 +00:00
dependabot[bot] 7a5342cc93 chore(deps-dev): bump scipy-stubs in the scipy group
Bumps the scipy group with 1 update: [scipy-stubs](https://github.com/scipy/scipy-stubs).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 03:04:40 +00:00
Matthias 52c73eabeb docs: add missing space in docs 2025-11-30 13:25:12 +01:00
Matthias 02de1f0a1e Merge pull request #12559 from freqtrade/feat/dry_stop
Enhance dry-run stoploss functionality
2025-11-29 13:44:45 +01:00
Matthias 3453bdf607 chore: bump version to 2025.12-dev 2025-11-29 13:10:16 +01:00
Matthias ac2723c3a0 test: add explicit test for dry stoploss order filling 2025-11-29 12:23:15 +01:00
Matthias 1536c09df3 test: improved test naming 2025-11-29 12:07:13 +01:00
Matthias fade66afd9 fix: ensure we always have a price when checking stops 2025-11-28 15:52:30 +01:00
Matthias f63484d0b0 feat: add dry-limit check for stoploss orders 2025-11-28 15:52:30 +01:00
Matthias 3543e96ec5 refactor: extract dry-market order slippage from function 2025-11-28 15:52:30 +01:00
Matthias c83ea0db4f chore: fix default behavior for crossed mode 2025-11-28 15:52:30 +01:00
Matthias ad256367be feat: dry-is-crossed should support stoploss 2025-11-28 15:52:30 +01:00
Matthias 73b427370b test: add tests for dry crossed stoploss 2025-11-28 15:52:30 +01:00
Matthias ca7234e33f test: fix dry-stop tests 2025-11-28 15:52:30 +01:00
Matthias cd7b267171 feat: record dry-run stop_price price separately 2025-11-28 15:52:30 +01:00
Matthias 0340d36af2 chore(ci): run ci against python 3.14 2025-11-11 07:02:19 +01:00
Matthias ad7816d51e chore: skip catboost tests on 3.14 for now 2025-11-11 07:01:21 +01:00
Matthias 73127c8179 chore: limit catboost to python < 3.14 2025-11-11 06:52:26 +01:00
105 changed files with 6704 additions and 6794 deletions
+15 -8
View File
@@ -2,7 +2,7 @@ version: 2
updates:
- package-ecosystem: docker
cooldown:
default-days: 4
default-days: 7
directories:
- "/"
- "/docker"
@@ -16,7 +16,7 @@ updates:
- package-ecosystem: devcontainers
directory: "/"
cooldown:
default-days: 4
default-days: 7
schedule:
interval: daily
open-pull-requests-limit: 10
@@ -24,13 +24,13 @@ updates:
- package-ecosystem: pip
directory: "/"
cooldown:
default-days: 4
default-days: 7
exclude:
- ccxt
schedule:
interval: weekly
time: "03:00"
timezone: "Etc/UTC"
interval: "cron"
# Monday at 03:00
cronjob: "0 3 * * 1"
open-pull-requests-limit: 15
target-branch: develop
groups:
@@ -51,8 +51,15 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
cooldown:
default-days: 4
default-days: 7
schedule:
interval: "weekly"
interval: "cron"
# Monday at 03:00
cronjob: "0 3 * * 1"
open-pull-requests-limit: 10
target-branch: develop
groups:
actions:
patterns:
# Combine updates for github provided actions
- "actions/*"
@@ -15,7 +15,7 @@ jobs:
environment:
name: develop
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -34,7 +34,7 @@ jobs:
run: python build_helpers/binance_update_lev_tiers.py
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: freqtrade/exchange/binance_leverage_tiers.json
+20 -19
View File
@@ -25,10 +25,10 @@ jobs:
strategy:
matrix:
os: [ "ubuntu-22.04", "ubuntu-24.04", "macos-14", "macos-15" , "windows-2022", "windows-2025" ]
python-version: ["3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -38,7 +38,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
with:
activate-environment: true
enable-cache: true
@@ -74,7 +74,7 @@ jobs:
run: |
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
with:
fail_ci_if_error: true
@@ -87,12 +87,12 @@ jobs:
rm -rf codecov codecov.SHA256SUM codecov.SHA256SUM.sig
- name: Run json schema extract
# This should be kept before the repository check to ensure that the schema is up-to-date
# This must be kept before the repository check to ensure that the schema is up-to-date
run: |
python build_helpers/extract_config_json_schema.py
- name: Run command docs partials extract
# This should be kept before the repository check to ensure that the docs are up-to-date
# This must be kept before the repository check to ensure that the docs are up-to-date
if: ${{ (matrix.python-version == '3.13') }}
run: |
python build_helpers/create_command_partials.py
@@ -110,7 +110,7 @@ jobs:
fi
- name: Check for repository changes - Windows
if: ${{ runner.os == 'Windows' && (matrix.python-version != '3.13') }}
if: ${{ runner.os == 'Windows' }}
run: |
if (git status --porcelain) {
Write-Host "Repository is dirty, changes detected:"
@@ -159,6 +159,7 @@ jobs:
shell: powershell
run: |
$PSVersionTable
Get-PSRepository | Format-List *
Set-PSRepository psgallery -InstallationPolicy trusted
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
$Error.clear()
@@ -177,7 +178,7 @@ jobs:
name: "Mypy Version Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -195,7 +196,7 @@ jobs:
name: "Pre-commit checks"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -208,7 +209,7 @@ jobs:
name: "Documentation build"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -240,7 +241,7 @@ jobs:
name: "Tests and Linting - Online tests"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -250,7 +251,7 @@ jobs:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
with:
activate-environment: true
enable-cache: true
@@ -320,7 +321,7 @@ jobs:
with:
jobs: ${{ toJSON(needs) }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -335,7 +336,7 @@ jobs:
python -m build --sdist --wheel
- name: Upload artifacts 📦
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: freqtrade-build
path: |
@@ -348,7 +349,7 @@ jobs:
python -m build --sdist --wheel ft_client
- name: Upload artifacts 📦
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: freqtrade-client-build
path: |
@@ -367,12 +368,12 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
- name: Download artifact 📦
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
pattern: freqtrade*-build
path: dist
@@ -396,12 +397,12 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
- name: Download artifact 📦
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
pattern: freqtrade*-build
path: dist
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
name: Deploy Docs through mike
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: true
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
- name: Login to GitHub Container Registry
+18 -3
View File
@@ -33,10 +33,21 @@ jobs:
if: github.repository == 'freqtrade/freqtrade'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
- name: Visualize disk usage before build
run: df -h
- name: Cleanup some disk space
run: |
docker system prune -a --force || true
docker builder prune -af || true
- name: Visualize disk usage after cleanup
run: df -h
- name: Set docker tag names
id: tags
uses: ./.github/actions/docker-tags
@@ -54,7 +65,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3.12.0
- name: Available platforms
run: echo ${PLATFORMS}
@@ -142,6 +153,9 @@ jobs:
run: |
docker images
- name: Visualize disk usage after build
run: df -h
deploy-arm:
name: "Deploy Docker ARM64"
permissions:
@@ -152,7 +166,7 @@ jobs:
if: github.repository == 'freqtrade/freqtrade'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -276,6 +290,7 @@ jobs:
docker buildx imagetools create \
--tag ${GHCR_IMAGE_NAME}:${TAG} \
--tag ${GHCR_IMAGE_NAME}:latest \
--tag ${IMAGE_NAME}:latest \
${IMAGE_NAME}:${TAG}
- name: Docker images
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
auto-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6.0.1
with:
persist-credentials: false
@@ -28,7 +28,7 @@ jobs:
- name: Run auto-update
run: pre-commit autoupdate
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
- uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with:
token: ${{ secrets.REPO_SCOPED_TOKEN }}
add-paths: .pre-commit-config.yaml
+2 -2
View File
@@ -22,9 +22,9 @@ jobs:
# actions: read # only needed for private repos
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@v6.0.1
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
+5 -5
View File
@@ -21,7 +21,7 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.18.2"
rev: "v1.19.1"
hooks:
- id: mypy
exclude: build_helpers
@@ -31,8 +31,8 @@ repos:
- types-requests==2.32.4.20250913
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20251115
- scipy-stubs==1.16.3.0
- SQLAlchemy==2.0.44
- scipy-stubs==1.16.3.3
- SQLAlchemy==2.0.45
# stages: [push]
- repo: https://github.com/pycqa/isort
@@ -44,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.14.6'
rev: 'v0.14.10'
hooks:
- id: ruff
- id: ruff-format
@@ -83,6 +83,6 @@ repos:
# Ensure github actions remain safe
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.16.3
rev: v1.19.0
hooks:
- id: zizmor
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.13.8-slim-bookworm AS base
FROM python:3.13.11-slim-bookworm AS base
# Setup env
ENV LANG=C.UTF-8
+2 -2
View File
@@ -15,7 +15,7 @@ This software is for educational purposes only. Do not risk money which
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
Always start by running a trading bot in Dry-run and do not engage money
Always start by running a trading bot in Dry-Run and do not engage money
before you understand how it works and what profit/loss you should
expect.
@@ -24,7 +24,7 @@ hesitate to read the source code and understand the mechanism of this bot.
## Supported Exchange marketplaces
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
Please read the [exchange-specific notes](docs/exchanges.md) to learn about special configurations that maybe needed for each exchange.
- [X] [Binance](https://www.binance.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)
+39 -5
View File
@@ -1,5 +1,7 @@
import os
import subprocess # noqa: S404, RUF100
import sys
from io import StringIO
from pathlib import Path
@@ -8,7 +10,20 @@ def _write_partial_file(filename: str, content: str):
f.write(f"``` output\n{content}\n```\n")
def _get_help_output(parser) -> str:
"""Capture the help output from a parser."""
output = StringIO()
parser.print_help(file=output)
return output.getvalue()
def extract_command_partials():
# Set terminal width to 80 columns for consistent output formatting
os.environ["COLUMNS"] = "80"
# Import Arguments here to avoid circular imports and ensure COLUMNS is set
from freqtrade.commands.arguments import Arguments
subcommands = [
"trade",
"create-userdir",
@@ -46,16 +61,35 @@ def extract_command_partials():
"recursive-analysis",
]
result = subprocess.run(["freqtrade", "--help"], capture_output=True, text=True)
# Build the Arguments class to get the parser with all subcommands
args = Arguments(None)
args._build_subcommands()
_write_partial_file("docs/commands/main.md", result.stdout)
# Get main help output
main_help = _get_help_output(args.parser)
_write_partial_file("docs/commands/main.md", main_help)
# Get subparsers from the main parser
# The subparsers are stored in _subparsers._group_actions[0].choices
subparsers_action = None
for action in args.parser._subparsers._group_actions:
if hasattr(action, "choices"):
subparsers_action = action
break
if subparsers_action is None:
raise RuntimeError("Could not find subparsers in the main parser")
for command in subcommands:
print(f"Running for {command}")
result = subprocess.run(["freqtrade", command, "--help"], capture_output=True, text=True)
_write_partial_file(f"docs/commands/{command}.md", result.stdout)
if command in subparsers_action.choices:
subparser = subparsers_action.choices[command]
help_output = _get_help_output(subparser)
_write_partial_file(f"docs/commands/{command}.md", help_output)
else:
print(f" Warning: subcommand '{command}' not found in parser")
# freqtrade-client still uses subprocess as requested
print("Running for freqtrade-client")
result_client = subprocess.run(["freqtrade-client", "--show"], capture_output=True, text=True)
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11.13-slim-bookworm AS base
FROM python:3.11.14-slim-bookworm AS base
# Setup env
ENV LANG=C.UTF-8
+3 -1
View File
@@ -43,7 +43,9 @@ options:
separated.
--eps, --enable-position-stacking
Allow buying the same pair multiple times (position
stacking).
stacking). Only applicable to backtesting and
hyperopt. Results archived by this cannot be
reproduced in dry/live trading.
--enable-protections, --enableprotections
Enable protections for backtesting. Will slow
backtesting down by a considerable amount, but will
+6
View File
@@ -11,6 +11,7 @@ usage: freqtrade download-data [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--data-format-ohlcv {json,jsongz,feather,parquet}]
[--data-format-trades {json,jsongz,feather,parquet}]
[--trading-mode {spot,margin,futures}]
[--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]]
[--prepend]
options:
@@ -50,6 +51,11 @@ options:
`feather`).
--trading-mode, --tradingmode {spot,margin,futures}
Select Trading mode
--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]
Select candle type to download. Defaults to the
necessary candles for the selected trading mode (e.g.
'spot' or ('futures', 'funding_rate' and 'mark') for
futures).
--prepend Allow data prepending. (Data-appending is disabled)
Common arguments:
+3 -1
View File
@@ -41,7 +41,9 @@ options:
functions.
--eps, --enable-position-stacking
Allow buying the same pair multiple times (position
stacking).
stacking). Only applicable to backtesting and
hyperopt. Results archived by this cannot be
reproduced in dry/live trading.
--enable-protections, --enableprotections
Enable protections for backtesting. Will slow
backtesting down by a considerable amount, but will
+1
View File
@@ -60,6 +60,7 @@ freqtrade download-data --exchange binance --pairs ".*/USDT"
* Given starting points are ignored if data is already available, downloading only missing data up to today.
* Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
* To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
* When downloading futures data (`--trading-mode futures` or a configuration specifying futures mode), freqtrade will automatically download the necessary candle types (e.g. `mark` and `funding_rate` candles) unless specified otherwise via `--candle-types`.
??? Note "Permission denied errors"
If your configuration directory `user_data` was made by docker, you may get the following error:
+47
View File
@@ -98,3 +98,50 @@ Please use configuration based [log setup](advanced-setup.md#advanced-logging) i
The edge module has been deprecated in 2023.9 and removed in 2025.6.
All functionalities of edge have been removed, and having edge configured will result in an error.
## Adjustment to dynamic funding rate handling
With version 2025.12, the handling of dynamic funding rates has been adjusted to also support dynamic funding rates down to 1h funding intervals.
As a consequence, the mark and funding rate timeframes have been changed to 1h for every supported futures exchange.
As the timeframe for both mark and funding_fee candles has changed (usually from 8h to 1h) - already downloaded data will have to be adjusted or partially re-downloaded.
You can either re-download everything (`freqtrade download-data [...] --erase` - :warning: can take a long time) - or download the updated data selectively.
### Strategy
Most strategies should not need adjustments to continue to work as expected - however, strategies using `@informative("8h", candle_type="funding_rate")` or similar will have to switch the timeframe to 1h.
The same is true for `dp.get_pair_dataframe(metadata["pair"], "8h", candle_type="funding_rate")` - which will need to be switched to 1h.
freqtrade will auto-adjust the timeframe and return `funding_rates` despite the wrongly given timeframe. It'll issue a warning - and may still break your strategy.
### Selective data re-download
The script below should serve as an example - you may need to adjust the timeframe and exchange to your needs!
``` bash
# Cleanup no longer needed data
rm user_data/data/<exchange>/futures/*-mark-*
rm user_data/data/<exchange>/futures/*-funding_rate-*
# download new data (only required once to fix the mark and funding fee data)
freqtrade download-data -t 1h --trading-mode futures --candle-types funding_rate mark [...] --timerange <full timerange you've got other data for>
```
The result of the above will be that your funding_rates and mark data will have the 1h timeframe.
you can verify this with `freqtrade list-data --exchange <yourexchange> --show`.
!!! Note "Additional arguments"
Additional arguments to the above commands may be necessary, like configuration files or explicit user_data if they deviate from the default.
**Hyperliquid** is a special case now - which will no longer require 1h mark data - but will use regular candles instead (this data never existed and is identical to 1h futures candles). As we don't support download-data for hyperliquid (they don't provide historic data) - there won't be actions necessary for hyperliquid users.
## Catboost models in freqAI
CatBoost models have been removed with version 2025.12 and are no longer actively supported.
If you have existing bots using CatBoost models, you can still use them in your custom models by copy/pasting them from the git history (as linked below) and installing the Catboost library manually.
We do however recommend switching to other supported model libraries like LightGBM or XGBoost for better support and future compatibility.
* [CatboostRegressor](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostRegressor.py)
* [CatboostClassifier](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostClassifier.py)
* [CatboostClassifierMultiTarget](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py)
+6 -6
View File
@@ -200,15 +200,15 @@ If this value is set, FreqAI will initially use the predictions from the trainin
## Using different prediction models
FreqAI has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `CatBoost`, `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`.
FreqAI has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`.
Regression and classification models differ in what targets they predict - a regression model will predict a target of continuous values, for example what price BTC will be at tomorrow, whilst a classifier will predict a target of discrete values, for example if the price of BTC will go up tomorrow or not. This means that you have to specify your targets differently depending on which model type you are using (see details [below](#setting-model-targets)).
All of the aforementioned model libraries implement gradient boosted decision tree algorithms. They all work on the principle of ensemble learning, where predictions from multiple simple learners are combined to get a final prediction that is more stable and generalized. The simple learners in this case are decision trees. Gradient boosting refers to the method of learning, where each simple learner is built in sequence - the subsequent learner is used to improve on the error from the previous learner. If you want to learn more about the different model libraries you can find the information in their respective docs:
* CatBoost: https://catboost.ai/en/docs/
* LightGBM: https://lightgbm.readthedocs.io/en/v3.3.2/#
* XGBoost: https://xgboost.readthedocs.io/en/stable/#
* LightGBM: <https://lightgbm.readthedocs.io/en/v3.3.2/#>
* XGBoost: <https://xgboost.readthedocs.io/en/stable/#>
* CatBoost: <https://catboost.ai/en/docs/> (No longer actively supported since 2025.12)
There are also numerous online articles describing and comparing the algorithms. Some relatively lightweight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model.
@@ -219,7 +219,7 @@ Make sure to use unique names to avoid overriding built-in models.
#### Regressors
If you are using a regressor, you need to specify a target that has continuous values. FreqAI includes a variety of regressors, such as the `CatboostRegressor`via the flag `--freqaimodel CatboostRegressor`. An example of how you could set a regression target for predicting the price 100 candles into the future would be
If you are using a regressor, you need to specify a target that has continuous values. FreqAI includes a variety of regressors, such as the `LightGBMRegressor`via the flag `--freqaimodel LightGBMRegressor`. An example of how you could set a regression target for predicting the price 100 candles into the future would be
```python
df['&s-close_price'] = df['close'].shift(-100)
@@ -229,7 +229,7 @@ If you want to predict multiple targets, you need to define multiple labels usin
#### Classifiers
If you are using a classifier, you need to specify a target that has discrete values. FreqAI includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example, if you want to predict if the price 100 candles into the future goes up or down you would set
If you are using a classifier, you need to specify a target that has discrete values. FreqAI includes a variety of classifiers, such as the `LightGBMClassifier` via the flag `--freqaimodel LightGBMClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example, if you want to predict if the price 100 candles into the future goes up or down you would set
```python
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')
+1 -1
View File
@@ -107,7 +107,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `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`.
### Additional parameters
| Parameter | Description |
@@ -116,3 +115,4 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
| `freqai.conv_width` | The width of a neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
| `freqai.override_exchange_check` | Override the exchange check to force FreqAI to use exchanges that may not have enough historic data. Turn this to True if you know your FreqAI model and strategy do not require historical data. <br> **Datatype:** Boolean. <br> Default: `False`.
+1 -1
View File
@@ -2,6 +2,6 @@ markdown==3.10
mkdocs==1.6.1
mkdocs-material==9.7.0
mdx_truly_sane_lists==1.3
pymdown-extensions==10.17.1
pymdown-extensions==10.19.1
jinja2==3.1.6
mike==2.1.3
+6 -1
View File
@@ -31,9 +31,14 @@ The Order-type will be ignored if only one mode is available.
--8<-- "includes/exchange-features.md"
!!! Note "Tight stoploss"
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
Do not set too low/tight stoploss value when using stop loss on exchange!
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
!!! Warning "Loose stoploss"
Using stoploss on exchange with a very wide stoploss (e.g. -1) may fail to place the stoploss order on exchange due to exchange limitations.
In that case, the bot will fallback to using the `emergency_exit` order type to place a market order as placing the stoploss order failed.
Freqtrade currently does not implement a limitation to avoid this situation, so please ensure your stoploss values are within reasonable limits for your exchange or disable stoploss on exchange.
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
Enable or Disable stop loss on exchange.
+2 -2
View File
@@ -634,7 +634,7 @@ class AwesomeStrategy(IStrategy):
## Custom order price rules
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
By default, freqtrade use the orderbook to automatically set an order price ([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
@@ -644,7 +644,7 @@ Each of these methods are called right before placing an order on the exchange.
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
!!! Note
Using custom_entry_price, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
When using `custom_entry_price()`, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
### Custom order entry and exit price example
+1 -1
View File
@@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2025.11.1"
__version__ = "2025.12"
if "dev" in __version__:
from pathlib import Path
+7 -1
View File
@@ -3,6 +3,7 @@ This module contains the argument manager class
"""
from argparse import ArgumentParser, Namespace, _ArgumentGroup
from copy import deepcopy
from functools import partial
from pathlib import Path
from typing import Any
@@ -174,6 +175,7 @@ ARGS_DOWNLOAD_DATA = [
"dataformat_ohlcv",
"dataformat_trades",
"trading_mode",
"candle_types",
"prepend_data",
]
@@ -348,7 +350,11 @@ class Arguments:
def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val]
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
options = deepcopy(opt.kwargs)
help_text = options.pop("help", None)
if opt.fthelp and isinstance(opt.fthelp, dict) and hasattr(parser, "prog"):
help_text = opt.fthelp.get(parser.prog, help_text)
parser.add_argument(*opt.cli, dest=val, help=help_text, **options)
def _build_subcommands(self) -> None:
"""
+20 -2
View File
@@ -38,8 +38,14 @@ def check_int_nonzero(value: str) -> int:
class Arg:
# Optional CLI arguments
def __init__(self, *args, **kwargs):
def __init__(self, *args, fthelp: dict[str, str] | None = None, **kwargs):
"""
CLI Arguments - used to build subcommand parsers consistently.
:param fthelp: dict - fthelp per command - should be "freqtrade <command>": help_text
If not provided or not found, 'help' from kwargs is used instead.
"""
self.cli = args
self.fthelp = fthelp
self.kwargs = kwargs
@@ -174,7 +180,11 @@ AVAILABLE_CLI_OPTIONS = {
"position_stacking": Arg(
"--eps",
"--enable-position-stacking",
help="Allow buying the same pair multiple times (position stacking).",
help=(
"Allow buying the same pair multiple times (position stacking). "
"Only applicable to backtesting and hyperopt. "
"Results archived by this cannot be reproduced in dry/live trading."
),
action="store_true",
default=False,
),
@@ -422,6 +432,14 @@ AVAILABLE_CLI_OPTIONS = {
),
"candle_types": Arg(
"--candle-types",
fthelp={
"freqtrade download-data": (
"Select candle type to download. "
"Defaults to the necessary candles for the selected trading mode "
"(e.g. 'spot' or ('futures', 'funding_rate' and 'mark') for futures)."
),
"_": "Select candle type to convert. Defaults to all available types.",
},
help="Select candle type to convert. Defaults to all available types.",
choices=[c.value for c in CandleType],
nargs="+",
+2 -1
View File
@@ -38,7 +38,8 @@ def ohlcv_to_dataframe(
cols = DEFAULT_DATAFRAME_COLUMNS
df = DataFrame(ohlcv, columns=cols)
df["date"] = to_datetime(df["date"], unit="ms", utc=True)
# Floor date to seconds to account for exchange imprecisions
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s")
# Some exchanges return int values for Volume and even for OHLC.
# Convert them since TA-LIB indicators used in the strategy assume floats
+26
View File
@@ -348,6 +348,22 @@ class DataProvider:
)
return total_candles
def __fix_funding_rate_timeframe(
self, pair: str, timeframe: str | None, candle_type: str
) -> str | None:
if (
candle_type == CandleType.FUNDING_RATE
and (ff_tf := self.get_funding_rate_timeframe()) != timeframe
):
# TODO: does this message make sense? might be pointless as funding fees don't
# have a timeframe
logger.warning(
f"{pair}, {timeframe} requested - funding rate timeframe not matching {ff_tf}."
)
return ff_tf
return timeframe
def get_pair_dataframe(
self, pair: str, timeframe: str | None = None, candle_type: str = ""
) -> DataFrame:
@@ -361,6 +377,7 @@ class DataProvider:
:return: Dataframe for this pair
:param candle_type: '', mark, index, premiumIndex, or funding_rate
"""
timeframe = self.__fix_funding_rate_timeframe(pair, timeframe, candle_type)
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
# Get live OHLCV data.
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
@@ -620,3 +637,12 @@ class DataProvider:
except ExchangeError:
logger.warning(f"Could not fetch market data for {pair}. Assuming no delisting.")
return None
def get_funding_rate_timeframe(self) -> str:
"""
Get the funding rate timeframe from exchange options
:return: Timeframe string
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
return self._exchange.get_option("funding_fee_timeframe")
@@ -397,6 +397,9 @@ class IDataHandler(ABC):
pairdf = self._ohlcv_load(
pair, timeframe, timerange=timerange_startup, candle_type=candle_type
)
if not pairdf.empty and candle_type == CandleType.FUNDING_RATE:
# Funding rate data is sometimes off by a couple of ms - floor to seconds
pairdf["date"] = pairdf["date"].dt.floor("s")
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
return pairdf
else:
@@ -508,8 +511,15 @@ class IDataHandler(ABC):
Applies to bybit and okx, where funding-fee and mark candles have different timeframes.
"""
paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES)
ff_timeframe_s = timeframe_to_seconds(ff_timeframe)
funding_rate_combs = [
f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe
f
for f in paircombs
if f[2] == CandleType.FUNDING_RATE
and f[1] != ff_timeframe
# Only allow smaller timeframes to move from smaller to larger timeframes
and timeframe_to_seconds(f[1]) < ff_timeframe_s
]
if funding_rate_combs:
+48 -34
View File
@@ -308,11 +308,15 @@ def _download_pair_history(
candle_type=candle_type,
until_ms=until_ms if until_ms else None,
)
logger.info(f"Downloaded data for {pair} with length {len(new_dataframe)}.")
logger.info(
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
f"{len(new_dataframe)}."
)
else:
new_dataframe = pair_candles
logger.info(
f"Downloaded data for {pair} with length {len(new_dataframe)}. Parallel Method."
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
f"{len(new_dataframe)}. Parallel Method."
)
if data.empty:
@@ -349,6 +353,7 @@ def _download_pair_history(
def refresh_backtest_ohlcv_data(
exchange: Exchange,
*,
pairs: list[str],
timeframes: list[str],
datadir: Path,
@@ -359,6 +364,7 @@ def refresh_backtest_ohlcv_data(
data_format: str | None = None,
prepend: bool = False,
progress_tracker: CustomProgress | None = None,
candle_types: list[CandleType] | None = None,
no_parallel_download: bool = False,
) -> list[str]:
"""
@@ -371,10 +377,44 @@ def refresh_backtest_ohlcv_data(
pairs_not_available = []
fast_candles: dict[PairWithTimeframe, DataFrame] = {}
data_handler = get_datahandler(datadir, data_format)
candle_type = CandleType.get_default(trading_mode)
def_candletype = CandleType.SPOT if trading_mode != "futures" else CandleType.FUTURES
if trading_mode != "futures":
# Ignore user passed candle types for non-futures trading
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
else:
# Filter out SPOT candle type for futures trading
candle_types = (
[ct for ct in candle_types if ct != CandleType.SPOT] if candle_types else None
)
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
if candle_types:
for ct in candle_types:
exchange.verify_candle_type_support(ct)
timeframes_with_candletype = [
(tf, ct)
for ct in candle_types
for tf in timeframes
if ct != CandleType.FUNDING_RATE
]
else:
# Default behavior
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
timeframes_with_candletype.append((tf_mark, fr_candle_type))
if not candle_types or CandleType.FUNDING_RATE in candle_types:
# All exchanges need FundingRate for futures trading.
# The timeframe is aligned to the mark-price timeframe.
timeframes_with_candletype.append((tf_funding_rate, CandleType.FUNDING_RATE))
# Deduplicate list ...
timeframes_with_candletype = list(dict.fromkeys(timeframes_with_candletype))
logger.debug(
"Downloading %s.", ", ".join(f'"{tf} {ct}"' for tf, ct in timeframes_with_candletype)
)
with progress_tracker as progress:
tf_length = len(timeframes) if trading_mode != "futures" else len(timeframes) + 2
timeframe_task = progress.add_task("Timeframe", total=tf_length)
timeframe_task = progress.add_task("Timeframe", total=len(timeframes_with_candletype))
pair_task = progress.add_task("Downloading data...", total=len(pairs))
for pair in pairs:
@@ -385,7 +425,7 @@ def refresh_backtest_ohlcv_data(
pairs_not_available.append(f"{pair}: Pair not available on exchange.")
logger.info(f"Skipping pair {pair}...")
continue
for timeframe in timeframes:
for timeframe, candle_type in timeframes_with_candletype:
# Get fast candles via parallel method on first loop through per timeframe
# and candle type. Downloads all the pairs in the list and stores them.
# Also skips if only 1 pair/timeframe combination is scheduled for download.
@@ -412,7 +452,7 @@ def refresh_backtest_ohlcv_data(
# get the already downloaded pair candles if they exist
pair_candles = fast_candles.pop((pair, timeframe, candle_type), None)
progress.update(timeframe_task, description=f"Timeframe {timeframe}")
progress.update(timeframe_task, description=f"Timeframe {timeframe} {candle_type}")
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
_download_pair_history(
pair=pair,
@@ -428,33 +468,6 @@ def refresh_backtest_ohlcv_data(
pair_candles=pair_candles, # optional pass of dataframe of parallel candles
)
progress.update(timeframe_task, advance=1)
if trading_mode == "futures":
# Predefined candletype (and timeframe) depending on exchange
# Downloads what is necessary to backtest based on futures data.
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
# All exchanges need FundingRate for futures trading.
# The timeframe is aligned to the mark-price timeframe.
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
for candle_type_f, tf in combs:
logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.")
_download_pair_history(
pair=pair,
datadir=datadir,
exchange=exchange,
timerange=timerange,
data_handler=data_handler,
timeframe=str(tf),
new_pairs_days=new_pairs_days,
candle_type=candle_type_f,
erase=erase,
prepend=prepend,
)
progress.update(
timeframe_task, advance=1, description=f"Timeframe {candle_type_f}, {tf}"
)
progress.update(pair_task, advance=1)
progress.update(timeframe_task, description="Timeframe")
@@ -800,6 +813,7 @@ def download_data(
trading_mode=config.get("trading_mode", "spot"),
prepend=config.get("prepend_data", False),
progress_tracker=progress_tracker,
candle_types=config.get("candle_types"),
no_parallel_download=config.get("no_parallel_download", False),
)
finally:
+2 -1
View File
@@ -74,9 +74,10 @@ def combined_dataframes_with_rel_mean(
df_comb = combine_dataframes_by_column(data, column)
# Trim dataframes to the given timeframe
df_comb = df_comb.iloc[(df_comb.index >= fromdt) & (df_comb.index < todt)]
rel_mean = df_comb.pct_change().mean(axis=1).fillna(0).cumsum()
df_comb["count"] = df_comb.count(axis=1)
df_comb["mean"] = df_comb.mean(axis=1)
df_comb["rel_mean"] = df_comb["mean"].pct_change().fillna(0).cumsum()
df_comb["rel_mean"] = rel_mean
return df_comb[["mean", "rel_mean", "count"]]
+1 -1
View File
@@ -4,7 +4,7 @@ from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
from freqtrade.exchange.exchange import Exchange
# isort: on
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.binance import Binance, Binanceus, Binanceusdm
from freqtrade.exchange.bingx import Bingx
from freqtrade.exchange.bitget import Bitget
from freqtrade.exchange.bitmart import Bitmart
+26 -15
View File
@@ -17,7 +17,7 @@ from freqtrade.exchange.binance_public_data import (
download_archive_trades,
)
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas, Tickers
from freqtrade.exchange.exchange_types import FtHas, Tickers
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.util import FtTTLCache
@@ -51,6 +51,8 @@ class Binance(Exchange):
"funding_fee_candle_limit": 1000,
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
"stoploss_query_requires_stop_flag": True,
"stoploss_algo_order_info_id": "actualOrderId",
"tickers_have_price": False,
"floor_leverage": True,
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
@@ -145,20 +147,6 @@ class Binance(Exchange):
except ccxt.BaseError as e:
raise OperationalException(e) from e
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
if self.trading_mode == TradingMode.FUTURES:
params = params or {}
params.update({"stop": True})
return self.fetch_order(order_id, pair, params)
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
if self.trading_mode == TradingMode.FUTURES:
params = params or {}
params.update({"stop": True})
return self.cancel_order(order_id=order_id, pair=pair, params=params)
def get_historic_ohlcv(
self,
pair: str,
@@ -558,3 +546,26 @@ class Binance(Exchange):
cache[ft_symbol] = delist_dt
return cache.get(pair, None)
class Binanceusdm(Binance):
"""Binacne USDM Exchange
Same as Binance - only futures trading is supported (via ccxt).
Not actually necessary, binance should be preferred.
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
class Binanceus(Binance):
"""Binance US exchange class.
Minimal adjustment to disable futures trading for the US subsidiary of Binance
"""
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.SPOT, MarginMode.NONE),
]
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -31,11 +31,11 @@ class Bitget(Exchange):
"stop_price_prop": "stopPrice",
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
"stoploss_order_types": {"limit": "limit", "market": "market"},
"stoploss_query_requires_stop_flag": True,
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
}
_ft_has_futures: FtHas = {
"mark_ohlcv_timeframe": "4h",
"funding_fee_candle_limit": 100,
"has_delisting": True,
}
@@ -129,9 +129,6 @@ class Bitget(Exchange):
return self._fetch_stop_order_fallback(order_id, pair)
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
@retrier
def additional_exchange_init(self) -> None:
"""
-2
View File
@@ -38,8 +38,6 @@ class Bybit(Exchange):
}
_ft_has_futures: FtHas = {
"ohlcv_has_history": True,
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"funding_fee_candle_limit": 200,
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"},
+5 -2
View File
@@ -45,8 +45,6 @@ BAD_EXCHANGES = {
}
MAP_EXCHANGE_CHILDCLASS = {
"binanceus": "binance",
"binanceusdm": "binance",
"okex": "okx",
"gateio": "gate",
"huboi": "htx",
@@ -54,6 +52,8 @@ MAP_EXCHANGE_CHILDCLASS = {
SUPPORTED_EXCHANGES = [
"binance",
"binanceus",
"binanceusdm",
"bingx",
"bitmart",
"bitget",
@@ -97,6 +97,9 @@ EXCHANGE_HAS_OPTIONAL = [
# 'fetchLeverageTiers', # Futures initialization
# 'fetchMarketLeverageTiers', # Futures initialization
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
# "fetchPremiumIndexOHLCV", # Futures additional data
# "fetchMarkOHLCV", # Futures additional data
# "fetchIndexOHLCV", # Futures additional data
# ccxt.pro
"watchOHLCV",
]
+155 -41
View File
@@ -104,6 +104,7 @@ from freqtrade.misc import (
deep_merge_dicts,
file_dump_json,
file_load_json,
safe_value_fallback,
safe_value_fallback2,
)
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
@@ -131,6 +132,7 @@ class Exchange:
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
"stoploss_order_types": {},
"stoploss_blocks_assets": True, # By default stoploss orders block assets
"stoploss_query_requires_stop_flag": False, # Require "stop": True" to fetch stop orders
"order_time_in_force": ["GTC"],
"ohlcv_params": {},
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
@@ -152,8 +154,8 @@ class Exchange:
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"l2_limit_upper": None, # Upper limit for L2 limit
"mark_ohlcv_price": "mark",
"mark_ohlcv_timeframe": "8h",
"funding_fee_timeframe": "8h",
"mark_ohlcv_timeframe": "1h",
"funding_fee_timeframe": "1h",
"ccxt_futures_name": "swap",
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
"order_props_in_contracts": ["amount", "filled", "remaining"],
@@ -707,7 +709,7 @@ class Exchange:
self._markets = self._api_async.markets
self._api.set_markets_from_exchange(self._api_async)
# Assign options array, as it contains some temporary information from the exchange.
# TODO: investigate with ccxt if it's safe to remove `.options`
# ccxt does not implicitly copy options over in set_markets_from_exchange
self._api.options = self._api_async.options
if self._exchange_ws:
# Set markets to avoid reloading on websocket api
@@ -877,19 +879,20 @@ class Exchange:
# Only allow 5 calls per pair to somewhat limit the impact
raise ConfigurationError(
f"This strategy requires {startup_candles} candles to start, "
"which is more than 5x "
f"which is more than 5x ({candle_limit * 5 - 1} candles) "
f"the amount of candles {self.name} provides for {timeframe}."
)
elif required_candle_call_count > 1:
raise ConfigurationError(
f"This strategy requires {startup_candles} candles to start, which is more than "
f"This strategy requires {startup_candles} candles to start, "
f"which is more than ({candle_limit - 1} candles) "
f"the amount of candles {self.name} provides for {timeframe}."
)
if required_candle_call_count > 1:
logger.warning(
f"Using {required_candle_call_count} calls to get OHLCV. "
f"This can result in slower operations for the bot. Please check "
f"if you really need {startup_candles} candles for your strategy"
f"if you really need {startup_candles} candles for your strategy."
)
return required_candle_call_count
@@ -1119,6 +1122,7 @@ class Exchange:
leverage: float,
params: dict | None = None,
stop_loss: bool = False,
stop_price: float | None = None,
) -> CcxtOrder:
now = dt_now()
order_id = f"dry_run_{side}_{pair}_{now.timestamp()}"
@@ -1145,7 +1149,7 @@ class Exchange:
}
if stop_loss:
dry_order["info"] = {"stopPrice": dry_order["price"]}
dry_order[self._ft_has["stop_price_prop"]] = dry_order["price"]
dry_order[self._ft_has["stop_price_prop"]] = stop_price or dry_order["price"]
# Workaround to avoid filling stoploss orders immediately
dry_order["ft_order_type"] = "stoploss"
orderbook: OrderBook | None = None
@@ -1163,7 +1167,11 @@ class Exchange:
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
# Update market order pricing
average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
slippage = 0.05
worst_rate = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
average = self.get_dry_market_fill_price(
pair, side, amount, rate, worst_rate, orderbook
)
dry_order.update(
{
"average": average,
@@ -1203,7 +1211,13 @@ class Exchange:
return dry_order
def get_dry_market_fill_price(
self, pair: str, side: str, amount: float, rate: float, orderbook: OrderBook | None
self,
pair: str,
side: str,
amount: float,
rate: float,
worst_rate: float,
orderbook: OrderBook | None,
) -> float:
"""
Get the market order fill price based on orderbook interpolation
@@ -1212,8 +1226,6 @@ class Exchange:
if not orderbook:
orderbook = self.fetch_l2_order_book(pair, 20)
ob_type: OBLiteral = "asks" if side == "buy" else "bids"
slippage = 0.05
max_slippage_val = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
remaining_amount = amount
filled_value = 0.0
@@ -1237,11 +1249,10 @@ class Exchange:
forecast_avg_filled_price = max(filled_value, 0) / amount
# Limit max. slippage to specified value
if side == "buy":
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
forecast_avg_filled_price = min(forecast_avg_filled_price, worst_rate)
else:
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
forecast_avg_filled_price = max(forecast_avg_filled_price, worst_rate)
return self.price_to_precision(pair, forecast_avg_filled_price)
return rate
@@ -1253,13 +1264,15 @@ class Exchange:
limit: float,
orderbook: OrderBook | None = None,
offset: float = 0.0,
is_stop: bool = False,
) -> bool:
if not self.exchange_has("fetchL2OrderBook"):
return True
# True unless checking a stoploss order
return not is_stop
if not orderbook:
orderbook = self.fetch_l2_order_book(pair, 1)
try:
if side == "buy":
if (side == "buy" and not is_stop) or (side == "sell" and is_stop):
price = orderbook["asks"][0][0]
if limit * (1 - offset) >= price:
return True
@@ -1278,6 +1291,38 @@ class Exchange:
"""
Check dry-run limit order fill and update fee (if it filled).
"""
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
pair = order["symbol"]
if not orderbook and self.exchange_has("fetchL2OrderBook"):
orderbook = self.fetch_l2_order_book(pair, 20)
price = safe_value_fallback(order, self._ft_has["stop_price_prop"], "price")
crossed = self._dry_is_price_crossed(
pair, order["side"], price, orderbook, is_stop=True
)
if crossed:
average = self.get_dry_market_fill_price(
pair,
order["side"],
order["amount"],
price,
worst_rate=order["price"],
orderbook=orderbook,
)
order.update(
{
"status": "closed",
"filled": order["amount"],
"remaining": 0,
"average": average,
"cost": order["amount"] * average,
}
)
self.add_dry_order_fee(
pair,
order,
"taker" if immediate else "maker",
)
return order
if (
order["status"] != "closed"
and order["type"] in ["limit"]
@@ -1362,8 +1407,9 @@ class Exchange:
amount: float,
rate: float,
leverage: float,
reduceOnly: bool = False,
time_in_force: str = "GTC",
reduceOnly: bool = False,
initial_order: bool = True,
) -> CcxtOrder:
if self._config["dry_run"]:
dry_order = self.create_dry_run_order(
@@ -1380,7 +1426,7 @@ class Exchange:
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
if not reduceOnly:
self._lev_prep(pair, leverage, side)
self._lev_prep(pair, leverage, side, accept_fail=not initial_order)
order = self._api.create_order(
pair,
@@ -1517,8 +1563,9 @@ class Exchange:
ordertype,
side,
amount,
stop_price_norm,
limit_rate or stop_price_norm,
stop_loss=True,
stop_price=stop_price_norm,
leverage=leverage,
)
return dry_order
@@ -1642,7 +1689,24 @@ class Exchange:
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
return self.fetch_order(order_id, pair, params)
if self.get_option("stoploss_query_requires_stop_flag"):
params = params or {}
params["stop"] = True
order = self.fetch_order(order_id, pair, params)
val = self.get_option("stoploss_algo_order_info_id")
if val and order.get("status", "open") == "closed":
if new_orderid := order.get("info", {}).get(val):
# Fetch real order, which was placed by the algo order.
actual_order = self.fetch_order(order_id=new_orderid, pair=pair, params=None)
actual_order["id_stop"] = actual_order["id"]
actual_order["id"] = order_id
actual_order["type"] = "stoploss"
actual_order["stopPrice"] = order.get("stopPrice")
actual_order["status_stop"] = "triggered"
return actual_order
return order
def fetch_order_or_stoploss_order(
self, order_id: str, pair: str, stoploss_order: bool = False
@@ -1696,6 +1760,9 @@ class Exchange:
raise OperationalException(e) from e
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
if self.get_option("stoploss_query_requires_stop_flag"):
params = params or {}
params["stop"] = True
return self.cancel_order(order_id, pair, params)
def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
@@ -1790,9 +1857,9 @@ class Exchange:
if self._config["dry_run"] or self.trading_mode != TradingMode.FUTURES:
return []
try:
symbols = []
symbols = None
if pair:
symbols.append(pair)
symbols = [pair]
positions: list[CcxtPosition] = self._api.fetch_positions(symbols)
self._log_exchange_response("fetch_positions", positions)
return positions
@@ -2646,24 +2713,25 @@ class Exchange:
input_coroutines: list[Coroutine[Any, Any, OHLCVResponse]] = []
cached_pairs = []
for pair, timeframe, candle_type in set(pair_list):
invalid_funding = (
candle_type == CandleType.FUNDING_RATE
and timeframe != self.get_option("funding_fee_timeframe")
)
if candle_type == CandleType.FUNDING_RATE and timeframe != (
ff_tf := self.get_option("funding_fee_timeframe")
):
# TODO: does this message make sense? would docs be better?
# if any, this should be cached to avoid log spam!
logger.warning(
f"Wrong funding rate timeframe {timeframe} for pair {pair}, "
f"downloading {ff_tf} instead."
)
timeframe = ff_tf
invalid_timeframe = timeframe not in self.timeframes and candle_type in (
CandleType.SPOT,
CandleType.FUTURES,
)
if invalid_timeframe or invalid_funding:
timeframes_ = (
", ".join(self.timeframes)
if candle_type != CandleType.FUNDING_RATE
else self.get_option("funding_fee_timeframe")
)
if invalid_timeframe:
logger.warning(
f"Cannot download ({pair}, {timeframe}, {candle_type}) combination as this "
f"timeframe is not available on {self.name}. Available timeframes are "
f"{timeframes_}."
f"{', '.join(self.timeframes)}."
)
continue
@@ -2700,7 +2768,11 @@ class Exchange:
has_cache = cache and (pair, timeframe, c_type) in self._klines
# in case of existing cache, fill_missing happens after concatenation
ohlcv_df = ohlcv_to_dataframe(
ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
ticks,
timeframe,
pair=pair,
fill_missing=not has_cache and c_type != CandleType.FUNDING_RATE,
drop_incomplete=drop_incomplete,
)
# keeping parsed dataframe in cache
if cache:
@@ -2711,7 +2783,7 @@ class Exchange:
concat([old, ohlcv_df], axis=0),
timeframe,
pair,
fill_missing=True,
fill_missing=c_type != CandleType.FUNDING_RATE,
drop_incomplete=False,
)
candle_limit = self.ohlcv_candle_limit(timeframe, self._config["candle_type_def"])
@@ -2846,9 +2918,10 @@ class Exchange:
timeframe, candle_type=candle_type, since_ms=since_ms
)
if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES):
params.update({"price": candle_type.value})
if candle_type != CandleType.FUNDING_RATE:
if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES):
self.verify_candle_type_support(candle_type)
params.update({"price": str(candle_type)})
data = await self._api_async.fetch_ohlcv(
pair, timeframe=timeframe, since=since_ms, limit=candle_limit, params=params
)
@@ -2913,6 +2986,38 @@ class Exchange:
data = [[x["timestamp"], x["fundingRate"], 0, 0, 0, 0] for x in data]
return data
def check_candle_type_support(self, candle_type: CandleType) -> bool:
"""
Check that the exchange supports the given candle type.
:param candle_type: CandleType to verify
:return: True if supported, False otherwise
"""
if candle_type == CandleType.FUNDING_RATE:
if not self.exchange_has("fetchFundingRateHistory"):
return False
elif candle_type not in (CandleType.SPOT, CandleType.FUTURES):
mapping = {
CandleType.MARK: "fetchMarkOHLCV",
CandleType.INDEX: "fetchIndexOHLCV",
CandleType.PREMIUMINDEX: "fetchPremiumIndexOHLCV",
CandleType.FUNDING_RATE: "fetchFundingRateHistory",
}
_method = mapping.get(candle_type, "fetchOHLCV")
if not self.exchange_has(_method):
return False
return True
def verify_candle_type_support(self, candle_type: CandleType) -> None:
"""
Verify that the exchange supports the given candle type.
:param candle_type: CandleType to verify
:raises OperationalException: if the candle type is not supported
"""
if not self.check_candle_type_support(candle_type):
raise OperationalException(
f"Exchange {self._api.name} does not support fetching {candle_type} candles."
)
# fetch Trade data stuff
def needed_candle_for_trades_ms(self, timeframe: str, candle_type: CandleType) -> int:
@@ -3740,10 +3845,11 @@ class Exchange:
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
"""
relevant_cols = ["date", "open_mark", "open_fund"]
if futures_funding_rate is None:
return mark_rates.merge(
funding_rates, on="date", how="inner", suffixes=["_mark", "_fund"]
)
)[relevant_cols]
else:
if len(funding_rates) == 0:
# No funding rate candles - full fillup with fallback variable
@@ -3756,15 +3862,23 @@ class Exchange:
"low": "low_mark",
"volume": "volume_mark",
}
)
)[relevant_cols]
else:
# Fill up missing funding_rate candles with fallback value
combined = mark_rates.merge(
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
)
combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
return combined
# Fill only leading missing funding rates so gaps stay untouched
first_valid_idx = combined["open_fund"].first_valid_index()
if first_valid_idx is None:
combined["open_fund"] = futures_funding_rate
else:
is_leading_na = (combined.index <= first_valid_idx) & combined[
"open_fund"
].isna()
combined.loc[is_leading_na, "open_fund"] = futures_funding_rate
return combined[relevant_cols].dropna()
def calculate_funding_fees(
self,
+2
View File
@@ -19,6 +19,8 @@ class FtHas(TypedDict, total=False):
stop_price_type_value_mapping: dict
stoploss_order_types: dict[str, str]
stoploss_blocks_assets: bool
stoploss_query_requires_stop_flag: bool
stoploss_algo_order_info_id: str
# ohlcv
ohlcv_params: dict
ohlcv_candle_limit: int
+3 -22
View File
@@ -30,6 +30,8 @@ class Gate(Exchange):
"stoploss_order_types": {"limit": "limit"},
"stop_price_param": "stopPrice",
"stop_price_prop": "stopPrice",
"stoploss_query_requires_stop_flag": True,
"stoploss_algo_order_info_id": "fired_order_id",
"l2_limit_upper": 1000,
"marketOrderRequiresPrice": True,
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
@@ -42,6 +44,7 @@ class Gate(Exchange):
"stop_price_type_field": "price_type",
"l2_limit_upper": 300,
"stoploss_blocks_assets": False,
"stoploss_algo_order_info_id": "trade_id",
"stop_price_type_value_mapping": {
PriceType.LAST: 0,
PriceType.MARK: 1,
@@ -132,25 +135,3 @@ class Gate(Exchange):
def get_order_id_conditional(self, order: CcxtOrder) -> str:
return safe_value_fallback2(order, order, "id_stop", "id")
def fetch_stoploss_order(
self, order_id: str, pair: str, params: dict | None = None
) -> CcxtOrder:
order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True})
if order.get("status", "open") == "closed":
# Places a real order - which we need to fetch explicitly.
val = "trade_id" if self.trading_mode == TradingMode.FUTURES else "fired_order_id"
if new_orderid := order.get("info", {}).get(val):
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
order1["id_stop"] = order1["id"]
order1["id"] = order_id
order1["type"] = "stoploss"
order1["stopPrice"] = order.get("stopPrice")
order1["status_stop"] = "triggered"
return order1
return order
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
+1 -1
View File
@@ -37,9 +37,9 @@ class Hyperliquid(Exchange):
"stoploss_order_types": {"limit": "limit"},
"stoploss_blocks_assets": False,
"stop_price_prop": "stopPrice",
"funding_fee_timeframe": "1h",
"funding_fee_candle_limit": 500,
"uses_leverage_tiers": False,
"mark_ohlcv_price": "futures",
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
-1
View File
@@ -35,7 +35,6 @@ class Kraken(Exchange):
"trades_pagination_arg": "since",
"trades_pagination_overlap": False,
"trades_has_history": True,
"mark_ohlcv_timeframe": "4h",
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
+3 -1
View File
@@ -44,8 +44,9 @@ class Kucoin(Exchange):
amount: float,
rate: float,
leverage: float,
reduceOnly: bool = False,
time_in_force: str = "GTC",
reduceOnly: bool = False,
initial_order: bool = True,
) -> CcxtOrder:
res = super().create_order(
pair=pair,
@@ -56,6 +57,7 @@ class Kucoin(Exchange):
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
initial_order=initial_order,
)
# Kucoin returns only the order-id.
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
+3 -14
View File
@@ -29,10 +29,9 @@ class Okx(Exchange):
_ft_has: FtHas = {
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
"stoploss_query_requires_stop_flag": True,
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
"ws_enabled": True,
}
@@ -41,8 +40,8 @@ class Okx(Exchange):
"stop_price_type_field": "slTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "index",
PriceType.INDEX: "mark",
PriceType.MARK: "mark",
PriceType.INDEX: "index",
},
"stoploss_blocks_assets": False,
"ws_enabled": True,
@@ -265,16 +264,6 @@ class Okx(Exchange):
return safe_value_fallback2(order, order, "id_stop", "id")
return order["id"]
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
params1 = {"stop": True}
# 'ordType': 'conditional'
#
return self.cancel_order(
order_id=order_id,
pair=pair,
params=params1,
)
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
orders = []
@@ -18,7 +18,7 @@ class BaseClassifierModel(IFreqaiModel):
"""
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
User *must* inherit from this class and set fit(). See example scripts
such as prediction_models/CatboostClassifier.py for guidance.
such as prediction_models/XGBoostClassifier.py for guidance.
"""
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
@@ -18,7 +18,7 @@ class BaseRegressionModel(IFreqaiModel):
"""
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
User *must* inherit from this class and set fit(). See example scripts
such as prediction_models/CatboostRegressor.py for guidance.
such as prediction_models/XGBoostRegressor.py for guidance.
"""
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
+2 -2
View File
@@ -948,7 +948,7 @@ class IFreqaiModel(ABC):
return dk
# Following methods which are overridden by user made prediction models.
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
# See freqai/prediction_models/XGBoostRegressor.py for an example.
@abstractmethod
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
@@ -964,7 +964,7 @@ class IFreqaiModel(ABC):
def fit(self, data_dictionary: dict[str, Any], dk: FreqaiDataKitchen, **kwargs) -> Any:
"""
Most regressors use the same function names and arguments e.g. user
can drop in LGBMRegressor in place of CatBoostRegressor and all data
can drop in LGBMRegressor in place of XGBoostRegressor and all data
management will be properly handled by Freqai.
:param data_dictionary: Dict = the dictionary constructed by DataHandler to hold
all the training and test data/labels.
@@ -1,61 +0,0 @@
import logging
from pathlib import Path
from typing import Any
from catboost import CatBoostClassifier, Pool
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
logger = logging.getLogger(__name__)
class CatboostClassifier(BaseClassifierModel):
"""
User created prediction model. The class inherits IFreqaiModel, which
means it has full access to all Frequency AI functionality. Typically,
users would use this to override the common `fit()`, `train()`, or
`predict()` methods to add their custom data handling tools or change
various aspects of the training that cannot be configured via the
top level config.json file.
"""
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary holding all data for train, test,
labels, weights
:param dk: The datakitchen object for the current coin/model
"""
train_data = Pool(
data=data_dictionary["train_features"],
label=data_dictionary["train_labels"],
weight=data_dictionary["train_weights"],
)
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0:
test_data = None
else:
test_data = Pool(
data=data_dictionary["test_features"],
label=data_dictionary["test_labels"],
weight=data_dictionary["test_weights"],
)
cbr = CatBoostClassifier(
allow_writing_files=True,
loss_function="MultiClass",
train_dir=Path(dk.data_path),
**self.model_training_parameters,
)
init_model = self.get_init_model(dk.pair)
cbr.fit(
X=train_data,
eval_set=test_data,
init_model=init_model,
)
return cbr
@@ -1,79 +0,0 @@
import logging
from pathlib import Path
from typing import Any
from catboost import CatBoostClassifier, Pool
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
logger = logging.getLogger(__name__)
class CatboostClassifierMultiTarget(BaseClassifierModel):
"""
User created prediction model. The class inherits IFreqaiModel, which
means it has full access to all Frequency AI functionality. Typically,
users would use this to override the common `fit()`, `train()`, or
`predict()` methods to add their custom data handling tools or change
various aspects of the training that cannot be configured via the
top level config.json file.
"""
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary holding all data for train, test,
labels, weights
:param dk: The datakitchen object for the current coin/model
"""
cbc = CatBoostClassifier(
allow_writing_files=True,
loss_function="MultiClass",
train_dir=Path(dk.data_path),
**self.model_training_parameters,
)
X = data_dictionary["train_features"]
y = data_dictionary["train_labels"]
sample_weight = data_dictionary["train_weights"]
eval_sets = [None] * y.shape[1]
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) != 0:
eval_sets = [None] * data_dictionary["test_labels"].shape[1]
for i in range(data_dictionary["test_labels"].shape[1]):
eval_sets[i] = Pool(
data=data_dictionary["test_features"],
label=data_dictionary["test_labels"].iloc[:, i],
weight=data_dictionary["test_weights"],
)
init_model = self.get_init_model(dk.pair)
if init_model:
init_models = init_model.estimators_
else:
init_models = [None] * y.shape[1]
fit_params = []
for i in range(len(eval_sets)):
fit_params.append(
{
"eval_set": eval_sets[i],
"init_model": init_models[i],
}
)
model = FreqaiMultiOutputClassifier(estimator=cbc)
thread_training = self.freqai_info.get("multitarget_parallel_training", False)
if thread_training:
model.n_jobs = y.shape[1]
model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params)
return model
@@ -1,60 +0,0 @@
import logging
from pathlib import Path
from typing import Any
from catboost import CatBoostRegressor, Pool
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
logger = logging.getLogger(__name__)
class CatboostRegressor(BaseRegressionModel):
"""
User created prediction model. The class inherits IFreqaiModel, which
means it has full access to all Frequency AI functionality. Typically,
users would use this to override the common `fit()`, `train()`, or
`predict()` methods to add their custom data handling tools or change
various aspects of the training that cannot be configured via the
top level config.json file.
"""
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary holding all data for train, test,
labels, weights
:param dk: The datakitchen object for the current coin/model
"""
train_data = Pool(
data=data_dictionary["train_features"],
label=data_dictionary["train_labels"],
weight=data_dictionary["train_weights"],
)
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0:
test_data = None
else:
test_data = Pool(
data=data_dictionary["test_features"],
label=data_dictionary["test_labels"],
weight=data_dictionary["test_weights"],
)
init_model = self.get_init_model(dk.pair)
model = CatBoostRegressor(
allow_writing_files=True,
train_dir=Path(dk.data_path),
**self.model_training_parameters,
)
model.fit(
X=train_data,
eval_set=test_data,
init_model=init_model,
)
return model
@@ -1,78 +0,0 @@
import logging
from pathlib import Path
from typing import Any
from catboost import CatBoostRegressor, Pool
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
logger = logging.getLogger(__name__)
class CatboostRegressorMultiTarget(BaseRegressionModel):
"""
User created prediction model. The class inherits IFreqaiModel, which
means it has full access to all Frequency AI functionality. Typically,
users would use this to override the common `fit()`, `train()`, or
`predict()` methods to add their custom data handling tools or change
various aspects of the training that cannot be configured via the
top level config.json file.
"""
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary holding all data for train, test,
labels, weights
:param dk: The datakitchen object for the current coin/model
"""
cbr = CatBoostRegressor(
allow_writing_files=True,
train_dir=Path(dk.data_path),
**self.model_training_parameters,
)
X = data_dictionary["train_features"]
y = data_dictionary["train_labels"]
sample_weight = data_dictionary["train_weights"]
eval_sets = [None] * y.shape[1]
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) != 0:
eval_sets = [None] * data_dictionary["test_labels"].shape[1]
for i in range(data_dictionary["test_labels"].shape[1]):
eval_sets[i] = Pool(
data=data_dictionary["test_features"],
label=data_dictionary["test_labels"].iloc[:, i],
weight=data_dictionary["test_weights"],
)
init_model = self.get_init_model(dk.pair)
if init_model:
init_models = init_model.estimators_
else:
init_models = [None] * y.shape[1]
fit_params = []
for i in range(len(eval_sets)):
fit_params.append(
{
"eval_set": eval_sets[i],
"init_model": init_models[i],
}
)
model = FreqaiMultiOutputRegressor(estimator=cbr)
thread_training = self.freqai_info.get("multitarget_parallel_training", False)
if thread_training:
model.n_jobs = y.shape[1]
model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params)
return model
+3 -1
View File
@@ -97,7 +97,7 @@ def plot_feature_importance(
"""
Plot Best and worst features by importance for a single sub-train.
:param model: Any = A model which was `fit` using a common library
such as catboost or lightgbm
such as XGBoost or lightgbm
:param pair: str = pair e.g. BTC/USD
:param dk: FreqaiDataKitchen = non-persistent data container for current coin/loop
:param count_max: int = the amount of features to be loaded per column
@@ -115,6 +115,8 @@ def plot_feature_importance(
for label in models:
mdl = models[label]
if "catboost.core" in str(mdl.__class__):
# CatBoost is no longer actively supported since 2025.12
# However users can still use it in their custom models
feature_importance = mdl.get_feature_importance()
elif "lightgbm.sklearn" in str(mdl.__class__):
feature_importance = mdl.feature_importances_
+41 -23
View File
@@ -937,6 +937,7 @@ class FreqtradeBot(LoggingMixin):
reduceOnly=False,
time_in_force=time_in_force,
leverage=leverage,
initial_order=trade is None,
)
order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
order_obj.ft_order_tag = enter_tag
@@ -1063,7 +1064,16 @@ class FreqtradeBot(LoggingMixin):
return True
def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
def cancel_stoploss_on_exchange(self, trade: Trade, allow_nonblocking: bool = False) -> Trade:
"""
Cancels on exchange stoploss orders for the given trade.
:param trade: Trade for which to cancel stoploss order
:param allow_nonblocking: If True, will skip cancelling stoploss on exchange
if the exchange supports blocking stoploss orders.
"""
if allow_nonblocking and not self.exchange.get_option("stoploss_blocks_assets", True):
logger.info(f"Skipping cancelling stoploss on exchange for {trade}.")
return trade
# First cancelling stoploss on exchange ...
for oslo in trade.open_sl_orders:
try:
@@ -2002,14 +2012,14 @@ class FreqtradeBot(LoggingMixin):
def _safe_exit_amount(self, trade: Trade, pair: str, amount: float) -> float:
"""
Get sellable amount.
Get exitable amount.
Should be trade.amount - but will fall back to the available amount if necessary.
This should cover cases where get_real_amount() was not able to update the amount
for whatever reason.
:param trade: Trade we're working with
:param pair: Pair we're trying to sell
:param pair: Pair we're trying to exit
:param amount: amount we expect to be available
:return: amount to sell
:return: amount to exit
:raise: DependencyException: if available balance is not within 2% of the available amount.
"""
# Update wallets to ensure amounts tied up in a stoploss is now free!
@@ -2045,11 +2055,12 @@ class FreqtradeBot(LoggingMixin):
exit_tag: str | None = None,
ordertype: str | None = None,
sub_trade_amt: float | None = None,
skip_custom_exit_price: bool = False,
) -> bool:
"""
Executes a trade exit for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:param limit: limit rate for the exit order
:param exit_check: CheckTuple with signal and reason
:return: True if it succeeds False
"""
@@ -2071,29 +2082,33 @@ class FreqtradeBot(LoggingMixin):
):
exit_type = "stoploss"
order_type = (
(ordertype or self.strategy.order_types[exit_type])
if exit_check.exit_type != ExitType.EMERGENCY_EXIT
else self.strategy.order_types.get("emergency_exit", "market")
)
# set custom_exit_price if available
proposed_limit_rate = limit
custom_exit_price = limit
current_profit = trade.calc_profit_ratio(limit)
custom_exit_price = strategy_safe_wrapper(
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
)(
pair=trade.pair,
trade=trade,
current_time=datetime.now(UTC),
proposed_rate=proposed_limit_rate,
current_profit=current_profit,
exit_tag=exit_reason,
)
if order_type == "limit" and not skip_custom_exit_price:
custom_exit_price = strategy_safe_wrapper(
self.strategy.custom_exit_price, default_retval=proposed_limit_rate
)(
pair=trade.pair,
trade=trade,
current_time=datetime.now(UTC),
proposed_rate=proposed_limit_rate,
current_profit=current_profit,
exit_tag=exit_reason,
)
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
# First cancelling stoploss on exchange ...
trade = self.cancel_stoploss_on_exchange(trade)
order_type = ordertype or self.strategy.order_types[exit_type]
if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
# Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergency_exit", "market")
trade = self.cancel_stoploss_on_exchange(trade, allow_nonblocking=True)
amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
time_in_force = self.strategy.order_time_in_force["exit"]
@@ -2121,7 +2136,7 @@ class FreqtradeBot(LoggingMixin):
return False
try:
# Execute sell and update trade record
# Execute exit and update trade record
order = self.exchange.create_order(
pair=trade.pair,
ordertype=order_type,
@@ -2131,6 +2146,7 @@ class FreqtradeBot(LoggingMixin):
leverage=trade.leverage,
reduceOnly=self.trading_mode == TradingMode.FUTURES,
time_in_force=time_in_force,
initial_order=False,
)
except InsufficientFundsError as e:
logger.warning(f"Unable to place order {e}.")
@@ -2148,7 +2164,7 @@ class FreqtradeBot(LoggingMixin):
trade.exit_reason = exit_reason
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
# In case of market sell orders the order can be closed immediately
# In case of market exit orders the order can be closed immediately
if order.get("status", "unknown") in ("closed", "expired"):
self.update_trade_state(trade, order_obj.order_id, order)
Trade.commit()
@@ -2378,6 +2394,8 @@ class FreqtradeBot(LoggingMixin):
self.strategy.ft_stoploss_adjust(
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
)
if not trade.is_open:
self.cancel_stoploss_on_exchange(trade)
# Updating wallets when order is closed
self.wallets.update()
return trade
+1
View File
@@ -374,6 +374,7 @@ class Backtesting:
timerange=self.timerange,
startup_candles=0,
fail_without_data=True,
fill_up_missing=False,
data_format=self.config["dataformat_ohlcv"],
candle_type=CandleType.FUNDING_RATE,
)
+5 -4
View File
@@ -48,7 +48,7 @@ from freqtrade.leverage import interest
from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.base import ModelBase, SessionType
from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none
from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none, round_value
logger = logging.getLogger(__name__)
@@ -654,9 +654,10 @@ class LocalTrade:
)
return (
f"Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, "
f"is_short={self.is_short or False}, leverage={self.leverage or 1.0}, "
f"open_rate={self.open_rate:.8f}, open_since={open_since})"
f"Trade(id={self.id}, pair={self.pair}, amount={round_value(self.amount, 8)}, "
f"is_short={self.is_short or False}, "
f"leverage={round_value(self.leverage or 1.0, 1)}, "
f"open_rate={round_value(self.open_rate, 8)}, open_since={open_since})"
)
def to_json(self, minified: bool = False) -> dict[str, Any]:
+8 -12
View File
@@ -52,29 +52,25 @@ def __run_backtest_bg(btconfig: Config):
lastconfig = ApiBG.bt["last_config"]
strat = StrategyResolver.load_strategy(btconfig)
validate_config_consistency(btconfig)
if (
not ApiBG.bt["bt"]
or lastconfig.get("timeframe") != strat.timeframe
time_settings_changed = (
lastconfig.get("timeframe") != strat.timeframe
or lastconfig.get("timeframe_detail") != btconfig.get("timeframe_detail")
or lastconfig.get("timerange") != btconfig["timerange"]
):
)
if not ApiBG.bt["bt"] or time_settings_changed:
from freqtrade.optimize.backtesting import Backtesting
ApiBG.bt["bt"] = Backtesting(btconfig)
else:
ApiBG.bt["bt"].config = deep_merge_dicts(btconfig, ApiBG.bt["bt"].config)
ApiBG.bt["bt"].init_backtest()
# Only reload data if timeframe changed.
if (
not ApiBG.bt["data"]
or not ApiBG.bt["timerange"]
or lastconfig.get("timeframe") != strat.timeframe
or lastconfig.get("timerange") != btconfig["timerange"]
):
# Only reload data if timerange is open or settings changed
if not ApiBG.bt["data"] or not ApiBG.bt["timerange"] or time_settings_changed:
ApiBG.bt["data"], ApiBG.bt["timerange"] = ApiBG.bt["bt"].load_bt_data()
lastconfig["timerange"] = btconfig["timerange"]
lastconfig["timeframe_detail"] = btconfig.get("timeframe_detail")
lastconfig["timeframe"] = strat.timeframe
lastconfig["enable_protections"] = btconfig.get("enable_protections")
lastconfig["dry_run_wallet"] = btconfig.get("dry_run_wallet")
@@ -63,6 +63,8 @@ def pairlists_evaluate(
config_loc["timeframes"] = payload.timeframes
config_loc["erase"] = payload.erase
config_loc["download_trades"] = payload.download_trades
if payload.candle_types is not None:
config_loc["candle_types"] = payload.candle_types
handleExchangePayload(payload, config_loc)
+2
View File
@@ -426,6 +426,7 @@ class ForceExitPayload(BaseModel):
tradeid: str | int
ordertype: OrderTypeValues | None = None
amount: float | None = None
price: float | None = None
class BlacklistPayload(BaseModel):
@@ -506,6 +507,7 @@ class DownloadDataPayload(ExchangeModePayloadMixin, BaseModel):
timerange: str | None = None
erase: bool = False
download_trades: bool = False
candle_types: list[str] | None = None
@model_validator(mode="before")
def check_mutually_exclusive(cls, values):
+6 -2
View File
@@ -91,7 +91,9 @@ logger = logging.getLogger(__name__)
# 2.41: Add download-data endpoint
# 2.42: Add /pair_history endpoint with live data
# 2.43: Add /profit_all endpoint
API_VERSION = 2.43
# 2.44: Add candle_types parameter to download-data endpoint
# 2.45: Add price to forceexit endpoint
API_VERSION = 2.45
# Public API, requires no auth.
router_public = APIRouter()
@@ -324,7 +326,9 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
@router.post("/forcesell", response_model=ResultMsg, tags=["trading"])
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_force_exit(str(payload.tradeid), ordertype, amount=payload.amount)
return rpc._rpc_force_exit(
str(payload.tradeid), ordertype, amount=payload.amount, price=payload.price
)
@router.get("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"])
+26 -7
View File
@@ -940,7 +940,11 @@ class RPC:
return {"status": "Reloaded from orders from exchange"}
def __exec_force_exit(
self, trade: Trade, ordertype: str | None, amount: float | None = None
self,
trade: Trade,
ordertype: str | None,
amount: float | None = None,
price: float | None = None,
) -> bool:
# Check if there is there are open orders
trade_entry_cancelation_registry = []
@@ -964,8 +968,13 @@ class RPC:
# Order cancellation failed, so we can't exit.
return False
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True
current_rate = (
self._freqtrade.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True
)
if ordertype == "market" or price is None
else price
)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
@@ -983,18 +992,28 @@ class RPC:
sub_amount = amount
self._freqtrade.execute_trade_exit(
trade, current_rate, exit_check, ordertype=order_type, sub_trade_amt=sub_amount
trade,
current_rate,
exit_check,
ordertype=order_type,
sub_trade_amt=sub_amount,
skip_custom_exit_price=price is not None and ordertype == "limit",
)
return True
return False
def _rpc_force_exit(
self, trade_id: str, ordertype: str | None = None, *, amount: float | None = None
self,
trade_id: str,
ordertype: str | None = None,
*,
amount: float | None = None,
price: float | None = None,
) -> dict[str, str]:
"""
Handler for forceexit <id>.
Sells the given trade at current price
exits the given trade. Uses current price if price is None.
"""
if self._freqtrade.state == State.STOPPED:
@@ -1024,7 +1043,7 @@ class RPC:
logger.warning("force_exit: Invalid argument received")
raise RPCException("invalid argument")
result = self.__exec_force_exit(trade, ordertype, amount)
result = self.__exec_force_exit(trade, ordertype, amount, price)
Trade.commit()
self._freqtrade.wallets.update()
if not result:
+6 -3
View File
@@ -104,8 +104,11 @@ def _create_and_merge_informative_pair(
):
asset = inf_data.asset or ""
timeframe = inf_data.timeframe
timeframe1 = inf_data.timeframe
fmt = inf_data.fmt
candle_type = inf_data.candle_type
if candle_type == CandleType.FUNDING_RATE:
timeframe1 = strategy.dp.get_funding_rate_timeframe()
config = strategy.config
@@ -132,10 +135,10 @@ def _create_and_merge_informative_pair(
fmt = "{base}_{quote}_" + fmt # Informatives of other pairs
inf_metadata = {"pair": asset, "timeframe": timeframe}
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe, candle_type)
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe1, candle_type)
if inf_dataframe.empty:
raise ValueError(
f"Informative dataframe for ({asset}, {timeframe}, {candle_type}) is empty. "
f"Informative dataframe for ({asset}, {timeframe1}, {candle_type}) is empty. "
"Can't populate informative indicators."
)
inf_dataframe = populate_indicators_fn(strategy, inf_dataframe, inf_metadata)
@@ -163,7 +166,7 @@ def _create_and_merge_informative_pair(
dataframe,
inf_dataframe,
strategy.timeframe,
timeframe,
timeframe1,
ffill=inf_data.ffill,
append_timeframe=False,
date_column=date_column,
+1 -1
View File
@@ -1718,7 +1718,7 @@ class IStrategy(ABC, HyperStrategyMixin):
timeout_unit = self.config.get("unfilledtimeout", {}).get("unit", "minutes")
timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = current_time + timedelta(**timeout_kwargs)
timedout = order.status == "open" and order.order_date_utc < timeout_threshold
timedout = order.status == "open" and order.order_date_utc <= timeout_threshold
if timedout:
return True
time_method = (
@@ -20,7 +20,7 @@ class FreqaiExampleHybridStrategy(IStrategy):
Launching this strategy would be:
freqtrade trade --strategy FreqaiExampleHybridStrategy --strategy-path freqtrade/templates
--freqaimodel CatboostClassifier --config config_examples/config_freqai.example.json
--freqaimodel XGBoostClassifier --config config_examples/config_freqai.example.json
or the user simply adds this to their config:
+2 -2
View File
@@ -205,8 +205,8 @@ class FreqaiExampleStrategy(IStrategy):
# If user wishes to use multiple targets, they can add more by
# appending more columns with '&'. User should keep in mind that multi targets
# requires a multioutput prediction model such as
# freqai/prediction_models/CatboostRegressorMultiTarget.py,
# freqtrade trade --freqaimodel CatboostRegressorMultiTarget
# freqai/prediction_models/LightGBMClassifierMultiTarget.py,
# freqtrade trade --freqaimodel LightGBMClassifierMultiTarget
# df["&-s_range"] = (
# df["close"]
+3 -2
View File
@@ -90,15 +90,16 @@ def dt_humanize_delta(dt: datetime):
return humanize.naturaltime(dt)
def format_date(date: datetime | None) -> str:
def format_date(date: datetime | None, fallback: str = "") -> str:
"""
Return a formatted date string.
Returns an empty string if date is None.
:param date: datetime to format
:param fallback: value to return if date is None
"""
if date:
return date.strftime(DATETIME_PRINT_FORMAT)
return ""
return fallback
def format_ms_time(date: int | float) -> str:
+2 -2
View File
@@ -23,7 +23,7 @@ def strip_trailing_zeros(value: str) -> str:
return value.rstrip("0").rstrip(".")
def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
def round_value(value: float | None, decimals: int, keep_trailing_zeros=False) -> str:
"""
Round value to given decimals
:param value: Value to be rounded
@@ -31,7 +31,7 @@ def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str:
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
:return: Rounded value as string
"""
if isnan(value):
if value is None or isnan(value):
return "N/A"
val = f"{value:.{decimals}f}"
if not keep_trailing_zeros:
+1 -1
View File
@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.11.1"
__version__ = "2025.12"
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
python-rapidjson==1.22
python-rapidjson==1.23
+1 -1
View File
@@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: MacOS",
"Operating System :: Unix",
"Topic :: Office/Business :: Financial :: Investment",
@@ -85,7 +86,6 @@ hyperopt = [
freqai = [
"scikit-learn",
"joblib",
"catboost; platform_machine != 'arm'",
"lightgbm",
"xgboost",
"tensorboard",
+7 -6
View File
@@ -6,10 +6,10 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.14.5
mypy==1.18.2
pre-commit==4.4.0
pytest==9.0.1
ruff==0.14.9
mypy==1.19.1
pre-commit==4.5.1
pytest==9.0.2
pytest-asyncio==1.3.0
pytest-cov==7.0.0
pytest-mock==3.15.1
@@ -18,15 +18,16 @@ pytest-timeout==2.4.0
pytest-xdist==3.8.0
isort==7.0.0
# For datetime mocking
time-machine==3.0.0
time-machine==3.2.0
# Convert jupyter notebooks to markdown documents
nbconvert==7.16.6
# mypy types
scipy-stubs==1.16.3.0 # keep in sync with `scipy` in `requirements-hyperopt.txt`
scipy-stubs==1.16.3.3 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.2.0.20251022
types-filelock==3.2.7
types-requests==2.32.4.20250913
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20251115
pip-audit==2.10.0
+1 -1
View File
@@ -5,7 +5,7 @@
torch==2.9.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==1.2.2
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
stable_baselines3==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
sb3_contrib>=2.2.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
# Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.1
+3 -4
View File
@@ -3,10 +3,9 @@
-r requirements-plot.txt
# Required for freqai
scikit-learn==1.7.2
joblib==1.5.2
catboost==1.2.8; 'arm' not in platform_machine
scikit-learn==1.8.0
joblib==1.5.3
lightgbm==4.6.0
xgboost==3.1.1
xgboost==3.1.2
tensorboard==2.20.0
datasieve==0.1.9
+2 -2
View File
@@ -3,7 +3,7 @@
# Required for hyperopt
scipy==1.16.3
scikit-learn==1.7.2
filelock==3.20.0
scikit-learn==1.8.0
filelock==3.20.1
optuna==4.6.0
cmaes==0.12.0
+9 -9
View File
@@ -7,38 +7,38 @@ ft-pandas-ta==0.3.16
ta-lib==0.6.8
technical==1.5.3
ccxt==4.5.27
ccxt==4.5.29
cryptography==46.0.3
aiohttp==3.13.2
SQLAlchemy==2.0.44
SQLAlchemy==2.0.45
python-telegram-bot==22.5
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.14.0
cachetools==6.2.2
cachetools==6.2.4
requests==2.32.5
urllib3==2.5.0
urllib3==2.6.2
certifi==2025.11.12
jsonschema==4.25.1
tabulate==0.9.0
pycoingecko==3.2.0
jinja2==3.1.6
joblib==1.5.2
joblib==1.5.3
rich==14.2.0
pyarrow==22.0.0; platform_machine != 'armv7l'
# Load ticker files 30% faster
python-rapidjson==1.22
python-rapidjson==1.23
# Properly format api responses
orjson==3.11.4
orjson==3.11.5
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.121.3
pydantic==2.12.4
fastapi==0.125.0
pydantic==2.12.5
uvicorn==0.38.0
pyjwt==2.10.1
aiofiles==25.1.0
+3 -3
View File
@@ -1767,7 +1767,7 @@ def test_start_list_data(testdatadir, capsys):
pargs["config"] = None
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 16 pair / timeframe combinations." in captured.out
assert "Found 18 pair / timeframe combinations." in captured.out
assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out)
assert re.search(r"\n.* UNITTEST/BTC .* 1m, 5m, 8m, 30m .* spot |\n", captured.out)
@@ -1801,10 +1801,10 @@ def test_start_list_data(testdatadir, capsys):
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 6 pair / timeframe combinations." in captured.out
assert "Found 5 pair / timeframe combinations." in captured.out
assert re.search(r".*Pair.*Timeframe.*Type.*\n", captured.out)
assert re.search(r"\n.* XRP/USDT:USDT .* 5m, 1h .* futures |\n", captured.out)
assert re.search(r"\n.* XRP/USDT:USDT .* 1h, 8h .* mark |\n", captured.out)
assert re.search(r"\n.* XRP/USDT:USDT .* 1h.* mark |\n", captured.out)
args = [
"list-data",
+8 -5
View File
@@ -290,20 +290,23 @@ def test_combine_dataframes_with_mean(testdatadir):
def test_combined_dataframes_with_rel_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
pairs = ["BTC/USDT", "XRP/USDT"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
df = combined_dataframes_with_rel_mean(
data, datetime(2018, 1, 12, tzinfo=UTC), datetime(2018, 1, 28, tzinfo=UTC)
data,
fromdt=data["BTC/USDT"].at[0, "date"],
todt=data["BTC/USDT"].at[data["BTC/USDT"].index[-1], "date"],
)
assert isinstance(df, DataFrame)
assert "ETH/BTC" not in df.columns
assert "ADA/BTC" not in df.columns
assert "BTC/USDT" not in df.columns
assert "XRP/USDT" not in df.columns
assert "mean" in df.columns
assert "rel_mean" in df.columns
assert "count" in df.columns
assert df.iloc[0]["count"] == 2
assert df.iloc[-1]["count"] == 2
assert len(df) < len(data["ETH/BTC"])
assert len(df) < len(data["BTC/USDT"])
assert df["rel_mean"].between(-0.5, 0.5).all()
def test_combine_dataframes_with_mean_no_data(testdatadir):
+1 -9
View File
@@ -39,12 +39,6 @@ def populate_dataframe_with_trades_trades(testdatadir):
return pd.read_feather(testdatadir / "orderflow/populate_dataframe_with_trades_TRADES.feather")
@pytest.fixture
def candles(testdatadir):
# TODO: this fixture isn't really necessary and could be removed
return pd.read_json(testdatadir / "orderflow/candles.json").copy()
@pytest.fixture
def public_trades_list(testdatadir):
return read_csv(testdatadir / "orderflow/public_trades_list.csv").copy()
@@ -293,7 +287,7 @@ def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
assert t["price"] == 234.72
def test_public_trades_put_volume_profile_into_ohlcv_candles(public_trades_list_simple, candles):
def test_public_trades_put_volume_profile_into_ohlcv_candles(public_trades_list_simple):
"""
Tests the integration of volume profile data into OHLCV candles.
@@ -412,13 +406,11 @@ def test_public_trades_config_max_trades(
def test_public_trades_testdata_sanity(
candles,
public_trades_list,
public_trades_list_simple,
populate_dataframe_with_trades_dataframe,
populate_dataframe_with_trades_trades,
):
assert 10999 == len(candles)
assert 1000 == len(public_trades_list)
assert 999 == len(populate_dataframe_with_trades_dataframe)
assert 293532 == len(populate_dataframe_with_trades_trades)
+5 -2
View File
@@ -40,6 +40,8 @@ def test_datahandler_ohlcv_get_pairs(testdatadir):
"NXT/BTC",
"DASH/BTC",
"XRP/ETH",
"BTC/USDT",
"XRP/USDT",
}
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, "8m", candle_type=CandleType.SPOT)
@@ -111,6 +113,8 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
("DASH/BTC", "5m", CandleType.SPOT),
("XRP/ETH", "1m", CandleType.SPOT),
("XRP/ETH", "5m", CandleType.SPOT),
("BTC/USDT", "5m", CandleType.SPOT),
("XRP/USDT", "5m", CandleType.SPOT),
("UNITTEST/BTC", "30m", CandleType.SPOT),
("UNITTEST/BTC", "8m", CandleType.SPOT),
}
@@ -122,8 +126,7 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
("XRP/USDT:USDT", "5m", "futures"),
("XRP/USDT:USDT", "1h", "futures"),
("XRP/USDT:USDT", "1h", "mark"),
("XRP/USDT:USDT", "8h", "mark"),
("XRP/USDT:USDT", "8h", "funding_rate"),
("XRP/USDT:USDT", "1h", "funding_rate"),
}
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT)
+41 -1
View File
@@ -9,7 +9,7 @@ from freqtrade.enums import CandleType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.util import dt_utc
from tests.conftest import EXMS, generate_test_data, get_patched_exchange
from tests.conftest import EXMS, generate_test_data, get_patched_exchange, log_has_re
@pytest.mark.parametrize(
@@ -185,6 +185,28 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type):
assert len(df) == 2 # ohlcv_history is limited to 2 rows now
def test_get_pair_dataframe_funding_rate(mocker, default_conf, ohlcv_history, caplog):
default_conf["runmode"] = RunMode.DRY_RUN
timeframe = "1h"
exchange = get_patched_exchange(mocker, default_conf)
candletype = CandleType.FUNDING_RATE
exchange._klines[("XRP/BTC", timeframe, candletype)] = ohlcv_history
exchange._klines[("UNITTEST/BTC", timeframe, candletype)] = ohlcv_history
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN
assert ohlcv_history.equals(
dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type="funding_rate")
)
msg = r".*funding rate timeframe not matching"
assert not log_has_re(msg, caplog)
assert ohlcv_history.equals(
dp.get_pair_dataframe("UNITTEST/BTC", "5h", candle_type="funding_rate")
)
assert log_has_re(msg, caplog)
def test_available_pairs(mocker, default_conf, ohlcv_history):
exchange = get_patched_exchange(mocker, default_conf)
timeframe = default_conf["timeframe"]
@@ -636,3 +658,21 @@ def test_check_delisting(mocker, default_conf_usdt):
assert res == dt_utc(2025, 10, 2)
assert delist_mock2.call_count == 1
def test_get_funding_rate_timeframe(mocker, default_conf_usdt):
default_conf_usdt["trading_mode"] = "futures"
default_conf_usdt["margin_mode"] = "isolated"
exchange = get_patched_exchange(mocker, default_conf_usdt)
mock_get_option = mocker.spy(exchange, "get_option")
dp = DataProvider(default_conf_usdt, exchange)
assert dp.get_funding_rate_timeframe() == "1h"
mock_get_option.assert_called_once_with("funding_fee_timeframe")
def test_get_funding_rate_timeframe_no_exchange(default_conf_usdt):
dp = DataProvider(default_conf_usdt, None)
with pytest.raises(OperationalException, match=r"Exchange is not available to DataProvider."):
dp.get_funding_rate_timeframe()
+29 -9
View File
@@ -534,18 +534,19 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
@pytest.mark.parametrize(
"trademode,callcount",
"trademode,callcount, callcount_parallel",
[
("spot", 4),
("margin", 4),
("futures", 8), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
("spot", 4, 2),
("margin", 4, 2),
("futures", 8, 4), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
],
)
def test_refresh_backtest_ohlcv_data(
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount, callcount_parallel
):
caplog.set_level(logging.DEBUG)
dl_mock = mocker.patch("freqtrade.data.history.history_utils._download_pair_history")
mocker.patch(f"{EXMS}.verify_candle_type_support", MagicMock())
def parallel_mock(pairs, timeframe, candle_type, **kwargs):
return {(pair, timeframe, candle_type): DataFrame() for pair in pairs}
@@ -573,14 +574,15 @@ def test_refresh_backtest_ohlcv_data(
)
# Called once per timeframe (as we return an empty dataframe)
assert parallel_mock.call_count == 2
# called twice for spot/margin and 4 times for futures
assert parallel_mock.call_count == callcount_parallel
assert dl_mock.call_count == callcount
assert dl_mock.call_args[1]["timerange"].starttype == "date"
assert log_has_re(r"Downloading pair ETH/BTC, .* interval 1m\.", caplog)
if trademode == "futures":
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 8h\.", caplog)
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 4h\.", caplog)
assert log_has_re(r"Downloading pair ETH/BTC, funding_rate, interval 1h\.", caplog)
assert log_has_re(r"Downloading pair ETH/BTC, mark, interval 1h\.", caplog)
# Test with only one pair - no parallel download should happen 1 pair/timeframe combination
# doesn't justify parallelization
@@ -599,6 +601,24 @@ def test_refresh_backtest_ohlcv_data(
)
assert parallel_mock.call_count == 0
if trademode == "futures":
dl_mock.reset_mock()
refresh_backtest_ohlcv_data(
exchange=ex,
pairs=[
"ETH/BTC",
],
timeframes=["5m", "1h"],
datadir=testdatadir,
timerange=timerange,
erase=False,
trading_mode=trademode,
no_parallel_download=True,
candle_types=["premiumIndex", "funding_rate"],
)
assert parallel_mock.call_count == 0
assert dl_mock.call_count == 3 # 2 timeframes premiumIndex + 1x funding_rate
def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
dl_mock = mocker.patch(
@@ -896,7 +916,7 @@ def test_download_pair_history_with_pair_candles(mocker, default_conf, tmp_path,
assert get_historic_ohlcv_mock.call_count == 0
# Verify the log message indicating parallel method was used (line 315-316)
assert log_has("Downloaded data for TEST/BTC with length 3. Parallel Method.", caplog)
assert log_has("Downloaded data for TEST/BTC, 5m, spot with length 3. Parallel Method.", caplog)
# Verify data was stored
assert data_handler_mock.ohlcv_store.call_count == 1
+2 -1
View File
@@ -157,7 +157,8 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
assert "type" in order
assert order["type"] == order_type
assert order["price"] == 220
assert order["price"] == 217.8
assert order["stopPrice"] == 220
assert order["amount"] == 1
+207 -19
View File
@@ -1012,7 +1012,7 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
ex._ft_has["ohlcv_has_history"] = False
with pytest.raises(
OperationalException,
match=r"This strategy requires 2500.*, " r"which is more than the amount.*",
match=r"This strategy requires 2500.*, " r"which is more than .* the amount",
):
ex.validate_required_startup_candles(2500, "5m")
@@ -1111,21 +1111,29 @@ def test_create_dry_run_order_fees(
@pytest.mark.parametrize(
"side,limit,offset,expected",
"side,limit,offset,is_stop,expected",
[
("buy", 46.0, 0.0, True),
("buy", 26.0, 0.0, True),
("buy", 25.55, 0.0, False),
("buy", 1, 0.0, False), # Very far away
("sell", 25.5, 0.0, True),
("sell", 50, 0.0, False), # Very far away
("sell", 25.58, 0.0, False),
("sell", 25.563, 0.01, False),
("sell", 5.563, 0.01, True),
("buy", 46.0, 0.0, False, True),
("buy", 46.0, 0.0, True, False),
("buy", 26.0, 0.0, False, True),
("buy", 26.0, 0.0, True, False), # Stop - didn't trigger
("buy", 25.55, 0.0, False, False),
("buy", 25.55, 0.0, True, True), # Stop - triggered
("buy", 1, 0.0, False, False), # Very far away
("buy", 1, 0.0, True, True), # Current price is above stop - triggered
("sell", 25.5, 0.0, False, True),
("sell", 50, 0.0, False, False), # Very far away
("sell", 25.58, 0.0, False, False),
("sell", 25.563, 0.01, False, False),
("sell", 25.563, 0.0, True, False), # stop order - Not triggered, best bid
("sell", 25.566, 0.0, True, True), # stop order - triggered
("sell", 26, 0.01, True, True), # stop order - triggered
("sell", 5.563, 0.01, False, True),
("sell", 5.563, 0.0, True, False), # stop order - not triggered
],
)
def test__dry_is_price_crossed_with_orderbook(
default_conf, mocker, order_book_l2_usd, side, limit, offset, expected
default_conf, mocker, order_book_l2_usd, side, limit, offset, is_stop, expected
):
# Best bid 25.563
# Best ask 25.566
@@ -1134,14 +1142,14 @@ def test__dry_is_price_crossed_with_orderbook(
exchange.fetch_l2_order_book = order_book_l2_usd
orderbook = order_book_l2_usd.return_value
result = exchange._dry_is_price_crossed(
"LTC/USDT", side, limit, orderbook=orderbook, offset=offset
"LTC/USDT", side, limit, orderbook=orderbook, offset=offset, is_stop=is_stop
)
assert result is expected
assert order_book_l2_usd.call_count == 0
# Test without passing orderbook
order_book_l2_usd.reset_mock()
result = exchange._dry_is_price_crossed("LTC/USDT", side, limit, offset=offset)
result = exchange._dry_is_price_crossed("LTC/USDT", side, limit, offset=offset, is_stop=is_stop)
assert result is expected
@@ -1165,7 +1173,10 @@ def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
exchange.fetch_l2_order_book = MagicMock()
mocker.patch(f"{EXMS}.exchange_has", return_value=False)
assert exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0)
assert exchange._dry_is_price_crossed("LTC/USDT", "sell", 1.0)
assert exchange.fetch_l2_order_book.call_count == 0
assert not exchange._dry_is_price_crossed("LTC/USDT", "buy", 1.0, is_stop=True)
assert not exchange._dry_is_price_crossed("LTC/USDT", "sell", 1.0, is_stop=True)
@pytest.mark.parametrize(
@@ -1176,7 +1187,7 @@ def test__dry_is_price_crossed_without_orderbook_support(default_conf, mocker):
(False, False, "sell", 1.0, "open", None, 0, None),
],
)
def test_check_dry_limit_order_filled_parametrized(
def test_check_dry_limit_order_filled(
default_conf,
mocker,
crossed,
@@ -1220,6 +1231,70 @@ def test_check_dry_limit_order_filled_parametrized(
assert fee_mock.call_count == expected_calls
@pytest.mark.parametrize(
"immediate,crossed,expected_status,expected_fee_type",
[
(True, True, "closed", "taker"),
(False, True, "closed", "maker"),
(True, False, "open", None),
],
)
def test_check_dry_limit_order_filled_stoploss(
default_conf, mocker, immediate, crossed, expected_status, expected_fee_type, order_book_l2_usd
):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.multiple(
EXMS,
exchange_has=MagicMock(return_value=True),
_dry_is_price_crossed=MagicMock(return_value=crossed),
fetch_l2_order_book=order_book_l2_usd,
)
average_mock = mocker.patch(f"{EXMS}.get_dry_market_fill_price", return_value=24.25)
fee_mock = mocker.patch(
f"{EXMS}.add_dry_order_fee",
autospec=True,
side_effect=lambda self, pair, dry_order, taker_or_maker: dry_order,
)
amount = 1.75
order = {
"symbol": "LTC/USDT",
"status": "open",
"type": "limit",
"side": "sell",
"amount": amount,
"filled": 0.0,
"remaining": amount,
"price": 25.0,
"average": 0.0,
"cost": 0.0,
"fee": None,
"ft_order_type": "stoploss",
"stopLossPrice": 24.5,
}
result = exchange.check_dry_limit_order_filled(order, immediate=immediate)
assert result["status"] == expected_status
assert order_book_l2_usd.call_count == 1
if crossed:
assert result["filled"] == amount
assert result["remaining"] == 0
assert result["average"] == 24.25
assert result["cost"] == pytest.approx(amount * 24.25)
assert average_mock.call_count == 1
assert fee_mock.call_count == 1
assert fee_mock.call_args[0][1] == "LTC/USDT"
assert fee_mock.call_args[0][3] == expected_fee_type
else:
assert result["filled"] == 0.0
assert result["remaining"] == amount
assert result["average"] == 0.0
assert average_mock.call_count == 0
assert fee_mock.call_count == 0
@pytest.mark.parametrize(
"side,price,filled,converted",
[
@@ -2314,6 +2389,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
]
]
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
mocker.patch.object(exchange, "verify_candle_type_support")
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
@@ -2364,6 +2440,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf_usdt, caplog, candle_type) ->
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf_usdt)
mocker.patch.object(exchange, "verify_candle_type_support")
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pairs = [("IOTA/USDT", "5m", candle_type), ("XRP/USDT", "5m", candle_type)]
@@ -2614,6 +2691,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach
time_machine.move_to(start + timedelta(hours=99, minutes=30))
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(exchange, "verify_candle_type_support")
exchange._set_startup_candle_count(default_conf)
mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100)
@@ -2762,6 +2840,29 @@ def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None:
assert ohlcv_mock.call_args_list[0][0][0] == pairs
def test_refresh_latest_ohlcv_funding_rate(mocker, default_conf_usdt, caplog) -> None:
ohlcv = generate_test_data_raw("1h", 24, "2025-01-02 12:00:00+00:00")
funding_data = [{"timestamp": x[0], "fundingRate": x[1]} for x in ohlcv]
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf_usdt)
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
exchange._api_async.fetch_funding_rate_history = get_mock_coro(funding_data)
pairs = [
("IOTA/USDT:USDT", "8h", CandleType.FUNDING_RATE),
("XRP/USDT:USDT", "1h", CandleType.FUNDING_RATE),
]
# empty dicts
assert not exchange._klines
res = exchange.refresh_latest_ohlcv(pairs, cache=False)
assert len(res) == len(pairs)
assert log_has_re(r"Wrong funding rate timeframe 8h for pair IOTA/USDT:USDT", caplog)
assert not log_has_re(r"Wrong funding rate timeframe 8h for pair XRP/USDT:USDT", caplog)
assert exchange._api_async.fetch_ohlcv.call_count == 0
@pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
ohlcv = [
@@ -5221,6 +5322,7 @@ def test_combine_funding_and_mark(
{"date": trade_date, "open": mark_price},
]
)
# Test fallback to futures funding rate for missing funding rates
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
if futures_funding_rate is not None:
@@ -5248,6 +5350,34 @@ def test_combine_funding_and_mark(
assert len(df) == 0
# Test fallback to futures funding rate for middle missing funding rate
funding_rates = DataFrame(
[
{"date": prior2_date, "open": funding_rate},
# missing 1 hour
{"date": trade_date, "open": funding_rate},
],
)
mark_rates = DataFrame(
[
{"date": prior2_date, "open": mark_price},
{"date": prior_date, "open": mark_price},
{"date": trade_date, "open": mark_price},
]
)
df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate)
if futures_funding_rate is not None:
assert len(df) == 2
assert df.iloc[0]["open_fund"] == funding_rate
# assert df.iloc[1]["open_fund"] == futures_funding_rate
assert df.iloc[-1]["open_fund"] == funding_rate
# Mid-candle is dropped ...
assert df["date"].to_list() == [prior2_date, trade_date]
else:
assert len(df) == 2
assert df["date"].to_list() == [prior2_date, trade_date]
@pytest.mark.parametrize(
"exchange,rate_start,rate_end,d1,d2,amount,expected_fees",
@@ -5337,8 +5467,13 @@ def test__fetch_and_calculate_funding_fees(
api_mock = MagicMock()
api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history)
api_mock.fetch_ohlcv = get_mock_coro(return_value=mark_ohlcv)
type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True})
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True})
type(api_mock).has = PropertyMock(
return_value={
"fetchFundingRateHistory": True,
"fetchMarkOHLCV": True,
"fetchOHLCV": True,
}
)
ex = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange)
mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["1h", "4h", "8h"]))
@@ -5382,8 +5517,13 @@ def test__fetch_and_calculate_funding_fees_datetime_called(
api_mock.fetch_funding_rate_history = get_mock_coro(
return_value=funding_rate_history_octohourly
)
type(api_mock).has = PropertyMock(return_value={"fetchOHLCV": True})
type(api_mock).has = PropertyMock(return_value={"fetchFundingRateHistory": True})
type(api_mock).has = PropertyMock(
return_value={
"fetchFundingRateHistory": True,
"fetchMarkOHLCV": True,
"fetchOHLCV": True,
}
)
mocker.patch(f"{EXMS}.timeframes", PropertyMock(return_value=["4h", "8h"]))
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange)
d1 = datetime.strptime("2021-08-31 23:00:01 +0000", "%Y-%m-%d %H:%M:%S %z")
@@ -6470,3 +6610,51 @@ def test_fetch_funding_rate(default_conf, mocker, exchange_name):
with pytest.raises(DependencyException, match=r"Pair XRP/ETH not available"):
exchange.fetch_funding_rate(pair="XRP/ETH")
def test_verify_candle_type_support(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).has = PropertyMock(
return_value={
"fetchFundingRateHistory": True,
"fetchIndexOHLCV": True,
"fetchMarkOHLCV": True,
"fetchPremiumIndexOHLCV": False,
}
)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Should pass
exchange.verify_candle_type_support("futures")
exchange.verify_candle_type_support(CandleType.FUTURES)
exchange.verify_candle_type_support(CandleType.FUNDING_RATE)
exchange.verify_candle_type_support(CandleType.SPOT)
exchange.verify_candle_type_support(CandleType.MARK)
# Should fail:
with pytest.raises(
OperationalException,
match=r"Exchange .* does not support fetching premiumindex candles\.",
):
exchange.verify_candle_type_support(CandleType.PREMIUMINDEX)
type(api_mock).has = PropertyMock(
return_value={
"fetchFundingRateHistory": False,
"fetchIndexOHLCV": False,
"fetchMarkOHLCV": False,
"fetchPremiumIndexOHLCV": True,
}
)
for candle_type in [
CandleType.FUNDING_RATE,
CandleType.INDEX,
CandleType.MARK,
]:
with pytest.raises(
OperationalException,
match=rf"Exchange .* does not support fetching {candle_type.value} candles\.",
):
exchange.verify_candle_type_support(candle_type)
exchange.verify_candle_type_support(CandleType.PREMIUMINDEX)
+8 -10
View File
@@ -16,9 +16,9 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert fetch_order_mock.call_count == 1
assert fetch_order_mock.call_args_list[0][1]["order_id"] == "1234"
assert fetch_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
assert fetch_order_mock.call_args_list[0][1]["params"] == {"stop": True}
assert fetch_order_mock.call_args_list[0][0][0] == "1234"
assert fetch_order_mock.call_args_list[0][0][1] == "ETH/BTC"
assert fetch_order_mock.call_args_list[0][0][2] == {"stop": True}
default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated"
@@ -36,21 +36,19 @@ def test_fetch_stoploss_order_gate(default_conf, mocker):
exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert exchange.fetch_order.call_count == 2
assert exchange.fetch_order.call_args_list[0][1]["order_id"] == "1234"
assert exchange.fetch_order.call_args_list[0][0][0] == "1234"
assert exchange.fetch_order.call_args_list[1][1]["order_id"] == "222555"
def test_cancel_stoploss_order_gate(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, exchange="gate")
cancel_order_mock = MagicMock()
exchange.cancel_order = cancel_order_mock
cancel_order_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
exchange.cancel_stoploss_order("1234", "ETH/BTC")
assert cancel_order_mock.call_count == 1
assert cancel_order_mock.call_args_list[0][1]["order_id"] == "1234"
assert cancel_order_mock.call_args_list[0][1]["pair"] == "ETH/BTC"
assert cancel_order_mock.call_args_list[0][1]["params"] == {"stop": True}
assert cancel_order_mock.call_args_list[0][0][0] == "1234"
assert cancel_order_mock.call_args_list[0][0][1] == "ETH/BTC"
assert cancel_order_mock.call_args_list[0][0][2] == {"stop": True}
@pytest.mark.parametrize(
+2 -1
View File
@@ -123,7 +123,8 @@ def test_create_stoploss_order_dry_run_htx(default_conf, mocker):
assert "type" in order
assert order["type"] == order_type
assert order["price"] == 220
assert order["price"] == 217.8
assert order["stopPrice"] == 220
assert order["amount"] == 1
+6 -6
View File
@@ -661,14 +661,14 @@ def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side):
def test_stoploss_cancel_okx(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, exchange="okx")
exchange.cancel_order = MagicMock()
co_mock = mocker.patch.object(exchange, "cancel_order", autospec=True)
exchange.cancel_stoploss_order("1234", "ETH/USDT")
assert exchange.cancel_order.call_count == 1
assert exchange.cancel_order.call_args_list[0][1]["order_id"] == "1234"
assert exchange.cancel_order.call_args_list[0][1]["pair"] == "ETH/USDT"
assert exchange.cancel_order.call_args_list[0][1]["params"] == {"stop": True}
assert co_mock.call_count == 1
args, _ = co_mock.call_args
assert args[0] == "1234"
assert args[1] == "ETH/USDT"
assert args[2] == {"stop": True}
def test__get_stop_params_okx(mocker, default_conf):
+1 -1
View File
@@ -515,7 +515,7 @@ EXCHANGES = {
],
},
"hyperliquid": {
"pair": "UBTC/USDC",
"pair": "BTC/USDC",
"stake_currency": "USDC",
"hasQuoteVolume": False,
"timeframe": "30m",
+43 -11
View File
@@ -270,11 +270,14 @@ class TestCCXTExchange:
assert exch.klines(pair_tf).iloc[-1]["date"] >= timeframe_to_prev_date(timeframe, now)
assert exch.klines(pair_tf)["date"].astype(int).iloc[0] // 1e6 == since_ms
def _ccxt__async_get_candle_history(self, exchange, pair, timeframe, candle_type, factor=0.9):
def _ccxt__async_get_candle_history(
self, exchange, pair: str, timeframe: str, candle_type: CandleType, factor: float = 0.9
):
timeframe_ms = timeframe_to_msecs(timeframe)
timeframe_ms_8h = timeframe_to_msecs("8h")
now = timeframe_to_prev_date(timeframe, datetime.now(UTC))
for offset in (360, 120, 30, 10, 5, 2):
since = now - timedelta(days=offset)
for offset_days in (360, 120, 30, 10, 5, 2):
since = now - timedelta(days=offset_days)
since_ms = int(since.timestamp() * 1000)
res = exchange.loop.run_until_complete(
@@ -289,8 +292,15 @@ class TestCCXTExchange:
candles = res[3]
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor
assert len(candles) >= min(candle_count, candle_count1), (
f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}"
# funding fees can be 1h or 8h - depending on pair and time.
candle_count2 = (now.timestamp() * 1000 - since_ms) // timeframe_ms_8h * factor
min_value = min(
candle_count,
candle_count1,
candle_count2 if candle_type == CandleType.FUNDING_RATE else candle_count1,
)
assert len(candles) >= min_value, (
f"{len(candles)} < {candle_count} in {timeframe} {offset_days=} {factor=}"
)
# Check if first-timeframe is either the start, or start + 1
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
@@ -309,6 +319,8 @@ class TestCCXTExchange:
[
CandleType.FUTURES,
CandleType.FUNDING_RATE,
CandleType.INDEX,
CandleType.PREMIUMINDEX,
CandleType.MARK,
],
)
@@ -322,6 +334,10 @@ class TestCCXTExchange:
timeframe = exchange._ft_has.get(
"funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"]
)
else:
# never skip funding rate!
if not exchange.check_candle_type_support(candle_type):
pytest.skip(f"Exchange does not support candle type {candle_type}")
self._ccxt__async_get_candle_history(
exchange,
pair=pair,
@@ -337,6 +353,7 @@ class TestCCXTExchange:
timeframe_ff = exchange._ft_has.get(
"funding_fee_timeframe", exchange._ft_has["mark_ohlcv_timeframe"]
)
timeframe_ff_8h = "8h"
pair_tf = (pair, timeframe_ff, CandleType.FUNDING_RATE)
funding_ohlcv = exchange.refresh_latest_ohlcv(
@@ -350,14 +367,26 @@ class TestCCXTExchange:
hour1 = timeframe_to_prev_date(timeframe_ff, this_hour - timedelta(minutes=1))
hour2 = timeframe_to_prev_date(timeframe_ff, hour1 - timedelta(minutes=1))
hour3 = timeframe_to_prev_date(timeframe_ff, hour2 - timedelta(minutes=1))
val0 = rate[rate["date"] == this_hour].iloc[0]["open"]
val1 = rate[rate["date"] == hour1].iloc[0]["open"]
val2 = rate[rate["date"] == hour2].iloc[0]["open"]
val3 = rate[rate["date"] == hour3].iloc[0]["open"]
# Alternative 8h timeframe - funding fee timeframe is not stable.
h8_this_hour = timeframe_to_prev_date(timeframe_ff_8h)
h8_hour1 = timeframe_to_prev_date(timeframe_ff_8h, h8_this_hour - timedelta(minutes=1))
h8_hour2 = timeframe_to_prev_date(timeframe_ff_8h, h8_hour1 - timedelta(minutes=1))
h8_hour3 = timeframe_to_prev_date(timeframe_ff_8h, h8_hour2 - timedelta(minutes=1))
row0 = rate.iloc[-1]
row1 = rate.iloc[-2]
row2 = rate.iloc[-3]
row3 = rate.iloc[-4]
assert row0["date"] == this_hour or row0["date"] == h8_this_hour
assert row1["date"] == hour1 or row1["date"] == h8_hour1
assert row2["date"] == hour2 or row2["date"] == h8_hour2
assert row3["date"] == hour3 or row3["date"] == h8_hour3
# Test For last 4 hours
# Avoids random test-failure when funding-fees are 0 for a few hours.
assert val0 != 0.0 or val1 != 0.0 or val2 != 0.0 or val3 != 0.0
assert (
row0["open"] != 0.0 or row1["open"] != 0.0 or row2["open"] != 0.0 or row3["open"] != 0.0
)
# We expect funding rates to be different from 0.0 - or moving around.
assert (
rate["open"].max() != 0.0
@@ -369,7 +398,10 @@ class TestCCXTExchange:
exchange, exchangename = exchange_futures
pair = EXCHANGES[exchangename].get("futures_pair", EXCHANGES[exchangename]["pair"])
since = int((datetime.now(UTC) - timedelta(days=5)).timestamp() * 1000)
pair_tf = (pair, "1h", CandleType.MARK)
candle_type = CandleType.from_string(
exchange.get_option("mark_ohlcv_price", default=CandleType.MARK)
)
pair_tf = (pair, "1h", candle_type)
mark_ohlcv = exchange.refresh_latest_ohlcv([pair_tf], since_ms=since, drop_incomplete=False)
-9
View File
@@ -31,9 +31,6 @@ from tests.freqai.conftest import (
def can_run_model(model: str) -> None:
is_pytorch_model = "Reinforcement" in model or "PyTorch" in model
if is_arm() and "Catboost" in model:
pytest.skip("CatBoost is not supported on ARM.")
if is_pytorch_model and is_mac():
pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.")
@@ -44,7 +41,6 @@ def can_run_model(model: str) -> None:
("LightGBMRegressor", True, False, True, True, False, 0, 0),
("XGBoostRegressor", False, True, False, True, False, 10, 0.05),
("XGBoostRFRegressor", False, False, False, True, False, 0, 0),
("CatboostRegressor", False, False, False, True, True, 0, 0),
("PyTorchMLPRegressor", False, False, False, False, False, 0, 0),
("PyTorchTransformerRegressor", False, False, False, False, False, 0, 0),
("ReinforcementLearner", False, True, False, True, False, 0, 0),
@@ -138,9 +134,7 @@ def test_extract_data_and_train_model_Standard(
[
("LightGBMRegressorMultiTarget", "freqai_test_multimodel_strat"),
("XGBoostRegressorMultiTarget", "freqai_test_multimodel_strat"),
("CatboostRegressorMultiTarget", "freqai_test_multimodel_strat"),
("LightGBMClassifierMultiTarget", "freqai_test_multimodel_classifier_strat"),
("CatboostClassifierMultiTarget", "freqai_test_multimodel_classifier_strat"),
],
)
@pytest.mark.filterwarnings(r"ignore:.*__sklearn_tags__.*:DeprecationWarning")
@@ -184,7 +178,6 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s
"model",
[
"LightGBMClassifier",
"CatboostClassifier",
"XGBoostClassifier",
"XGBoostRFClassifier",
"SKLearnRandomForestClassifier",
@@ -246,13 +239,11 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
[
("LightGBMRegressor", 2, "freqai_test_strat"),
("XGBoostRegressor", 2, "freqai_test_strat"),
("CatboostRegressor", 2, "freqai_test_strat"),
("PyTorchMLPRegressor", 2, "freqai_test_strat"),
("PyTorchTransformerRegressor", 2, "freqai_test_strat"),
("ReinforcementLearner", 3, "freqai_rl_test_strat"),
("XGBoostClassifier", 2, "freqai_test_classifier"),
("LightGBMClassifier", 2, "freqai_test_classifier"),
("CatboostClassifier", 2, "freqai_test_classifier"),
("PyTorchMLPClassifier", 2, "freqai_test_classifier"),
],
)
+14 -14
View File
@@ -2548,9 +2548,9 @@ def test_manage_open_orders_exception(
caplog.clear()
freqtrade.manage_open_orders()
assert log_has_re(
r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30.00000000, "
r"is_short=False, leverage=1.0, "
r"open_rate=2.00000000, open_since="
r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30, "
r"is_short=False, leverage=1, "
r"open_rate=2, open_since="
f"{open_trade_usdt.open_date.strftime('%Y-%m-%d %H:%M:%S')}"
r"\) due to Traceback \(most recent call last\):\n*",
caplog,
@@ -3092,7 +3092,7 @@ def test_execute_trade_exit_custom_exit_price(
"exit_reason": "foo",
"open_date": ANY,
"close_date": ANY,
"close_rate": ANY,
"close_rate": 2.25, # the custom exit price
"sub_trade": False,
"cumulative_profit": 0.0,
"stake_amount": pytest.approx(60),
@@ -3751,8 +3751,8 @@ def test_get_real_amount_quote(
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == (amount * 0.001)
assert log_has(
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,"
" leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.008.",
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, is_short=False,"
" leverage=1, open_rate=0.245441, open_since=closed), fee=0.008.",
caplog,
)
@@ -3805,8 +3805,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) is None
assert log_has(
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: "
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed) failed: "
"myTrade-dict empty found",
caplog,
)
@@ -3825,8 +3825,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
0,
True,
(
"Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False, "
"leverage=1.0, open_rate=0.24544100, open_since=closed) [buy]: 0.00094518 BNB -"
"Fee for Trade Trade(id=None, pair=LTC/ETH, amount=8, is_short=False, "
"leverage=1, open_rate=0.245441, open_since=closed) [buy]: 0.00094518 BNB -"
" rate: None"
),
),
@@ -3836,8 +3836,8 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
0.004,
False,
(
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.004."
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed), fee=0.004."
),
),
# invalid, no currency in from fee dict
@@ -3941,8 +3941,8 @@ def test_get_real_amount_multi(
assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
assert log_has(
(
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, "
"is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), "
"Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8, "
"is_short=False, leverage=1, open_rate=0.245441, open_since=closed), "
f"fee={expected_amount}."
),
caplog,
+12 -1
View File
@@ -50,7 +50,14 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
stoploss_order_mock = MagicMock(side_effect=stop_orders)
# Sell 3rd trade (not called for the first trade)
should_sell_mock = MagicMock(side_effect=[[], [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]])
cancel_order_mock = MagicMock()
def patch_stoploss(order_id, *args, **kwargs):
slo = stoploss_order_open.copy()
slo["id"] = order_id
slo["status"] = "canceled"
return slo
cancel_order_mock = MagicMock(side_effect=patch_stoploss)
mocker.patch.multiple(
EXMS,
fetch_ticker=ticker,
@@ -796,9 +803,13 @@ def test_dca_handle_similar_open_order(
# Should Create a new exit order
freqtrade.exchange.amount_to_contract_precision = MagicMock(return_value=2)
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-2)
msg = r"Skipping cancelling stoploss on exchange for.*"
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
assert not log_has_re(msg, caplog)
freqtrade.process()
assert log_has_re(msg, caplog)
trade = Trade.get_trades().first()
assert trade.orders[-2].status == "closed"
+12 -9
View File
@@ -879,6 +879,10 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
patch_exchange(mocker)
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
default_conf_usdt["unfilledtimeout"] = {
"entry": 11,
"exit": 30,
}
if use_detail:
default_conf_usdt["timeframe_detail"] = "1m"
@@ -916,7 +920,7 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
)
results = result["results"]
assert not results.empty
# Timeout settings from default_conf = entry: 10, exit: 30
# Timeout settings from = entry: 11, exit: 30
assert len(results) == (2 if use_detail else 3)
assert "orders" in results.columns
@@ -966,8 +970,8 @@ def test_backtest_one_detail(default_conf_usdt, mocker, testdatadir, use_detail)
@pytest.mark.parametrize(
"use_detail,exp_funding_fee, exp_ff_updates",
[
(True, -0.018054162, 10),
(False, -0.01780296, 6),
(True, -0.0180457882, 15),
(False, -0.0178000543, 12),
],
)
def test_backtest_one_detail_futures(
@@ -1077,8 +1081,8 @@ def test_backtest_one_detail_futures(
@pytest.mark.parametrize(
"use_detail,entries,max_stake,ff_updates,expected_ff",
[
(True, 50, 3000, 55, -1.18038144),
(False, 6, 360, 11, -0.14679994),
(True, 50, 3000, 78, -1.17988972),
(False, 6, 360, 34, -0.14673681),
],
)
def test_backtest_one_detail_futures_funding_fees(
@@ -1800,7 +1804,7 @@ def test_backtest_multi_pair_detail_simplified(
if use_detail:
# Backtest loop is called once per candle per pair
# Exact numbers depend on trade state - but should be around 2_600
assert bl_spy.call_count > 2_170
assert bl_spy.call_count > 2_159
assert bl_spy.call_count < 2_800
assert len(evaluate_result_multi(results["results"], "1h", 3)) > 0
else:
@@ -2378,13 +2382,12 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, caplog, testda
f"Using data directory: {testdatadir} ...",
"Loading data from 2021-11-17 01:00:00 up to 2021-11-21 04:00:00 (4 days).",
"Backtesting with data from 2021-11-17 21:00:00 up to 2021-11-21 04:00:00 (3 days).",
"XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00",
"XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00",
"XRP/USDT:USDT, funding_rate, 1h, data starts at 2021-11-18 00:00:00",
f"Running backtesting for Strategy {CURRENT_TEST_STRATEGY}",
]
for line in exists:
assert log_has(line, caplog)
assert log_has(line, caplog), line
captured = capsys.readouterr()
assert "BACKTESTING REPORT" in captured.out
+10 -10
View File
@@ -372,8 +372,8 @@ def test_borrowed(fee, is_short, lev, borrowed, trading_mode):
@pytest.mark.parametrize(
"is_short,open_rate,close_rate,lev,profit,trading_mode",
[
(False, 2.0, 2.2, 1.0, 0.09451372, spot),
(True, 2.2, 2.0, 3.0, 0.25894253, margin),
(False, 2, 2.2, 1, 0.09451372, spot),
(True, 2.2, 2.0, 3, 0.25894253, margin),
],
)
@pytest.mark.usefixtures("init_persistence")
@@ -493,8 +493,8 @@ def test_update_limit_order(
assert trade.close_date is None
assert log_has_re(
f"LIMIT_{entry_side.upper()} has been fulfilled for "
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
r"Trade\(id=2, pair=ADA/USDT, amount=30, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}, "
r"open_since=.*\).",
caplog,
)
@@ -511,8 +511,8 @@ def test_update_limit_order(
assert trade.close_date is not None
assert log_has_re(
f"LIMIT_{exit_side.upper()} has been fulfilled for "
r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
r"Trade\(id=2, pair=ADA/USDT, amount=30, "
f"is_short={is_short}, leverage={lev}, open_rate={open_rate}, "
r"open_since=.*\).",
caplog,
)
@@ -545,8 +545,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
assert trade.close_date is None
assert log_has_re(
r"MARKET_BUY has been fulfilled for Trade\(id=1, "
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
r"open_rate=2.00000000, open_since=.*\).",
r"pair=ADA/USDT, amount=30, is_short=False, leverage=1, "
r"open_rate=2, open_since=.*\).",
caplog,
)
@@ -561,8 +561,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
assert trade.close_date is not None
assert log_has_re(
r"MARKET_SELL has been fulfilled for Trade\(id=1, "
r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, "
r"open_rate=2.00000000, open_since=.*\).",
r"pair=ADA/USDT, amount=30, is_short=False, leverage=1, "
r"open_rate=2, open_since=.*\).",
caplog,
)
+29 -2
View File
@@ -1852,9 +1852,35 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
last_order = trade.orders[-1]
assert last_order.side == "sell"
assert last_order.status == "closed"
assert last_order.order_type == "market"
assert last_order.amount == 23
assert pytest.approx(trade.amount) == 100
assert trade.is_open is True
# Test with explicit price
rc = client_post(
client,
f"{BASE_URI}/forceexit",
data={"tradeid": "5", "ordertype": "limit", "amount": 25, "price": 0.12345},
)
assert_response(rc)
assert rc.json() == {"result": "Created exit order for trade 5."}
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
last_order = trade.orders[-1]
assert last_order.status == "closed"
assert last_order.order_type == "limit"
assert pytest.approx(last_order.safe_price) == 0.12345
assert pytest.approx(last_order.amount) == 25
assert pytest.approx(trade.amount) == 75
assert trade.is_open is True
rc = client_post(client, f"{BASE_URI}/forceexit", data={"tradeid": "5"})
assert_response(rc)
assert rc.json() == {"result": "Created exit order for trade 5."}
@@ -2757,12 +2783,12 @@ def test_list_available_pairs(botclient):
rc = client_get(client, f"{BASE_URI}/available_pairs")
assert_response(rc)
assert rc.json()["length"] == 12
assert rc.json()["length"] == 14
assert isinstance(rc.json()["pairs"], list)
rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m")
assert_response(rc)
assert rc.json()["length"] == 12
assert rc.json()["length"] == 14
rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH")
assert_response(rc)
@@ -3250,6 +3276,7 @@ def test_api_download_data(botclient, mocker, tmp_path):
body = {
"pairs": ["ETH/BTC", "XRP/BTC"],
"timeframes": ["5m"],
"candle_types": ["spot"],
}
# Fail, already running
@@ -64,7 +64,6 @@ def test_hyperopt_real_parameter():
def test_hyperopt_decimal_parameter():
HyperoptStateContainer.set_state(HyperoptState.INDICATORS)
# TODO: Check for get_space??
from freqtrade.optimize.space import SKDecimal
with pytest.raises(OperationalException, match=r"DecimalParameter space must be.*"):
+92
View File
@@ -0,0 +1,92 @@
"""
Run pip audit to check for known security vulnerabilities in installed packages.
Original Idea and base for this implementation by Michael Kennedy's blog:
https://mkennedy.codes/posts/python-supply-chain-security-made-easy/
"""
import subprocess
import sys
from pathlib import Path
import pytest
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: 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
command = [
sys.executable,
"-m",
"pip_audit",
# "--format=json",
"--progress-spinner=off",
"--ignore-vuln",
"CVE-2025-53000",
"--skip-editable",
]
# Run pip-audit with JSON output for easier parsing
try:
result = subprocess.run(
command,
cwd=project_root,
capture_output=True,
text=True,
timeout=120, # 2 minute timeout
)
except subprocess.TimeoutExpired:
pytest.fail("pip-audit command timed out after 120 seconds")
except FileNotFoundError:
pytest.fail("pip-audit not installed or not accessible")
# Check if pip-audit found any vulnerabilities
if result.returncode != 0:
# pip-audit returns non-zero when vulnerabilities are found
error_output = result.stdout + "\n" + result.stderr
# Check if it's an actual vulnerability vs an error
if "vulnerabilities found" in error_output.lower() or '"dependencies"' in result.stdout:
pytest.fail(
f"pip-audit detected security vulnerabilities!\n\n"
f"Output:\n{result.stdout}\n\n"
f"Please review and update vulnerable packages.\n"
f"Run manually with: {' '.join(command)}"
)
else:
# Some other error occurred
pytest.fail(
f"pip-audit failed to run properly:\n\nReturn code: {result.returncode}\n"
f"Output: {error_output}\n"
)
# Success - no vulnerabilities found
assert result.returncode == 0, "pip-audit should return 0 when no vulnerabilities are found"
def test_pip_audit_runs_successfully():
"""
Verify that pip-audit can run successfully (even if vulnerabilities are found).
This is a smoke test to ensure pip-audit is properly installed and functional.
"""
try:
result = subprocess.run(
[sys.executable, "-m", "pip_audit", "--version"],
capture_output=True,
text=True,
timeout=10,
)
assert result.returncode == 0, f"pip-audit --version failed: {result.stderr}"
assert "pip-audit" in result.stdout.lower(), "pip-audit version output unexpected"
except FileNotFoundError:
pytest.fail("pip-audit not installed")
except subprocess.TimeoutExpired:
pytest.fail("pip-audit --version timed out")
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More