Compare commits

...

201 Commits

Author SHA1 Message Date
Matthias a23409b72f Merge pull request #11436 from freqtrade/new_release
New release 2025.2
2025-02-28 10:33:14 +01:00
Matthias 186bc4d272 chore: bump version to 2025.2 2025-02-28 06:30:43 +01:00
Matthias 06fd176286 Merge branch 'stable' into develop 2025-02-28 06:30:27 +01:00
Matthias c5fd6f4519 Merge pull request #11432 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-02-27 07:25:48 +01:00
Freqtrade Bot efe0275ae1 chore: update pre-commit hooks 2025-02-27 05:50:03 +00:00
Matthias ba98f1a9be chore: update pre-commit author to freqtradeBot 2025-02-27 06:49:19 +01:00
Matthias 5380b22729 chore: update leverage tiers author to use the bot email 2025-02-27 06:48:54 +01:00
Matthias 338bc7c677 chore: Bump docker image to 3.12.9 2025-02-27 06:33:05 +01:00
Matthias 449a5e85b4 Merge pull request #11431 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-02-27 06:20:15 +01:00
xmatthias 0f5cb87c65 chore: update pre-commit hooks 2025-02-27 03:08:16 +00:00
Matthias 822e7bad1f Merge pull request #11426 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-02-25 06:23:10 +01:00
xmatthias 86a5240174 chore: update pre-commit hooks 2025-02-25 03:08:51 +00:00
Matthias 7b7c8d9374 chore(deps): bump ccxt from 4.4.61 to 4.4.62
closes #11405
2025-02-24 19:51:54 +01:00
Matthias fde3547dbb docs: add warning about duplicate symbols in coingecko 2025-02-24 18:06:52 +01:00
Matthias 9bd7fac86f Merge pull request #11424 from freqtrade/dependabot/pip/develop/pyarrow-19.0.1
chore(deps): bump pyarrow from 19.0.0 to 19.0.1
2025-02-24 09:57:58 +01:00
Matthias 8085d8cd00 chore: update prebuilt pyarrow binary 2025-02-24 09:40:06 +01:00
Matthias 3f656a9758 Merge pull request #11421 from freqtrade/dependabot/pip/develop/humanize-4.12.1
chore(deps): bump humanize from 4.12.0 to 4.12.1
2025-02-24 08:12:28 +01:00
Matthias 38f1981d6f Merge pull request #11412 from freqtrade/feat/adjust_order_price
Add adjust_order_price callback
2025-02-24 07:02:27 +01:00
Matthias d55b3e0228 Merge pull request #11420 from freqtrade/dependabot/pip/develop/ccxt-4.4.61
chore(deps): bump ccxt from 4.4.59 to 4.4.61
2025-02-24 06:55:12 +01:00
Matthias d3f8bef2ef Merge pull request #11422 from freqtrade/dependabot/pip/develop/ruff-0.9.7
chore(deps-dev): bump ruff from 0.9.6 to 0.9.7
2025-02-24 06:45:43 +01:00
dependabot[bot] f78f52f78e chore(deps): bump humanize from 4.12.0 to 4.12.1
Bumps [humanize](https://github.com/python-humanize/humanize) from 4.12.0 to 4.12.1.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/4.12.0...4.12.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 05:26:05 +00:00
Matthias d451dcf28b Merge pull request #11419 from freqtrade/dependabot/pip/develop/mkdocs-ab7c602706
chore(deps): bump mkdocs-material from 9.6.4 to 9.6.5 in the mkdocs group
2025-02-24 06:25:31 +01:00
Matthias 063964ed38 Merge pull request #11423 from freqtrade/dependabot/pip/develop/cachetools-5.5.2
chore(deps): bump cachetools from 5.5.1 to 5.5.2
2025-02-24 06:24:15 +01:00
dependabot[bot] ac70d559a4 chore(deps): bump pyarrow from 19.0.0 to 19.0.1
Bumps [pyarrow](https://github.com/apache/arrow) from 19.0.0 to 19.0.1.
- [Release notes](https://github.com/apache/arrow/releases)
- [Commits](https://github.com/apache/arrow/compare/apache-arrow-19.0.0...apache-arrow-19.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 03:24:23 +00:00
dependabot[bot] 685e575f6b chore(deps): bump cachetools from 5.5.1 to 5.5.2
Bumps [cachetools](https://github.com/tkem/cachetools) from 5.5.1 to 5.5.2.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v5.5.1...v5.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 03:24:18 +00:00
dependabot[bot] 1640267798 chore(deps-dev): bump ruff from 0.9.6 to 0.9.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.6 to 0.9.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.9.6...0.9.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 03:24:14 +00:00
dependabot[bot] d5585dd5e3 chore(deps): bump ccxt from 4.4.59 to 4.4.61
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.59 to 4.4.61.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/go/v4.4.59...go/v4.4.61)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 03:23:46 +00:00
dependabot[bot] 90e2db708e chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 03:23:30 +00:00
Matthias 64303e90d6 feat: Be more verbose on startup regarding what the bot is doing
closes #11416
2025-02-23 17:16:46 +01:00
Matthias 901dfa21fc Merge pull request #11418 from Cygra/patch-1
fix: typo in strategy-customization.md
2025-02-23 16:24:55 +01:00
Cygra Wang 5e7d883732 fix: typo in strategy-customization.md 2025-02-23 15:11:05 +08:00
Matthias 840f074c66 chore: simplify rpc imports 2025-02-22 12:50:51 +01:00
Matthias 0408ad1095 fix: exception when rounded price is nan
closes #11414
2025-02-22 08:21:24 +01:00
Matthias e262110c3b test: add test case for #11414 2025-02-22 08:19:38 +01:00
Matthias 09b9ff2c68 fix: provide default for exit reason 2025-02-20 20:32:51 +01:00
Matthias e76574b79f docs: update documentation for adjust_order_price() 2025-02-20 20:25:35 +01:00
Matthias 08587d826e test: Long adjust-exit-price test 2025-02-20 20:07:44 +01:00
Matthias 584b84a941 test: extend backtest-detail tests for exit_adjust 2025-02-20 20:02:18 +01:00
Matthias 44182783c0 feat: don't limit backtest calls to entry orders 2025-02-20 20:02:03 +01:00
Matthias 90f52ba8ad test: add integration test for adjust_exit_price 2025-02-20 19:45:34 +01:00
Matthias f8f10f27e9 chore: improved variable naming 2025-02-20 19:39:33 +01:00
Matthias 7f393252e2 docs: update bot-basics with new callback 2025-02-20 19:24:03 +01:00
Matthias dd8938ced2 test: add test for adjust_order_price and adjust_entry_price collision 2025-02-20 19:22:03 +01:00
Matthias 67ce9a41f2 feat: implement load error when colliding methods are decected 2025-02-20 19:21:37 +01:00
Matthias 756bada570 feat: Add "replace-exit_order" to backtesting 2025-02-20 19:09:36 +01:00
Matthias 25c0036012 feat: Update strategy template 2025-02-20 07:25:38 +01:00
Matthias 3b03fae939 chore: improve variable naming 2025-02-20 07:24:07 +01:00
Matthias 0f9e61371c feat: Implement live "replace_exit_order" functionality 2025-02-20 07:23:36 +01:00
Matthias 1970cc65c0 feat: add "is_entry" attribute for order_replacement 2025-02-20 07:04:59 +01:00
Matthias 8d74e8b8dd feat: add adjust_order_price callback 2025-02-20 06:55:54 +01:00
Matthias fc4e09f398 Merge pull request #11404 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-02-20 06:35:26 +01:00
xmatthias a45d941b11 chore: update pre-commit hooks 2025-02-20 03:05:52 +00:00
Matthias 39fd27c7ec Merge pull request #11396 from freqtrade/fix/constant_exit_cancels
Fix constant exit cancels and replacements
2025-02-18 19:43:54 +01:00
Matthias fa939a7f66 Merge pull request #11399 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-02-18 06:36:19 +01:00
xmatthias 6fc81fa9c7 chore: update pre-commit hooks 2025-02-18 03:04:55 +00:00
Matthias 89cd46d2e5 test: adapt test for "evaluate exit signal once once" behavior 2025-02-17 20:00:44 +01:00
Matthias f287d0ad24 chore: improve variable naming 2025-02-17 20:00:27 +01:00
Matthias 93e3bbea5a fix: only evaluate the same exit-reason once
Limit this behavior to if it caused an actual exit.
2025-02-17 19:51:39 +01:00
Matthias 6be25bd86a chore: simplify IF condition 2025-02-17 19:40:56 +01:00
Matthias ec82894990 Merge pull request #11390 from freqtrade/dependabot/pip/develop/cryptography-44.0.1
chore(deps): bump cryptography from 42.0.8 to 44.0.1
2025-02-17 08:20:57 +01:00
Matthias ea1b7c692a Merge pull request #11384 from freqtrade/dependabot/pip/develop/websockets-15.0
chore(deps): bump websockets from 14.2 to 15.0
2025-02-17 08:04:01 +01:00
Matthias 0e08173235 Merge pull request #11394 from freqtrade/dependabot/pip/develop/lightgbm-4.6.0
chore(deps): bump lightgbm from 4.5.0 to 4.6.0
2025-02-17 07:46:26 +01:00
Matthias cdf33761c0 Merge pull request #11383 from freqtrade/dependabot/pip/develop/mkdocs-f2d694b1ef
chore(deps): bump mkdocs-material from 9.6.3 to 9.6.4 in the mkdocs group
2025-02-17 07:11:37 +01:00
Matthias 63ca07aeac chore: bump cryptography for armv7
Starting with 44.0.0, cryptography provides wheels for armv7.

Testing these, it installs fine on the rpi image, so the split hard pin can be removed.
2025-02-17 06:52:30 +01:00
Matthias 5022407021 Merge pull request #11392 from freqtrade/dependabot/pip/develop/psutil-7.0.0
chore(deps): bump psutil from 6.1.1 to 7.0.0
2025-02-17 06:51:50 +01:00
dependabot[bot] f54b7d56d3 chore(deps): bump lightgbm from 4.5.0 to 4.6.0
Bumps [lightgbm](https://github.com/microsoft/LightGBM) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/microsoft/LightGBM/releases)
- [Commits](https://github.com/microsoft/LightGBM/compare/v4.5.0...v4.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 05:30:51 +00:00
dependabot[bot] 382befb053 chore(deps): bump cryptography from 42.0.8 to 44.0.1
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.8 to 44.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.8...44.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 05:30:25 +00:00
Matthias 11404ef230 Merge pull request #11385 from freqtrade/dependabot/pip/develop/scipy-1.15.2
chore(deps): bump scipy from 1.15.1 to 1.15.2
2025-02-17 06:29:32 +01:00
Matthias 8964cfc123 Merge pull request #11391 from freqtrade/dependabot/pip/develop/humanize-4.12.0
chore(deps): bump humanize from 4.11.0 to 4.12.0
2025-02-17 06:28:45 +01:00
Matthias ec97723c54 Merge pull request #11386 from freqtrade/dependabot/pip/develop/tensorboard-2.19.0
chore(deps): bump tensorboard from 2.18.0 to 2.19.0
2025-02-17 06:28:19 +01:00
Matthias 4cfbcefb33 Merge pull request #11388 from freqtrade/dependabot/pip/develop/ccxt-4.4.59
chore(deps): bump ccxt from 4.4.58 to 4.4.59
2025-02-17 06:27:48 +01:00
Matthias 1f575d95e6 Merge pull request #11389 from freqtrade/dependabot/pip/develop/ruff-0.9.6
chore(deps-dev): bump ruff from 0.9.5 to 0.9.6
2025-02-17 06:26:59 +01:00
dependabot[bot] d4900c28e1 chore(deps): bump psutil from 6.1.1 to 7.0.0
Bumps [psutil](https://github.com/giampaolo/psutil) from 6.1.1 to 7.0.0.
- [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/release-6.1.1...release-7.0.0)

---
updated-dependencies:
- dependency-name: psutil
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 03:27:29 +00:00
dependabot[bot] f12964f543 chore(deps): bump humanize from 4.11.0 to 4.12.0
Bumps [humanize](https://github.com/python-humanize/humanize) from 4.11.0 to 4.12.0.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/4.11.0...4.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 03:27:25 +00:00
dependabot[bot] c339853ec0 chore(deps-dev): bump ruff from 0.9.5 to 0.9.6
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.5 to 0.9.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.9.5...0.9.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 03:27:13 +00:00
dependabot[bot] 3924514d2b chore(deps): bump ccxt from 4.4.58 to 4.4.59
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.58 to 4.4.59.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/go/v4.4.58...go/v4.4.59)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 03:27:04 +00:00
dependabot[bot] aae92c29db chore(deps): bump tensorboard from 2.18.0 to 2.19.0
Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.18.0 to 2.19.0.
- [Release notes](https://github.com/tensorflow/tensorboard/releases)
- [Changelog](https://github.com/tensorflow/tensorboard/blob/2.19.0/RELEASE.md)
- [Commits](https://github.com/tensorflow/tensorboard/compare/2.18.0...2.19.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 03:26:45 +00:00
dependabot[bot] 5683063379 chore(deps): bump scipy from 1.15.1 to 1.15.2
Bumps [scipy](https://github.com/scipy/scipy) from 1.15.1 to 1.15.2.
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](https://github.com/scipy/scipy/compare/v1.15.1...v1.15.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 03:26:40 +00:00
dependabot[bot] 5ab993b6d5 chore(deps): bump websockets from 14.2 to 15.0
Bumps [websockets](https://github.com/python-websockets/websockets) from 14.2 to 15.0.
- [Release notes](https://github.com/python-websockets/websockets/releases)
- [Commits](https://github.com/python-websockets/websockets/compare/14.2...15.0)

---
updated-dependencies:
- dependency-name: websockets
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 03:26:30 +00:00
dependabot[bot] 9584f6c549 chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 03:26:27 +00:00
Matthias 2a95a3aefe Merge pull request #11306 from Vaadasch/patch-1
Add some distinction on the trailing_only_offset_is_reached
2025-02-16 17:28:24 +01:00
Matthias 4a190bd13c chore(ci): Move permissions into job 2025-02-16 17:03:18 +01:00
Matthias cf6a89e161 chore: use zizmor to scan actions for security problems 2025-02-16 16:55:44 +01:00
Matthias a29c5322ab chore(ci): Improve CI security 2025-02-16 16:55:13 +01:00
Matthias 4e2d2639ae fix: dockerfile.armhf should pin numpy<2 2025-02-16 16:48:22 +01:00
Matthias a54f895ea9 chore(ci): don't persist credentials 2025-02-16 15:31:17 +01:00
Matthias ad55615ecb chore(ci): Improve structure in devcontainer build 2025-02-16 15:30:47 +01:00
Matthias c8aff5687d chore(ci): don't persist credentials 2025-02-16 15:30:21 +01:00
Matthias 02c2e451b1 chore(ci): don't persist credentials 2025-02-16 15:29:34 +01:00
Matthias 84d49a0f53 chore: disable all permissions on dockerhub permission update ci 2025-02-16 15:25:01 +01:00
Matthias 22b096be25 chore: Improve security of workflows 2025-02-16 15:24:23 +01:00
Matthias b285cdd491 doc: add timeframe to 101 strategy so it's "auto-runnable"
closes #11379
2025-02-16 15:11:14 +01:00
Matthias b2881affc0 fix: accidental modification 2025-02-16 15:09:02 +01:00
Matthias b3e0b42155 test: add test for backtest-from_json 2025-02-16 13:27:48 +01:00
Matthias 314c017dd3 feat: enable trade.from_json to import backtested trades 2025-02-16 13:27:42 +01:00
Matthias f14c4ebcc8 chore: improve code structure in logging module 2025-02-15 08:24:35 +01:00
Robert Caulk c3c6207cda Merge pull request #11368 from freqtrade/fix/nan_metadata
Allow NAN in Freqai datadrawer metadata
2025-02-13 13:46:36 +01:00
Matthias 8a5ff4ed0d Merge pull request #11373 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-02-13 06:33:55 +01:00
xmatthias 7d19e0e692 chore: update pre-commit hooks 2025-02-13 03:05:01 +00:00
Matthias e3a9b0aa17 Merge pull request #11371 from freqtrade/dependabot/pip/develop/ccxt-4.4.58
chore(deps): bump ccxt from 4.4.52 to 4.4.58
2025-02-12 06:23:01 +01:00
dependabot[bot] 7caa22c7a7 chore(deps): bump ccxt from 4.4.52 to 4.4.58
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.52 to 4.4.58.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.4.52...go/v4.4.58)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-11 20:13:52 +00:00
Matthias 91131d7884 Merge pull request #11370 from freqtrade/dependabot/pip/cryptography-44.0.1
chore(deps): bump cryptography from 42.0.8 to 44.0.1
2025-02-11 21:12:25 +01:00
dependabot[bot] 967ad2f8ff chore(deps): bump cryptography from 42.0.8 to 44.0.1
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.8 to 44.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.8...44.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-11 18:41:14 +00:00
Matthias d00f82a641 chore: remove deprecated image form CI matrix 2025-02-11 18:15:16 +01:00
Matthias 2f83f07fd3 fix: use constant for metadata number mode 2025-02-11 09:22:39 +01:00
Matthias 78d213a8a0 test: add test coverage for unwatch_ohlcv 2025-02-11 09:15:03 +01:00
Matthias 44d798239d feat: unwatch_ohlcv - improved error handling 2025-02-11 09:15:03 +01:00
Matthias 08e1a616a0 feat: use unwatch to stop watching a websocket 2025-02-11 09:14:57 +01:00
Matthias 8009c03629 Merge pull request #11117 from arenstar/ftclient_timeout
feat: add timeout to FtRestClient
2025-02-11 08:09:45 +01:00
Matthias ae068bd6f5 chore: fix formatting 2025-02-11 07:53:33 +01:00
Matthias 2350aa6a27 Merge branch 'develop' into ftclient_timeout 2025-02-11 07:52:25 +01:00
Matthias 8394826d62 docs: improve wording in trailing stop docs 2025-02-11 07:21:24 +01:00
Matthias a46d41a594 Merge pull request #11367 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-02-11 06:39:25 +01:00
xmatthias a0d8055917 chore: update pre-commit hooks 2025-02-11 03:06:02 +00:00
Matthias 3ec892a5ab Merge pull request #11355 from freqtrade/dependabot/pip/develop/ta-lib-0.6.2
chore(deps): bump ta-lib from 0.4.34 to 0.4.38
2025-02-10 10:53:09 +01:00
Matthias 3deffb57ce chore: update ta-lib pre-built binaries 2025-02-10 08:25:01 +01:00
Matthias db8c1917e0 Merge pull request #11360 from freqtrade/dependabot/pip/develop/mypy-1.15.0
chore(deps-dev): bump mypy from 1.14.1 to 1.15.0
2025-02-10 07:57:42 +01:00
Matthias 57f55d416c Merge pull request #11357 from freqtrade/dependabot/pip/develop/sqlalchemy-2.0.38
chore(deps): bump sqlalchemy from 2.0.37 to 2.0.38
2025-02-10 07:35:26 +01:00
dependabot[bot] 0b4a6e7e11 chore(deps-dev): bump mypy from 1.14.1 to 1.15.0
Bumps [mypy](https://github.com/python/mypy) from 1.14.1 to 1.15.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.14.1...v1.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 05:57:49 +00:00
Matthias f918a66292 Merge pull request #11359 from freqtrade/dependabot/pip/develop/ruff-0.9.5
chore(deps-dev): bump ruff from 0.9.4 to 0.9.5
2025-02-10 06:56:44 +01:00
Matthias d795af5921 chore: bump sqlalchemy in pre-commit config 2025-02-10 06:36:25 +01:00
Matthias 21b8982e55 chore: ta-lib bump to 0.4.38 2025-02-10 06:34:07 +01:00
Matthias 5a9f66e6d6 Merge pull request #11351 from freqtrade/dependabot/pip/develop/mkdocs-694d3e3e45
chore(deps): bump mkdocs-material from 9.6.1 to 9.6.3 in the mkdocs group
2025-02-10 06:30:34 +01:00
Matthias 4fd77e7ee7 Merge pull request #11356 from freqtrade/dependabot/pip/develop/xgboost-2.1.4
chore(deps): bump xgboost from 2.1.3 to 2.1.4
2025-02-10 06:30:22 +01:00
dependabot[bot] 428ca972e2 chore(deps-dev): bump ruff from 0.9.4 to 0.9.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.4 to 0.9.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.9.4...0.9.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 03:28:24 +00:00
dependabot[bot] 3913e4fa96 chore(deps): bump sqlalchemy from 2.0.37 to 2.0.38
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.37 to 2.0.38.
- [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-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 03:27:58 +00:00
dependabot[bot] e296242072 chore(deps): bump xgboost from 2.1.3 to 2.1.4
Bumps [xgboost](https://github.com/dmlc/xgboost) from 2.1.3 to 2.1.4.
- [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/v2.1.3...v2.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 03:27:46 +00:00
dependabot[bot] 82001d1e8b chore(deps): bump ta-lib from 0.4.34 to 0.6.2
Bumps [ta-lib](https://github.com/ta-lib/ta-lib-python) from 0.4.34 to 0.6.2.
- [Changelog](https://github.com/TA-Lib/ta-lib-python/blob/master/CHANGELOG)
- [Commits](https://github.com/ta-lib/ta-lib-python/compare/TA_Lib-0.4.34...TA_Lib-0.6.2)

---
updated-dependencies:
- dependency-name: ta-lib
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 03:27:40 +00:00
dependabot[bot] 137bfc2b56 chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 03:27:15 +00:00
Matthias a8ae767576 fix: Hyperopt-magic - allow parent classes within the same file 2025-02-09 19:48:26 +01:00
Matthias b9c77124fc chore: add typing for hyperopt-pickle-magic 2025-02-09 19:43:32 +01:00
Matthias f060548134 chore: remove unused method 2025-02-09 08:10:10 +01:00
Matthias 18ce8af084 fix: validate config when calling pair_history
closes #11271
2025-02-08 16:51:51 +01:00
Matthias 7f7cff85c1 chore: use dt_now in backtesting 2025-02-07 20:39:14 +01:00
Matthias 9a22e5c775 chore: try downgrade aiohttp
part of #11317
2025-02-07 06:56:42 +01:00
Matthias 2cfb8834b6 chore: rework docstring of MultiOutputRegressor
closes #11309
2025-02-06 06:34:19 +01:00
Matthias 013006050f chore: add comment to clarify usage of retrier 2025-02-06 06:33:55 +01:00
Matthias 99df346bf5 Merge pull request #11339 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-02-06 06:30:49 +01:00
xmatthias 2e57158556 chore: update pre-commit hooks 2025-02-06 03:04:59 +00:00
Matthias b75eb85e66 fix: Calculate liquidation price only once the initial order filled
closes #11318
2025-02-05 18:13:52 +01:00
Matthias dbb9f225d0 fix: Runtime error on ohlcv deepcopy
closes #11335
2025-02-05 18:07:03 +01:00
Matthias 85753be7cb chore: Bump armhf dockerversion to 3.11.11 2025-02-05 07:02:44 +01:00
Matthias 18b35b0657 chore: bump dockerfile base from 3.12.7 to 3.12.8 2025-02-05 07:01:59 +01:00
Matthias 857aeaa9eb chore: fix init sequence to avoid risk of "unbound error" 2025-02-04 19:34:26 +01:00
Matthias 3e06971038 Merge pull request #11333 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-02-04 06:47:37 +01:00
Matthias 323b9f8693 fix(api): only write export when backtest happened
closes freqtrade/frequi#2282
2025-02-04 06:35:37 +01:00
xmatthias a412ff76bc chore: update pre-commit hooks 2025-02-04 03:03:58 +00:00
Matthias 2bfd66d271 chore: use loop_lock for all run_until_complete usages 2025-02-03 19:43:08 +01:00
Matthias 8cad9c8436 chore: add improved exception logging 2025-02-03 19:43:08 +01:00
Matthias 3504073428 fix: random fail on parallel webserver requests 2025-02-03 19:43:08 +01:00
Matthias b4af89380b Merge pull request #11326 from freqtrade/dependabot/pip/develop/torch-2.6.0
chore(deps): bump torch from 2.5.1 to 2.6.0
2025-02-03 19:40:45 +01:00
Matthias c131493eca Merge pull request #11328 from freqtrade/dependabot/pip/develop/stable-baselines3-2.5.0
chore(deps): bump stable-baselines3 from 2.4.1 to 2.5.0
2025-02-03 11:55:31 +01:00
Matthias 37031ba83d chore: pin sb3 for macos x86 2025-02-03 11:13:36 +01:00
Matthias 69106633cd Merge pull request #11331 from freqtrade/dependabot/pip/develop/fastapi-0.115.8
chore(deps): bump fastapi from 0.115.7 to 0.115.8
2025-02-03 09:42:17 +01:00
Matthias a2e674b237 Merge pull request #11329 from freqtrade/dependabot/pip/develop/isort-6.0.0
chore(deps-dev): bump isort from 5.13.2 to 6.0.0
2025-02-03 09:20:38 +01:00
Matthias c1c9f0151e Merge pull request #11332 from freqtrade/dependabot/pip/develop/pymdown-extensions-10.14.3
chore(deps): bump pymdown-extensions from 10.14.1 to 10.14.3
2025-02-03 08:01:03 +01:00
Matthias 963ec23ae2 Merge pull request #11330 from freqtrade/dependabot/pip/develop/plotly-6.0.0
chore(deps): bump plotly from 5.24.1 to 6.0.0
2025-02-03 07:14:18 +01:00
dependabot[bot] 0c35f3a47a chore(deps): bump pymdown-extensions from 10.14.1 to 10.14.3
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.14.1 to 10.14.3.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.14.1...10.14.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 05:58:16 +00:00
Matthias f4fdcfc78d Merge pull request #11323 from freqtrade/dependabot/pip/develop/ccxt-4.4.52
chore(deps): bump ccxt from 4.4.50 to 4.4.52
2025-02-03 06:57:17 +01:00
Matthias 97c67d55c6 Merge pull request #11322 from freqtrade/dependabot/pip/develop/mkdocs-03e80ceb70
chore(deps): bump mkdocs-material from 9.5.50 to 9.6.1 in the mkdocs group
2025-02-03 06:53:46 +01:00
Matthias 9983b44e6f Merge pull request #11325 from freqtrade/dependabot/pip/develop/pytz-2025.1
chore(deps): bump pytz from 2024.2 to 2025.1
2025-02-03 06:53:05 +01:00
dependabot[bot] 78bf9bf60d chore(deps-dev): bump isort from 5.13.2 to 6.0.0
Bumps [isort](https://github.com/pycqa/isort) from 5.13.2 to 6.0.0.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.13.2...6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 05:44:16 +00:00
Matthias 9d22dac28d Merge pull request #11327 from freqtrade/dependabot/pip/develop/nbconvert-7.16.6
chore(deps-dev): bump nbconvert from 7.16.5 to 7.16.6
2025-02-03 06:34:17 +01:00
Matthias 37e16acc80 Merge pull request #11324 from freqtrade/dependabot/pip/develop/ruff-0.9.4
chore(deps-dev): bump ruff from 0.9.3 to 0.9.4
2025-02-03 06:23:55 +01:00
Matthias 6d5cd69764 Merge pull request #11321 from freqtrade/dependabot/pip/develop/pytest-ccc7fb3faf
chore(deps-dev): bump pytest-asyncio from 0.25.2 to 0.25.3 in the pytest group
2025-02-03 06:23:43 +01:00
dependabot[bot] 3a8c594ae8 chore(deps): bump fastapi from 0.115.7 to 0.115.8
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.115.7 to 0.115.8.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.7...0.115.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:30:46 +00:00
dependabot[bot] 5f72ed9b3c chore(deps): bump plotly from 5.24.1 to 6.0.0
Bumps [plotly](https://github.com/plotly/plotly.py) from 5.24.1 to 6.0.0.
- [Release notes](https://github.com/plotly/plotly.py/releases)
- [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.py/compare/v5.24.1...v6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:30:42 +00:00
dependabot[bot] bee897f5b7 chore(deps): bump stable-baselines3 from 2.4.1 to 2.5.0
Bumps [stable-baselines3](https://github.com/DLR-RM/stable-baselines3) from 2.4.1 to 2.5.0.
- [Release notes](https://github.com/DLR-RM/stable-baselines3/releases)
- [Commits](https://github.com/DLR-RM/stable-baselines3/compare/v2.4.1...v2.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:30:27 +00:00
dependabot[bot] 75542cdd93 chore(deps-dev): bump nbconvert from 7.16.5 to 7.16.6
Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.16.5 to 7.16.6.
- [Release notes](https://github.com/jupyter/nbconvert/releases)
- [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter/nbconvert/compare/v7.16.5...v7.16.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:30:24 +00:00
dependabot[bot] 97da78c499 chore(deps): bump torch from 2.2.2 to 2.6.0
Bumps [torch](https://github.com/pytorch/pytorch) from 2.2.2 to 2.6.0.
- [Release notes](https://github.com/pytorch/pytorch/releases)
- [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md)
- [Commits](https://github.com/pytorch/pytorch/compare/v2.2.2...v2.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:30:20 +00:00
dependabot[bot] d8d35ec234 chore(deps): bump pytz from 2024.2 to 2025.1
Bumps [pytz](https://github.com/stub42/pytz) from 2024.2 to 2025.1.
- [Release notes](https://github.com/stub42/pytz/releases)
- [Commits](https://github.com/stub42/pytz/compare/release_2024.2...release_2025.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:30:10 +00:00
dependabot[bot] 86d65f8428 chore(deps-dev): bump ruff from 0.9.3 to 0.9.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.3 to 0.9.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.9.3...0.9.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:30:06 +00:00
dependabot[bot] 973aa6a0c3 chore(deps): bump ccxt from 4.4.50 to 4.4.52
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.50 to 4.4.52.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.4.50...4.4.52)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:29:58 +00:00
dependabot[bot] aaab2a1c86 chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:29:48 +00:00
dependabot[bot] 16924525ff chore(deps-dev): bump pytest-asyncio in the pytest group
Bumps the pytest group with 1 update: [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio).


Updates `pytest-asyncio` from 0.25.2 to 0.25.3
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.25.2...v0.25.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 03:29:40 +00:00
Matthias f1f355330c chore: re-add candle-limit for funding fee candles 2025-02-02 17:15:21 +01:00
Matthias 4c5c054bca fix: since_ms is mandatory 2025-02-02 14:33:45 +01:00
Matthias 0181e00c19 test: add test for /markets endpoint 2025-02-02 14:33:13 +01:00
Matthias ecf013dd18 feat: include exchangeID in /markets response 2025-02-02 13:30:17 +01:00
Matthias 59837ef6ac feat: add "/markets" endpoint 2025-02-02 13:22:54 +01:00
Matthias b42a91d783 test: remove unused parameter 2025-02-02 13:17:06 +01:00
Matthias 187c8b8070 test: add test for "live" mode of /pair_history 2025-02-01 18:20:27 +01:00
Matthias a807d2d6c8 test: Add test for /pair_history without strategy 2025-02-01 18:09:35 +01:00
Matthias fda7b0f1ef chore: increment api_version 2025-02-01 17:39:40 +01:00
Matthias ae42f8d512 feat: api /pair_history - make Strategy optional 2025-02-01 17:39:28 +01:00
Matthias 6fb74fa166 feat: add "live_mode" flag to pair_history 2025-02-01 17:25:58 +01:00
Matthias 7dd0f2174c feat: Add "exchange" fields to pair_history 2025-02-01 15:47:33 +01:00
Matthias e1cc7dcfdf feat: allow pair-history from "live" candles 2025-02-01 15:47:10 +01:00
Matthias 6b3fff6b7e refactor: pair_history should only be available in webserver mode 2025-02-01 14:09:56 +01:00
Matthias 5d4324dd55 test: update percentChange pairlist test
there's little reason to have this test for a particular number
2025-02-01 13:53:14 +01:00
Matthias fc0cae21e6 fix: improve reliability when fetching from "features" 2025-02-01 13:49:17 +01:00
Matthias 89b6cc89a3 test: update test for removed manual limits 2025-02-01 13:49:04 +01:00
Matthias 26af62e4dd feat: remove manual ohlcv limit definitions where possible 2025-02-01 08:13:33 +01:00
Matthias f90cd44a9e feat: default ohlcv_limit to ccxt's features definition 2025-02-01 08:12:19 +01:00
Matthias 35f7c97c76 test: add "ohlcv_limit" live test 2025-01-31 18:27:47 +01:00
Matthias d530527f11 test: add test for exchange.features 2025-01-31 07:03:06 +01:00
Matthias 8af8260b7c feat: initial "features" support 2025-01-31 06:56:41 +01:00
Matthias 7a17cd781a chore: remove coinbasepro exchange
ccxt removed this exchange in https://github.com/ccxt/ccxt/pull/22502.
2025-01-31 06:30:39 +01:00
Matthias ba244cd2f6 chore: bump develop version to 2025.2 2025-01-30 19:43:51 +01:00
Vaadasch 7d3c8b48dd Add some distinction on the trailing_only_offset_is_reached 2025-01-30 14:48:15 +01:00
David Arena 7d5f021717 feat: add timeout to FtRestClient 2024-12-20 23:47:18 +01:00
77 changed files with 3755 additions and 1868 deletions
@@ -16,6 +16,8 @@ jobs:
name: develop name: develop
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
@@ -42,6 +44,7 @@ jobs:
branch: update/binance-leverage-tiers branch: update/binance-leverage-tiers
title: Update Binance Leverage Tiers title: Update Binance Leverage Tiers
commit-message: "chore: update pre-commit hooks" commit-message: "chore: update pre-commit hooks"
committer: Freqtrade Bot <noreply@github.com> committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
body: Update binance leverage tiers. body: Update binance leverage tiers.
delete-branch: true delete-branch: true
+28 -2
View File
@@ -24,11 +24,13 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ] os: [ "ubuntu-22.04", "ubuntu-24.04" ]
python-version: ["3.10", "3.11", "3.12"] python-version: ["3.10", "3.11", "3.12"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -157,6 +159,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -281,6 +285,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -366,6 +372,8 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -381,6 +389,8 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
@@ -391,6 +401,8 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Documentation syntax - name: Documentation syntax
run: | run: |
@@ -420,6 +432,8 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -506,6 +520,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -551,6 +567,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Download artifact 📦 - name: Download artifact 📦
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -578,6 +596,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Download artifact 📦 - name: Download artifact 📦
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -598,6 +618,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -633,7 +655,9 @@ jobs:
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Available platforms - name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }} run: echo ${PLATFORMS}
env:
PLATFORMS: ${{ steps.buildx.outputs.platforms }}
- name: Build and test and push docker images - name: Build and test and push docker images
env: env:
@@ -652,6 +676,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Extract branch name - name: Extract branch name
id: extract-branch id: extract-branch
+8 -2
View File
@@ -20,6 +20,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: true
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -43,12 +45,16 @@ jobs:
- name: Build and push Mike - name: Build and push Mike
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
run: | run: |
mike deploy ${{ github.ref_name }} latest --push --update-aliases mike deploy ${REF_NAME} latest --push --update-aliases
env:
REF_NAME: ${{ github.ref_name }}
- name: Build and push Mike - Release - name: Build and push Mike - Release
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
run: | run: |
mike deploy ${{ github.ref_name }} stable --push --update-aliases mike deploy ${REF_NAME} stable --push --update-aliases
env:
REF_NAME: ${{ github.ref_name }}
- name: Show mike versions - name: Show mike versions
run: | run: |
+7 -10
View File
@@ -17,26 +17,23 @@ concurrency:
group: "${{ github.workflow }}" group: "${{ github.workflow }}"
cancel-in-progress: true cancel-in-progress: true
permissions:
packages: write
jobs: jobs:
build-and-push: build-and-push:
permissions:
packages: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - uses: actions/checkout@v4
name: Checkout with:
id: checkout persist-credentials: false
uses: actions/checkout@v4 - name: Login to GitHub Container Registry
-
name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- - name: Pre-build dev container image
name: Pre-build dev container image
uses: devcontainers/ci@v0.3 uses: devcontainers/ci@v0.3
with: with:
subFolder: .github subFolder: .github
@@ -4,11 +4,16 @@ on:
branches: branches:
- stable - stable
# disable permissions for all of the available permissions
permissions: {}
jobs: jobs:
dockerHubDescription: dockerHubDescription:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Docker Hub Description - name: Docker Hub Description
uses: peter-evans/dockerhub-description@v4 uses: peter-evans/dockerhub-description@v4
+4 -1
View File
@@ -14,6 +14,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
@@ -36,6 +38,7 @@ jobs:
branch: update/pre-commit-hooks branch: update/pre-commit-hooks
title: Update pre-commit hooks title: Update pre-commit hooks
commit-message: "chore: update pre-commit hooks" commit-message: "chore: update pre-commit hooks"
committer: Freqtrade Bot <noreply@github.com> committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
body: Update versions of pre-commit hooks to latest version. body: Update versions of pre-commit hooks to latest version.
delete-branch: true delete-branch: true
+13 -7
View File
@@ -2,14 +2,14 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: "7.1.1" rev: "7.1.2"
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [Flake8-pyproject] additional_dependencies: [Flake8-pyproject]
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.14.1" rev: "v1.15.0"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
@@ -19,11 +19,11 @@ repos:
- types-requests==2.32.0.20241016 - types-requests==2.32.0.20241016
- types-tabulate==0.9.0.20241207 - types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20241206 - types-python-dateutil==2.9.0.20241206
- SQLAlchemy==2.0.37 - SQLAlchemy==2.0.38
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: "6.0.0" rev: "6.0.1"
hooks: hooks:
- id: isort - id: isort
name: isort (python) name: isort (python)
@@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.9.3' rev: 'v0.9.7'
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format
@@ -57,13 +57,19 @@ repos:
)$ )$
- repo: https://github.com/stefmolin/exif-stripper - repo: https://github.com/stefmolin/exif-stripper
rev: 0.6.1 rev: 0.6.2
hooks: hooks:
- id: strip-exif - id: strip-exif
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.4.0 rev: v2.4.1
hooks: hooks:
- id: codespell - id: codespell
additional_dependencies: additional_dependencies:
- tomli - tomli
# Ensure github actions remain safe
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.4.1
hooks:
- id: zizmor
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.12.7-slim-bookworm as base FROM python:3.12.9-slim-bookworm as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
+2 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.11.10-slim-bookworm as base FROM python:3.11.11-slim-bookworm as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
@@ -34,7 +34,7 @@ COPY build_helpers/* /tmp/
# Install dependencies # Install dependencies
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/ COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
USER ftuser USER ftuser
RUN pip install --user --no-cache-dir numpy \ RUN pip install --user --no-cache-dir "numpy<2" \
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \ && pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
&& pip install --user --no-cache-dir -r requirements.txt && pip install --user --no-cache-dir -r requirements.txt
+7 -3
View File
@@ -54,11 +54,13 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec
* Check timeouts for open orders. * Check timeouts for open orders.
* Calls `check_entry_timeout()` strategy callback for open entry orders. * Calls `check_entry_timeout()` strategy callback for open entry orders.
* Calls `check_exit_timeout()` strategy callback for open exit orders. * Calls `check_exit_timeout()` strategy callback for open exit orders.
* Calls `adjust_entry_price()` strategy callback for open entry orders. * Calls `adjust_order_price()` strategy callback for open orders.
* Calls `adjust_entry_price()` strategy callback for open entry orders. *only called when `adjust_order_price()` is not implemented*
* Calls `adjust_exit_price()` strategy callback for open exit orders. *only called when `adjust_order_price()` is not implemented*
* Verifies existing positions and eventually places exit orders. * Verifies existing positions and eventually places exit orders.
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`. * Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
* Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback. * Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback.
* Before a exit order is placed, `confirm_trade_exit()` strategy callback is called. * Before an exit order is placed, `confirm_trade_exit()` strategy callback is called.
* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required. * Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required.
* Check if trade-slots are still available (if `max_open_trades` is reached). * Check if trade-slots are still available (if `max_open_trades` is reached).
* Verifies entry signal trying to enter new positions. * Verifies entry signal trying to enter new positions.
@@ -80,7 +82,9 @@ This loop will be repeated again and again until the bot is stopped.
* Loops per candle simulating entry and exit points. * Loops per candle simulating entry and exit points.
* Calls `bot_loop_start()` strategy callback. * Calls `bot_loop_start()` strategy callback.
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks. * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
* Calls `adjust_entry_price()` strategy callback for open entry orders. * Calls `adjust_order_price()` strategy callback for open orders.
* Calls `adjust_entry_price()` strategy callback for open entry orders. *only called when `adjust_order_price()` is not implemented!*
* Calls `adjust_exit_price()` strategy callback for open exit orders. *only called when `adjust_order_price()` is not implemented!*
* Check for trade entry signals (`enter_long` / `enter_short` columns). * Check for trade entry signals (`enter_long` / `enter_short` columns).
* Confirm trade entry / exits (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy). * Confirm trade entry / exits (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle). * Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
+3
View File
@@ -377,6 +377,9 @@ If an incorrect category string is chosen, the plugin will print the available c
!!! Warning "Many categories" !!! Warning "Many categories"
Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues. Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues.
!!! Danger "Duplicate symbols in coingecko"
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
#### AgeFilter #### AgeFilter
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity). Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
+2 -2
View File
@@ -1,7 +1,7 @@
markdown==3.7 markdown==3.7
mkdocs==1.6.1 mkdocs==1.6.1
mkdocs-material==9.5.50 mkdocs-material==9.6.5
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.14.1 pymdown-extensions==10.14.3
jinja2==3.1.5 jinja2==3.1.5
mike==2.1.3 mike==2.1.3
+6 -4
View File
@@ -154,10 +154,10 @@ For example, simplified math:
In summary: The stoploss will be adjusted to be always be -10% of the highest observed price. In summary: The stoploss will be adjusted to be always be -10% of the highest observed price.
### Trailing stop loss, custom positive loss ### Trailing stop loss, different positive loss
You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, which can have a different value. You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, with a different value.
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. For example, your default stop loss is -10%, but once you have reached profitability (example 0.1%) a different trailing stoploss will be used.
!!! Note !!! Note
If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset).
@@ -208,7 +208,9 @@ Before this, `stoploss` is used for the trailing stoploss.
You can also keep a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns. You can also keep a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss` and is not trailing.
Leaving this value as `trailing_only_offset_is_reached=False` will allow the trailing stoploss to start trailing as soon as the asset price increases above the initial entry price.
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
Configuration (offset is buy-price + 3%): Configuration (offset is buy-price + 3%):
+2
View File
@@ -79,6 +79,8 @@ import talib.abstract as ta
class MyStrategy(IStrategy): class MyStrategy(IStrategy):
timeframe = '15m'
# set the initial stoploss to -10% # set the initial stoploss to -10%
stoploss = -0.10 stoploss = -0.10
+48 -17
View File
@@ -934,28 +934,25 @@ class DigDeeperStrategy(IStrategy):
The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`). The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`).
## Adjust Entry Price ## Adjust order Price
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles. The `adjust_order_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.
This callback is called once every iteration unless the order has been (re)placed within the current candle - limiting the maximum (re)placement of each order to once per candle. This callback is called once every iteration unless the order has been (re)placed within the current candle - limiting the maximum (re)placement of each order to once per candle.
This also means that the first call will be at the start of the next candle after the initial order was placed. This also means that the first call will be at the start of the next candle after the initial order was placed.
Be aware that `custom_entry_price()` is still the one dictating initial entry limit order price target at the time of entry trigger. Be aware that `custom_entry_price()`/`custom_exit_price()` is still the one dictating initial limit order price target at the time of the signal.
Orders can be cancelled out of this callback by returning `None`. Orders can be cancelled out of this callback by returning `None`.
Returning `current_order_rate` will keep the order on the exchange "as is". Returning `current_order_rate` will keep the order on the exchange "as is".
Returning any other price will cancel the existing order, and replace it with a new order. Returning any other price will cancel the existing order, and replace it with a new order.
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
If the cancellation of the original order fails, then the order will not be replaced - though the order will most likely have been canceled on exchange. Having this happen on initial entries will result in the deletion of the order, while on position adjustment orders, it'll result in the trade size remaining as is. If the cancellation of the original order fails, then the order will not be replaced - though the order will most likely have been canceled on exchange. Having this happen on initial entries will result in the deletion of the order, while on position adjustment orders, it'll result in the trade size remaining as is.
If the order has been partially filled, the order will not be replaced. You can however use [`adjust_trade_position()`](#adjust-trade-position) to adjust the trade size to the full, expected position size, should this be necessary / desired. If the order has been partially filled, the order will not be replaced. You can however use [`adjust_trade_position()`](#adjust-trade-position) to adjust the trade size to the expected position size, should this be necessary / desired.
!!! Warning "Regular timeout" !!! Warning "Regular timeout"
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`) takes precedence over this. Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`/`check_exit_timeout()`) takes precedence over this callback.
Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations. Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
```python ```python
# Default imports # Default imports
@@ -964,14 +961,26 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def adjust_entry_price(self, trade: Trade, order: Order | None, pair: str, def adjust_order_price(
current_time: datetime, proposed_rate: float, current_order_rate: float, self,
entry_tag: str | None, side: str, **kwargs) -> float: trade: Trade,
order: Order | None,
pair: str,
current_time: datetime,
proposed_rate: float,
current_order_rate: float,
entry_tag: str | None,
side: str,
is_entry: bool,
**kwargs,
) -> float:
""" """
Entry price re-adjustment logic, returning the user desired limit price. Exit and entry order price re-adjustment logic, returning the user desired limit price.
This only executes when a order was already placed, still open (unfilled fully or partially) This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger. and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default. When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained. If current_order_rate is returned then the existing order is maintained.
If None is returned then order gets canceled but not replaced by a new one. If None is returned then order gets canceled but not replaced by a new one.
@@ -983,14 +992,16 @@ class AwesomeStrategy(IStrategy):
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing. :param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
:param current_order_rate: Rate of the existing order in place. :param current_order_rate: Rate of the existing order in place.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: "long" or "short" - indicating the direction of the proposed trade :param side: 'long' or 'short' - indicating the direction of the proposed trade
:param is_entry: True if the order is an entry order, False if it's an exit order.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided :return float: New entry price value if provided
""" """
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
# Limit entry orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
if ( if (
pair == "BTC/USDT" is_entry
and pair == "BTC/USDT"
and entry_tag == "long_sma200" and entry_tag == "long_sma200"
and side == "long" and side == "long"
and (current_time - timedelta(minutes=10)) <= trade.open_date_utc and (current_time - timedelta(minutes=10)) <= trade.open_date_utc
@@ -1007,6 +1018,26 @@ class AwesomeStrategy(IStrategy):
return current_order_rate return current_order_rate
``` ```
!!! danger "Incompatibility with `adjust_*_price()`"
If you have both `adjust_order_price()` and `adjust_entry_price()`/`adjust_exit_price()` implemented, only `adjust_order_price()` will be used.
If you need to adjust entry/exit prices, you can either implement the logic in `adjust_order_price()`, or use the split `adjust_entry_price()` / `adjust_exit_price()` callbacks, but not both.
Mixing these is not supported and will raise an error during bot startup.
### Adjust Entry Price
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace entry limit orders upon arrival.
It's a sub-set of `adjust_order_price()` and is called only for entry orders.
All remaining behavior is identical to `adjust_order_price()`.
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
### Adjust Exit Price
The `adjust_exit_price()` callback may be used by strategy developer to refresh/replace exit limit orders upon arrival.
It's a sub-set of `adjust_order_price()` and is called only for exit orders.
All remaining behavior is identical to `adjust_order_price()`.
## Leverage Callback ## Leverage Callback
When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage). When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage).
+1 -1
View File
@@ -513,7 +513,7 @@ By default, freqtrade will attempt to load strategies from all `.py` files withi
Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade in dry (or live, depending on your configuration) mode with: Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade in dry (or live, depending on your configuration) mode with:
```bash ```bash
freqtrade trade --strategy AwesomeStrategy` freqtrade trade --strategy AwesomeStrategy
``` ```
Note that we're using the class name, not the file name. Note that we're using the class name, not the file name.
+1 -1
View File
@@ -1,6 +1,6 @@
"""Freqtrade bot""" """Freqtrade bot"""
__version__ = "2025.1" __version__ = "2025.2"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
+1 -2
View File
@@ -18,8 +18,7 @@ from freqtrade.enums import CandleType, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
from freqtrade.util import dt_now, dt_ts, format_ms_time from freqtrade.util import dt_now, dt_ts, format_ms_time, format_ms_time_det
from freqtrade.util.datetime_helpers import format_ms_time_det
from freqtrade.util.migrations import migrate_data from freqtrade.util.migrations import migrate_data
from freqtrade.util.progress_tracker import CustomProgress, retrieve_progress_tracker from freqtrade.util.progress_tracker import CustomProgress, retrieve_progress_tracker
-1
View File
@@ -10,7 +10,6 @@ from freqtrade.exchange.bitmart import Bitmart
from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bitvavo import Bitvavo from freqtrade.exchange.bitvavo import Bitvavo
from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.bybit import Bybit
from freqtrade.exchange.coinbasepro import Coinbasepro
from freqtrade.exchange.cryptocom import Cryptocom from freqtrade.exchange.cryptocom import Cryptocom
from freqtrade.exchange.exchange_utils import ( from freqtrade.exchange.exchange_utils import (
ROUND_DOWN, ROUND_DOWN,
+3 -1
View File
@@ -29,7 +29,6 @@ class Binance(Exchange):
"stop_price_prop": "stopPrice", "stop_price_prop": "stopPrice",
"stoploss_order_types": {"limit": "stop_loss_limit"}, "stoploss_order_types": {"limit": "stop_loss_limit"},
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"], "order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
"ohlcv_candle_limit": 1000,
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
"trades_has_history": True, "trades_has_history": True,
@@ -37,6 +36,7 @@ class Binance(Exchange):
"ws_enabled": True, "ws_enabled": True,
} }
_ft_has_futures: FtHas = { _ft_has_futures: FtHas = {
"funding_fee_candle_limit": 1000,
"stoploss_order_types": {"limit": "stop", "market": "stop_market"}, "stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"order_time_in_force": ["GTC", "FOK", "IOC"], "order_time_in_force": ["GTC", "FOK", "IOC"],
"tickers_have_price": False, "tickers_have_price": False,
@@ -140,6 +140,7 @@ class Binance(Exchange):
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
""" """
if is_new_pair: if is_new_pair:
with self._loop_lock:
x = self.loop.run_until_complete( x = self.loop.run_until_complete(
self._async_get_candle_history(pair, timeframe, candle_type, 0) self._async_get_candle_history(pair, timeframe, candle_type, 0)
) )
@@ -201,6 +202,7 @@ class Binance(Exchange):
""" """
Fastly fetch OHLCV data by leveraging https://data.binance.vision. Fastly fetch OHLCV data by leveraging https://data.binance.vision.
""" """
with self._loop_lock:
df = self.loop.run_until_complete( df = self.loop.run_until_complete(
download_archive_ohlcv( download_archive_ohlcv(
candle_type=candle_type, candle_type=candle_type,
File diff suppressed because it is too large Load Diff
-1
View File
@@ -31,7 +31,6 @@ class Bybit(Exchange):
unified_account = False unified_account = False
_ft_has: FtHas = { _ft_has: FtHas = {
"ohlcv_candle_limit": 1000,
"ohlcv_has_history": True, "ohlcv_has_history": True,
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"], "order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
"ws_enabled": True, "ws_enabled": True,
-24
View File
@@ -1,24 +0,0 @@
"""CoinbasePro exchange subclass"""
import logging
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
logger = logging.getLogger(__name__)
class Coinbasepro(Exchange):
"""
CoinbasePro exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: FtHas = {
"ohlcv_candle_limit": 300,
}
+34 -8
View File
@@ -12,7 +12,7 @@ from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import floor, isnan from math import floor, isnan
from threading import Lock from threading import Lock
from typing import Any, Literal, TypeGuard from typing import Any, Literal, TypeGuard, TypeVar
import ccxt import ccxt
import ccxt.pro as ccxt_pro import ccxt.pro as ccxt_pro
@@ -113,6 +113,8 @@ from freqtrade.util.periodic_cache import PeriodicCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T = TypeVar("T")
class Exchange: class Exchange:
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement) # Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
@@ -131,7 +133,6 @@ class Exchange:
"stoploss_order_types": {}, "stoploss_order_types": {},
"order_time_in_force": ["GTC"], "order_time_in_force": ["GTC"],
"ohlcv_params": {}, "ohlcv_params": {},
"ohlcv_candle_limit": 500,
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv "ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
"ohlcv_partial_candle": True, "ohlcv_partial_candle": True,
"ohlcv_require_since": False, "ohlcv_require_since": False,
@@ -276,6 +277,11 @@ class Exchange:
logger.info(f'Using Exchange "{self.name}"') logger.info(f'Using Exchange "{self.name}"')
self.required_candle_call_count = 1 self.required_candle_call_count = 1
# Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = (
exchange_conf.get("markets_refresh_interval", 60) * 60 * 1000
)
if validate: if validate:
# Initial markets load # Initial markets load
self.reload_markets(True, load_leverage_tiers=False) self.reload_markets(True, load_leverage_tiers=False)
@@ -285,11 +291,6 @@ class Exchange:
self._startup_candle_count, config.get("timeframe", "") self._startup_candle_count, config.get("timeframe", "")
) )
# Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = (
exchange_conf.get("markets_refresh_interval", 60) * 60 * 1000
)
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers: if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
self.fill_leverage_tiers() self.fill_leverage_tiers()
self.additional_exchange_init() self.additional_exchange_init()
@@ -466,7 +467,12 @@ class Exchange:
:return: Candle limit as integer :return: Candle limit as integer
""" """
fallback_val = self._ft_has.get("ohlcv_candle_limit") ccxt_val = self.features(
"spot" if candle_type == CandleType.SPOT else "futures", "fetchOHLCV", "limit", 500
)
if not isinstance(ccxt_val, float | int):
ccxt_val = 500
fallback_val = self._ft_has.get("ohlcv_candle_limit", ccxt_val)
if candle_type == CandleType.FUNDING_RATE: if candle_type == CandleType.FUNDING_RATE:
fallback_val = self._ft_has.get("funding_fee_candle_limit", fallback_val) fallback_val = self._ft_has.get("funding_fee_candle_limit", fallback_val)
return int( return int(
@@ -642,6 +648,7 @@ class Exchange:
def _load_async_markets(self, reload: bool = False) -> dict[str, Any]: def _load_async_markets(self, reload: bool = False) -> dict[str, Any]:
try: try:
with self._loop_lock:
markets = self.loop.run_until_complete(self._api_reload_markets(reload=reload)) markets = self.loop.run_until_complete(self._api_reload_markets(reload=reload))
if isinstance(markets, Exception): if isinstance(markets, Exception):
@@ -887,6 +894,24 @@ class Exchange:
return self._ft_has["exchange_has_overrides"][endpoint] return self._ft_has["exchange_has_overrides"][endpoint]
return endpoint in self._api_async.has and self._api_async.has[endpoint] return endpoint in self._api_async.has and self._api_async.has[endpoint]
def features(
self, market_type: Literal["spot", "futures"], endpoint, attribute, default: T
) -> T:
"""
Returns the exchange features for the given markettype
https://docs.ccxt.com/#/README?id=features
attributes are in a nested dict, with spot and swap.linear
e.g. spot.fetchOHLCV.limit
swap.linear.fetchOHLCV.limit
"""
feat = (
self._api_async.features.get("spot", {})
if market_type == "spot"
else self._api_async.features.get("swap", {}).get("linear", {})
)
return feat.get(endpoint, {}).get(attribute, default)
def get_precision_amount(self, pair: str) -> float | None: def get_precision_amount(self, pair: str) -> float | None:
""" """
Returns the amount precision of the exchange. Returns the amount precision of the exchange.
@@ -2318,6 +2343,7 @@ class Exchange:
:param until_ms: Timestamp in milliseconds to get history up to :param until_ms: Timestamp in milliseconds to get history up to
:return: Dataframe with candle (OHLCV) data :return: Dataframe with candle (OHLCV) data
""" """
with self._loop_lock:
pair, _, _, data, _ = self.loop.run_until_complete( pair, _, _, data, _ = self.loop.run_until_complete(
self._async_get_historic_ohlcv( self._async_get_historic_ohlcv(
pair=pair, pair=pair,
+2 -2
View File
@@ -4,7 +4,7 @@ Exchange support utils
import inspect import inspect
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import ceil, floor from math import ceil, floor, isnan
from typing import Any from typing import Any
import ccxt import ccxt
@@ -305,7 +305,7 @@ def price_to_precision(
:param rounding_mode: rounding mode to use. Defaults to ROUND :param rounding_mode: rounding mode to use. Defaults to ROUND
:return: price rounded up to the precision the Exchange accepts :return: price rounded up to the precision the Exchange accepts
""" """
if price_precision is not None and precisionMode is not None: if price_precision is not None and precisionMode is not None and not isnan(price):
if rounding_mode not in (ROUND_UP, ROUND_DOWN): if rounding_mode not in (ROUND_UP, ROUND_DOWN):
# Use CCXT code where possible. # Use CCXT code where possible.
return float( return float(
+22
View File
@@ -9,6 +9,8 @@ import ccxt
from freqtrade.constants import Config, PairWithTimeframe from freqtrade.constants import Config, PairWithTimeframe
from freqtrade.enums.candletype import CandleType from freqtrade.enums.candletype import CandleType
from freqtrade.exceptions import TemporaryError
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_seconds
from freqtrade.exchange.exchange_types import OHLCVResponse from freqtrade.exchange.exchange_types import OHLCVResponse
from freqtrade.util import dt_ts, format_ms_time, format_ms_time_det from freqtrade.util import dt_ts, format_ms_time, format_ms_time_det
@@ -82,14 +84,21 @@ class ExchangeWS:
Remove history for a pair/timeframe combination from ccxt cache Remove history for a pair/timeframe combination from ccxt cache
""" """
self._ccxt_object.ohlcvs.get(paircomb[0], {}).pop(paircomb[1], None) self._ccxt_object.ohlcvs.get(paircomb[0], {}).pop(paircomb[1], None)
self.klines_last_refresh.pop(paircomb, None)
@retrier(retries=3)
def ohlcvs(self, pair: str, timeframe: str) -> list[list]: def ohlcvs(self, pair: str, timeframe: str) -> list[list]:
""" """
Returns a copy of the klines for a pair/timeframe combination Returns a copy of the klines for a pair/timeframe combination
Note: this will only contain the data received from the websocket Note: this will only contain the data received from the websocket
so the data will build up over time. so the data will build up over time.
""" """
try:
return deepcopy(self._ccxt_object.ohlcvs.get(pair, {}).get(timeframe, [])) return deepcopy(self._ccxt_object.ohlcvs.get(pair, {}).get(timeframe, []))
except RuntimeError as e:
# Capture runtime errors and retry
# TemporaryError does not cause backoff - so we're essentially retrying immediately
raise TemporaryError(f"Error deepcopying: {e}") from e
def cleanup_expired(self) -> None: def cleanup_expired(self) -> None:
""" """
@@ -130,6 +139,15 @@ class ExchangeWS:
) )
) )
async def _unwatch_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None:
try:
await self._ccxt_object.un_watch_ohlcv_for_symbols([[pair, timeframe]])
except ccxt.NotSupported as e:
logger.debug("un_watch_ohlcv_for_symbols not supported: %s", e)
pass
except Exception:
logger.exception("Exception in _unwatch_ohlcv")
def _continuous_stopped( def _continuous_stopped(
self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType
): ):
@@ -142,6 +160,10 @@ class ExchangeWS:
result = str(result1) result = str(result1)
logger.info(f"{pair}, {timeframe}, {candle_type} - Task finished - {result}") logger.info(f"{pair}, {timeframe}, {candle_type} - Task finished - {result}")
asyncio.run_coroutine_threadsafe(
self._unwatch_ohlcv(pair, timeframe, candle_type), loop=self._loop
)
self._klines_scheduled.discard((pair, timeframe, candle_type)) self._klines_scheduled.discard((pair, timeframe, candle_type))
self._pop_history((pair, timeframe, candle_type)) self._pop_history((pair, timeframe, candle_type))
-1
View File
@@ -30,7 +30,6 @@ class Gate(Exchange):
unified_account = False unified_account = False
_ft_has: FtHas = { _ft_has: FtHas = {
"ohlcv_candle_limit": 1000,
"order_time_in_force": ["GTC", "IOC"], "order_time_in_force": ["GTC", "IOC"],
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit"}, "stoploss_order_types": {"limit": "limit"},
-1
View File
@@ -21,7 +21,6 @@ class Htx(Exchange):
"stop_price_param": "stopPrice", "stop_price_param": "stopPrice",
"stop_price_prop": "stopPrice", "stop_price_prop": "stopPrice",
"stoploss_order_types": {"limit": "stop-limit"}, "stoploss_order_types": {"limit": "stop-limit"},
"ohlcv_candle_limit": 1000,
"l2_limit_range": [5, 10, 20], "l2_limit_range": [5, 10, 20],
"l2_limit_range_required": False, "l2_limit_range_required": False,
"ohlcv_candle_limit_per_timeframe": { "ohlcv_candle_limit_per_timeframe": {
-1
View File
@@ -22,7 +22,6 @@ class Hyperliquid(Exchange):
_ft_has: FtHas = { _ft_has: FtHas = {
"ohlcv_has_history": False, "ohlcv_has_history": False,
"ohlcv_candle_limit": 5000,
"l2_limit_range": [20], "l2_limit_range": [20],
"trades_has_history": False, "trades_has_history": False,
"tickers_have_bid_ask": False, "tickers_have_bid_ask": False,
-1
View File
@@ -26,7 +26,6 @@ class Kraken(Exchange):
"stop_price_prop": "stopLossPrice", "stop_price_prop": "stopLossPrice",
"stoploss_order_types": {"limit": "limit", "market": "market"}, "stoploss_order_types": {"limit": "limit", "market": "market"},
"order_time_in_force": ["GTC", "IOC", "PO"], "order_time_in_force": ["GTC", "IOC", "PO"],
"ohlcv_candle_limit": 720,
"ohlcv_has_history": False, "ohlcv_has_history": False,
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
-1
View File
@@ -28,7 +28,6 @@ class Kucoin(Exchange):
"l2_limit_range": [20, 100], "l2_limit_range": [20, 100],
"l2_limit_range_required": False, "l2_limit_range_required": False,
"order_time_in_force": ["GTC", "FOK", "IOC"], "order_time_in_force": ["GTC", "FOK", "IOC"],
"ohlcv_candle_limit": 1500,
} }
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict: def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict:
@@ -6,26 +6,21 @@ from sklearn.utils.validation import has_fit_parameter, validate_data
class FreqaiMultiOutputRegressor(MultiOutputRegressor): class FreqaiMultiOutputRegressor(MultiOutputRegressor):
def fit(self, X, y, sample_weight=None, fit_params=None): def fit(self, X, y, sample_weight=None, fit_params=None):
"""Fit the model to data, separately for each output variable. """Fit the model to data, separately for each output variable.
Parameters :param X: {array-like, sparse matrix} of shape (n_samples, n_features)
----------
X : {array-like, sparse matrix} of shape (n_samples, n_features)
The input data. The input data.
y : {array-like, sparse matrix} of shape (n_samples, n_outputs) :param y: {array-like, sparse matrix} of shape (n_samples, n_outputs)
Multi-output targets. An indicator matrix turns on multilabel Multi-output targets. An indicator matrix turns on multilabel
estimation. estimation.
sample_weight : array-like of shape (n_samples,), default=None :param sample_weight: array-like of shape (n_samples,), default=None
Sample weights. If `None`, then samples are equally weighted. Sample weights. If `None`, then samples are equally weighted.
Only supported if the underlying regressor supports sample Only supported if the underlying regressor supports sample
weights. weights.
fit_params : A list of dicts for the fit_params
:param fit_params: A list of dicts for the fit_params
Parameters passed to the ``estimator.fit`` method of each step. Parameters passed to the ``estimator.fit`` method of each step.
Each dict may contain same or different values (e.g. different Each dict may contain same or different values (e.g. different
eval_sets or init_models) eval_sets or init_models)
.. versionadded:: 0.23
Returns
-------
self : object
Returns a fitted instance.
""" """
if not hasattr(self.estimator, "fit"): if not hasattr(self.estimator, "fit"):
+6 -4
View File
@@ -33,6 +33,8 @@ LABEL_PIPELINE = "label_pipeline"
TRAINDF = "trained_df" TRAINDF = "trained_df"
METADATA = "metadata" METADATA = "metadata"
METADATA_NUMBER_MODE = rapidjson.NM_NATIVE | rapidjson.NM_NAN
class pair_info(TypedDict): class pair_info(TypedDict):
model_filename: str model_filename: str
@@ -495,7 +497,7 @@ class FreqaiDataDrawer:
dk.data["label_list"] = dk.label_list dk.data["label_list"] = dk.label_list
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp: with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=METADATA_NUMBER_MODE)
return return
@@ -526,7 +528,7 @@ class FreqaiDataDrawer:
dk.data["label_list"] = dk.label_list dk.data["label_list"] = dk.label_list
# store the metadata # store the metadata
with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp: with (save_path / f"{dk.model_filename}_{METADATA}.json").open("w") as fp:
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=METADATA_NUMBER_MODE)
# save the pipelines to pickle files # save the pipelines to pickle files
with (save_path / f"{dk.model_filename}_{FEATURE_PIPELINE}.pkl").open("wb") as fp: with (save_path / f"{dk.model_filename}_{FEATURE_PIPELINE}.pkl").open("wb") as fp:
@@ -563,7 +565,7 @@ class FreqaiDataDrawer:
presaved backtesting (prediction file loading). presaved backtesting (prediction file loading).
""" """
with (dk.data_path / f"{dk.model_filename}_{METADATA}.json").open("r") as fp: with (dk.data_path / f"{dk.model_filename}_{METADATA}.json").open("r") as fp:
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) dk.data = rapidjson.load(fp, number_mode=METADATA_NUMBER_MODE)
dk.training_features_list = dk.data["training_features_list"] dk.training_features_list = dk.data["training_features_list"]
dk.label_list = dk.data["label_list"] dk.label_list = dk.data["label_list"]
@@ -587,7 +589,7 @@ class FreqaiDataDrawer:
dk.label_pipeline = self.meta_data_dictionary[coin][LABEL_PIPELINE] dk.label_pipeline = self.meta_data_dictionary[coin][LABEL_PIPELINE]
else: else:
with (dk.data_path / f"{dk.model_filename}_{METADATA}.json").open("r") as fp: with (dk.data_path / f"{dk.model_filename}_{METADATA}.json").open("r") as fp:
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) dk.data = rapidjson.load(fp, number_mode=METADATA_NUMBER_MODE)
with (dk.data_path / f"{dk.model_filename}_{FEATURE_PIPELINE}.pkl").open("rb") as fp: with (dk.data_path / f"{dk.model_filename}_{FEATURE_PIPELINE}.pkl").open("rb") as fp:
dk.feature_pipeline = cloudpickle.load(fp) dk.feature_pipeline = cloudpickle.load(fp)
+57 -25
View File
@@ -64,7 +64,7 @@ from freqtrade.rpc.rpc_types import (
) )
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise, MeasureTime, dt_from_ts from freqtrade.util import FtPrecise, MeasureTime, PeriodicCache, dt_from_ts, dt_now
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@@ -145,6 +145,10 @@ class FreqtradeBot(LoggingMixin):
else None else None
) )
logger.info("Starting initial pairlist refresh")
with MeasureTime(
lambda duration, _: logger.info(f"Initial Pairlist refresh took {duration:.2f}s"), 0
):
self.active_pair_whitelist = self._refresh_active_whitelist() self.active_pair_whitelist = self._refresh_active_whitelist()
# Set initial bot state from config # Set initial bot state from config
@@ -154,6 +158,7 @@ class FreqtradeBot(LoggingMixin):
# Protect exit-logic from forcesell and vice versa # Protect exit-logic from forcesell and vice versa
self._exit_lock = Lock() self._exit_lock = Lock()
timeframe_secs = timeframe_to_seconds(self.strategy.timeframe) timeframe_secs = timeframe_to_seconds(self.strategy.timeframe)
self._exit_reason_cache = PeriodicCache(100, ttl=timeframe_secs)
LoggingMixin.__init__(self, logger, timeframe_secs) LoggingMixin.__init__(self, logger, timeframe_secs)
self._schedule = Scheduler() self._schedule = Scheduler()
@@ -1374,6 +1379,15 @@ class FreqtradeBot(LoggingMixin):
for should_exit in exits: for should_exit in exits:
if should_exit.exit_flag: if should_exit.exit_flag:
exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
if trade.has_open_orders:
if prev_eval := self._exit_reason_cache.get(
f"{trade.pair}_{trade.id}_{exit_tag1 or should_exit.exit_reason}", None
):
logger.debug(
f"Exit reason already seen this candle, first seen at {prev_eval}"
)
continue
logger.info( logger.info(
f"Exit for {trade.pair} detected. Reason: {should_exit.exit_type}" f"Exit for {trade.pair} detected. Reason: {should_exit.exit_type}"
f"{f' Tag: {exit_tag1}' if exit_tag1 is not None else ''}" f"{f' Tag: {exit_tag1}' if exit_tag1 is not None else ''}"
@@ -1593,19 +1607,20 @@ class FreqtradeBot(LoggingMixin):
self.replace_order(order, open_order, trade) self.replace_order(order, open_order, trade)
def handle_cancel_order( def handle_cancel_order(
self, order: CcxtOrder, order_obj: Order, trade: Trade, reason: str self, order: CcxtOrder, order_obj: Order, trade: Trade, reason: str, replacing: bool = False
) -> None: ) -> bool:
""" """
Check if current analyzed order timed out and cancel if necessary. Check if current analyzed order timed out and cancel if necessary.
:param order: Order dict grabbed with exchange.fetch_order() :param order: Order dict grabbed with exchange.fetch_order()
:param order_obj: Order object from the database. :param order_obj: Order object from the database.
:param trade: Trade object. :param trade: Trade object.
:return: None :return: True if the order was canceled, False otherwise.
""" """
if order["side"] == trade.entry_side: if order["side"] == trade.entry_side:
self.handle_cancel_enter(trade, order, order_obj, reason) return self.handle_cancel_enter(trade, order, order_obj, reason, replacing)
else: else:
canceled = self.handle_cancel_exit(trade, order, order_obj, reason) canceled = self.handle_cancel_exit(trade, order, order_obj, reason)
if not replacing:
canceled_count = trade.get_canceled_exit_order_count() canceled_count = trade.get_canceled_exit_order_count()
max_timeouts = self.config.get("unfilledtimeout", {}).get("exit_timeout_count", 0) max_timeouts = self.config.get("unfilledtimeout", {}).get("exit_timeout_count", 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
@@ -1614,6 +1629,7 @@ class FreqtradeBot(LoggingMixin):
f"timed out {max_timeouts} times. force selling {order['amount']}." f"timed out {max_timeouts} times. force selling {order['amount']}."
) )
self.emergency_exit(trade, order["price"], order["amount"]) self.emergency_exit(trade, order["price"], order["amount"])
return canceled
def emergency_exit( def emergency_exit(
self, trade: Trade, price: float, sub_trade_amt: float | None = None self, trade: Trade, price: float, sub_trade_amt: float | None = None
@@ -1649,9 +1665,9 @@ class FreqtradeBot(LoggingMixin):
def replace_order(self, order: CcxtOrder, order_obj: Order | None, trade: Trade) -> None: def replace_order(self, order: CcxtOrder, order_obj: Order | None, trade: Trade) -> None:
""" """
Check if current analyzed entry order should be replaced or simply cancelled. Check if current analyzed entry order should be replaced or simply cancelled.
To simply cancel the existing order(no replacement) adjust_entry_price() should return None To simply cancel the existing order(no replacement) adjust_order_price() should return None
To maintain existing order adjust_entry_price() should return order_obj.price To maintain existing order adjust_order_price() should return order_obj.price
To replace existing order adjust_entry_price() should return desired price for limit order To replace existing order adjust_order_price() should return desired price for limit order
:param order: Order dict grabbed with exchange.fetch_order() :param order: Order dict grabbed with exchange.fetch_order()
:param order_obj: Order object. :param order_obj: Order object.
:param trade: Trade object. :param trade: Trade object.
@@ -1665,17 +1681,17 @@ class FreqtradeBot(LoggingMixin):
self.strategy.timeframe, latest_candle_open_date self.strategy.timeframe, latest_candle_open_date
) )
# Check if new candle # Check if new candle
if ( if order_obj and latest_candle_close_date > order_obj.order_date_utc:
order_obj is_entry = order_obj.side == trade.entry_side
and order_obj.side == trade.entry_side
and latest_candle_close_date > order_obj.order_date_utc
):
# New candle # New candle
proposed_rate = self.exchange.get_rate( proposed_rate = self.exchange.get_rate(
trade.pair, side="entry", is_short=trade.is_short, refresh=True trade.pair,
side="entry" if is_entry else "exit",
is_short=trade.is_short,
refresh=True,
) )
adjusted_entry_price = strategy_safe_wrapper( adjusted_price = strategy_safe_wrapper(
self.strategy.adjust_entry_price, default_retval=order_obj.safe_placement_price self.strategy.adjust_order_price, default_retval=order_obj.safe_placement_price
)( )(
trade=trade, trade=trade,
order=order_obj, order=order_obj,
@@ -1685,36 +1701,51 @@ class FreqtradeBot(LoggingMixin):
current_order_rate=order_obj.safe_placement_price, current_order_rate=order_obj.safe_placement_price,
entry_tag=trade.enter_tag, entry_tag=trade.enter_tag,
side=trade.trade_direction, side=trade.trade_direction,
is_entry=is_entry,
) )
replacing = True replacing = True
cancel_reason = constants.CANCEL_REASON["REPLACE"] cancel_reason = constants.CANCEL_REASON["REPLACE"]
if not adjusted_entry_price: if not adjusted_price:
replacing = False replacing = False
cancel_reason = constants.CANCEL_REASON["USER_CANCEL"] cancel_reason = constants.CANCEL_REASON["USER_CANCEL"]
if order_obj.safe_placement_price != adjusted_entry_price:
if order_obj.safe_placement_price != adjusted_price:
# cancel existing order if new price is supplied or None # cancel existing order if new price is supplied or None
res = self.handle_cancel_enter( res = self.handle_cancel_order(
trade, order, order_obj, cancel_reason, replacing=replacing order, order_obj, trade, cancel_reason, replacing=replacing
) )
if not res: if not res:
self.replace_order_failed( self.replace_order_failed(
trade, f"Could not fully cancel order for {trade}, therefore not replacing." trade, f"Could not fully cancel order for {trade}, therefore not replacing."
) )
return return
if adjusted_entry_price: if adjusted_price:
# place new order only if new price is supplied # place new order only if new price is supplied
try: try:
if not self.execute_entry( if is_entry:
succeeded = self.execute_entry(
pair=trade.pair, pair=trade.pair,
stake_amount=( stake_amount=(
order_obj.safe_remaining * order_obj.safe_price / trade.leverage order_obj.safe_remaining * order_obj.safe_price / trade.leverage
), ),
price=adjusted_entry_price, price=adjusted_price,
trade=trade, trade=trade,
is_short=trade.is_short, is_short=trade.is_short,
mode="replace", mode="replace",
): )
else:
succeeded = self.execute_trade_exit(
trade,
adjusted_price,
exit_check=ExitCheckTuple(
exit_type=ExitType.CUSTOM_EXIT,
exit_reason=order_obj.ft_order_tag or "order_replaced",
),
ordertype="limit",
sub_trade_amt=order_obj.safe_remaining,
)
if not succeeded:
self.replace_order_failed( self.replace_order_failed(
trade, f"Could not replace order for {trade}." trade, f"Could not replace order for {trade}."
) )
@@ -1774,7 +1805,7 @@ class FreqtradeBot(LoggingMixin):
if trade.has_open_orders: if trade.has_open_orders:
oo = trade.select_order(side, True) oo = trade.select_order(side, True)
if oo is not None: if oo is not None:
if (price == oo.price) and (side == oo.side) and (amount == oo.amount): if price == oo.price and side == oo.side and amount == oo.amount:
logger.info( logger.info(
f"A similar open order was found for {trade.pair}. " f"A similar open order was found for {trade.pair}. "
f"Keeping existing {trade.exit_side} order. {price=}, {amount=}" f"Keeping existing {trade.exit_side} order. {price=}, {amount=}"
@@ -2092,6 +2123,7 @@ class FreqtradeBot(LoggingMixin):
self.handle_insufficient_funds(trade) self.handle_insufficient_funds(trade)
return False return False
self._exit_reason_cache[f"{trade.pair}_{trade.id}_{exit_reason}"] = dt_now()
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit) order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
order_obj.ft_order_tag = exit_reason order_obj.ft_order_tag = exit_reason
trade.orders.append(order_obj) trade.orders.append(order_obj)
+1
View File
@@ -35,6 +35,7 @@ def update_liquidation_prices(
open_trades: list[Trade] = Trade.get_open_trades() open_trades: list[Trade] = Trade.get_open_trades()
for t in open_trades: for t in open_trades:
if t.has_open_position:
# TODO: This should be done in a batch update # TODO: This should be done in a batch update
t.set_liquidation_price( t.set_liquidation_price(
exchange.get_liquidation_price( exchange.get_liquidation_price(
+3 -6
View File
@@ -75,8 +75,7 @@ def setup_logging(config: Config) -> None:
# config['logfilename']), which defaults to '/dev/log', applicable for most # config['logfilename']), which defaults to '/dev/log', applicable for most
# of the systems. # of the systems.
address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else "/dev/log" address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else "/dev/log"
handler_sl = get_existing_handlers(SysLogHandler) if handler_sl := get_existing_handlers(SysLogHandler):
if handler_sl:
logging.root.removeHandler(handler_sl) logging.root.removeHandler(handler_sl)
handler_sl = SysLogHandler(address=address) handler_sl = SysLogHandler(address=address)
# No datetime field for logging into syslog, to allow syslog # No datetime field for logging into syslog, to allow syslog
@@ -92,8 +91,7 @@ def setup_logging(config: Config) -> None:
"You need the cysystemd python package be installed in " "You need the cysystemd python package be installed in "
"order to use logging to journald." "order to use logging to journald."
) )
handler_jd = get_existing_handlers(JournaldLogHandler) if handler_jd := get_existing_handlers(JournaldLogHandler):
if handler_jd:
logging.root.removeHandler(handler_jd) logging.root.removeHandler(handler_jd)
handler_jd = JournaldLogHandler() handler_jd = JournaldLogHandler()
# No datetime field for logging into journald, to allow syslog # No datetime field for logging into journald, to allow syslog
@@ -102,8 +100,7 @@ def setup_logging(config: Config) -> None:
handler_jd.setFormatter(Formatter("%(name)s - %(levelname)s - %(message)s")) handler_jd.setFormatter(Formatter("%(name)s - %(levelname)s - %(message)s"))
logging.root.addHandler(handler_jd) logging.root.addHandler(handler_jd)
else: else:
handler_rf = get_existing_handlers(RotatingFileHandler) if handler_rf := get_existing_handlers(RotatingFileHandler):
if handler_rf:
logging.root.removeHandler(handler_rf) logging.root.removeHandler(handler_rf)
try: try:
logfile_path = Path(logfile) logfile_path = Path(logfile)
+32 -15
View File
@@ -7,7 +7,7 @@ This module contains the backtesting logic
import logging import logging
from collections import defaultdict from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from typing import Any from typing import Any
from numpy import nan from numpy import nan
@@ -63,7 +63,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise from freqtrade.util import FtPrecise, dt_now
from freqtrade.util.migrations import migrate_data from freqtrade.util.migrations import migrate_data
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@@ -396,6 +396,8 @@ class Backtesting:
self.canceled_trade_entries = 0 self.canceled_trade_entries = 0
self.canceled_entry_orders = 0 self.canceled_entry_orders = 0
self.replaced_entry_orders = 0 self.replaced_entry_orders = 0
self.canceled_exit_orders = 0
self.replaced_exit_orders = 0
self.dataprovider.clear_cache() self.dataprovider.clear_cache()
if enable_protections: if enable_protections:
self._load_protections(self.strategy) self._load_protections(self.strategy)
@@ -1234,8 +1236,8 @@ class Backtesting:
for order in [o for o in trade.orders if o.ft_is_open]: for order in [o for o in trade.orders if o.ft_is_open]:
if order.side == trade.entry_side: if order.side == trade.entry_side:
self.canceled_entry_orders += 1 self.canceled_entry_orders += 1
# elif order.side == trade.exit_side: elif order.side == trade.exit_side:
# self.canceled_exit_orders += 1 self.canceled_exit_orders += 1
# canceled orders are removed from the trade # canceled orders are removed from the trade
del trade.orders[trade.orders.index(order)] del trade.orders[trade.orders.index(order)]
@@ -1299,9 +1301,10 @@ class Backtesting:
Returns True if the trade should be deleted. Returns True if the trade should be deleted.
""" """
# only check on new candles for open entry orders # only check on new candles for open entry orders
if order.side == trade.entry_side and current_time > order.order_date_utc: if current_time > order.order_date_utc:
is_entry = order.side == trade.entry_side
requested_rate = strategy_safe_wrapper( requested_rate = strategy_safe_wrapper(
self.strategy.adjust_entry_price, default_retval=order.ft_price self.strategy.adjust_order_price, default_retval=order.ft_price
)( )(
trade=trade, # type: ignore[arg-type] trade=trade, # type: ignore[arg-type]
order=order, order=order,
@@ -1311,6 +1314,7 @@ class Backtesting:
current_order_rate=order.ft_price, current_order_rate=order.ft_price,
entry_tag=trade.enter_tag, entry_tag=trade.enter_tag,
side=trade.trade_direction, side=trade.trade_direction,
is_entry=is_entry,
) # default value is current order price ) # default value is current order price
# cancel existing order whenever a new rate is requested (or None) # cancel existing order whenever a new rate is requested (or None)
@@ -1319,10 +1323,14 @@ class Backtesting:
return False return False
else: else:
del trade.orders[trade.orders.index(order)] del trade.orders[trade.orders.index(order)]
if is_entry:
self.canceled_entry_orders += 1 self.canceled_entry_orders += 1
else:
self.canceled_exit_orders += 1
# place new order if result was not None # place new order if result was not None
if requested_rate: if requested_rate:
if is_entry:
self._enter_trade( self._enter_trade(
pair=trade.pair, pair=trade.pair,
row=row, row=row,
@@ -1331,10 +1339,19 @@ class Backtesting:
requested_stake=(order.safe_remaining * order.ft_price / trade.leverage), requested_stake=(order.safe_remaining * order.ft_price / trade.leverage),
direction="short" if trade.is_short else "long", direction="short" if trade.is_short else "long",
) )
# Delete trade if no successful entries happened (if placing the new order failed)
if not trade.has_open_orders and trade.nr_of_successful_entries == 0:
return True
self.replaced_entry_orders += 1 self.replaced_entry_orders += 1
else:
self._exit_trade(
trade=trade,
sell_row=row,
close_rate=requested_rate,
amount=order.safe_remaining,
exit_reason=order.ft_order_tag,
)
self.replaced_exit_orders += 1
# Delete trade if no successful entries happened (if placing the new order failed)
if not trade.has_open_orders and is_entry and trade.nr_of_successful_entries == 0:
return True
else: else:
# assumption: there can't be multiple open entry orders at any given time # assumption: there can't be multiple open entry orders at any given time
return trade.nr_of_successful_entries == 0 return trade.nr_of_successful_entries == 0
@@ -1656,7 +1673,7 @@ class Backtesting:
self.progress.init_step(BacktestState.ANALYZE, 0) self.progress.init_step(BacktestState.ANALYZE, 0)
strategy_name = strat.get_strategy_name() strategy_name = strat.get_strategy_name()
logger.info(f"Running backtesting for Strategy {strategy_name}") logger.info(f"Running backtesting for Strategy {strategy_name}")
backtest_start_time = datetime.now(timezone.utc) backtest_start_time = dt_now()
self._set_strategy(strat) self._set_strategy(strat)
# need to reprocess data every time to populate signals # need to reprocess data every time to populate signals
@@ -1683,7 +1700,7 @@ class Backtesting:
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
) )
backtest_end_time = datetime.now(timezone.utc) backtest_end_time = dt_now()
results.update( results.update(
{ {
"run_id": self.run_ids.get(strategy_name, ""), "run_id": self.run_ids.get(strategy_name, ""),
@@ -1710,14 +1727,14 @@ class Backtesting:
def _get_min_cached_backtest_date(self): def _get_min_cached_backtest_date(self):
min_backtest_date = None min_backtest_date = None
backtest_cache_age = self.config.get("backtest_cache", constants.BACKTEST_CACHE_DEFAULT) backtest_cache_age = self.config.get("backtest_cache", constants.BACKTEST_CACHE_DEFAULT)
if self.timerange.stopts == 0 or self.timerange.stopdt > datetime.now(tz=timezone.utc): if self.timerange.stopts == 0 or self.timerange.stopdt > dt_now():
logger.warning("Backtest result caching disabled due to use of open-ended timerange.") logger.warning("Backtest result caching disabled due to use of open-ended timerange.")
elif backtest_cache_age == "day": elif backtest_cache_age == "day":
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(days=1) min_backtest_date = dt_now() - timedelta(days=1)
elif backtest_cache_age == "week": elif backtest_cache_age == "week":
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(weeks=1) min_backtest_date = dt_now() - timedelta(weeks=1)
elif backtest_cache_age == "month": elif backtest_cache_age == "month":
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(weeks=4) min_backtest_date = dt_now() - timedelta(weeks=4)
return min_backtest_date return min_backtest_date
def load_prior_backtest(self): def load_prior_backtest(self):
-13
View File
@@ -6,7 +6,6 @@ This module contains the hyperopt logic
import logging import logging
import random import random
import sys
from datetime import datetime from datetime import datetime
from math import ceil from math import ceil
from multiprocessing import Manager from multiprocessing import Manager
@@ -15,7 +14,6 @@ from typing import Any
import rapidjson import rapidjson
from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects
from joblib.externals import cloudpickle
from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
from freqtrade.enums import HyperoptState from freqtrade.enums import HyperoptState
@@ -110,17 +108,6 @@ class Hyperopt:
logger.info(f"Removing `{p}`.") logger.info(f"Removing `{p}`.")
p.unlink() p.unlink()
def hyperopt_pickle_magic(self, bases) -> None:
"""
Hyperopt magic to allow strategy inheritance across files.
For this to properly work, we need to register the module of the imported class
to pickle as value.
"""
for modules in bases:
if modules.__name__ != "IStrategy":
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
self.hyperopt_pickle_magic(modules.__bases__)
def _save_result(self, epoch: dict) -> None: def _save_result(self, epoch: dict) -> None:
""" """
Save hyperopt results to file Save hyperopt results to file
@@ -114,7 +114,7 @@ class HyperOptimizer:
def get_strategy_name(self) -> str: def get_strategy_name(self) -> str:
return self.backtesting.strategy.get_strategy_name() return self.backtesting.strategy.get_strategy_name()
def hyperopt_pickle_magic(self, bases) -> None: def hyperopt_pickle_magic(self, bases: tuple[type, ...]) -> None:
""" """
Hyperopt magic to allow strategy inheritance across files. Hyperopt magic to allow strategy inheritance across files.
For this to properly work, we need to register the module of the imported class For this to properly work, we need to register the module of the imported class
@@ -122,7 +122,8 @@ class HyperOptimizer:
""" """
for modules in bases: for modules in bases:
if modules.__name__ != "IStrategy": if modules.__name__ != "IStrategy":
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__]) if mod := sys.modules.get(modules.__module__):
cloudpickle.register_pickle_by_value(mod)
self.hyperopt_pickle_magic(modules.__bases__) self.hyperopt_pickle_magic(modules.__bases__)
def _get_params_dict( def _get_params_dict(
+37 -33
View File
@@ -1535,45 +1535,47 @@ class LocalTrade:
:param json_str: json string to parse :param json_str: json string to parse
:return: Trade instance :return: Trade instance
""" """
from uuid import uuid4
import rapidjson import rapidjson
data = rapidjson.loads(json_str) data = rapidjson.loads(json_str)
trade = cls( trade = cls(
__FROM_JSON=True, __FROM_JSON=True,
id=data["trade_id"], id=data.get("trade_id"),
pair=data["pair"], pair=data["pair"],
base_currency=data["base_currency"], base_currency=data.get("base_currency"),
stake_currency=data["quote_currency"], stake_currency=data.get("quote_currency"),
is_open=data["is_open"], is_open=data["is_open"],
exchange=data["exchange"], exchange=data.get("exchange", "import"),
amount=data["amount"], amount=data["amount"],
amount_requested=data["amount_requested"], amount_requested=data.get("amount_requested", data["amount"]),
stake_amount=data["stake_amount"], stake_amount=data["stake_amount"],
strategy=data["strategy"], strategy=data.get("strategy"),
enter_tag=data["enter_tag"], enter_tag=data["enter_tag"],
timeframe=data["timeframe"], timeframe=data.get("timeframe"),
fee_open=data["fee_open"], fee_open=data["fee_open"],
fee_open_cost=data["fee_open_cost"], fee_open_cost=data.get("fee_open_cost"),
fee_open_currency=data["fee_open_currency"], fee_open_currency=data.get("fee_open_currency"),
fee_close=data["fee_close"], fee_close=data["fee_close"],
fee_close_cost=data["fee_close_cost"], fee_close_cost=data.get("fee_close_cost"),
fee_close_currency=data["fee_close_currency"], fee_close_currency=data.get("fee_close_currency"),
open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc), open_date=datetime.fromtimestamp(data["open_timestamp"] // 1000, tz=timezone.utc),
open_rate=data["open_rate"], open_rate=data["open_rate"],
open_rate_requested=data["open_rate_requested"], open_rate_requested=data.get("open_rate_requested", data["open_rate"]),
open_trade_value=data["open_trade_value"], open_trade_value=data.get("open_trade_value"),
close_date=( close_date=(
datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc) datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
if data["close_timestamp"] if data["close_timestamp"]
else None else None
), ),
realized_profit=data["realized_profit"], realized_profit=data.get("realized_profit", 0),
close_rate=data["close_rate"], close_rate=data["close_rate"],
close_rate_requested=data["close_rate_requested"], close_rate_requested=data.get("close_rate_requested", data["close_rate"]),
close_profit=data["close_profit"], close_profit=data.get("close_profit", data.get("profit_ratio")),
close_profit_abs=data["close_profit_abs"], close_profit_abs=data.get("close_profit_abs", data.get("profit_abs")),
exit_reason=data["exit_reason"], exit_reason=data["exit_reason"],
exit_order_status=data["exit_order_status"], exit_order_status=data.get("exit_order_status"),
stop_loss=data["stop_loss_abs"], stop_loss=data["stop_loss_abs"],
stop_loss_pct=data["stop_loss_ratio"], stop_loss_pct=data["stop_loss_ratio"],
initial_stop_loss=data["initial_stop_loss_abs"], initial_stop_loss=data["initial_stop_loss_abs"],
@@ -1581,11 +1583,11 @@ class LocalTrade:
min_rate=data["min_rate"], min_rate=data["min_rate"],
max_rate=data["max_rate"], max_rate=data["max_rate"],
leverage=data["leverage"], leverage=data["leverage"],
interest_rate=data["interest_rate"], interest_rate=data.get("interest_rate"),
liquidation_price=data["liquidation_price"], liquidation_price=data.get("liquidation_price"),
is_short=data["is_short"], is_short=data["is_short"],
trading_mode=data["trading_mode"], trading_mode=data.get("trading_mode"),
funding_fees=data["funding_fees"], funding_fees=data.get("funding_fees"),
amount_precision=data.get("amount_precision", None), amount_precision=data.get("amount_precision", None),
price_precision=data.get("price_precision", None), price_precision=data.get("price_precision", None),
precision_mode=data.get("precision_mode", None), precision_mode=data.get("precision_mode", None),
@@ -1597,23 +1599,25 @@ class LocalTrade:
amount=order["amount"], amount=order["amount"],
ft_amount=order["amount"], ft_amount=order["amount"],
ft_order_side=order["ft_order_side"], ft_order_side=order["ft_order_side"],
ft_pair=order["pair"], ft_pair=order.get("pair", data["pair"]),
ft_is_open=order["is_open"], ft_is_open=order.get("is_open", False),
order_id=order["order_id"], order_id=order.get("order_id", uuid4().hex),
status=order["status"], status=order.get("status"),
average=order["average"], average=order.get("average", order.get("safe_price")),
cost=order["cost"], cost=order["cost"],
filled=order["filled"], filled=order.get("filled", order["amount"]),
order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT), order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT)
if order.get("order_date")
else None,
order_filled_date=( order_filled_date=(
datetime.fromtimestamp(order["order_filled_timestamp"] // 1000, tz=timezone.utc) datetime.fromtimestamp(order["order_filled_timestamp"] // 1000, tz=timezone.utc)
if order["order_filled_timestamp"] if order["order_filled_timestamp"]
else None else None
), ),
order_type=order["order_type"], order_type=order.get("order_type"),
price=order["price"], price=order.get("price", order.get("safe_price")),
ft_price=order["price"], ft_price=order.get("price", order.get("safe_price")),
remaining=order["remaining"], remaining=order.get("remaining", 0.0),
funding_fee=order.get("funding_fee", None), funding_fee=order.get("funding_fee", None),
ft_order_tag=order.get("ft_order_tag", None), ft_order_tag=order.get("ft_order_tag", None),
) )
+1 -1
View File
@@ -9,7 +9,7 @@ from typing import Any
import freqtrade.exchange as exchanges import freqtrade.exchange as exchanges
from freqtrade.constants import Config, ExchangeConfig from freqtrade.constants import Config, ExchangeConfig
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, Exchange from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, Exchange
from freqtrade.resolvers import IResolver from freqtrade.resolvers.iresolver import IResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
+9 -1
View File
@@ -16,7 +16,7 @@ from freqtrade.configuration.config_validation import validate_migrated_strategy
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES, Config from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES, Config
from freqtrade.enums import TradingMode from freqtrade.enums import TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import IResolver from freqtrade.resolvers.iresolver import IResolver
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
@@ -242,6 +242,14 @@ class StrategyResolver(IResolver):
if has_after_fill: if has_after_fill:
strategy._ft_stop_uses_after_fill = True strategy._ft_stop_uses_after_fill = True
if check_override(strategy, IStrategy, "adjust_order_price") and (
check_override(strategy, IStrategy, "adjust_entry_price")
or check_override(strategy, IStrategy, "adjust_exit_price")
):
raise OperationalException(
"If you implement `adjust_order_price`, `adjust_entry_price` and "
"`adjust_exit_price` will not be used. Please pick one approach for your strategy."
)
return strategy return strategy
@staticmethod @staticmethod
+3 -1
View File
@@ -100,7 +100,9 @@ def __run_backtest_bg(btconfig: Config):
) )
if btconfig.get("export", "none") == "trades": if btconfig.get("export", "none") == "trades":
combined_res = combined_dataframes_with_rel_mean(ApiBG.bt["data"], min_date, max_date) combined_res = combined_dataframes_with_rel_mean(
ApiBG.bt["data"], min_date, max_date
)
fn = store_backtest_results( fn = store_backtest_results(
btconfig, btconfig,
ApiBG.bt["bt"].results, ApiBG.bt["bt"].results,
@@ -0,0 +1,77 @@
import logging
from copy import deepcopy
from fastapi import APIRouter, Depends, HTTPException
from freqtrade.configuration import validate_config_consistency
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
from freqtrade.rpc.api_server.api_schemas import PairHistory, PairHistoryRequest
from freqtrade.rpc.api_server.deps import get_config, get_exchange
from freqtrade.rpc.rpc import RPC
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/pair_history", response_model=PairHistory, tags=["candle data"])
def pair_history(
pair: str,
timeframe: str,
timerange: str,
strategy: str,
freqaimodel: str | None = None,
config=Depends(get_config),
exchange=Depends(get_exchange),
):
# The initial call to this endpoint can be slow, as it may need to initialize
# the exchange class.
config_loc = deepcopy(config)
config_loc.update(
{
"timeframe": timeframe,
"strategy": strategy,
"timerange": timerange,
"freqaimodel": freqaimodel if freqaimodel else config_loc.get("freqaimodel"),
}
)
validate_config_consistency(config_loc)
try:
return RPC._rpc_analysed_history_full(config_loc, pair, timeframe, exchange, None, False)
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@router.post("/pair_history", response_model=PairHistory, tags=["candle data"])
def pair_history_filtered(payload: PairHistoryRequest, config=Depends(get_config)):
# The initial call to this endpoint can be slow, as it may need to initialize
# the exchange class.
config_loc = deepcopy(config)
config_loc.update(
{
"timeframe": payload.timeframe,
"strategy": payload.strategy,
"timerange": payload.timerange,
"freqaimodel": (
payload.freqaimodel if payload.freqaimodel else config_loc.get("freqaimodel")
),
}
)
handleExchangePayload(payload, config_loc)
exchange = get_exchange(config_loc)
validate_config_consistency(config_loc)
try:
return RPC._rpc_analysed_history_full(
config_loc,
payload.pair,
payload.timeframe,
exchange,
payload.columns,
payload.live_mode,
)
except Exception as e:
logger.exception("Error in pair_history_filtered")
raise HTTPException(status_code=502, detail=str(e))
+21 -2
View File
@@ -524,10 +524,11 @@ class PairCandlesRequest(BaseModel):
columns: list[str] | None = None columns: list[str] | None = None
class PairHistoryRequest(PairCandlesRequest): class PairHistoryRequest(PairCandlesRequest, ExchangeModePayloadMixin):
timerange: str timerange: str
strategy: str strategy: str | None = None
freqaimodel: str | None = None freqaimodel: str | None = None
live_mode: bool = False
class PairHistory(BaseModel): class PairHistory(BaseModel):
@@ -606,6 +607,24 @@ class BacktestMarketChange(BaseModel):
data: list[list[Any]] data: list[list[Any]]
class MarketRequest(ExchangeModePayloadMixin, BaseModel):
base: str | None = None
quote: str | None = None
class MarketModel(BaseModel):
symbol: str
base: str
quote: str
spot: bool
swap: bool
class MarketResponse(BaseModel):
markets: dict[str, MarketModel]
exchange_id: str
class SysInfo(BaseModel): class SysInfo(BaseModel):
cpu_pct: list[float] cpu_pct: list[float]
ram_pct: float ram_pct: float
+30 -55
View File
@@ -1,14 +1,16 @@
import logging import logging
from copy import deepcopy from copy import deepcopy
from typing import Annotated
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.data.history import get_datahandler from freqtrade.data.history import get_datahandler
from freqtrade.enums import CandleType, State, TradingMode from freqtrade.enums import CandleType, RunMode, State, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_pairlists import handleExchangePayload
from freqtrade.rpc.api_server.api_schemas import ( from freqtrade.rpc.api_server.api_schemas import (
AvailablePairs, AvailablePairs,
Balances, Balances,
@@ -30,11 +32,12 @@ from freqtrade.rpc.api_server.api_schemas import (
Locks, Locks,
LocksPayload, LocksPayload,
Logs, Logs,
MarketRequest,
MarketResponse,
MixTag, MixTag,
OpenTradeSchema, OpenTradeSchema,
PairCandlesRequest, PairCandlesRequest,
PairHistory, PairHistory,
PairHistoryRequest,
PerformanceEntry, PerformanceEntry,
Ping, Ping,
PlotConfig, PlotConfig,
@@ -84,7 +87,8 @@ logger = logging.getLogger(__name__)
# 2.35: pair_candles and pair_history endpoints as Post variant # 2.35: pair_candles and pair_history endpoints as Post variant
# 2.40: Add hyperopt-loss endpoint # 2.40: Add hyperopt-loss endpoint
# 2.41: Add download-data endpoint # 2.41: Add download-data endpoint
API_VERSION = 2.41 # 2.42: Add /pair_history endpoint with live data
API_VERSION = 2.42
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()
@@ -342,58 +346,6 @@ def pair_candles_filtered(payload: PairCandlesRequest, rpc: RPC = Depends(get_rp
) )
@router.get("/pair_history", response_model=PairHistory, tags=["candle data"])
def pair_history(
pair: str,
timeframe: str,
timerange: str,
strategy: str,
freqaimodel: str | None = None,
config=Depends(get_config),
exchange=Depends(get_exchange),
):
# The initial call to this endpoint can be slow, as it may need to initialize
# the exchange class.
config = deepcopy(config)
config.update(
{
"timeframe": timeframe,
"strategy": strategy,
"timerange": timerange,
"freqaimodel": freqaimodel if freqaimodel else config.get("freqaimodel"),
}
)
try:
return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange, None)
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@router.post("/pair_history", response_model=PairHistory, tags=["candle data"])
def pair_history_filtered(
payload: PairHistoryRequest, config=Depends(get_config), exchange=Depends(get_exchange)
):
# The initial call to this endpoint can be slow, as it may need to initialize
# the exchange class.
config = deepcopy(config)
config.update(
{
"timeframe": payload.timeframe,
"strategy": payload.strategy,
"timerange": payload.timerange,
"freqaimodel": (
payload.freqaimodel if payload.freqaimodel else config.get("freqaimodel")
),
}
)
try:
return RPC._rpc_analysed_history_full(
config, payload.pair, payload.timeframe, exchange, payload.columns
)
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@router.get("/plot_config", response_model=PlotConfig, tags=["candle data"]) @router.get("/plot_config", response_model=PlotConfig, tags=["candle data"])
def plot_config( def plot_config(
strategy: str | None = None, strategy: str | None = None,
@@ -525,6 +477,29 @@ def list_available_pairs(
return result return result
@router.get("/markets", response_model=MarketResponse, tags=["candle data", "webserver"])
def markets(
query: Annotated[MarketRequest, Query()],
config=Depends(get_config),
rpc: RPC | None = Depends(get_rpc_optional),
):
if not rpc or config["runmode"] == RunMode.WEBSERVER:
# webserver mode
config_loc = deepcopy(config)
handleExchangePayload(query, config_loc)
exchange = get_exchange(config_loc)
else:
exchange = rpc._freqtrade.exchange
return {
"markets": exchange.get_markets(
base_currencies=[query.base] if query.base else None,
quote_currencies=[query.quote] if query.quote else None,
),
"exchange_id": exchange.id,
}
@router.get("/sysinfo", response_model=SysInfo, tags=["info"]) @router.get("/sysinfo", response_model=SysInfo, tags=["info"])
def sysinfo(): def sysinfo():
return RPC._rpc_sysinfo() return RPC._rpc_sysinfo()
+6
View File
@@ -120,6 +120,7 @@ class ApiServer(RPCHandler):
from freqtrade.rpc.api_server.api_background_tasks import router as api_bg_tasks from freqtrade.rpc.api_server.api_background_tasks import router as api_bg_tasks
from freqtrade.rpc.api_server.api_backtest import router as api_backtest from freqtrade.rpc.api_server.api_backtest import router as api_backtest
from freqtrade.rpc.api_server.api_download_data import router as api_download_data from freqtrade.rpc.api_server.api_download_data import router as api_download_data
from freqtrade.rpc.api_server.api_pair_history import router as api_pair_history
from freqtrade.rpc.api_server.api_pairlists import router as api_pairlists from freqtrade.rpc.api_server.api_pairlists import router as api_pairlists
from freqtrade.rpc.api_server.api_v1 import router as api_v1 from freqtrade.rpc.api_server.api_v1 import router as api_v1
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
@@ -145,6 +146,11 @@ class ApiServer(RPCHandler):
prefix="/api/v1", prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)], dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
) )
app.include_router(
api_pair_history,
prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)],
)
app.include_router( app.include_router(
api_pairlists, api_pairlists,
prefix="/api/v1", prefix="/api/v1",
+35 -8
View File
@@ -6,13 +6,12 @@ import logging
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Generator, Sequence from collections.abc import Generator, Sequence
from datetime import date, datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
from math import isnan
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import psutil import psutil
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from dateutil.tz import tzlocal from dateutil.tz import tzlocal
from numpy import inf, int64, mean, nan from numpy import inf, int64, isnan, mean, nan
from pandas import DataFrame, NaT from pandas import DataFrame, NaT
from sqlalchemy import func, select from sqlalchemy import func, select
@@ -31,7 +30,7 @@ from freqtrade.enums import (
TradingMode, TradingMode,
) )
from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msecs
from freqtrade.exchange.exchange_utils import price_to_precision from freqtrade.exchange.exchange_utils import price_to_precision
from freqtrade.loggers import bufferHandler from freqtrade.loggers import bufferHandler
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
@@ -42,12 +41,13 @@ from freqtrade.rpc.rpc_types import RPCSendMsg
from freqtrade.util import ( from freqtrade.util import (
decimals_per_coin, decimals_per_coin,
dt_from_ts, dt_from_ts,
dt_humanize_delta,
dt_now, dt_now,
dt_ts,
dt_ts_def, dt_ts_def,
format_date, format_date,
shorten_date, shorten_date,
) )
from freqtrade.util.datetime_helpers import dt_humanize_delta
from freqtrade.wallets import PositionWallet, Wallet from freqtrade.wallets import PositionWallet, Wallet
@@ -1436,7 +1436,12 @@ class RPC:
@staticmethod @staticmethod
def _rpc_analysed_history_full( def _rpc_analysed_history_full(
config: Config, pair: str, timeframe: str, exchange, selected_cols: list[str] | None config: Config,
pair: str,
timeframe: str,
exchange: Exchange,
selected_cols: list[str] | None,
live: bool,
) -> dict[str, Any]: ) -> dict[str, Any]:
timerange_parsed = TimeRange.parse_timerange(config.get("timerange")) timerange_parsed = TimeRange.parse_timerange(config.get("timerange"))
@@ -1444,9 +1449,25 @@ class RPC:
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy_name = ""
startup_candles = 0
if config.get("strategy"):
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
startup_candles = strategy.startup_candle_count startup_candles = strategy.startup_candle_count
strategy_name = strategy.get_strategy_name()
if live:
data = exchange.get_historic_ohlcv(
pair=pair,
timeframe=timeframe,
since_ms=timerange_parsed.startts * 1000
if timerange_parsed.startts
else dt_ts(dt_now() - timedelta(days=30)),
is_new_pair=True, # history is never available - so always treat as new pair
candle_type=config.get("candle_type_def", CandleType.SPOT),
until_ms=timerange_parsed.stopts,
)
else:
_data = load_data( _data = load_data(
datadir=config["datadir"], datadir=config["datadir"],
pairs=[pair], pairs=[pair],
@@ -1460,15 +1481,21 @@ class RPC:
raise RPCException( raise RPCException(
f"No data for {pair}, {timeframe} in {config.get('timerange')} found." f"No data for {pair}, {timeframe} in {config.get('timerange')} found."
) )
data = _data[pair]
if config.get("strategy"):
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None) strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
strategy.ft_bot_start() strategy.ft_bot_start()
df_analyzed = strategy.analyze_ticker(_data[pair], {"pair": pair}) df_analyzed = strategy.analyze_ticker(data, {"pair": pair})
df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles) df_analyzed = trim_dataframe(
df_analyzed, timerange_parsed, startup_candles=startup_candles
)
else:
df_analyzed = data
return RPC._convert_dataframe_to_dict( return RPC._convert_dataframe_to_dict(
strategy.get_strategy_name(), strategy_name,
pair, pair,
timeframe, timeframe,
df_analyzed.copy(), df_analyzed.copy(),
+98
View File
@@ -690,6 +690,104 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return current_order_rate return current_order_rate
def adjust_exit_price(
self,
trade: Trade,
order: Order | None,
pair: str,
current_time: datetime,
proposed_rate: float,
current_order_rate: float,
entry_tag: str | None,
side: str,
**kwargs,
) -> float:
"""
Exit price re-adjustment logic, returning the user desired limit price.
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
If None is returned then order gets canceled but not replaced by a new one.
:param pair: Pair that's currently analyzed
:param trade: Trade object.
:param order: Order object
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
:param current_order_rate: Rate of the existing order in place.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided
"""
return current_order_rate
def adjust_order_price(
self,
trade: Trade,
order: Order | None,
pair: str,
current_time: datetime,
proposed_rate: float,
current_order_rate: float,
entry_tag: str | None,
side: str,
is_entry: bool,
**kwargs,
) -> float:
"""
Exit and entry order price re-adjustment logic, returning the user desired limit price.
This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
When not implemented by a strategy, returns current_order_rate as default.
If current_order_rate is returned then the existing order is maintained.
If None is returned then order gets canceled but not replaced by a new one.
:param pair: Pair that's currently analyzed
:param trade: Trade object.
:param order: Order object
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
:param current_order_rate: Rate of the existing order in place.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param is_entry: True if the order is an entry order, False if it's an exit order.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided
"""
if is_entry:
return self.adjust_entry_price(
trade=trade,
order=order,
pair=pair,
current_time=current_time,
proposed_rate=proposed_rate,
current_order_rate=current_order_rate,
entry_tag=entry_tag,
side=side,
**kwargs,
)
else:
return self.adjust_exit_price(
trade=trade,
order=order,
pair=pair,
current_time=current_time,
proposed_rate=proposed_rate,
current_order_rate=current_order_rate,
entry_tag=entry_tag,
side=side,
**kwargs,
)
def leverage( def leverage(
self, self,
pair: str, pair: str,
@@ -40,7 +40,7 @@ def custom_entry_price(
""" """
return proposed_rate return proposed_rate
def adjust_entry_price( def adjust_order_price(
self, self,
trade: Trade, trade: Trade,
order: Order | None, order: Order | None,
@@ -50,10 +50,11 @@ def adjust_entry_price(
current_order_rate: float, current_order_rate: float,
entry_tag: str | None, entry_tag: str | None,
side: str, side: str,
is_entry: bool,
**kwargs, **kwargs,
) -> float: ) -> float:
""" """
Entry price re-adjustment logic, returning the user desired limit price. Exit and entry order price re-adjustment logic, returning the user desired limit price.
This only executes when a order was already placed, still open (unfilled fully or partially) This only executes when a order was already placed, still open (unfilled fully or partially)
and not timed out on subsequent candles after entry trigger. and not timed out on subsequent candles after entry trigger.
@@ -71,6 +72,7 @@ def adjust_entry_price(
:param current_order_rate: Rate of the existing order in place. :param current_order_rate: Rate of the existing order in place.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal. :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade :param side: 'long' or 'short' - indicating the direction of the proposed trade
:param is_entry: True if the order is an entry order, False if it's an exit order.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided :return float: New entry price value if provided
+1 -1
View File
@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.1" __version__ = "2025.2"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
+12 -2
View File
@@ -23,10 +23,18 @@ PostDataT = dict[str, Any] | list[dict[str, Any]] | None
class FtRestClient: class FtRestClient:
def __init__( def __init__(
self, serverurl, username=None, password=None, *, pool_connections=10, pool_maxsize=10 self,
serverurl,
username=None,
password=None,
*,
pool_connections=10,
pool_maxsize=10,
timeout=10,
): ):
self._serverurl = serverurl self._serverurl = serverurl
self._session = requests.Session() self._session = requests.Session()
self._timeout = timeout
# allow configuration of pool # allow configuration of pool
adapter = HTTPAdapter(pool_connections=pool_connections, pool_maxsize=pool_maxsize) adapter = HTTPAdapter(pool_connections=pool_connections, pool_maxsize=pool_maxsize)
@@ -50,7 +58,9 @@ class FtRestClient:
url = urlunparse((schema, netloc, path, par, query, fragment)) url = urlunparse((schema, netloc, path, par, query, fragment))
try: try:
resp = self._session.request(method, url, headers=hd, data=json.dumps(data)) resp = self._session.request(
method, url, headers=hd, timeout=self._timeout, data=json.dumps(data)
)
# return resp.text # return resp.text
return resp.json() return resp.json()
except RequestConnectionError: except RequestConnectionError:
+5 -5
View File
@@ -7,22 +7,22 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==4.0.1 coveralls==4.0.1
ruff==0.9.3 ruff==0.9.7
mypy==1.14.1 mypy==1.15.0
pre-commit==4.1.0 pre-commit==4.1.0
pytest==8.3.4 pytest==8.3.4
pytest-asyncio==0.25.2 pytest-asyncio==0.25.3
pytest-cov==6.0.0 pytest-cov==6.0.0
pytest-mock==3.14.0 pytest-mock==3.14.0
pytest-random-order==1.1.1 pytest-random-order==1.1.1
pytest-timeout==2.3.1 pytest-timeout==2.3.1
pytest-xdist==3.6.1 pytest-xdist==3.6.1
isort==5.13.2 isort==6.0.0
# For datetime mocking # For datetime mocking
time-machine==2.16.0 time-machine==2.16.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.16.5 nbconvert==7.16.6
# mypy types # mypy types
types-cachetools==5.5.0.20240820 types-cachetools==5.5.0.20240820
+4 -2
View File
@@ -3,9 +3,11 @@
# Required for freqai-rl # Required for freqai-rl
torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64' torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'
torch==2.5.1; sys_platform != 'darwin' or platform_machine != 'x86_64' torch==2.6.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1 gymnasium==0.29.1
stable_baselines3==2.4.1 # SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable_baselines3==2.4.1; sys_platform == 'darwin' and platform_machine == 'x86_64'
stable_baselines3==2.5.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
sb3_contrib>=2.2.1 sb3_contrib>=2.2.1
# Progress bar for stable-baselines3 and sb3-contrib # Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.1 tqdm==4.67.1
+3 -3
View File
@@ -6,7 +6,7 @@
scikit-learn==1.6.1 scikit-learn==1.6.1
joblib==1.4.2 joblib==1.4.2
catboost==1.2.7; 'arm' not in platform_machine catboost==1.2.7; 'arm' not in platform_machine
lightgbm==4.5.0 lightgbm==4.6.0
xgboost==2.1.3 xgboost==2.1.4
tensorboard==2.18.0 tensorboard==2.19.0
datasieve==0.1.7 datasieve==0.1.7
+1 -1
View File
@@ -2,7 +2,7 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.15.1 scipy==1.15.2
scikit-learn==1.6.1 scikit-learn==1.6.1
ft-scikit-optimize==0.9.2 ft-scikit-optimize==0.9.2
filelock==3.17.0 filelock==3.17.0
+1 -1
View File
@@ -1,4 +1,4 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==5.24.1 plotly==6.0.0
+12 -13
View File
@@ -4,27 +4,26 @@ bottleneck==1.4.2
numexpr==2.10.2 numexpr==2.10.2
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==4.4.50 ccxt==4.4.62
cryptography==42.0.8; platform_machine == 'armv7l' cryptography==44.0.1
cryptography==44.0.0; platform_machine != 'armv7l' aiohttp==3.9.5
aiohttp==3.10.11 SQLAlchemy==2.0.38
SQLAlchemy==2.0.37
python-telegram-bot==21.10 python-telegram-bot==21.10
# can't be hard-pinned due to telegram-bot pinning httpx with ~ # can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1 httpx>=0.24.1
humanize==4.11.0 humanize==4.12.1
cachetools==5.5.1 cachetools==5.5.2
requests==2.32.3 requests==2.32.3
urllib3==2.3.0 urllib3==2.3.0
jsonschema==4.23.0 jsonschema==4.23.0
TA-Lib==0.4.34 TA-Lib==0.4.38
technical==1.5.0 technical==1.5.0
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.2.0 pycoingecko==3.2.0
jinja2==3.1.5 jinja2==3.1.5
joblib==1.4.2 joblib==1.4.2
rich==13.9.4 rich==13.9.4
pyarrow==19.0.0; platform_machine != 'armv7l' pyarrow==19.0.1; platform_machine != 'armv7l'
# find first, C search in arrays # find first, C search in arrays
py_find_1st==1.1.7 py_find_1st==1.1.7
@@ -38,25 +37,25 @@ orjson==3.10.15
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.115.7 fastapi==0.115.8
pydantic==2.10.6 pydantic==2.10.6
uvicorn==0.34.0 uvicorn==0.34.0
pyjwt==2.10.1 pyjwt==2.10.1
aiofiles==24.1.0 aiofiles==24.1.0
psutil==6.1.1 psutil==7.0.0
# Building config files interactively # Building config files interactively
questionary==2.1.0 questionary==2.1.0
prompt-toolkit==3.0.50 prompt-toolkit==3.0.50
# Extensions to datetime library # Extensions to datetime library
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytz==2024.2 pytz==2025.1
#Futures #Futures
schedule==1.2.2 schedule==1.2.2
#WS Messages #WS Messages
websockets==14.2 websockets==15.0
janus==2.0.0 janus==2.0.0
ast-comments==1.2.2 ast-comments==1.2.2
+24 -1
View File
@@ -4436,7 +4436,7 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
pytest.skip("Tested separately for okx") pytest.skip("Tested separately for okx")
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
timeframes = ("1m", "5m", "1h") timeframes = ("1m", "5m", "1h")
expected = exchange._ft_has["ohlcv_candle_limit"] expected = exchange._ft_has.get("ohlcv_candle_limit", 500)
for timeframe in timeframes: for timeframe in timeframes:
# if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has: # if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has:
# expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe] # expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
@@ -6259,3 +6259,26 @@ def test_price_to_precision_with_default_conf(default_conf, mocker):
prec_price = patched_ex.price_to_precision("XRP/USDT", 1.0000000101) prec_price = patched_ex.price_to_precision("XRP/USDT", 1.0000000101)
assert prec_price == 1.00000001 assert prec_price == 1.00000001
assert prec_price == 1.00000001 assert prec_price == 1.00000001
def test_exchange_features(default_conf, mocker):
conf = copy.deepcopy(default_conf)
exchange = get_patched_exchange(mocker, conf)
exchange._api_async.features = {
"spot": {
"fetchOHLCV": {
"limit": 995,
}
},
"swap": {
"linear": {
"fetchOHLCV": {
"limit": 997,
}
}
},
}
assert exchange.features("spot", "fetchOHLCV", "limit", 500) == 995
assert exchange.features("futures", "fetchOHLCV", "limit", 500) == 997
# Fall back to default
assert exchange.features("futures", "fetchOHLCV_else", "limit", 601) == 601
+7 -4
View File
@@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring, protected-access, invalid-name # pragma pylint: disable=missing-docstring, protected-access, invalid-name
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import isnan, nan
import pytest import pytest
from ccxt import ( from ccxt import (
@@ -321,6 +322,7 @@ def test_amount_to_precision(
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND), (2.9977, TICK_SIZE, 0.005, 3.0, ROUND),
(234.24, TICK_SIZE, 0.5, 234.0, ROUND), (234.24, TICK_SIZE, 0.5, 234.0, ROUND),
(234.26, TICK_SIZE, 0.5, 234.5, ROUND), (234.26, TICK_SIZE, 0.5, 234.5, ROUND),
(nan, TICK_SIZE, 3, nan, ROUND),
# Tests for TRUNCATTE # Tests for TRUNCATTE
(2.34559, DECIMAL_PLACES, 4, 2.3455, TRUNCATE), (2.34559, DECIMAL_PLACES, 4, 2.3455, TRUNCATE),
(2.34559, DECIMAL_PLACES, 5, 2.34559, TRUNCATE), (2.34559, DECIMAL_PLACES, 5, 2.34559, TRUNCATE),
@@ -359,10 +361,11 @@ def test_amount_to_precision(
], ],
) )
def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode): def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode):
assert ( result = price_to_precision(price, precision, precision_mode, rounding_mode=rounding_mode)
price_to_precision(price, precision, precision_mode, rounding_mode=rounding_mode) if not isnan(expected):
== expected assert result == expected
) else:
assert isnan(result)
@pytest.mark.parametrize( @pytest.mark.parametrize(
+11 -1
View File
@@ -1,9 +1,12 @@
import asyncio import asyncio
import logging
import threading import threading
from datetime import timedelta from datetime import timedelta
from time import sleep from time import sleep
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from ccxt import NotSupported
from freqtrade.enums import CandleType from freqtrade.enums import CandleType
from freqtrade.exchange.exchange_ws import ExchangeWS from freqtrade.exchange.exchange_ws import ExchangeWS
from ft_client.test_client.test_rest_client import log_has_re from ft_client.test_client.test_rest_client import log_has_re
@@ -61,15 +64,18 @@ def patch_eventloop_threading(exchange):
pass pass
async def test_exchangews_ohlcv(mocker, time_machine): async def test_exchangews_ohlcv(mocker, time_machine, caplog):
config = MagicMock() config = MagicMock()
ccxt_object = MagicMock() ccxt_object = MagicMock()
caplog.set_level(logging.DEBUG)
async def sleeper(*args, **kwargs): async def sleeper(*args, **kwargs):
# pass # pass
await asyncio.sleep(0.12) await asyncio.sleep(0.12)
return MagicMock() return MagicMock()
ccxt_object.un_watch_ohlcv_for_symbols = AsyncMock(side_effect=NotSupported)
ccxt_object.watch_ohlcv = AsyncMock(side_effect=sleeper) ccxt_object.watch_ohlcv = AsyncMock(side_effect=sleeper)
ccxt_object.close = AsyncMock() ccxt_object.close = AsyncMock()
time_machine.move_to("2024-11-01 01:00:02 +00:00") time_machine.move_to("2024-11-01 01:00:02 +00:00")
@@ -101,11 +107,14 @@ async def test_exchangews_ohlcv(mocker, time_machine):
time_machine.shift(timedelta(minutes=5)) time_machine.shift(timedelta(minutes=5))
exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT) exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT)
await asyncio.sleep(1) await asyncio.sleep(1)
assert log_has_re("un_watch_ohlcv_for_symbols not supported: ", caplog)
# XRP/BTC should be cleaned up. # XRP/BTC should be cleaned up.
assert exchange_ws._klines_watching == { assert exchange_ws._klines_watching == {
("ETH/BTC", "1m", CandleType.SPOT), ("ETH/BTC", "1m", CandleType.SPOT),
} }
# Cleanup happened. # Cleanup happened.
ccxt_object.un_watch_ohlcv_for_symbols = AsyncMock(side_effect=ValueError)
exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT) exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT)
assert exchange_ws._klines_watching == { assert exchange_ws._klines_watching == {
("ETH/BTC", "1m", CandleType.SPOT), ("ETH/BTC", "1m", CandleType.SPOT),
@@ -117,6 +126,7 @@ async def test_exchangews_ohlcv(mocker, time_machine):
finally: finally:
# Cleanup # Cleanup
exchange_ws.cleanup() exchange_ws.cleanup()
assert log_has_re("Exception in _unwatch_ohlcv", caplog)
async def test_exchangews_get_ohlcv(mocker, caplog): async def test_exchangews_get_ohlcv(mocker, caplog):
+12 -1
View File
@@ -21,6 +21,7 @@ EXCHANGES = {
"use_ci_proxy": True, "use_ci_proxy": True,
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 1000,
"futures": True, "futures": True,
"futures_pair": "BTC/USDT:USDT", "futures_pair": "BTC/USDT:USDT",
"hasQuoteVolumeFutures": True, "hasQuoteVolumeFutures": True,
@@ -96,6 +97,7 @@ EXCHANGES = {
"stake_currency": "USDT", "stake_currency": "USDT",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 1000,
"futures": False, "futures": False,
"skip_ws_tests": True, "skip_ws_tests": True,
"sample_order": [ "sample_order": [
@@ -136,6 +138,7 @@ EXCHANGES = {
"stake_currency": "USD", "stake_currency": "USD",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 720,
"leverage_tiers_public": False, "leverage_tiers_public": False,
"leverage_in_spot_market": True, "leverage_in_spot_market": True,
"trades_lookback_hours": 12, "trades_lookback_hours": 12,
@@ -162,6 +165,7 @@ EXCHANGES = {
"stake_currency": "USDT", "stake_currency": "USDT",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 1500,
"leverage_tiers_public": False, "leverage_tiers_public": False,
"leverage_in_spot_market": True, "leverage_in_spot_market": True,
"sample_order": [ "sample_order": [
@@ -229,6 +233,7 @@ EXCHANGES = {
"stake_currency": "USDT", "stake_currency": "USDT",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 1000,
"futures": True, "futures": True,
"futures_pair": "BTC/USDT:USDT", "futures_pair": "BTC/USDT:USDT",
"hasQuoteVolumeFutures": True, "hasQuoteVolumeFutures": True,
@@ -345,6 +350,7 @@ EXCHANGES = {
"stake_currency": "USDT", "stake_currency": "USDT",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 300,
"futures": True, "futures": True,
"futures_pair": "BTC/USDT:USDT", "futures_pair": "BTC/USDT:USDT",
"hasQuoteVolumeFutures": False, "hasQuoteVolumeFutures": False,
@@ -358,6 +364,7 @@ EXCHANGES = {
"hasQuoteVolume": True, "hasQuoteVolume": True,
"use_ci_proxy": True, "use_ci_proxy": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 1000,
"futures_pair": "BTC/USDT:USDT", "futures_pair": "BTC/USDT:USDT",
"futures": True, "futures": True,
"orderbook_max_entries": 50, "orderbook_max_entries": 50,
@@ -398,6 +405,7 @@ EXCHANGES = {
"stake_currency": "USDT", "stake_currency": "USDT",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 200,
"orderbook_max_entries": 50, "orderbook_max_entries": 50,
}, },
"htx": { "htx": {
@@ -405,13 +413,14 @@ EXCHANGES = {
"stake_currency": "BTC", "stake_currency": "BTC",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"futures": False, "candle_count": 1000,
}, },
"bitvavo": { "bitvavo": {
"pair": "BTC/EUR", "pair": "BTC/EUR",
"stake_currency": "EUR", "stake_currency": "EUR",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 1440,
"leverage_tiers_public": False, "leverage_tiers_public": False,
"leverage_in_spot_market": False, "leverage_in_spot_market": False,
}, },
@@ -420,6 +429,7 @@ EXCHANGES = {
"stake_currency": "USDT", "stake_currency": "USDT",
"hasQuoteVolume": True, "hasQuoteVolume": True,
"timeframe": "1h", "timeframe": "1h",
"candle_count": 1000,
"futures": False, "futures": False,
"sample_order": [ "sample_order": [
{ {
@@ -482,6 +492,7 @@ EXCHANGES = {
"hasQuoteVolume": False, "hasQuoteVolume": False,
"timeframe": "1h", "timeframe": "1h",
"futures": True, "futures": True,
"candle_count": 5000,
"orderbook_max_entries": 20, "orderbook_max_entries": 20,
"futures_pair": "BTC/USDC:USDC", "futures_pair": "BTC/USDC:USDC",
"hasQuoteVolumeFutures": True, "hasQuoteVolumeFutures": True,
+16
View File
@@ -48,6 +48,22 @@ class TestCCXTExchange:
} }
) )
def test_ohlcv_limit(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
expected_count = EXCHANGES[exchangename].get("candle_count")
if not expected_count:
pytest.skip("No expected candle count for exchange")
assert exch.ohlcv_candle_limit("1m", CandleType.SPOT) == expected_count
def test_ohlcv_limit_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange_futures
expected_count = EXCHANGES[exchangename].get("candle_count")
if not expected_count:
pytest.skip("No expected candle count for exchange")
assert exch.ohlcv_candle_limit("1m", CandleType.SPOT) == expected_count
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures exchange, exchangename = exchange_futures
pair = EXCHANGES[exchangename]["pair"] pair = EXCHANGES[exchangename]["pair"]
+9 -1
View File
@@ -3743,8 +3743,9 @@ def test_trailing_stop_loss_positive(
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_disable_ignore_roi_if_entry_signal( def test_disable_ignore_roi_if_entry_signal(
default_conf_usdt, limit_order, limit_order_open, is_short, fee, mocker default_conf_usdt, limit_order, limit_order_open, is_short, fee, mocker, time_machine
) -> None: ) -> None:
time_machine.move_to("2025-01-10 08:00:16 +00:00")
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
eside = entry_side(is_short) eside = entry_side(is_short)
@@ -3773,6 +3774,13 @@ def test_disable_ignore_roi_if_entry_signal(
patch_get_signal(freqtrade, enter_long=not is_short, enter_short=is_short, exit_short=is_short) patch_get_signal(freqtrade, enter_long=not is_short, enter_short=is_short, exit_short=is_short)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
# Test if entry-signal is absent
patch_get_signal(freqtrade)
# Signal was evaluated already - no action.
assert freqtrade.handle_trade(trade) is False
# Move to after the candle expired
time_machine.shift(timedelta(hours=5))
# Test if entry-signal is absent # Test if entry-signal is absent
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
+21 -1
View File
@@ -436,6 +436,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
# Replace new order with diff. order at a lower price # Replace new order with diff. order at a lower price
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95) freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95)
freqtrade.strategy.adjust_exit_price = MagicMock(side_effect=ValueError)
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
@@ -445,6 +446,8 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert pytest.approx(trade.stake_amount) == 60 assert pytest.approx(trade.stake_amount) == 60
assert trade.orders[-1].price == 1.95 assert trade.orders[-1].price == 1.95
assert pytest.approx(trade.orders[-1].cost) == 120 * leverage assert pytest.approx(trade.orders[-1].cost) == 120 * leverage
assert freqtrade.strategy.adjust_entry_price.call_count == 1
assert freqtrade.strategy.adjust_exit_price.call_count == 0
# Fill DCA order # Fill DCA order
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
@@ -469,6 +472,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False) mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
freqtrade.strategy.custom_exit = MagicMock(return_value="Exit now") freqtrade.strategy.custom_exit = MagicMock(return_value="Exit now")
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.02) freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.02)
freqtrade.strategy.adjust_exit_price = MagicMock(side_effect=ValueError)
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
assert len(trade.orders) == 5 assert len(trade.orders) == 5
@@ -478,8 +482,9 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert pytest.approx(trade.amount) == 91.689215 * leverage assert pytest.approx(trade.amount) == 91.689215 * leverage
assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage
assert freqtrade.strategy.adjust_entry_price.call_count == 0 assert freqtrade.strategy.adjust_entry_price.call_count == 0
assert freqtrade.strategy.adjust_exit_price.call_count == 0
# Process again, should not adjust entry price # Process again, should not adjust price
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
@@ -490,6 +495,21 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
assert trade.orders[-1].price == 2.02 assert trade.orders[-1].price == 2.02
# Adjust entry price cannot be called - this is an exit order # Adjust entry price cannot be called - this is an exit order
assert freqtrade.strategy.adjust_entry_price.call_count == 0 assert freqtrade.strategy.adjust_entry_price.call_count == 0
assert freqtrade.strategy.adjust_exit_price.call_count == 1
freqtrade.strategy.adjust_exit_price = MagicMock(return_value=2.03)
# Process again, should adjust exit price
freqtrade.process()
trade = Trade.get_trades().first()
assert trade.orders[-2].status == "canceled"
assert len(trade.orders) == 6
assert trade.orders[-1].side == trade.exit_side
assert trade.orders[-1].status == "open"
assert trade.orders[-1].price == 2.03
assert freqtrade.strategy.adjust_entry_price.call_count == 0
assert freqtrade.strategy.adjust_exit_price.call_count == 1
@pytest.mark.parametrize("leverage", [1, 2]) @pytest.mark.parametrize("leverage", [1, 2])
+1
View File
@@ -45,6 +45,7 @@ class BTContainer(NamedTuple):
leverage: float = 1.0 leverage: float = 1.0
timeout: int | None = None timeout: int | None = None
adjust_entry_price: float | None = None adjust_entry_price: float | None = None
adjust_exit_price: float | None = None
adjust_trade_position: list[float] | None = None adjust_trade_position: list[float] | None = None
+44
View File
@@ -1217,6 +1217,46 @@ tc57 = BTContainer(
], ],
) )
# Test 58: Custom-exit-price short - below all candles
tc58 = BTContainer(
data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0],
[1, 5000, 5200, 4951, 5000, 6172, 0, 0, 0, 0], # enter trade (signal on last candle)
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 1], # Exit - delayed
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0], #
[4, 4750, 5100, 4350, 4750, 6172, 0, 0, 0, 0],
],
stop_loss=-0.10,
roi={"0": 1.00},
profit_perc=-0.01,
use_exit_signal=True,
timeout=1000,
custom_exit_price=4300,
adjust_exit_price=5050,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)],
)
# Test 59: Custom-exit-price above all candles - readjust order
tc59 = BTContainer(
data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
[2, 4900, 5250, 4500, 5100, 6172, 0, 1], # exit
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], # order readjust
[4, 4750, 4950, 4350, 4750, 6172, 0, 0],
],
stop_loss=-0.2,
roi={"0": 0.10},
profit_perc=-0.02,
use_exit_signal=True,
timeout=1000,
custom_exit_price=5300,
adjust_exit_price=4900,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False)],
)
TESTS = [ TESTS = [
tc0, tc0,
@@ -1277,6 +1317,8 @@ TESTS = [
tc55, tc55,
tc56, tc56,
tc57, tc57,
tc58,
tc59,
] ]
@@ -1330,6 +1372,8 @@ def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> No
) )
if data.adjust_entry_price: if data.adjust_entry_price:
backtesting.strategy.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price) backtesting.strategy.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price)
if data.adjust_exit_price:
backtesting.strategy.adjust_exit_price = MagicMock(return_value=data.adjust_exit_price)
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
backtesting.strategy.leverage = lambda **kwargs: data.leverage backtesting.strategy.leverage = lambda **kwargs: data.leverage
+64
View File
@@ -276,3 +276,67 @@ def test_trade_serialize_load_back(fee):
trade3 = LocalTrade.from_json(trade_string) trade3 = LocalTrade.from_json(trade_string)
assert len(trade3.orders) == len(t.orders) assert len(trade3.orders) == len(t.orders)
@pytest.mark.usefixtures("init_persistence")
def test_trade_fromjson_backtesting():
"""
trade.from_json should be able to load a trade output via backtesting.
"""
trade_string = """
{
"pair":"XRP/USDT:USDT",
"stake_amount":3015.294001,
"max_stake_amount":60305.88002,
"amount":94612.3,
"open_date":"2024-03-02 10:40:00+00:00",
"close_date":"2024-03-02 11:10:00+00:00",
"open_rate":0.6374,
"close_rate":0.6399,
"fee_open":0.0005,
"fee_close":0.0005,
"trade_duration":30,
"profit_ratio":-0.09853216535933962,
"profit_abs":-296.95489539,
"exit_reason":"trailing_stop_loss",
"initial_stop_loss_abs":0.6689,
"initial_stop_loss_ratio":-0.99,
"stop_loss_abs":0.6399,
"stop_loss_ratio":-0.3368287257705749,
"min_rate":0.6294,
"max_rate":0.6421,
"is_open":false,
"enter_tag":"[0.6373, 0.5993, 0.64]",
"leverage":20,
"is_short":true,
"open_timestamp":1709376000000,
"close_timestamp":1709377800000,
"orders":[
{
"amount":94612.3,
"safe_price":0.6374,
"ft_order_side":"sell",
"order_filled_timestamp":1709376000000,
"ft_is_entry":true,
"ft_order_tag":"[0.6373, 0.5993, 0.64]",
"cost":60336.032960009994
},
{
"amount":94612.3,
"safe_price":0.6399,
"ft_order_side":"buy",
"order_filled_timestamp":1709377800000,
"ft_is_entry":false,
"ft_order_tag":"trailing_stop_loss",
"cost":60572.681975385
}
]
}
"""
trade = Trade.from_json(trade_string)
Trade.session.add(trade)
Trade.commit()
# Trade-id not given - use first available
assert trade.id == 1
+1 -1
View File
@@ -107,7 +107,7 @@ def test_volume_change_pair_list_init_wrong_lookback_period(mocker, rpl_config):
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
match=r"ChangeFilter requires lookback_period to not exceed" match=r"ChangeFilter requires lookback_period to not exceed"
r" exchange max request size \(1000\)", r" exchange max request size \(\d+\)",
): ):
get_patched_freqtradebot(mocker, rpl_config) get_patched_freqtradebot(mocker, rpl_config)
+144 -1
View File
@@ -36,6 +36,7 @@ from tests.conftest import (
EXMS, EXMS,
create_mock_trades, create_mock_trades,
create_mock_trades_usdt, create_mock_trades_usdt,
generate_test_data,
get_mock_coro, get_mock_coro,
get_patched_freqtradebot, get_patched_freqtradebot,
log_has, log_has,
@@ -1914,6 +1915,15 @@ def test_api_pair_history(botclient, tmp_path, mocker):
timeframe = "5m" timeframe = "5m"
lfm = mocker.patch("freqtrade.strategy.interface.IStrategy.load_freqAI_model") lfm = mocker.patch("freqtrade.strategy.interface.IStrategy.load_freqAI_model")
# Wrong mode
rc = client_get(
client,
f"{BASE_URI}/pair_history?timeframe={timeframe}"
f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}",
)
assert_response(rc, 503)
_ftbot.config["runmode"] = RunMode.WEBSERVER
# No pair # No pair
rc = client_get( rc = client_get(
client, client,
@@ -2025,6 +2035,87 @@ def test_api_pair_history(botclient, tmp_path, mocker):
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json()["detail"] == ("No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") assert rc.json()["detail"] == ("No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
# No strategy
rc = client_post(
client,
f"{BASE_URI}/pair_history",
data={
"pair": "UNITTEST/BTC",
"timeframe": timeframe,
"timerange": "20180111-20180112",
# "strategy": CURRENT_TEST_STRATEGY,
"columns": ["rsi", "fastd", "fastk"],
},
)
assert_response(rc, 200)
result = rc.json()
assert result["length"] == 289
assert len(result["data"]) == result["length"]
assert "columns" in result
assert "data" in result
# Result without strategy won't have enter_long assigned.
assert "enter_long" not in result["columns"]
assert result["columns"] == ["date", "open", "high", "low", "close", "volume", "__date_ts"]
def test_api_pair_history_live_mode(botclient, tmp_path, mocker):
_ftbot, client = botclient
_ftbot.config["user_data_dir"] = tmp_path
_ftbot.config["runmode"] = RunMode.WEBSERVER
mocker.patch("freqtrade.strategy.interface.IStrategy.load_freqAI_model")
# no strategy, live data
gho = mocker.patch(
"freqtrade.exchange.binance.Binance.get_historic_ohlcv",
return_value=generate_test_data("1h", 100),
)
rc = client_post(
client,
f"{BASE_URI}/pair_history",
data={
"pair": "UNITTEST/BTC",
"timeframe": "1h",
"timerange": "20240101-",
# "strategy": CURRENT_TEST_STRATEGY,
"columns": ["rsi", "fastd", "fastk"],
"live_mode": True,
},
)
assert_response(rc, 200)
result = rc.json()
# 100 candles - as in the generate_test_data call above
assert result["length"] == 100
assert len(result["data"]) == result["length"]
assert result["columns"] == ["date", "open", "high", "low", "close", "volume", "__date_ts"]
assert gho.call_count == 1
gho.reset_mock()
rc = client_post(
client,
f"{BASE_URI}/pair_history",
data={
"pair": "UNITTEST/BTC",
"timeframe": "1h",
"timerange": "20240101-",
"strategy": CURRENT_TEST_STRATEGY,
"columns": ["rsi", "fastd", "fastk"],
"live_mode": True,
},
)
assert_response(rc, 200)
result = rc.json()
# 80 candles - as in the generate_test_data call above - 20 startup candles
assert result["length"] == 100 - 20
assert len(result["data"]) == result["length"]
assert "rsi" in result["columns"]
assert "enter_long" in result["columns"]
assert "fastd" in result["columns"]
assert "date" in result["columns"]
assert gho.call_count == 1
def test_api_plot_config(botclient, mocker, tmp_path): def test_api_plot_config(botclient, mocker, tmp_path):
ftbot, client = botclient ftbot, client = botclient
@@ -2849,7 +2940,7 @@ def test_api_ws_send_msg(default_conf, mocker, caplog):
ApiServer.shutdown() ApiServer.shutdown()
def test_api_download_data(botclient, mocker, tmp_path, caplog): def test_api_download_data(botclient, mocker, tmp_path):
ftbot, client = botclient ftbot, client = botclient
rc = client_post(client, f"{BASE_URI}/download_data", data={}) rc = client_post(client, f"{BASE_URI}/download_data", data={})
@@ -2918,3 +3009,55 @@ def test_api_download_data(botclient, mocker, tmp_path, caplog):
assert response["job_category"] == "download_data" assert response["job_category"] == "download_data"
assert response["status"] == "failed" assert response["status"] == "failed"
assert response["error"] == "Download error" assert response["error"] == "Download error"
def test_api_markets_live(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/markets")
assert_response(rc, 200)
response = rc.json()
assert "markets" in response
assert len(response["markets"]) >= 0
assert response["markets"]["XRP/USDT"] == {
"base": "XRP",
"quote": "USDT",
"symbol": "XRP/USDT",
"spot": True,
"swap": False,
}
assert "BTC/USDT" in response["markets"]
assert "XRP/BTC" in response["markets"]
rc = client_get(
client,
f"{BASE_URI}/markets?base=XRP",
)
assert_response(rc, 200)
response = rc.json()
assert "XRP/USDT" in response["markets"]
assert "XRP/BTC" in response["markets"]
assert "BTC/USDT" not in response["markets"]
def test_api_markets_webserver(botclient):
# Ensure webserver exchanges are reset
ApiBG.exchanges = {}
ftbot, client = botclient
# Test in webserver mode
ftbot.config["runmode"] = RunMode.WEBSERVER
rc = client_get(client, f"{BASE_URI}/markets?exchange=binance")
assert_response(rc, 200)
response = rc.json()
assert "markets" in response
assert len(response["markets"]) >= 0
assert response["exchange_id"] == "binance"
rc = client_get(client, f"{BASE_URI}/markets?exchange=hyperliquid")
assert_response(rc, 200)
assert "hyperliquid_spot" in ApiBG.exchanges
assert "binance_spot" in ApiBG.exchanges
@@ -21,10 +21,12 @@ class TestStrategyNoImplementSell(TestStrategyNoImplements):
return super().populate_entry_trend(dataframe, metadata) return super().populate_entry_trend(dataframe, metadata)
class TestStrategyImplementCustomSell(TestStrategyNoImplementSell): class TestStrategyImplementEmptyWorking(TestStrategyNoImplementSell):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_exit_trend(dataframe, metadata) return super().populate_exit_trend(dataframe, metadata)
class TestStrategyImplementCustomSell(TestStrategyImplementEmptyWorking):
def custom_sell( def custom_sell(
self, self,
pair: str, pair: str,
@@ -55,3 +57,34 @@ class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell):
self, pair: str, trade, order: Order, current_time: datetime, **kwargs self, pair: str, trade, order: Order, current_time: datetime, **kwargs
) -> bool: ) -> bool:
return False return False
class TestStrategyAdjustOrderPrice(TestStrategyImplementEmptyWorking):
def adjust_entry_price(
self,
trade,
order,
pair,
current_time,
proposed_rate,
current_order_rate,
entry_tag,
side,
**kwargs,
):
return proposed_rate
def adjust_order_price(
self,
trade,
order,
pair,
current_time,
proposed_rate,
current_order_rate,
entry_tag,
side,
is_entry,
**kwargs,
):
return proposed_rate
+4
View File
@@ -460,6 +460,10 @@ def test_missing_implements(default_conf, caplog):
): ):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
default_conf["strategy"] = "TestStrategyAdjustOrderPrice"
with pytest.raises(OperationalException, match=r"If you implement `adjust_order_price`.*"):
StrategyResolver.load_strategy(default_conf)
def test_call_deprecated_function(default_conf): def test_call_deprecated_function(default_conf):
default_location = Path(__file__).parent / "strats/broken_strats/" default_location = Path(__file__).parent / "strats/broken_strats/"