Compare commits

...

228 Commits

Author SHA1 Message Date
Matthias
da52ef8729 chore: hot-fix ta-lib install on windows 2025-08-14 10:12:31 +02:00
Matthias
c9dda3480d Merge pull request #12052 from freqtrade/new_release
New release 2025.7
2025-07-31 19:47:30 +02:00
Matthias
45804e1bef chore: bump version to 2025.7 2025-07-31 06:29:50 +02:00
Matthias
6dd5dc5d2c Merge branch 'stable' into new_release 2025-07-31 06:29:29 +02:00
Matthias
a5db75d361 Merge pull request #12050 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-07-31 06:22:09 +02:00
Freqtrade Bot
4c4bc2be9b chore: update pre-commit hooks 2025-07-31 03:31:12 +00:00
Matthias
e9cc9bb38a test: add explaining comment about mark price test
closes #12046
2025-07-30 07:13:35 +02:00
Matthias
3b6395ddb8 test: improve ccxt compat test teardown 2025-07-29 20:12:00 +02:00
Matthias
fdcbb3c0d0 chore: enable test checking funding-fee returned is not 0. 2025-07-29 20:05:54 +02:00
Matthias
b07ffbf721 test: remove unused argument 2025-07-29 18:30:46 +02:00
Matthias
a7b7e10c09 docs: update and simplify webhook docs 2025-07-29 07:23:03 +02:00
Matthias
f18472479a chore: align rpc message fields to use order_rate 2025-07-29 07:23:03 +02:00
Matthias
18c37b9b8f Merge pull request #12044 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-07-29 06:42:24 +02:00
Freqtrade Bot
d28abc18ea chore: update pre-commit hooks 2025-07-29 03:34:34 +00:00
Matthias
80b50141ee Merge pull request #12037 from freqtrade/dependabot/pip/develop/ruff-0.12.5
chore(deps-dev): bump ruff from 0.12.4 to 0.12.5
2025-07-28 07:40:37 +02:00
Matthias
b15d49f9ae Merge pull request #12039 from freqtrade/dependabot/github_actions/develop/astral-sh/setup-uv-6.4.3
chore(deps): bump astral-sh/setup-uv from 6.4.1 to 6.4.3
2025-07-28 07:39:37 +02:00
Matthias
668635892c Merge pull request #12038 from freqtrade/dependabot/pip/develop/ccxt-4.4.96
chore(deps): bump ccxt from 4.4.95 to 4.4.96
2025-07-28 07:35:43 +02:00
Matthias
670a29d420 Merge pull request #12035 from freqtrade/dependabot/pip/develop/cmaes-0.12.0
chore(deps): bump cmaes from 0.11.1 to 0.12.0
2025-07-28 07:33:28 +02:00
Matthias
fd0d089f43 Merge pull request #12034 from freqtrade/dependabot/pip/develop/numpy-2.3.2
chore(deps): bump numpy from 2.3.1 to 2.3.2
2025-07-28 07:27:12 +02:00
Matthias
b5fb6888f0 Merge pull request #12032 from freqtrade/dependabot/pip/develop/orjson-3.11.1
chore(deps): bump orjson from 3.11.0 to 3.11.1
2025-07-28 07:20:35 +02:00
Matthias
edc23dcd2f Merge pull request #12031 from freqtrade/dependabot/pip/develop/rich-14.1.0
chore(deps): bump rich from 14.0.0 to 14.1.0
2025-07-28 07:16:11 +02:00
Matthias
1dc299f6f4 Merge pull request #12036 from freqtrade/dependabot/pip/develop/scipy-1.16.1
chore(deps): bump scipy from 1.16.0 to 1.16.1
2025-07-28 07:12:44 +02:00
Matthias
99d2423534 Merge pull request #12033 from freqtrade/dependabot/pip/develop/mkdocs-9c6b142b23
chore(deps): bump mkdocs-material from 9.6.15 to 9.6.16 in the mkdocs group
2025-07-28 07:09:41 +02:00
Matthias
b8b8da2eb2 Merge pull request #12030 from freqtrade/dependabot/pip/develop/stable-baselines3-2.7.0
chore(deps): bump stable-baselines3 from 2.6.0 to 2.7.0
2025-07-28 06:52:25 +02:00
dependabot[bot]
4237d9fd11 chore(deps): bump astral-sh/setup-uv from 6.4.1 to 6.4.3
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6.4.1 to 6.4.3.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](7edac99f96...e92bafb625)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:21:49 +00:00
dependabot[bot]
b98a91ba4c chore(deps): bump ccxt from 4.4.95 to 4.4.96
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.95 to 4.4.96.
- [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/v4.4.95...v4.4.96)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:06:42 +00:00
dependabot[bot]
e74db3e024 chore(deps-dev): bump ruff from 0.12.4 to 0.12.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.4 to 0.12.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.12.4...0.12.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:05:08 +00:00
dependabot[bot]
ead3282229 chore(deps): bump scipy from 1.16.0 to 1.16.1
Bumps [scipy](https://github.com/scipy/scipy) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](https://github.com/scipy/scipy/compare/v1.16.0...v1.16.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:03:27 +00:00
dependabot[bot]
dfd9e7e777 chore(deps): bump cmaes from 0.11.1 to 0.12.0
Bumps [cmaes](https://github.com/CyberAgentAILab/cmaes) from 0.11.1 to 0.12.0.
- [Release notes](https://github.com/CyberAgentAILab/cmaes/releases)
- [Commits](https://github.com/CyberAgentAILab/cmaes/compare/v0.11.1...v0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:02:06 +00:00
dependabot[bot]
71362c9140 chore(deps): bump numpy from 2.3.1 to 2.3.2
Bumps [numpy](https://github.com/numpy/numpy) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.3.1...v2.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:01:45 +00:00
dependabot[bot]
68114691c6 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.15 to 9.6.16
- [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.15...9.6.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:01:08 +00:00
dependabot[bot]
37a3bdd507 chore(deps): bump orjson from 3.11.0 to 3.11.1
Bumps [orjson](https://github.com/ijl/orjson) from 3.11.0 to 3.11.1.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.11.0...3.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:00:32 +00:00
dependabot[bot]
50389cbbf4 chore(deps): bump rich from 14.0.0 to 14.1.0
Bumps [rich](https://github.com/Textualize/rich) from 14.0.0 to 14.1.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.0.0...v14.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 04:00:17 +00:00
dependabot[bot]
a7e5d3e427 chore(deps): bump stable-baselines3 from 2.6.0 to 2.7.0
Bumps [stable-baselines3](https://github.com/DLR-RM/stable-baselines3) from 2.6.0 to 2.7.0.
- [Release notes](https://github.com/DLR-RM/stable-baselines3/releases)
- [Commits](https://github.com/DLR-RM/stable-baselines3/compare/v2.6.0...v2.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 03:58:59 +00:00
Matthias
48d1106546 fix: undefined candle_type_def in lookahead-analysis 2025-07-27 14:43:19 +02:00
Matthias
6452521002 Merge pull request #12026 from hippocritical/develop
Add Numpy2.0 np.NaN => np.nan conversion to strategy_updater
2025-07-26 13:19:28 +02:00
hippocritical
52374b39f2 adds numpy.NaN => np.nan conversion to the strategy updater since numpy2.0 changed that, now it throws errors.
To adjust more of those we can now easily expand on it.
This includes aliases for imports as well.
2025-07-26 11:22:07 +02:00
Matthias
300a866aac test: update test for new logic 2025-07-26 09:48:12 +02:00
Matthias
9873a862e0 fix: improve startup_candle_count assignment 2025-07-26 09:48:06 +02:00
Matthias
1c8bbc14b8 fix: initialize exchange before strategy 2025-07-26 09:21:02 +02:00
Matthias
afa1311ff8 chore: fix odd comment formatting 2025-07-26 09:12:46 +02:00
Matthias
3ecc9c30cb Merge pull request #12021 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-07-24 06:50:02 +02:00
Freqtrade Bot
a85bb040f2 chore: update pre-commit hooks 2025-07-24 03:30:04 +00:00
Matthias
841e480ff5 test: add test for get_dry_run_wallet 2025-07-22 07:14:37 +02:00
Matthias
a0e9b77f75 fix: fix problem in for dict based wallets 2025-07-22 07:14:37 +02:00
Matthias
47d59f8ff4 Merge pull request #12003 from freqtrade/dependabot/pip/develop/pyarrow-21.0.0
chore(deps): bump pyarrow from 20.0.0 to 21.0.0
2025-07-22 07:04:46 +02:00
Matthias
676c48411b Merge pull request #12014 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-07-22 06:52:41 +02:00
Matthias
954950c8cb chore: update pyarrow pre-compiled wheel 2025-07-22 06:43:50 +02:00
Matthias
2520b7cd51 chore: add missing fields 2025-07-22 06:43:16 +02:00
Matthias
f9d7f05d38 chore: sort conf_required alphabetically 2025-07-22 06:41:57 +02:00
Freqtrade Bot
46d4ff3d36 chore: update pre-commit hooks 2025-07-22 03:30:48 +00:00
Matthias
9347d823fc chore: re-word variable to match it's purpose
closes #12013
2025-07-21 21:08:42 +02:00
Matthias
5e8bd359c8 Merge pull request #12009 from freqtrade/dependabot/pip/develop/jsonschema-4.25.0
chore(deps): bump jsonschema from 4.24.0 to 4.25.0
2025-07-21 12:34:08 +02:00
Matthias
bae8a55f81 Merge pull request #12012 from freqtrade/dependabot/pip/develop/mypy-1.17.0
chore(deps-dev): bump mypy from 1.16.1 to 1.17.0
2025-07-21 10:42:31 +02:00
dependabot[bot]
56c672b6d2 chore(deps-dev): bump mypy from 1.16.1 to 1.17.0
---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.17.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 06:35:33 +00:00
Matthias
de3956f2f9 Merge pull request #12008 from freqtrade/dependabot/pip/develop/ruff-0.12.4
chore(deps-dev): bump ruff from 0.12.3 to 0.12.4
2025-07-21 08:30:43 +02:00
Matthias
ddd7009fa8 Merge pull request #12011 from freqtrade/dependabot/pip/develop/types-83c738a83e
chore(deps-dev): bump types-cachetools from 6.0.0.20250525 to 6.1.0.20250717 in the types group
2025-07-21 08:11:10 +02:00
dependabot[bot]
31891b25af chore(deps-dev): bump ruff from 0.12.3 to 0.12.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.3 to 0.12.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.12.3...0.12.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 05:57:17 +00:00
Matthias
187b19b1d6 Merge pull request #12010 from freqtrade/dependabot/pip/develop/pytest-b7c466671f
chore(deps-dev): bump pytest-asyncio from 1.0.0 to 1.1.0 in the pytest group
2025-07-21 07:55:50 +02:00
dependabot[bot]
e55d8512e7 chore(deps): bump pyarrow from 20.0.0 to 21.0.0
Bumps [pyarrow](https://github.com/apache/arrow) from 20.0.0 to 21.0.0.
- [Release notes](https://github.com/apache/arrow/releases)
- [Commits](https://github.com/apache/arrow/compare/apache-arrow-20.0.0...apache-arrow-21.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 05:46:12 +00:00
Matthias
697912f244 Merge pull request #12006 from freqtrade/dependabot/pip/develop/technical-1.5.2
chore(deps): bump technical from 1.5.1 to 1.5.2
2025-07-21 07:45:47 +02:00
Matthias
2f3ad09cf5 Merge pull request #12004 from freqtrade/dependabot/pip/develop/scikit-learn-1.7.1
chore(deps): bump scikit-learn from 1.7.0 to 1.7.1
2025-07-21 07:45:06 +02:00
Matthias
523dd305ba Merge pull request #12007 from freqtrade/dependabot/pip/develop/python-telegram-bot-22.3
chore(deps): bump python-telegram-bot from 22.2 to 22.3
2025-07-21 07:39:23 +02:00
Matthias
2d87d74829 Merge pull request #12005 from freqtrade/dependabot/pip/develop/orjson-3.11.0
chore(deps): bump orjson from 3.10.18 to 3.11.0
2025-07-21 07:30:59 +02:00
Matthias
820791e889 Merge pull request #12002 from freqtrade/dependabot/pip/develop/tensorboard-2.20.0
chore(deps): bump tensorboard from 2.19.0 to 2.20.0
2025-07-21 07:10:24 +02:00
Matthias
a130122cc6 chore: bump pre-commit types for cachetools 2025-07-21 06:49:52 +02:00
Matthias
3a58297a8d docs: improve hyperliquid doc clarity further 2025-07-21 06:29:32 +02:00
Matthias
65d22f9f1c Merge pull request #12001 from freqtrade/dependabot/github_actions/develop/astral-sh/setup-uv-6.4.1
chore(deps): bump astral-sh/setup-uv from 6.3.1 to 6.4.1
2025-07-21 06:22:12 +02:00
dependabot[bot]
ec5df87318 chore(deps-dev): bump types-cachetools in the types group
Bumps the types group with 1 update: [types-cachetools](https://github.com/typeshed-internal/stub_uploader).


Updates `types-cachetools` from 6.0.0.20250525 to 6.1.0.20250717
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-cachetools
  dependency-version: 6.1.0.20250717
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: types
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 04:22:04 +00:00
dependabot[bot]
cf1533c478 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 1.0.0 to 1.1.0
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v1.0.0...v1.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 04:22:03 +00:00
dependabot[bot]
fc34f8cad2 chore(deps): bump jsonschema from 4.24.0 to 4.25.0
Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.24.0 to 4.25.0.
- [Release notes](https://github.com/python-jsonschema/jsonschema/releases)
- [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.24.0...v4.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 04:21:50 +00:00
dependabot[bot]
3fbb38730c chore(deps): bump python-telegram-bot from 22.2 to 22.3
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 22.2 to 22.3.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v22.2...v22.3)

---
updated-dependencies:
- dependency-name: python-telegram-bot
  dependency-version: '22.3'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 04:20:43 +00:00
dependabot[bot]
6a327440e0 chore(deps): bump technical from 1.5.1 to 1.5.2
---
updated-dependencies:
- dependency-name: technical
  dependency-version: 1.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 04:20:25 +00:00
dependabot[bot]
f1c8d8325b chore(deps): bump orjson from 3.10.18 to 3.11.0
Bumps [orjson](https://github.com/ijl/orjson) from 3.10.18 to 3.11.0.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.10.18...3.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 04:20:24 +00:00
dependabot[bot]
d2557a70d7 chore(deps): bump scikit-learn from 1.7.0 to 1.7.1
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.7.0...1.7.1)

---
updated-dependencies:
- dependency-name: scikit-learn
  dependency-version: 1.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 04:20:08 +00:00
dependabot[bot]
bf76347751 chore(deps): bump tensorboard from 2.19.0 to 2.20.0
Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.19.0 to 2.20.0.
- [Release notes](https://github.com/tensorflow/tensorboard/releases)
- [Changelog](https://github.com/tensorflow/tensorboard/blob/master/RELEASE.md)
- [Commits](https://github.com/tensorflow/tensorboard/compare/2.19.0...2.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 04:17:28 +00:00
dependabot[bot]
a99a49e2af chore(deps): bump astral-sh/setup-uv from 6.3.1 to 6.4.1
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6.3.1 to 6.4.1.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](bd01e18f51...7edac99f96)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 03:32:53 +00:00
Matthias
b7fc924e82 docs: update backtest docs to explain missing metrics 2025-07-20 10:06:51 +02:00
Matthias
ef55bf6800 fix: use absolute daily profit instead of relative
closes #11987
2025-07-20 10:06:43 +02:00
Matthias
25ffc76655 docs(hyperliquid): documentation for hyperliquid vault / subaccount
closes #11500
2025-07-20 09:45:59 +02:00
Matthias
15b1cff9c2 docs: hyperliquid - clarify walletAddress further ... 2025-07-20 08:35:33 +02:00
Matthias
c1ba2936ec test: update ccxt_config test to set defaultSettle for bybit 2025-07-19 16:14:27 +02:00
Matthias
f08c0c9870 feat: bybit - set defaultSettle to allow fetchPositions to work in usdc markets 2025-07-19 15:58:18 +02:00
Matthias
3b5b8bd007 feat: enable USDC futures for bybit
closes #11809
2025-07-19 15:34:27 +02:00
Matthias
74067d454b feat: Update bybit liquidation price docstring 2025-07-19 15:26:19 +02:00
Matthias
16359d617d chore: disable ws tests for hyperliquid for now 2025-07-19 15:08:32 +02:00
Matthias
ae9f83ed30 refactor: simplify ws_test 2025-07-19 15:07:53 +02:00
Matthias
49626ca31c Merge pull request #11997 from freqtrade/bump/ccxt
Bump ccxt to 4.4.95
2025-07-19 09:41:12 +02:00
Matthias
884f543fb7 chore: bump ccxt to 4.4.95 2025-07-19 09:03:50 +02:00
Matthias
29df7499c5 chore: improve error output 2025-07-19 08:30:10 +02:00
Matthias
5e64416dfa test: add test for profit_all 2025-07-18 20:17:19 +02:00
Matthias
1118032c1f feat: add /profit_all endpoint 2025-07-18 20:17:11 +02:00
Matthias
277828b1d4 chore(tests): reduce hyperliquid timeframe
UBTC wasn't listed for long enough
2025-07-18 18:27:36 +02:00
Matthias
47dcf7bc93 chore(ci): Switch hyperliquid tests to a more active pair 2025-07-18 07:17:31 +02:00
Matthias
75081ab046 docs: add /profit_long and short to telegram docs 2025-07-18 06:58:20 +02:00
Matthias
571497e58b Merge pull request #11974 from qqqqqf-q/feat/telegram-profit-direction
feat(telegram): Add /profit long and /profit short commands
2025-07-18 06:54:21 +02:00
Matthias
d710c85cda chore: simplify profit-callback logic 2025-07-17 21:02:46 +02:00
Matthias
978f9c804b chore: improved code structure and types 2025-07-17 20:59:04 +02:00
Matthias
31522c681e feat: update get_best_pair to allow better filtering 2025-07-17 20:28:31 +02:00
Matthias
a5ac8a95a7 feat: update get_trading_volume interface to allow filtering for more props 2025-07-17 20:23:59 +02:00
Matthias
35dab9b566 test: fix typo in test name 2025-07-17 07:28:53 +02:00
Matthias
2b05a49671 test: Update telegram /profit tests 2025-07-17 07:26:45 +02:00
Matthias
b79b5b6c32 refactor: move profit test next to each other 2025-07-17 07:21:05 +02:00
Matthias
78124cd025 feat: support /profit long, too 2025-07-17 07:20:15 +02:00
Matthias
7c4c789711 chore: fix message formatting issue 2025-07-17 07:10:32 +02:00
Matthias
30b32a0d2e fix: don't call stoploss-adjust after trade has closed
closes #11990
2025-07-17 06:59:53 +02:00
Matthias
d33768ecee Merge pull request #11992 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-07-17 06:38:42 +02:00
Freqtrade Bot
ccf20b2fe1 chore: update pre-commit hooks 2025-07-17 03:29:45 +00:00
Matthias
23bbbddcdb chore: improve backtest doc clarity 2025-07-16 20:24:48 +02:00
Matthias
c6ed64746c chore: sort pairs by absolute profit 2025-07-16 20:22:56 +02:00
Matthias
8f4dfbf55d chore: remove long-deprecated and pointless metric 2025-07-16 20:18:26 +02:00
Matthias
5296a3be23 fix: order_filled errors must be ignored 2025-07-16 20:18:26 +02:00
qqqqqf
c92c64bac2 Modify the duplicate functions.
Modify the original three duplicate functions (_profit_short, _profit_long, _profit), and add _profit_handler and _format_profit_message.

Refactor telegram.py and rpc.py.

Sorry for the duplicate functions yesterday, I was a bit rushed.

Both pytest and ruff have passed.
2025-07-16 11:43:51 +08:00
Matthias
aaca904455 chore: improved pull request template wording 2025-07-15 20:11:00 +02:00
qqqqqf
19b57ad87e Add /profit long and /profit short commands#2
# Added `/profit_long` and `/profit_short` Commands

Users can now use commands like:

- `/profit_long [<n>]`
- `/profit_short [<n>]`
- `/profit [<n>]`

---

## Key Changes Implemented

### `freqtrade/rpc/telegram.py`:

- The `_profit` command handler has been updated to robustly parse `long` or `short` as optional arguments.
  - **Translation:** The `_profit` command handler has been improved to reliably interpret `long` or `short` as optional parameters.

- The determined direction is passed to the RPC layer.
  - **Translation:** The direction determined (either `long` or `short`) is passed to the RPC layer.

- The `/help` command documentation is updated.
  - **Translation:** The documentation for the `/help` command has been updated accordingly.

---

### `freqtrade/rpc/rpc.py`:

- The `_rpc_trade_statistics` method now accepts a direction parameter.
  - **Translation:** The `_rpc_trade_statistics` method has been updated to accept a `direction` parameter.

- The method has been refactored into a main function and a `_process_trade_stats` helper function to reduce complexity and improve readability.
  - **Translation:** The method has been refactored into a main function and a helper function, `_process_trade_stats`, to reduce complexity and improve readability.

- The database query filter is dynamically modified to include a condition on `Trade.is_short` when a direction is provided.
  - **Translation:** The database query filter dynamically adjusts to include a condition on `Trade.is_short` when a direction is specified.

---

### `tests/rpc/test_rpc_telegram.py`:

- Existing tests for `_profit` have been updated to match the new message format.
  - **Translation:** Existing tests for the `_profit` function have been updated to match the new message format.

- New test cases have been added to specifically validate the `long` and `short` filtering functionality.
  - **Translation:** New test cases have been added to specifically validate the filtering functionality for `long` and `short` trades.

---

## Testing

- All local `pytest` tests pass successfully.
  - **Translation:** All local `pytest` tests have passed successfully.

- All `ruff` linter checks pass.
  - **Translation:** All `ruff` code checks have passed.

- As I do not have a full local deployment, I am relying on the CI pipeline for final validation.
  - **Translation:** Since I don't have a complete local deployment, I am relying on the CI pipeline for final validation.

---
This time, only a little AI was used :)
Except for the translation.
2025-07-15 19:15:04 +08:00
qqqqqf
583738040c Merge branch 'freqtrade:develop' into feat/telegram-profit-direction 2025-07-15 16:49:02 +08:00
Matthias
6e38b72601 chore: Improve pull request template to be clear against AI 2025-07-15 07:05:41 +02:00
Matthias
8bf8ccfe50 Merge pull request #11985 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-07-15 06:25:00 +02:00
Freqtrade Bot
e721a741ca chore: update pre-commit hooks 2025-07-15 03:31:05 +00:00
Matthias
19e9effae4 Merge pull request #11980 from freqtrade/dependabot/pip/develop/python-rapidjson-1.21
chore(deps): bump python-rapidjson from 1.20 to 1.21
2025-07-14 18:16:13 +02:00
Matthias
84100ca7ae Merge pull request #11976 from freqtrade/dependabot/pip/develop/types-1a612854d0
chore(deps-dev): bump types-python-dateutil from 2.9.0.20250516 to 2.9.0.20250708 in the types group
2025-07-14 08:20:49 +02:00
Matthias
a6f94f7d24 Merge pull request #11978 from freqtrade/dependabot/pip/develop/certifi-2025.7.14
chore(deps): bump certifi from 2025.6.15 to 2025.7.14
2025-07-14 08:17:42 +02:00
Matthias
0a32be3fc3 Merge pull request #11982 from freqtrade/dependabot/pip/develop/pandas-2.3.1
chore(deps): bump pandas from 2.3.0 to 2.3.1
2025-07-14 08:11:32 +02:00
Matthias
065203c3db Merge pull request #11983 from freqtrade/dependabot/pip/develop/aiohttp-3.12.14
chore(deps): bump aiohttp from 3.12.13 to 3.12.14
2025-07-14 08:03:44 +02:00
Matthias
758ae42092 Merge pull request #11979 from freqtrade/dependabot/pip/develop/fastapi-0.116.1
chore(deps): bump fastapi from 0.115.14 to 0.116.1
2025-07-14 07:47:55 +02:00
dependabot[bot]
6fdc0f1b22 chore(deps): bump aiohttp from 3.12.13 to 3.12.14
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.12.13 to 3.12.14.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.12.13...v3.12.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 05:37:47 +00:00
dependabot[bot]
7c9fe22b9f chore(deps): bump pandas from 2.3.0 to 2.3.1
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Commits](https://github.com/pandas-dev/pandas/compare/v2.3.0...v2.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 05:37:46 +00:00
Matthias
53aecfa856 Merge pull request #11981 from freqtrade/dependabot/pip/develop/ccxt-4.4.94
chore(deps): bump ccxt from 4.4.92 to 4.4.94
2025-07-14 07:36:31 +02:00
Matthias
bf511f12d4 Merge pull request #11977 from freqtrade/dependabot/pip/develop/ruff-0.12.3
chore(deps-dev): bump ruff from 0.12.2 to 0.12.3
2025-07-14 07:19:19 +02:00
Matthias
0eed655e7c chore: bump types-dateutil in pre-commit config 2025-07-14 06:42:42 +02:00
dependabot[bot]
d2d283a4b2 chore(deps): bump ccxt from 4.4.92 to 4.4.94
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.92 to 4.4.94.
- [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/v4.4.92...v4.4.94)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 04:28:22 +00:00
dependabot[bot]
85492c1084 chore(deps): bump python-rapidjson from 1.20 to 1.21
Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.20 to 1.21.
- [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.20...v1.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 04:28:21 +00:00
dependabot[bot]
f07134bf66 chore(deps): bump fastapi from 0.115.14 to 0.116.1
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.115.14 to 0.116.1.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.14...0.116.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 04:28:19 +00:00
dependabot[bot]
7678a59b1f chore(deps): bump certifi from 2025.6.15 to 2025.7.14
Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.6.15 to 2025.7.14.
- [Commits](https://github.com/certifi/python-certifi/compare/2025.06.15...2025.07.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 04:27:14 +00:00
dependabot[bot]
daced63d00 chore(deps-dev): bump ruff from 0.12.2 to 0.12.3
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.2 to 0.12.3.
- [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.12.2...0.12.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 04:25:44 +00:00
dependabot[bot]
002d8c4022 chore(deps-dev): bump types-python-dateutil in the types group
Bumps the types group with 1 update: [types-python-dateutil](https://github.com/typeshed-internal/stub_uploader).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 04:21:02 +00:00
Matthias
8c85448ed7 chore: remove unused import 2025-07-13 16:04:25 +02:00
Matthias
2236b08fc2 refactor: improved exchange init sequence 2025-07-13 16:03:51 +02:00
Matthias
24f904efc4 test: fix failing test 2025-07-13 15:41:00 +02:00
Matthias
3eaa862caf chore: remove modetrade wrong imports 2025-07-13 13:34:03 +02:00
Matthias
469587e656 feat: add modetrade exchange base config 2025-07-13 13:08:47 +02:00
Matthias
b24064d706 refactor: move default trading mode determination to exchange 2025-07-13 13:04:37 +02:00
Matthias
c4a29a0178 test: ensure candle_type_def is set properly 2025-07-13 13:04:37 +02:00
Matthias
aedbe0d1d2 chore: add spot to available trading modes 2025-07-13 13:04:37 +02:00
Matthias
c5e3f7d0ba chore: default trading-mode to first in supported list 2025-07-13 13:04:37 +02:00
Matthias
bc019d6b6d chore: Explicitly type variable 2025-07-13 13:04:33 +02:00
Matthias
f889061b95 refactor: don't assume all exchanges support spot markets 2025-07-13 10:27:33 +02:00
Matthias
1f52ff3f94 feat: enable websocket support for hyperliquid
This should be treated as experimental (experimental).
2025-07-13 10:20:48 +02:00
Matthias
d0c3b30289 docs: add explanation message about "couldn't reuse" 2025-07-13 09:55:36 +02:00
Matthias
e77feafee0 fix: avoid unnecessary log message when filling ws message 2025-07-13 09:52:05 +02:00
Matthias
1889a315a3 chore: fix comment location 2025-07-13 09:41:48 +02:00
Matthias
cc998afb44 chore: explicitly disable hyperliquid websockets
Add comment explaining the reason
2025-07-13 09:37:05 +02:00
qqqqqf
97f30cf13d feat(telegram): Add /profit long and /profit short commands
This commit enhances the /profit Telegram command to allow filtering by trade direction.

- The `_profit` handler in `telegram.py` now parses 'long'/'short' arguments and passes the direction to the RPC layer.
- The `_rpc_trade_statistics` method in `rpc.py` is updated to filter trades based on the provided direction. It has also been refactored for lower complexity.
- The `/help` command documentation is updated to reflect the new functionality.
- Corresponding unit tests in `test_rpc_telegram.py` are updated and extended to cover the new cases.
2025-07-12 08:41:39 +08:00
Matthias
ccbc48b590 refactor: improve method ordering 2025-07-10 07:08:35 +02:00
Matthias
dde6001ad0 Merge pull request #11972 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-07-10 06:30:36 +02:00
Freqtrade Bot
aae3ab23ac chore: update pre-commit hooks 2025-07-10 03:27:16 +00:00
Matthias
604e08382e Merge pull request #11899 from mrpabloyeah/add-current-drawdown-in-telegram-profit-command
Add current drawdown in telegram profit command
2025-07-09 06:40:28 +02:00
Matthias
2ecadabd92 chore: align API endpoints 2025-07-08 20:09:17 +02:00
Matthias
17c7826756 Merge branch 'develop' into add-current-drawdown-in-telegram-profit-command 2025-07-08 19:59:42 +02:00
Matthias
fe92df7842 chore: revert unnecessary edits 2025-07-08 19:59:37 +02:00
mrpabloyeah
e6dd932436 Expand calculate_max_drawdown() to return the current drawdown data and use it instead of calculate_current_drawdown() 2025-07-08 13:00:48 +02:00
Matthias
8acc05bb2d chore: update wording in exception message 2025-07-08 06:59:52 +02:00
Matthias
c39f6f892b Merge pull request #11968 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-07-08 06:54:38 +02:00
Freqtrade Bot
f13b51cf91 chore: update pre-commit hooks 2025-07-08 03:26:25 +00:00
Matthias
e90c60c940 chore: improved response on delete order 2025-07-07 19:26:35 +02:00
Matthias
60029c2a8b fix: improved message when nonexisting trade is deleted
part of #11967
2025-07-07 19:20:24 +02:00
Matthias
14429d449c Merge pull request #11965 from freqtrade/dependabot/pip/develop/scipy-stubs-1.16.0.2
chore(deps-dev): bump scipy-stubs from 1.15.3.0 to 1.16.0.2
2025-07-07 07:15:41 +02:00
Matthias
cba9d6078c Merge pull request #11964 from freqtrade/dependabot/pip/develop/cryptography-45.0.5
chore(deps): bump cryptography from 45.0.4 to 45.0.5
2025-07-07 07:09:44 +02:00
Matthias
2dfad98ed6 Merge pull request #11962 from freqtrade/dependabot/pip/develop/mkdocs-7fcd3881cb
chore(deps): bump mkdocs-material from 9.6.14 to 9.6.15 in the mkdocs group
2025-07-07 06:55:46 +02:00
Matthias
9de9cac23a chore: bump scipy-stubs in pre-commit config 2025-07-07 06:46:39 +02:00
dependabot[bot]
d1fd7c3f6e chore(deps): bump cryptography from 45.0.4 to 45.0.5
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.4 to 45.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.4...45.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 04:45:48 +00:00
Matthias
8fd5b93127 Merge pull request #11961 from freqtrade/dependabot/pip/develop/pytest-21071ef1ad
chore(deps-dev): bump pytest-xdist from 3.7.0 to 3.8.0 in the pytest group
2025-07-07 06:44:58 +02:00
Matthias
dafa5d4bac Merge pull request #11966 from freqtrade/dependabot/pip/develop/ccxt-4.4.92
chore(deps): bump ccxt from 4.4.91 to 4.4.92
2025-07-07 06:44:34 +02:00
Matthias
d29cd5b9d9 Merge pull request #11963 from freqtrade/dependabot/pip/develop/ruff-0.12.2
chore(deps-dev): bump ruff from 0.12.1 to 0.12.2
2025-07-07 06:44:18 +02:00
dependabot[bot]
60094c6581 chore(deps): bump ccxt from 4.4.91 to 4.4.92
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.91 to 4.4.92.
- [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/v4.4.91...v4.4.92)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 03:58:24 +00:00
dependabot[bot]
dd32e228ce chore(deps-dev): bump scipy-stubs from 1.15.3.0 to 1.16.0.2
Bumps [scipy-stubs](https://github.com/scipy/scipy-stubs) from 1.15.3.0 to 1.16.0.2.
- [Release notes](https://github.com/scipy/scipy-stubs/releases)
- [Commits](https://github.com/scipy/scipy-stubs/compare/v1.15.3.0...v1.16.0.2)

---
updated-dependencies:
- dependency-name: scipy-stubs
  dependency-version: 1.16.0.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 03:58:09 +00:00
dependabot[bot]
81639c61a8 chore(deps-dev): bump ruff from 0.12.1 to 0.12.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.1 to 0.12.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.12.1...0.12.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 03:55:54 +00:00
dependabot[bot]
ae1fcbcffb 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.14 to 9.6.15
- [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.14...9.6.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 03:55:33 +00:00
dependabot[bot]
263d035181 chore(deps-dev): bump pytest-xdist in the pytest group
Bumps the pytest group with 1 update: [pytest-xdist](https://github.com/pytest-dev/pytest-xdist).


Updates `pytest-xdist` from 3.7.0 to 3.8.0
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v3.7.0...v3.8.0)

---
updated-dependencies:
- dependency-name: pytest-xdist
  dependency-version: 3.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: pytest
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 03:51:40 +00:00
Matthias
8ecd7011cb Merge pull request #11953 from freqtrade/drop_3.10
Drop support for python 3.10
2025-07-06 08:58:54 +02:00
Matthias
f7d42ba60d chore: add TODO to eventually remove warning 2025-07-05 19:58:47 +02:00
Matthias
701cf47c4b fix: implement fall-through for leverage tier finding
We'll for now issue a warning about this - and use the "current" tier
This way, gaps in tier data (between maxNotional and the next
minNotional) no longer cause an operational exception.

closes #11923
2025-07-05 19:56:52 +02:00
Matthias
315009a7f0 chore: move comment to correct location 2025-07-05 19:50:26 +02:00
Matthias
6e98a369de test: add test case for #11923 2025-07-05 19:44:24 +02:00
Matthias
fa69b7017b test: add TIA/USDT:USDT test leverage tier
part of #11923
2025-07-05 19:44:12 +02:00
Matthias
e98f3cfedf docs: Update "tag" examples with advanced examples 2025-07-05 08:56:10 +02:00
Matthias
5f4184536a docs: update max enter_tag length 2025-07-05 08:41:08 +02:00
Matthias
ac904c60c8 fix: powershell syntax error 2025-07-05 08:38:02 +02:00
Matthias
16defdbfdf chore: autoformat actual code 2025-07-04 19:29:12 +02:00
Matthias
d010a050d3 chore: autoformat tests 2025-07-04 19:29:12 +02:00
Matthias
7b541ff958 test: refactor test to use asyncio.timeout 2025-07-04 19:29:12 +02:00
Matthias
78a47920ef chore: Update dockerfile syntax 2025-07-04 19:26:38 +02:00
Matthias
e5a2011a85 ci: don't switch docker to experimental 2025-07-04 18:29:15 +02:00
Matthias
640cab2ca8 refactor: tests - update timezone.utc to UTC 2025-07-04 18:22:42 +02:00
Matthias
deb8bde078 refactor: tests - update timezone.utc to UTC 2025-07-04 18:22:42 +02:00
Matthias
79f0271720 refactor: Asyncio timeoutError update 2025-07-04 18:22:42 +02:00
Matthias
5b8b80cf59 refactor: Update timezone.utc to UTC 2025-07-04 18:22:42 +02:00
Matthias
a7ee45a0ba refactor: Update timezone.utc to UTC 2025-07-04 18:22:42 +02:00
Matthias
37cc949d94 refactor: Update utc to UTC 2025-07-04 18:22:42 +02:00
Matthias
5f59e1435e chore: bump known 3.11+ dependencies 2025-07-04 18:22:42 +02:00
Matthias
e1f26aeb4e chore!: drop support for python 3.10 2025-07-04 18:22:42 +02:00
Matthias
b49dafc412 docs: update to 3.11+ version requirement 2025-07-04 18:22:42 +02:00
Matthias
7c84059b3a chore: drop support for pytohn 3.10 in setup scripts 2025-07-04 18:22:42 +02:00
Matthias
10a0bf386b ci: Don't test against 3.10 2025-07-04 18:22:42 +02:00
Matthias
2936cb425d Merge pull request #11948 from jorenham/scipy-stubs
Add `scipy-stubs` as dev dependency
2025-07-04 15:55:56 +02:00
Matthias
e5a9c65e66 ci: improve pre-commit-update check 2025-07-04 15:25:27 +02:00
Matthias
81fdff1039 chore: simplify exchangews_ohlcv further 2025-07-04 15:24:44 +02:00
Matthias
5101b1767b test: slightly cleanup ws_ohlcv test 2025-07-04 15:04:24 +02:00
Matthias
e90aa6abda test: Increased asyncio.sleep for ws_ohlcv test 2025-07-04 14:39:19 +02:00
Matthias
1dd56df5d8 chore: improve sorting in mypy deps 2025-07-04 14:24:30 +02:00
Matthias
9bd8b7acf5 test: use event based init for telegram 2025-07-03 21:08:09 +02:00
Matthias
2a5a422079 test: attempt to fix test_exchangews_ohlcv flakyness 2025-07-03 21:08:09 +02:00
Matthias
e92afb74c6 test: improve event-loop mocking 2025-07-03 21:08:09 +02:00
Matthias
4421e54cde fix: don't disable existing loggers when reinitializing logging
closes #11944
2025-07-03 17:44:23 +02:00
Matthias
b5938985f2 fix: don't log at "root" level. 2025-07-03 15:21:51 +02:00
jorenham
0601f15bd0 Add scipy-stubs as dev dependency
https://github.com/scipy/scipy-stubs
2025-07-03 13:58:12 +02:00
Matthias
66020d250c test: update test trades pagination handling to use _ft_has 2025-07-03 09:00:27 +02:00
Matthias
909286c0f7 chore: luno - remove unnecessary config 2025-07-03 08:58:16 +02:00
Matthias
5efad94d3d chore: add zizmor github action 2025-07-03 08:41:59 +02:00
Matthias
1e24653e79 feat: add luno basic exchange configuration
closes #11928
2025-07-03 08:37:54 +02:00
Matthias
c9bc2b880a feat: add always_require_api_keys to ft_has 2025-07-03 08:33:15 +02:00
Matthias
545cd3a994 chore: ft_has should be before remove_credentials 2025-07-03 08:33:12 +02:00
Matthias
51a09585db refactor: extract final ft_has construction to it's own method 2025-07-03 08:19:04 +02:00
Matthias
b98816635d chore: simplify exchange class setup
remove attributes that are only used once.
2025-07-03 08:18:58 +02:00
Matthias
f6ed609134 Merge pull request #11946 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-07-03 07:34:36 +02:00
Freqtrade Bot
aec2dc5bb0 chore: update pre-commit hooks 2025-07-03 03:27:09 +00:00
Matthias
15c836c566 chore: bump version to 2025.7-dev 2025-07-02 20:18:15 +02:00
mrpabloyeah
2ebc5374f4 Add current drawdown in telegram profit command 2025-06-21 12:10:53 +02:00
136 changed files with 12902 additions and 9186 deletions

View File

@@ -1,5 +1,10 @@
<!-- Thank you for sending your pull request. But first, have you included
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
Did you use AI to create your changes?
If so, please state it clearly in the PR description (failing to do so may result in your PR being closed).
Also, please do a self review of the changes made before submitting the PR to make sure only relevant changes are included.
-->
## Summary

View File

@@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
os: [ "ubuntu-22.04", "ubuntu-24.04" ]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
@@ -38,7 +38,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -160,7 +160,7 @@ jobs:
strategy:
matrix:
os: [ "macos-14", "macos-15" ]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
@@ -174,7 +174,7 @@ jobs:
check-latest: true
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -288,7 +288,7 @@ jobs:
strategy:
matrix:
os: [ windows-latest ]
python-version: ["3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
@@ -301,7 +301,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true
@@ -449,7 +449,7 @@ jobs:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
activate-environment: true
enable-cache: true

View File

@@ -55,14 +55,6 @@ jobs:
run: |
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
# We need docker experimental to pull the ARM image.
- name: Switch docker to experimental
run: |
docker version -f '{{.Server.Experimental}}'
echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker version -f '{{.Server.Experimental}}'
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0

29
.github/workflows/zizmor.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: GitHub Actions Security Analysis with zizmor 🌈
on:
push:
branches:
- develop
- stable
pull_request:
branches:
- develop
- stable
permissions: {}
jobs:
zizmor:
runs-on: ubuntu-latest
permissions:
security-events: write
# contents: read # only needed for private repos
# actions: read # only needed for private repos
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1

View File

@@ -21,16 +21,17 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.16.1"
rev: "v1.17.0"
hooks:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==6.0.0.20250525
- types-cachetools==6.1.0.20250717
- types-filelock==3.2.7
- types-requests==2.32.4.20250611
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20250516
- types-python-dateutil==2.9.0.20250708
- scipy-stubs==1.16.0.2
- SQLAlchemy==2.0.41
# stages: [push]
@@ -43,7 +44,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.12.1'
rev: 'v0.12.5'
hooks:
- id: ruff
- id: ruff-format
@@ -69,7 +70,7 @@ repos:
)$
- repo: https://github.com/stefmolin/exif-stripper
rev: 1.0.0
rev: 1.1.0
hooks:
- id: strip-exif

View File

@@ -1,10 +1,10 @@
FROM python:3.13.5-slim-bookworm as base
FROM python:3.13.5-slim-bookworm AS base
# Setup env
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONFAULTHANDLER=1
ENV PATH=/home/ftuser/.local/bin:$PATH
ENV FT_APP_ENV="docker"
@@ -21,7 +21,7 @@ RUN mkdir /freqtrade \
WORKDIR /freqtrade
# Install dependencies
FROM base as python-deps
FROM base AS python-deps
RUN apt-get update \
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \
@@ -30,7 +30,7 @@ RUN apt-get update \
# Install TA-lib
COPY build_helpers/* /tmp/
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
ENV LD_LIBRARY_PATH /usr/local/lib
ENV LD_LIBRARY_PATH=/usr/local/lib
# Install dependencies
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
@@ -39,9 +39,9 @@ RUN pip install --user --no-cache-dir "numpy<3.0" \
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
# Copy dependencies to runtime-image
FROM base as runtime-image
FROM base AS runtime-image
COPY --from=python-deps /usr/local/lib /usr/local/lib
ENV LD_LIBRARY_PATH /usr/local/lib
ENV LD_LIBRARY_PATH=/usr/local/lib
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local

View File

@@ -64,7 +64,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
## Features
- [x] **Based on Python 3.10+**: For botting on any operating system - Windows, macOS and Linux.
- [x] **Based on Python 3.11+**: For botting on any operating system - Windows, macOS and Linux.
- [x] **Persistence**: Persistence is achieved through sqlite.
- [x] **Dry-run**: Run the bot without paying money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
@@ -146,6 +146,8 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
- `/stopentry`: Stop entering new trades.
- `/status <trade_id>|[table]`: Lists all or specific open trades.
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
- `/profit_long [<n>]`: Lists cumulative profit from all finished long trades, over the last n days.
- `/profit_short [<n>]`: Lists cumulative profit from all finished short trades, over the last n days.
- `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
- `/fx <trade_id>|all`: Alias to `/forceexit`
- `/performance`: Show performance of each finished trade grouped by pair
@@ -154,6 +156,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
- `/help`: Show help message.
- `/version`: Show version.
## Development branches
The project is currently setup in two main branches:
@@ -219,7 +222,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
### Software requirements
- [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
- [Python >= 3.11](http://docs.python-guide.org/en/latest/starting/installation/)
- [pip](https://pip.pypa.io/en/stable/installing/)
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)

View File

@@ -4,7 +4,7 @@ python -m pip install --upgrade pip
python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
pip install -U wheel "numpy<3.0"
pip install --only-binary ta-lib --find-links=build_helpers\ ta-lib
pip install --only-binary ta-lib --find-links=build_helpers\ "ta-lib<0.6.0"
pip install -r requirements-dev.txt
pip install -e .

View File

@@ -16,10 +16,12 @@ with require_dev.open("r") as rfile:
with require.open("r") as rfile:
requirements.extend(rfile.readlines())
# Extract types only
type_reqs = [
r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
]
# Extract relevant types only
supported = ("types-", "SQLAlchemy", "scipy-stubs")
# Find relevant dependencies
# Only keep the first part of the line up to the first space
type_reqs = [r.strip("\n").split()[0] for r in requirements if r.startswith(supported)]
with pre_commit_file.open("r") as file:
f = yaml.load(file, Loader=yaml.SafeLoader)

View File

@@ -1,10 +1,10 @@
FROM python:3.11.13-slim-bookworm as base
FROM python:3.11.13-slim-bookworm AS base
# Setup env
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONFAULTHANDLER 1
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONFAULTHANDLER=1
ENV PATH=/home/ftuser/.local/bin:$PATH
ENV FT_APP_ENV="docker"
@@ -22,7 +22,7 @@ RUN mkdir /freqtrade \
WORKDIR /freqtrade
# Install dependencies
FROM base as python-deps
FROM base AS python-deps
RUN apt-get update \
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \
@@ -39,9 +39,9 @@ RUN pip install --user --no-cache-dir "numpy<3.0" \
&& pip install --user --no-cache-dir -r requirements.txt
# Copy dependencies to runtime-image
FROM base as runtime-image
FROM base AS runtime-image
COPY --from=python-deps /usr/local/lib /usr/local/lib
ENV LD_LIBRARY_PATH /usr/local/lib
ENV LD_LIBRARY_PATH=/usr/local/lib
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local

View File

@@ -321,6 +321,7 @@ It contains some useful key metrics about performance of your strategy on backte
| SQN | 2.45 |
| Profit factor | 1.11 |
| Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. daily profit | 0.0001 BTC |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
@@ -374,9 +375,11 @@ It contains some useful key metrics about performance of your strategy on backte
- `Calmar`: Annualized Calmar ratio.
- `SQN`: System Quality Number (SQN) - by Van Tharp.
- `Profit factor`: profit / loss.
- `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable.
- `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`.
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Tot Profit %`.
- `Best Pair` / `Worst Pair`: Best and worst performing pair (based on absolute profit), and it's corresponding `Tot Profit %`.
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
- `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).

View File

@@ -339,13 +339,13 @@ This needs to be configured like this:
```json
"exchange": {
"name": "hyperliquid",
"walletAddress": "your_eth_wallet_address",
"walletAddress": "your_eth_wallet_address", // This should NOT be your API Wallet Address!
"privateKey": "your_api_private_key",
// ...
}
```
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your wallet address, not your API Wallet Address.
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your main wallet address, not your API Wallet Address.
* privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation.
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
@@ -363,6 +363,27 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
### Hyperliquid Vault / Subaccount
Hyperliquid allows you to create either a vault or a subaccount.
To use these with Freqtrade, you will need to use the following configuration pattern:
``` json
"exchange": {
"name": "hyperliquid",
"walletAddress": "your_vault_address", // Vault or subaccount address
"privateKey": "your_api_private_key",
"ccxt_config": {
"options": {
"vaultAddress": "your_vault_address" // Optional, only if you want to use a vault or subaccount
}
},
// ...
}
```
Your balance and trades will now be used from your vault / subaccount - and no longer from your main account.
### Historic Hyperliquid data
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.

View File

@@ -159,6 +159,14 @@ This warning can point to one of the below problems:
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
### I get the message "Couldn't reuse watch for xxx" in the log
This is an informational message that the bot tried to use candles from the websocket, but the exchange didn't provide the right information.
This can happen if there was an interruption to the websocket connection - or if the pair didn't have any trades happen in the timeframe you are using.
Freqtrade will handle this gracefully by falling back to the REST api.
While this makes the iteration slightly slower (due to the REST Api call) - it will not cause any problems to the bot's operation.
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io).

View File

@@ -87,7 +87,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
Alternatively
- Python 3.10+
- Python 3.11+
- pip (pip3)
- git
- TA-Lib

View File

@@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
!!! Note
Python3.10 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
Python3.11 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
!!! Warning "Up-to-date clock"
@@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
### Install guide
* [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
* [Python >= 3.11](http://docs.python-guide.org/en/latest/starting/installation/)
* [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
@@ -54,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
OS Specific steps are listed first, the common section below is necessary for all systems.
!!! Note
Python3.10 or higher and the corresponding pip are assumed to be available.
Python3.11 or higher and the corresponding pip are assumed to be available.
=== "Debian/Ubuntu"
#### Install necessary dependencies
@@ -179,7 +179,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
** --install **
With this option, the script will install the bot and most dependencies:
You will need to have git and python3.10+ installed beforehand for this to work.
You will need to have git and python3.11+ installed beforehand for this to work.
* Mandatory software as: `ta-lib`
* Setup your virtualenv under `.venv/`

View File

@@ -1,6 +1,6 @@
markdown==3.8.2
mkdocs==1.6.1
mkdocs-material==9.6.14
mkdocs-material==9.6.16
mdx_truly_sane_lists==1.3
pymdown-extensions==10.16
jinja2==3.1.6

View File

@@ -174,17 +174,27 @@ class AwesomeStrategy(IStrategy):
## Enter Tag
When your strategy has multiple buy signals, you can name the signal that triggered.
Then you can access your buy signal on `custom_exit`
When your strategy has multiple entry signals, you can name the signal that triggered.
Then you can access your entry signal on `custom_exit`
```python
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["enter_tag"] = ""
signal_rsi = (qtpylib.crossed_above(dataframe["rsi"], 35))
signal_bblower = (dataframe["bb_lowerband"] < dataframe["close"])
# Additional conditions
dataframe.loc[
(
(dataframe['rsi'] < 35) &
(dataframe['volume'] > 0)
),
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
signal_rsi
| signal_bblower
# ... additional signals to enter a long position
)
& (dataframe["volume"] > 0)
, "enter_long"
] = 1
# Concatenate the tags so all signals are kept
dataframe.loc[signal_rsi, "enter_tag"] += "long_signal_rsi "
dataframe.loc[signal_bblower, "enter_tag"] += "long_signal_bblower "
return dataframe
@@ -192,14 +202,17 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
return 'sell_signal_rsi'
if "long_signal_rsi" in trade.enter_tag and last_candle["rsi"] > 80:
return "exit_signal_rsi"
if "long_signal_bblower" in trade.enter_tag and last_candle["high"] > last_candle["bb_upperband"]:
return "exit_signal_bblower"
# ...
return None
```
!!! Note
`enter_tag` is limited to 100 characters, remaining data will be truncated.
`enter_tag` is limited to 255 characters, remaining data will be truncated.
!!! Warning
There is only one `enter_tag` column, which is used for both long and short trades.
@@ -213,17 +226,27 @@ Similar to [Entry Tagging](#enter-tag), you can also specify an exit tag.
``` python
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["exit_tag"] = ""
rsi_exit_signal = (dataframe["rsi"] > 70)
ema_exit_signal = (dataframe["ema20"] < dataframe["ema50"])
# Additional conditions
dataframe.loc[
(
(dataframe['rsi'] > 70) &
(dataframe['volume'] > 0)
),
['exit_long', 'exit_tag']] = (1, 'exit_rsi')
rsi_exit_signal
| ema_exit_signal
# ... additional signals to exit a long position
) &
(dataframe["volume"] > 0)
,
"exit_long"] = 1
# Concatenate the tags so all signals are kept
dataframe.loc[rsi_exit_signal, "exit_tag"] += "exit_signal_rsi "
dataframe.loc[rsi_exit_signal2, "exit_tag"] += "exit_signal_rsi "
return dataframe
```
The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
The provided exit-tag is then used as exit-reason - and shown as such in backtest results.
!!! Note
`exit_reason` is limited to 100 characters, remaining data will be truncated.

View File

@@ -229,6 +229,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
| **Metrics** |
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/profit_[long|short] [<n>]` | Display a summary of your profit/loss from close trades in one direction and some stats about your performance, over the last n days (all trades by default)
| `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show bot managed balance per currency
| `/balance full` | Show account balance per currency
@@ -309,6 +310,8 @@ current max
### /profit
Also available as `/profit_long` and `/profit_short` to show profit for long or short trades only.
Return a summary of your profit/loss and performance.
> **ROI:** Close trades

View File

@@ -117,9 +117,9 @@ Different payloads can be configured for different events. Not all fields are ne
## Webhook Message types
### Entry
### Entry / Entry fill
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
The fields in `webhook.entry` and `webhook.entry_fill` are filled when the bot places a long/short Order to increase a position, or when that order fills respectively. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
@@ -162,31 +162,9 @@ Possible parameters are:
* `current_rate`
* `enter_tag`
### Entry fill
### Exit / Exit fill
The fields in `webhook.entry_fill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `open_rate`
* `amount`
* `open_date`
* `stake_amount`
* `stake_currency`
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `order_type`
* `current_rate`
* `enter_tag`
### Exit
The fields in `webhook.exit` are filled when the bot exits a trade. Parameters are filled using string.format.
The fields in `webhook.exit` and `webhook.exit_fill` are filled when the bot places an exit order, or when that exit order fills respectively. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
@@ -195,34 +173,9 @@ Possible parameters are:
* `direction`
* `leverage`
* `gain`
* `limit`
* `amount`
* `open_rate`
* `profit_amount`
* `profit_ratio`
* `stake_currency`
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `exit_reason`
* `order_type`
* `open_date`
* `close_date`
### Exit fill
The fields in `webhook.exit_fill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `gain`
* `close_rate`
* `amount`
* `open_rate`
* `current_rate`
* `profit_amount`
* `profit_ratio`
@@ -230,10 +183,14 @@ Possible parameters are:
* `base_currency`
* `quote_currency`
* `fiat_currency`
* `enter_tag`
* `exit_reason`
* `order_type`
* `open_date`
* `close_date`
* `sub_trade`
* `is_final_exit`
### Exit cancel
@@ -246,7 +203,7 @@ Possible parameters are:
* `direction`
* `leverage`
* `gain`
* `limit`
* `order_rate`
* `amount`
* `open_rate`
* `current_rate`

View File

@@ -5,7 +5,7 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
Otherwise, please follow the instructions below.
All instructions assume that python 3.10+ is installed and available.
All instructions assume that python 3.11+ is installed and available.
## Clone the git repository
@@ -42,7 +42,7 @@ cd freqtrade
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.10, 3.11, 3.12 and 3.13) and for 64bit Windows.
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.11, 3.12 and 3.13) and for 64bit Windows.
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
Other versions must be downloaded from the above link.

View File

@@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2025.6"
__version__ = "2025.7"
if "dev" in __version__:
from pathlib import Path

View File

@@ -3,7 +3,7 @@
__main__.py for Freqtrade
To launch Freqtrade as a module
> python -m freqtrade (with Python >= 3.10)
> python -m freqtrade (with Python >= 3.11)
"""
from freqtrade import main

View File

@@ -258,24 +258,26 @@ ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs"
# Command level configs - keep at the bottom of the above definitions
NO_CONF_REQURIED = [
"backtest-filter",
"backtesting-show",
"convert-data",
"convert-trade-data",
"download-data",
"list-timeframes",
"hyperopt-list",
"hyperopt-show",
"list-data",
"list-freqaimodels",
"list-hyperoptloss",
"list-markets",
"list-pairs",
"list-strategies",
"list-freqaimodels",
"list-hyperoptloss",
"list-data",
"hyperopt-list",
"hyperopt-show",
"backtest-filter",
"list-timeframes",
"plot-dataframe",
"plot-profit",
"show-trades",
"trades-to-ohlcv",
"install-ui",
"strategy-updater",
"trades-to-ohlcv",
]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
@@ -311,8 +313,6 @@ class Arguments:
# (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting)
if "config" in parsed_arg and parsed_arg.config is None:
conf_required = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
user_dir = parsed_arg.user_data_dir
else:
@@ -325,7 +325,9 @@ class Arguments:
else:
# Else use "config.json".
cfgfile = Path.cwd() / DEFAULT_CONFIG
if cfgfile.is_file() or not conf_required:
conf_optional = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
if cfgfile.is_file() or not conf_optional:
# Only inject config if the file exists, or if the config is required
parsed_arg.config = [DEFAULT_CONFIG]
return parsed_arg

View File

@@ -18,10 +18,7 @@ from freqtrade.constants import Config
from freqtrade.enums import (
NON_UTIL_MODES,
TRADE_MODES,
CandleType,
MarginMode,
RunMode,
TradingMode,
)
from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging
@@ -397,11 +394,6 @@ class Configuration:
self._args_to_config(
config, argname="trading_mode", logstring="Detected --trading-mode: {}"
)
config["candle_type_def"] = CandleType.get_default(
config.get("trading_mode", "spot") or "spot"
)
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
config["margin_mode"] = MarginMode(config.get("margin_mode", "") or "")
self._args_to_config(
config, argname="candle_types", logstring="Detected --candle-types: {}"
)

View File

@@ -4,9 +4,8 @@ This module contains the argument manager class
import logging
import re
from datetime import datetime, timezone
from typing_extensions import Self
from datetime import UTC, datetime
from typing import Self
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import ConfigurationError
@@ -151,9 +150,7 @@ class TimeRange:
starts = rvals[index]
if stype[0] == "date" and len(starts) == 8:
start = int(
datetime.strptime(starts, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
datetime.strptime(starts, "%Y%m%d").replace(tzinfo=UTC).timestamp()
)
elif len(starts) == 13:
start = int(starts) // 1000
@@ -164,9 +161,7 @@ class TimeRange:
stops = rvals[index]
if stype[1] == "date" and len(stops) == 8:
stop = int(
datetime.strptime(stops, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
datetime.strptime(stops, "%Y%m%d").replace(tzinfo=UTC).timestamp()
)
elif len(stops) == 13:
stop = int(stops) // 1000

View File

@@ -5,7 +5,7 @@ Helpers when analyzing backtest data
import logging
import zipfile
from copy import copy
from datetime import datetime, timezone
from datetime import UTC, datetime
from io import BytesIO, StringIO
from pathlib import Path
from typing import Any, Literal
@@ -324,7 +324,7 @@ def find_existing_backtest_stats(
if min_backtest_date is not None:
backtest_date = strategy_metadata["backtest_start_time"]
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
backtest_date = datetime.fromtimestamp(backtest_date, tz=UTC)
if backtest_date < min_backtest_date:
# Do not use a cached result for this strategy as first result is too old.
del run_ids[strategy_name]

View File

@@ -7,7 +7,7 @@ Common Interface for bot and strategy to access data.
import logging
from collections import deque
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
@@ -98,7 +98,7 @@ class DataProvider:
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
pair_key = (pair, timeframe, candle_type)
self.__cached_pairs[pair_key] = (dataframe, datetime.now(timezone.utc))
self.__cached_pairs[pair_key] = (dataframe, datetime.now(UTC))
# For multiple producers we will want to merge the pairlists instead of overwriting
def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"):
@@ -131,7 +131,7 @@ class DataProvider:
"data": {
"key": pair_key,
"df": dataframe.tail(1),
"la": datetime.now(timezone.utc),
"la": datetime.now(UTC),
},
}
self.__rpc.send_msg(msg)
@@ -164,7 +164,7 @@ class DataProvider:
if producer_name not in self.__producer_pairs_df:
self.__producer_pairs_df[producer_name] = {}
_last_analyzed = datetime.now(timezone.utc) if not last_analyzed else last_analyzed
_last_analyzed = datetime.now(UTC) if not last_analyzed else last_analyzed
self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed)
logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.")
@@ -275,12 +275,12 @@ class DataProvider:
# If we have no data from this Producer yet
if producer_name not in self.__producer_pairs_df:
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
# If we do have data from that Producer, but no data on this pair_key
if pair_key not in self.__producer_pairs_df[producer_name]:
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
# We have it, return this data
df, la = self.__producer_pairs_df[producer_name][pair_key]
@@ -396,10 +396,10 @@ class DataProvider:
if (max_index := self.__slice_index.get(pair)) is not None:
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
return df, date
else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
@property
def runmode(self) -> RunMode:

View File

@@ -8,7 +8,7 @@ import logging
import re
from abc import ABC, abstractmethod
from copy import deepcopy
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from pandas import DataFrame, to_datetime
@@ -118,8 +118,8 @@ class IDataHandler(ABC):
df = self._ohlcv_load(pair, timeframe, None, candle_type)
if df.empty:
return (
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=UTC),
datetime.fromtimestamp(0, tz=UTC),
0,
)
return df.iloc[0]["date"].to_pydatetime(), df.iloc[-1]["date"].to_pydatetime(), len(df)
@@ -201,8 +201,8 @@ class IDataHandler(ABC):
df = self._trades_load(pair, trading_mode)
if df.empty:
return (
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=UTC),
datetime.fromtimestamp(0, tz=UTC),
0,
)
return (

View File

@@ -174,12 +174,18 @@ def calculate_underwater(
@dataclass()
class DrawDownResult:
# Max drawdown fields
drawdown_abs: float = 0.0
high_date: pd.Timestamp = None
low_date: pd.Timestamp = None
high_value: float = 0.0
low_value: float = 0.0
relative_account_drawdown: float = 0.0
# Current drawdown fields
current_high_date: pd.Timestamp = None
current_high_value: float = 0.0
current_drawdown_abs: float = 0.0
current_relative_account_drawdown: float = 0.0
def calculate_max_drawdown(
@@ -191,29 +197,31 @@ def calculate_max_drawdown(
relative: bool = False,
) -> DrawDownResult:
"""
Calculate max drawdown and the corresponding close dates
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
Calculate max drawdown and current drawdown with corresponding dates
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
:param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
:param relative: If True, use relative drawdown for max calculation instead of absolute
:return: DrawDownResult object
with absolute max drawdown, high and low time and high and low value,
and the relative account drawdown
relative account drawdown, and current drawdown information.
:raise: ValueError if trade-dataframe was found empty.
"""
if len(trades) == 0:
raise ValueError("Trade dataframe empty.")
profit_results = trades.sort_values(date_col).reset_index(drop=True)
max_drawdown_df = _calc_drawdown_series(
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
)
# Calculate maximum drawdown
idxmin = (
max_drawdown_df["drawdown_relative"].idxmax()
if relative
else max_drawdown_df["drawdown"].idxmin()
)
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
high_date = profit_results.loc[high_idx, date_col]
low_date = profit_results.loc[idxmin, date_col]
@@ -221,13 +229,27 @@ def calculate_max_drawdown(
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
# Calculate current drawdown
current_high_idx = max_drawdown_df["high_value"].iloc[:-1].idxmax()
current_high_date = profit_results.loc[current_high_idx, date_col]
current_high_value = max_drawdown_df.iloc[-1]["high_value"]
current_cumulative = max_drawdown_df.iloc[-1]["cumulative"]
current_drawdown_abs = current_high_value - current_cumulative
current_drawdown_relative = max_drawdown_df.iloc[-1]["drawdown_relative"]
return DrawDownResult(
# Max drawdown
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
high_date=high_date,
low_date=low_date,
high_value=high_val,
low_value=low_val,
relative_account_drawdown=max_drawdown_rel,
# Current drawdown
current_high_date=current_high_date,
current_high_value=current_high_value,
current_drawdown_abs=current_drawdown_abs,
current_relative_account_drawdown=current_drawdown_relative,
)

View File

@@ -43,4 +43,6 @@ from freqtrade.exchange.idex import Idex
from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin
from freqtrade.exchange.lbank import Lbank
from freqtrade.exchange.luno import Luno
from freqtrade.exchange.modetrade import Modetrade
from freqtrade.exchange.okx import Okx

View File

@@ -1,7 +1,7 @@
"""Binance exchange subclass"""
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
import ccxt
@@ -63,7 +63,7 @@ class Binance(Exchange):
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
(TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),
@@ -160,7 +160,7 @@ class Binance(Exchange):
since_ms = x[3][0][0]
logger.info(
f"Candle-data for {pair} available starting with "
f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}."
f"{datetime.fromtimestamp(since_ms // 1000, tz=UTC).isoformat()}."
)
if until_ms and since_ms >= until_ms:
logger.warning(
@@ -399,7 +399,7 @@ class Binance(Exchange):
trades = await self._api_async.fetch_trades(
pair,
params={
self._trades_pagination_arg: "0",
self._ft_has["trades_pagination_arg"]: "0",
},
limit=5,
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
"""Bitpanda exchange subclass"""
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from freqtrade.exchange import Exchange
@@ -34,5 +34,5 @@ class Bitpanda(Exchange):
:param pair: Pair the order is for
:param since: datetime object of the order creation time. Assumes object is in UTC.
"""
params = {"to": int(datetime.now(timezone.utc).timestamp() * 1000)}
params = {"to": int(datetime.now(UTC).timestamp() * 1000)}
return super().get_trades_for_order(order_id, pair, since, params)

View File

@@ -1,8 +1,5 @@
"""Bybit exchange subclass"""
import logging
from datetime import datetime, timedelta
from typing import Any
import ccxt
@@ -12,6 +9,7 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__)
@@ -64,9 +62,9 @@ class Bybit(Exchange):
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
(TradingMode.SPOT, MarginMode.NONE),
(TradingMode.FUTURES, MarginMode.ISOLATED),
# (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
@property
@@ -76,14 +74,11 @@ class Bybit(Exchange):
config = {}
if self.trading_mode == TradingMode.SPOT:
config.update({"options": {"defaultType": "spot"}})
config.update(super()._ccxt_config)
elif self.trading_mode == TradingMode.FUTURES:
config.update({"options": {"defaultSettle": self._config["stake_currency"]}})
config = deep_merge_dicts(config, super()._ccxt_config)
return config
def market_is_future(self, market: dict[str, Any]) -> bool:
main = super().market_is_future(market)
# For ByBit, we'll only support USDT markets for now.
return main and market["settle"] == "USDT"
@retrier
def additional_exchange_init(self) -> None:
"""
@@ -182,18 +177,36 @@ class Bybit(Exchange):
PERPETUAL:
bybit:
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
USDT:
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
USDC:
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#c
Long:
Long USDT:
Liquidation Price = (
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
- (Extra Margin Added/Contract Quantity))
Short USDT:
Liquidation Price = (
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
+ (Extra Margin Added/Contract Quantity))
Long USDC:
Liquidation Price = (
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
- (Extra Margin Added/Contract Quantity))
Short:
Position Entry Price - [
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
]
)
Short USDC:
Liquidation Price = (
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
+ (Extra Margin Added/Contract Quantity))
Position Entry Price + [
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
]
)
Implementation Note: Extra margin is currently not used.
Due to this - the liquidation formula between USDT and USDC is the same.
:param pair: Pair to calculate liquidation price for
:param open_rate: Entry price of position

View File

@@ -9,7 +9,7 @@ import logging
import signal
from collections.abc import Coroutine, Generator
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from math import floor, isnan
from threading import Lock
from typing import Any, Literal, TypeGuard, TypeVar
@@ -137,6 +137,7 @@ class Exchange:
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
"ohlcv_partial_candle": True,
"ohlcv_require_since": False,
"always_require_api_keys": False, # purge API keys for Dry-run. Must default to false.
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
"ohlcv_volume_currency": "base", # "base" or "quote"
"tickers_have_quoteVolume": True,
@@ -168,7 +169,8 @@ class Exchange:
_ft_has_futures: FtHas = {}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# Non-defined exchanges only support spot mode.
(TradingMode.SPOT, MarginMode.NONE),
]
def __init__(
@@ -197,7 +199,26 @@ class Exchange:
self.loop = self._init_async_loop()
self._config: Config = {}
# Leverage properties
self.trading_mode: TradingMode = TradingMode(
config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
)
self.margin_mode: MarginMode = MarginMode(
MarginMode(config.get("margin_mode"))
if config.get("margin_mode")
else self._supported_trading_mode_margin_pairs[0][1]
)
config["trading_mode"] = self.trading_mode
config["margin_mode"] = self.margin_mode
config["candle_type_def"] = CandleType.get_default(self.trading_mode)
self._config.update(config)
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
exchange_conf: ExchangeConfig = exchange_config if exchange_config else config["exchange"]
# Deep merge ft_has with default ft_has options
# Must be called before ft_has is used.
self.build_ft_has(exchange_conf)
# Holds last candle refreshed time of each pair
self._pairs_last_refresh_time: dict[PairWithTimeframe, int] = {}
@@ -227,33 +248,17 @@ class Exchange:
if config["dry_run"]:
logger.info("Instance is running with dry_run enabled")
logger.info(f"Using CCXT {ccxt.__version__}")
exchange_conf: dict[str, Any] = exchange_config if exchange_config else config["exchange"]
remove_exchange_credentials(exchange_conf, config.get("dry_run", False))
self.log_responses = exchange_conf.get("log_responses", False)
# Leverage properties
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
self.margin_mode: MarginMode = (
MarginMode(config.get("margin_mode")) if config.get("margin_mode") else MarginMode.NONE
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
remove_exchange_credentials(
exchange_conf,
not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
)
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
# Deep merge ft_has with default ft_has options
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
if self.trading_mode == TradingMode.FUTURES:
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
if exchange_conf.get("_ft_has_params"):
self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
self.log_responses = exchange_conf.get("log_responses", False)
# Assign this directly for easy access
self._ohlcv_partial_candle = self._ft_has["ohlcv_partial_candle"]
self._max_trades_limit = self._ft_has["trades_limit"]
self._trades_pagination = self._ft_has["trades_pagination"]
self._trades_pagination_arg = self._ft_has["trades_pagination_arg"]
# Initialize ccxt objects
ccxt_config = self._ccxt_config
ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config)
@@ -289,10 +294,6 @@ class Exchange:
# Initial markets load
self.reload_markets(True, load_leverage_tiers=False)
self.validate_config(config)
self._startup_candle_count: int = config.get("startup_candle_count", 0)
self.required_candle_call_count = self.validate_required_startup_candles(
self._startup_candle_count, config.get("timeframe", "")
)
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
self.fill_leverage_tiers()
@@ -331,6 +332,12 @@ class Exchange:
asyncio.set_event_loop(loop)
return loop
def _set_startup_candle_count(self, config: Config) -> None:
self._startup_candle_count: int = config.get("startup_candle_count", 0)
self.required_candle_call_count = self.validate_required_startup_candles(
self._startup_candle_count, config.get("timeframe", "")
)
def validate_config(self, config: Config) -> None:
# Check if timeframe is available
self.validate_timeframes(config.get("timeframe"))
@@ -345,6 +352,8 @@ class Exchange:
self.validate_orderflow(config["exchange"])
self.validate_freqai(config)
self._set_startup_candle_count(config)
def _init_ccxt(
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
) -> ccxt.Exchange:
@@ -657,7 +666,7 @@ class Exchange:
if isinstance(markets, Exception):
raise markets
return None
except asyncio.TimeoutError as e:
except TimeoutError as e:
logger.warning("Could not load markets. Reason: %s", e)
raise TemporaryError from e
@@ -881,6 +890,20 @@ class Exchange:
f"Freqtrade does not support '{mm_value}' '{trading_mode}' on {self.name}."
)
def build_ft_has(self, exchange_conf: ExchangeConfig) -> None:
"""
Deep merge ft_has with default ft_has options
and with exchange_conf._ft_has_params if available.
This is called on initialization of the exchange object.
It must be called before ft_has is used.
"""
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
if self.trading_mode == TradingMode.FUTURES:
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
if exchange_conf.get("_ft_has_params"):
self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
def get_option(self, param: str, default: Any | None = None) -> Any:
"""
Get parameter value from _ft_has
@@ -2208,7 +2231,7 @@ class Exchange:
_params = params if params else {}
my_trades = self._api.fetch_my_trades(
pair,
int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
int((since.replace(tzinfo=UTC).timestamp() - 5) * 1000),
params=_params,
)
matched_trades = [trade for trade in my_trades if trade["order"] == order_id]
@@ -2584,10 +2607,12 @@ class Exchange:
if ticks and cache:
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0]
# keeping parsed dataframe in cache
has_cache = cache and (pair, timeframe, c_type) in self._klines
# in case of existing cache, fill_missing happens after concatenation
ohlcv_df = ohlcv_to_dataframe(
ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=drop_incomplete
ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
)
# keeping parsed dataframe in cache
if cache:
if (pair, timeframe, c_type) in self._klines:
old = self._klines[(pair, timeframe, c_type)]
@@ -2995,7 +3020,7 @@ class Exchange:
returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
"""
try:
trades_limit = self._max_trades_limit
trades_limit = self._ft_has["trades_limit"]
# fetch trades asynchronously
if params:
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
@@ -3039,7 +3064,7 @@ class Exchange:
"""
if not trades:
return None
if self._trades_pagination == "id":
if self._ft_has["trades_pagination"] == "id":
return trades[-1].get("id")
else:
return trades[-1].get("timestamp")
@@ -3057,7 +3082,7 @@ class Exchange:
) -> tuple[str, list[list]]:
"""
Asynchronously gets trade history using fetch_trades
use this when exchange uses id-based iteration (check `self._trades_pagination`)
use this when exchange uses id-based iteration (check `self._ft_has["trades_pagination"]`)
:param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds
@@ -3083,7 +3108,7 @@ class Exchange:
while True:
try:
t, from_id_next = await self._async_fetch_trades(
pair, params={self._trades_pagination_arg: from_id}
pair, params={self._ft_has["trades_pagination_arg"]: from_id}
)
if t:
trades.extend(t[x])
@@ -3111,7 +3136,7 @@ class Exchange:
) -> tuple[str, list[list]]:
"""
Asynchronously gets trade history using fetch_trades,
when the exchange uses time-based iteration (check `self._trades_pagination`)
when the exchange uses time-based iteration (check `self._ft_has["trades_pagination"]`)
:param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds
@@ -3165,9 +3190,9 @@ class Exchange:
until = ccxt.Exchange.milliseconds()
logger.debug(f"Exchange milliseconds: {until}")
if self._trades_pagination == "time":
if self._ft_has["trades_pagination"] == "time":
return await self._async_get_trade_history_time(pair=pair, since=since, until=until)
elif self._trades_pagination == "id":
elif self._ft_has["trades_pagination"] == "id":
return await self._async_get_trade_history_id(
pair=pair, since=since, until=until, from_id=from_id
)
@@ -3335,7 +3360,7 @@ class Exchange:
if not filename.parent.is_dir():
filename.parent.mkdir(parents=True)
data = {
"updated": datetime.now(timezone.utc),
"updated": datetime.now(UTC),
"data": tiers,
}
file_dump_json(filename, data)
@@ -3357,7 +3382,7 @@ class Exchange:
updated = tiers.get("updated")
if updated:
updated_dt = parser.parse(updated)
if updated_dt < datetime.now(timezone.utc) - cache_time:
if updated_dt < datetime.now(UTC) - cache_time:
logger.info("Cached leverage tiers are outdated. Will update.")
return None
return tiers.get("data")
@@ -3416,17 +3441,26 @@ class Exchange:
# Find the appropriate tier based on stake_amount
prior_max_lev = None
for tier in pair_tiers:
# Adjust notional by leverage to do a proper comparison
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
max_stake = tier["maxNotional"] / tier["maxLeverage"]
prior_max_lev = tier["maxLeverage"]
# Adjust notional by leverage to do a proper comparison
if min_stake <= stake_amount <= max_stake:
return tier["maxLeverage"]
if stake_amount < min_stake and stake_amount <= max_stake:
# TODO: Remove this warning eventually
# Code could be simplified by removing the check for min-stake in the above
# condition, making this branch unnecessary.
logger.warning(
f"Fallback to next higher leverage tier for {pair}, stake: {stake_amount}, "
f"min_stake: {min_stake}."
)
return tier["maxLeverage"]
# else: # if on the last tier
if stake_amount > max_stake:
# If stake is > than max tradeable amount
raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}")
raise InvalidOrderException(f"Stake amount {stake_amount} too high for {pair}")
raise OperationalException(
f"Looped through all tiers without finding a max leverage for {pair}. "
@@ -3572,7 +3606,7 @@ class Exchange:
mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
if not close_date:
close_date = datetime.now(timezone.utc)
close_date = datetime.now(UTC)
since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date))
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)

View File

@@ -24,6 +24,7 @@ class FtHas(TypedDict, total=False):
ohlcv_require_since: bool
ohlcv_volume_currency: str
ohlcv_candle_limit_per_timeframe: dict[str, int]
always_require_api_keys: bool
# Tickers
tickers_have_quoteVolume: bool
tickers_have_percentage: bool

View File

@@ -3,7 +3,7 @@ Exchange support utils
"""
import inspect
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from math import ceil, floor, isnan
from typing import Any
@@ -27,7 +27,7 @@ from freqtrade.exchange.common import (
SUPPORTED_EXCHANGES,
)
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.ft_types import ValidExchangesType
from freqtrade.ft_types import TradeModeType, ValidExchangesType
from freqtrade.util import FtPrecise
@@ -110,7 +110,7 @@ def _build_exchange_list_entry(
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
}
if resolved := exchangeClasses.get(mapped_exchange_name):
supported_modes = [{"trading_mode": "spot", "margin_mode": ""}] + [
supported_modes: list[TradeModeType] = [
{"trading_mode": tm.value, "margin_mode": mm.value}
for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs
]
@@ -148,7 +148,7 @@ def date_minus_candles(timeframe: str, candle_count: int, date: datetime | None
"""
if not date:
date = datetime.now(timezone.utc)
date = datetime.now(UTC)
tf_min = timeframe_to_minutes(timeframe)
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
import ccxt
from ccxt import ROUND_DOWN, ROUND_UP
@@ -59,7 +59,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime | None = None) -> date
:returns: date of previous candle (with utc timezone)
"""
if not date:
date = datetime.now(timezone.utc)
date = datetime.now(UTC)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000
return dt_from_ts(new_timestamp)
@@ -73,6 +73,6 @@ def timeframe_to_next_date(timeframe: str, date: datetime | None = None) -> date
:returns: date of next candle (with utc timezone)
"""
if not date:
date = datetime.now(timezone.utc)
date = datetime.now(UTC)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000
return dt_from_ts(new_timestamp)

View File

@@ -55,10 +55,10 @@ class Gate(Exchange):
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
(TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED)
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
@retrier

View File

@@ -28,6 +28,7 @@ class Hyperliquid(Exchange):
"stoploss_on_exchange": False,
"exchange_has_overrides": {"fetchTrades": False},
"marketOrderRequiresPrice": True,
"ws_enabled": True,
}
_ft_has_futures: FtHas = {
"stoploss_on_exchange": True,
@@ -40,7 +41,8 @@ class Hyperliquid(Exchange):
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
(TradingMode.FUTURES, MarginMode.ISOLATED)
(TradingMode.SPOT, MarginMode.NONE),
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
@property

View File

@@ -35,7 +35,7 @@ class Kraken(Exchange):
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
(TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS)
]

View File

@@ -0,0 +1,24 @@
import logging
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
logger = logging.getLogger(__name__)
class Luno(Exchange):
"""
Luno 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_has_history": False, # Only provides the last 1000 candles
"always_require_api_keys": True, # Requires API keys to fetch candles
"trades_has_history": False, # Only the last 24h are available
}

View File

@@ -0,0 +1,27 @@
import logging
# from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange_types import FtHas
logger = logging.getLogger(__name__)
class Modetrade(Exchange):
"""
MOdetrade 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 = {
"always_require_api_keys": True, # Requires API keys to fetch candles
}
# _supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# (TradingMode.FUTURES, MarginMode.ISOLATED),
# ]

View File

@@ -49,7 +49,7 @@ class Okx(Exchange):
}
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
(TradingMode.SPOT, MarginMode.NONE),
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),

View File

@@ -3,7 +3,7 @@ import importlib
import logging
from abc import abstractmethod
from collections.abc import Callable
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -239,7 +239,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
pair, refresh=False, side="exit", is_short=trade.is_short
)
now = datetime.now(timezone.utc).timestamp()
now = datetime.now(UTC).timestamp()
trade_duration = int((now - trade.open_date_utc.timestamp()) / self.base_tf_seconds)
current_profit = trade.calc_profit_ratio(current_rate)
if trade.is_short:

View File

@@ -5,7 +5,7 @@ import re
import shutil
import threading
import warnings
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any, TypedDict
@@ -116,7 +116,7 @@ class FreqaiDataDrawer:
if metric not in self.metric_tracker[pair]:
self.metric_tracker[pair][metric] = {"timestamp": [], "value": []}
timestamp = int(datetime.now(timezone.utc).timestamp())
timestamp = int(datetime.now(UTC).timestamp())
self.metric_tracker[pair][metric]["value"].append(value)
self.metric_tracker[pair][metric]["timestamp"].append(timestamp)

View File

@@ -3,7 +3,7 @@ import inspect
import logging
import random
import shutil
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -341,7 +341,7 @@ class FreqaiDataKitchen:
full_timerange = TimeRange.parse_timerange(tr)
config_timerange = TimeRange.parse_timerange(self.config["timerange"])
if config_timerange.stopts == 0:
config_timerange.stopts = int(datetime.now(tz=timezone.utc).timestamp())
config_timerange.stopts = int(datetime.now(tz=UTC).timestamp())
timerange_train = copy.deepcopy(full_timerange)
timerange_backtest = copy.deepcopy(full_timerange)
@@ -525,7 +525,7 @@ class FreqaiDataKitchen:
:return:
bool = If the model is expired or not.
"""
time = datetime.now(tz=timezone.utc).timestamp()
time = datetime.now(tz=UTC).timestamp()
elapsed_time = (time - trained_timestamp) / 3600 # hours
max_time = self.freqai_config.get("expiration_hours", 0)
if max_time > 0:
@@ -536,7 +536,7 @@ class FreqaiDataKitchen:
def check_if_new_training_required(
self, trained_timestamp: int
) -> tuple[bool, TimeRange, TimeRange]:
time = datetime.now(tz=timezone.utc).timestamp()
time = datetime.now(tz=UTC).timestamp()
trained_timerange = TimeRange()
data_load_timerange = TimeRange()

View File

@@ -3,7 +3,7 @@ import threading
import time
from abc import ABC, abstractmethod
from collections import deque
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Literal
@@ -76,7 +76,7 @@ class IFreqaiModel(ABC):
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config)
# set current candle to arbitrary historical date
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc)
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=UTC)
self.dd.current_candle = self.current_candle
self.scanning = False
self.ft_params = self.freqai_info["feature_parameters"]

View File

@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -64,7 +64,7 @@ def get_required_data_timerange(config: Config) -> TimeRange:
Used to compute the required data download time range
for auto data-download in FreqAI
"""
time = datetime.now(tz=timezone.utc).timestamp()
time = datetime.now(tz=UTC).timestamp()
timeframes = config["freqai"]["feature_parameters"].get("include_timeframes")

View File

@@ -5,7 +5,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
import logging
import traceback
from copy import deepcopy
from datetime import datetime, time, timedelta, timezone
from datetime import UTC, datetime, time, timedelta
from math import isclose
from threading import Lock
from time import sleep
@@ -93,14 +93,16 @@ class FreqtradeBot(LoggingMixin):
# Remove credentials from original exchange config to avoid accidental credential exposure
remove_exchange_credentials(config["exchange"], True)
self.exchange = ExchangeResolver.load_exchange(
self.config, exchange_config=exchange_config, load_leverage_tiers=True
)
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
# Check config consistency here since strategies can set certain options
validate_config_consistency(config)
self.exchange = ExchangeResolver.load_exchange(
self.config, exchange_config=exchange_config, load_leverage_tiers=True
)
# Re-validate exchange compatibility
self.exchange.validate_config(self.config)
init_db(self.config["db_url"])
@@ -266,7 +268,7 @@ class FreqtradeBot(LoggingMixin):
)
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
current_time=datetime.now(timezone.utc)
current_time=datetime.now(UTC)
)
with self._measure_execution:
@@ -296,7 +298,7 @@ class FreqtradeBot(LoggingMixin):
self._schedule.run_pending()
Trade.commit()
self.rpc.process_msg_queue(self.dataprovider._msg_queue)
self.last_process = datetime.now(timezone.utc)
self.last_process = datetime.now(UTC)
def process_stopped(self) -> None:
"""
@@ -421,7 +423,7 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException as e:
logger.warning(f"Error updating Order {order.order_id} due to {e}.")
if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
if order.order_date_utc - timedelta(days=5) < datetime.now(UTC):
logger.warning(
"Order is older than 5 days. Assuming order was fully cancelled."
)
@@ -755,7 +757,7 @@ class FreqtradeBot(LoggingMixin):
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
trade=trade,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
current_rate=current_entry_rate,
current_profit=current_entry_profit,
min_stake=min_entry_stake,
@@ -916,7 +918,7 @@ class FreqtradeBot(LoggingMixin):
amount=amount,
rate=enter_limit_requested,
time_in_force=time_in_force,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
entry_tag=enter_tag,
side=trade_side,
):
@@ -987,7 +989,7 @@ class FreqtradeBot(LoggingMixin):
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker="maker")
base_currency = self.exchange.get_pair_base_currency(pair)
open_date = datetime.now(timezone.utc)
open_date = datetime.now(UTC)
funding_fees = self.exchange.get_funding_fees(
pair=pair,
@@ -1106,7 +1108,7 @@ class FreqtradeBot(LoggingMixin):
)(
pair=pair,
trade=trade,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
proposed_rate=enter_limit_requested,
entry_tag=entry_tag,
side=trade_side,
@@ -1124,7 +1126,7 @@ class FreqtradeBot(LoggingMixin):
else:
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
@@ -1157,7 +1159,7 @@ class FreqtradeBot(LoggingMixin):
self.strategy.custom_stake_amount, default_retval=stake_amount
)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
current_rate=enter_limit_requested,
proposed_stake=stake_amount,
min_stake=min_stake_amount,
@@ -1214,6 +1216,7 @@ class FreqtradeBot(LoggingMixin):
"leverage": trade.leverage if trade.leverage else None,
"direction": "Short" if trade.is_short else "Long",
"limit": open_rate, # Deprecated (?)
"order_rate": open_rate,
"open_rate": open_rate,
"order_type": order_type or "unknown",
"stake_amount": stake_amount,
@@ -1222,7 +1225,7 @@ class FreqtradeBot(LoggingMixin):
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
"fiat_currency": self.config.get("fiat_display_currency", None),
"amount": order.safe_amount_after_fee if fill else (order.safe_amount or trade.amount),
"open_date": trade.open_date_utc or datetime.now(timezone.utc),
"open_date": trade.open_date_utc or datetime.now(UTC),
"current_rate": current_rate,
"sub_trade": sub_trade,
}
@@ -1250,6 +1253,7 @@ class FreqtradeBot(LoggingMixin):
"leverage": trade.leverage,
"direction": "Short" if trade.is_short else "Long",
"limit": trade.open_rate,
"order_rate": trade.open_rate,
"order_type": order_type,
"stake_amount": trade.stake_amount,
"open_rate": trade.open_rate,
@@ -1361,7 +1365,7 @@ class FreqtradeBot(LoggingMixin):
exits: list[ExitCheckTuple] = self.strategy.should_exit(
trade,
exit_rate,
datetime.now(timezone.utc),
datetime.now(UTC),
enter=enter,
exit_=exit_,
force_stoploss=0,
@@ -1479,44 +1483,6 @@ class FreqtradeBot(LoggingMixin):
return False
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: CcxtOrder) -> None:
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
:param trade: Corresponding Trade
:param order: Current on exchange stoploss order
:return: None
"""
stoploss_norm = self.exchange.price_to_precision(
trade.pair,
trade.stoploss_or_liquidation,
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP,
)
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
# we check if the update is necessary
update_beat = self.strategy.order_types.get("stoploss_on_exchange_interval", 60)
upd_req = datetime.now(timezone.utc) - timedelta(seconds=update_beat)
if trade.stoploss_last_update_utc and upd_req >= trade.stoploss_last_update_utc:
# cancelling the current stoploss on exchange first
logger.info(
f"Cancelling current stoploss on exchange for pair {trade.pair} "
f"(orderid:{order['id']}) in order to add another one ..."
)
self.cancel_stoploss_on_exchange(trade)
if not trade.is_open:
logger.warning(
f"Trade {trade} is closed, not creating trailing stoploss order."
)
return
# Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
logger.warning(
f"Could not create trailing stoploss order for pair {trade.pair}."
)
def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: list[CcxtOrder]):
"""
Perform required actions according to existing stoploss orders of trade
@@ -1558,6 +1524,44 @@ class FreqtradeBot(LoggingMixin):
return
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: CcxtOrder) -> None:
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
:param trade: Corresponding Trade
:param order: Current on exchange stoploss order
:return: None
"""
stoploss_norm = self.exchange.price_to_precision(
trade.pair,
trade.stoploss_or_liquidation,
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP,
)
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
# we check if the update is necessary
update_beat = self.strategy.order_types.get("stoploss_on_exchange_interval", 60)
upd_req = datetime.now(UTC) - timedelta(seconds=update_beat)
if trade.stoploss_last_update_utc and upd_req >= trade.stoploss_last_update_utc:
# cancelling the current stoploss on exchange first
logger.info(
f"Cancelling current stoploss on exchange for pair {trade.pair} "
f"(orderid:{order['id']}) in order to add another one ..."
)
self.cancel_stoploss_on_exchange(trade)
if not trade.is_open:
logger.warning(
f"Trade {trade} is closed, not creating trailing stoploss order."
)
return
# Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
logger.warning(
f"Could not create trailing stoploss order for pair {trade.pair}."
)
def manage_open_orders(self) -> None:
"""
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
@@ -1583,9 +1587,7 @@ class FreqtradeBot(LoggingMixin):
if not_closed:
if fully_cancelled or (
open_order
and self.strategy.ft_check_timed_out(
trade, open_order, datetime.now(timezone.utc)
)
and self.strategy.ft_check_timed_out(trade, open_order, datetime.now(UTC))
):
self.handle_cancel_order(
order, open_order, trade, constants.CANCEL_REASON["TIMEOUT"]
@@ -1683,7 +1685,7 @@ class FreqtradeBot(LoggingMixin):
trade=trade,
order=order_obj,
pair=trade.pair,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
proposed_rate=proposed_rate,
current_order_rate=order_obj.safe_placement_price,
entry_tag=trade.enter_tag,
@@ -2075,7 +2077,7 @@ class FreqtradeBot(LoggingMixin):
)(
pair=trade.pair,
trade=trade,
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
proposed_rate=proposed_limit_rate,
current_profit=current_profit,
exit_tag=exit_reason,
@@ -2106,7 +2108,7 @@ class FreqtradeBot(LoggingMixin):
time_in_force=time_in_force,
exit_reason=exit_reason,
sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc),
current_time=datetime.now(UTC),
)
):
logger.info(f"User denied exit for {trade.pair}.")
@@ -2202,7 +2204,7 @@ class FreqtradeBot(LoggingMixin):
"enter_tag": trade.enter_tag,
"exit_reason": trade.exit_reason,
"open_date": trade.open_date_utc,
"close_date": trade.close_date_utc or datetime.now(timezone.utc),
"close_date": trade.close_date_utc or datetime.now(UTC),
"stake_amount": trade.stake_amount,
"stake_currency": self.config["stake_currency"],
"base_currency": self.exchange.get_pair_base_currency(trade.pair),
@@ -2247,6 +2249,7 @@ class FreqtradeBot(LoggingMixin):
"direction": "Short" if trade.is_short else "Long",
"gain": gain,
"limit": profit_rate or 0,
"order_rate": profit_rate or 0,
"order_type": order_type,
"amount": order.safe_amount_after_fee,
"open_rate": trade.open_rate,
@@ -2257,7 +2260,7 @@ class FreqtradeBot(LoggingMixin):
"enter_tag": trade.enter_tag,
"exit_reason": trade.exit_reason,
"open_date": trade.open_date,
"close_date": trade.close_date or datetime.now(timezone.utc),
"close_date": trade.close_date or datetime.now(UTC),
"stake_currency": self.config["stake_currency"],
"base_currency": self.exchange.get_pair_base_currency(trade.pair),
"quote_currency": self.exchange.get_pair_quote_currency(trade.pair),
@@ -2337,8 +2340,8 @@ class FreqtradeBot(LoggingMixin):
def _update_trade_after_fill(self, trade: Trade, order: Order, send_msg: bool) -> Trade:
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
strategy_safe_wrapper(self.strategy.order_filled, default_retval=None)(
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc)
strategy_safe_wrapper(self.strategy.order_filled, supress_error=True)(
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(UTC)
)
# If a entry order was closed, force update on stoploss on exchange
if order.ft_order_side == trade.entry_side:
@@ -2365,14 +2368,14 @@ class FreqtradeBot(LoggingMixin):
stake_currency=self.config["stake_currency"],
dry_run=self.config["dry_run"],
)
if self.strategy.use_custom_stoploss:
current_rate = self.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True
)
profit = trade.calc_profit_ratio(current_rate)
self.strategy.ft_stoploss_adjust(
current_rate, trade, datetime.now(timezone.utc), profit, 0, after_fill=True
)
if self.strategy.use_custom_stoploss and trade.is_open:
current_rate = self.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=True
)
profit = trade.calc_profit_ratio(current_rate)
self.strategy.ft_stoploss_adjust(
current_rate, trade, datetime.now(UTC), profit, 0, after_fill=True
)
# Updating wallets when order is closed
self.wallets.update()
return trade
@@ -2397,7 +2400,7 @@ class FreqtradeBot(LoggingMixin):
def handle_protections(self, pair: str, side: LongShort) -> None:
# Lock pair for one candle to prevent immediate re-entries
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason="Auto lock", side=side)
self.strategy.lock_pair(pair, datetime.now(UTC), reason="Auto lock", side=side)
prot_trig = self.protections.stop_per_pair(pair, side=side)
if prot_trig:
msg: RPCProtectionMsg = {

View File

@@ -8,4 +8,4 @@ from freqtrade.ft_types.backtest_result_type import (
get_BacktestResultType_default,
)
from freqtrade.ft_types.plot_annotation_type import AnnotationType
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType
from freqtrade.ft_types.valid_exchanges_type import TradeModeType, ValidExchangesType

View File

@@ -1,8 +1,8 @@
from datetime import datetime
from typing import Literal
from typing import Literal, Required
from pydantic import TypeAdapter
from typing_extensions import Required, TypedDict
from typing_extensions import TypedDict
class AnnotationType(TypedDict, total=False):

View File

@@ -58,7 +58,7 @@ def setup_logging_pre() -> None:
FT_LOGGING_CONFIG = {
"version": 1,
# "incremental": True,
# "disable_existing_loggers": False,
"disable_existing_loggers": False,
"formatters": {
"basic": {"format": "%(message)s"},
"standard": {
@@ -223,7 +223,7 @@ def setup_logging(config: Config) -> None:
logger.info("Enabling colorized output.")
error_console._color_system = error_console._detect_color_system()
logging.info("Logfile configured")
logger.info("Logfile configured")
# Set verbosity levels
logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)

View File

@@ -10,8 +10,8 @@ from typing import Any
# check min. python version
if sys.version_info < (3, 10): # pragma: no cover # noqa: UP036
sys.exit("Freqtrade requires Python version >= 3.10")
if sys.version_info < (3, 11): # pragma: no cover # noqa: UP036
sys.exit("Freqtrade requires Python version >= 3.11")
from freqtrade import __version__
from freqtrade.commands import Arguments

View File

@@ -125,6 +125,7 @@ class LookaheadAnalysis(BaseAnalysis):
backtesting = Backtesting(prepare_data_config, self.exchange)
self.exchange = backtesting.exchange
self.local_config["candle_type_def"] = prepare_data_config["candle_type_def"]
self._fee = backtesting.fee
backtesting._set_strategy(backtesting.strategylist[0])

View File

@@ -743,7 +743,7 @@ class Backtesting:
if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_date, trade)
self._run_funding_fees(trade, current_date, force=True)
strategy_safe_wrapper(self.strategy.order_filled, default_retval=None)(
strategy_safe_wrapper(self.strategy.order_filled, supress_error=True)(
pair=trade.pair,
trade=trade, # type: ignore[arg-type]
order=order,

View File

@@ -1,6 +1,6 @@
import logging
from copy import deepcopy
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from pandas import DataFrame
@@ -38,7 +38,7 @@ class BaseAnalysis:
@staticmethod
def dt_to_timestamp(dt: datetime):
timestamp = int(dt.replace(tzinfo=timezone.utc).timestamp())
timestamp = int(dt.replace(tzinfo=UTC).timestamp())
return timestamp
def fill_full_varholder(self):
@@ -48,12 +48,12 @@ class BaseAnalysis:
parsed_timerange = TimeRange.parse_timerange(self.local_config["timerange"])
if parsed_timerange.startdt is None:
self.full_varHolder.from_dt = datetime.fromtimestamp(0, tz=timezone.utc)
self.full_varHolder.from_dt = datetime.fromtimestamp(0, tz=UTC)
else:
self.full_varHolder.from_dt = parsed_timerange.startdt
if parsed_timerange.stopdt is None:
self.full_varHolder.to_dt = datetime.now(timezone.utc)
self.full_varHolder.to_dt = datetime.now(UTC)
else:
self.full_varHolder.to_dt = parsed_timerange.stopdt

View File

@@ -6,7 +6,7 @@ and will be sent to the hyperopt worker processes.
import logging
import sys
import warnings
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -273,7 +273,7 @@ class HyperOptimizer:
Keep this function as optimized as possible!
"""
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
backtest_start_time = datetime.now(timezone.utc)
backtest_start_time = datetime.now(UTC)
# Apply parameters
if HyperoptTools.has_space(self.config, "buy"):
@@ -330,7 +330,7 @@ class HyperOptimizer:
bt_results = self.backtesting.backtest(
processed=processed, start_date=self.min_date, end_date=self.max_date
)
backtest_end_time = datetime.now(timezone.utc)
backtest_end_time = datetime.now(UTC)
bt_results.update(
{
"backtest_start_time": int(backtest_start_time.timestamp()),

View File

@@ -1,7 +1,7 @@
import logging
from collections.abc import Iterator
from copy import deepcopy
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -71,7 +71,7 @@ class HyperoptTools:
"strategy_name": strategy_name,
"params": final_params,
"ft_stratparam_v": 1,
"export_time": datetime.now(timezone.utc),
"export_time": datetime.now(UTC),
}
logger.info(f"Dumping parameters to {filename}")
with filename.open("w") as f:

View File

@@ -332,8 +332,11 @@ def text_table_add_metrics(strat_results: dict) -> None:
),
),
(
"Avg. daily profit %",
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}",
"Avg. daily profit",
fmt_coin(
(strat_results["profit_total_abs"] / strat_results["backtest_days"]),
strat_results["stake_currency"],
),
),
(
"Avg. stake amount",

View File

@@ -1,6 +1,6 @@
import logging
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any, Literal
import numpy as np
@@ -83,7 +83,6 @@ def _generate_result_line(
"""
Generate one result dict, with "first_column" as key.
"""
profit_sum = result["profit_ratio"].sum()
# (end-capital - starting capital) / starting capital
profit_total = result["profit_abs"].sum() / starting_balance
backtest_days = (max_date - min_date).days or 1
@@ -108,8 +107,6 @@ def _generate_result_line(
"profit_mean_pct": (
round(result["profit_ratio"].mean() * 100.0, 2) if len(result) > 0 else 0.0
),
"profit_sum": profit_sum,
"profit_sum_pct": round(profit_sum * 100.0, 2),
"profit_total_abs": result["profit_abs"].sum(),
"profit_total": profit_total,
"profit_total_pct": round(profit_total * 100.0, 2),
@@ -518,14 +515,16 @@ def generate_strategy_stats(
best_pair = (
max(
[pair for pair in pair_results if pair["key"] != "TOTAL"], key=lambda x: x["profit_sum"]
[pair for pair in pair_results if pair["key"] != "TOTAL"],
key=lambda x: x["profit_total_abs"],
)
if len(pair_results) > 1
else None
)
worst_pair = (
min(
[pair for pair in pair_results if pair["key"] != "TOTAL"], key=lambda x: x["profit_sum"]
[pair for pair in pair_results if pair["key"] != "TOTAL"],
key=lambda x: x["profit_total_abs"],
)
if len(pair_results) > 1
else None
@@ -652,9 +651,9 @@ def generate_strategy_stats(
"max_drawdown_abs": 0.0,
"max_drawdown_low": 0.0,
"max_drawdown_high": 0.0,
"drawdown_start": datetime(1970, 1, 1, tzinfo=timezone.utc),
"drawdown_start": datetime(1970, 1, 1, tzinfo=UTC),
"drawdown_start_ts": 0,
"drawdown_end": datetime(1970, 1, 1, tzinfo=timezone.utc),
"drawdown_end": datetime(1970, 1, 1, tzinfo=UTC),
"drawdown_end_ts": 0,
"csum_min": 0,
"csum_max": 0,

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from enum import Enum
from typing import ClassVar, Literal
@@ -114,7 +114,7 @@ class KeyValueStore:
if kv.value_type == ValueTypesEnum.STRING:
return kv.string_value
if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None:
return kv.datetime_value.replace(tzinfo=timezone.utc)
return kv.datetime_value.replace(tzinfo=UTC)
if kv.value_type == ValueTypesEnum.FLOAT:
return kv.float_value
if kv.value_type == ValueTypesEnum.INT:
@@ -156,7 +156,7 @@ class KeyValueStore:
)
if kv is None or kv.datetime_value is None:
return None
return kv.datetime_value.replace(tzinfo=timezone.utc)
return kv.datetime_value.replace(tzinfo=UTC)
@staticmethod
def get_float_value(key: KeyStoreKeys) -> float | None:
@@ -207,5 +207,5 @@ def set_startup_time() -> None:
if t is not None:
KeyValueStore.store_value("bot_start_time", t.open_date_utc)
else:
KeyValueStore.store_value("bot_start_time", datetime.now(timezone.utc))
KeyValueStore.store_value("startup_time", datetime.now(timezone.utc))
KeyValueStore.store_value("bot_start_time", datetime.now(UTC))
KeyValueStore.store_value("startup_time", datetime.now(UTC))

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any, ClassVar
from sqlalchemy import ScalarResult, String, or_, select
@@ -69,11 +69,9 @@ class PairLock(ModelBase):
"id": self.id,
"pair": self.pair,
"lock_time": self.lock_time.strftime(DATETIME_PRINT_FORMAT),
"lock_timestamp": int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),
"lock_timestamp": int(self.lock_time.replace(tzinfo=UTC).timestamp() * 1000),
"lock_end_time": self.lock_end_time.strftime(DATETIME_PRINT_FORMAT),
"lock_end_timestamp": int(
self.lock_end_time.replace(tzinfo=timezone.utc).timestamp() * 1000
),
"lock_end_timestamp": int(self.lock_end_time.replace(tzinfo=UTC).timestamp() * 1000),
"reason": self.reason,
"side": self.side,
"active": self.active,

View File

@@ -1,6 +1,6 @@
import logging
from collections.abc import Sequence
from datetime import datetime, timezone
from datetime import UTC, datetime
from sqlalchemy import select
@@ -52,7 +52,7 @@ class PairLocks:
"""
lock = PairLock(
pair=pair,
lock_time=now or datetime.now(timezone.utc),
lock_time=now or datetime.now(UTC),
lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until),
reason=reason,
side=side,
@@ -77,7 +77,7 @@ class PairLocks:
:param side: Side get locks for, can be 'long', 'short', '*' or None
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
if PairLocks.use_db:
return PairLock.query_pair_locks(pair, now, side).all()
@@ -114,7 +114,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
logger.info(f"Releasing all locks for {pair}.")
locks = PairLocks.get_pair_locks(pair, now, side=side)
@@ -132,7 +132,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
if PairLocks.use_db:
# used in live modes
@@ -161,7 +161,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
return len(PairLocks.get_pair_locks("*", now, side)) > 0
@@ -173,7 +173,7 @@ class PairLocks:
defaults to datetime.now(timezone.utc)
"""
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
return len(PairLocks.get_pair_locks(pair, now, side)) > 0 or PairLocks.is_global_lock(
now, side

View File

@@ -6,9 +6,9 @@ import logging
from collections import defaultdict
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime, timezone
from datetime import UTC, datetime
from math import isclose
from typing import Any, ClassVar, Optional, cast
from typing import Any, ClassVar, Optional, Self, cast
from sqlalchemy import (
Enum,
@@ -25,7 +25,6 @@ from sqlalchemy import (
select,
)
from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates
from typing_extensions import Self
from freqtrade.constants import (
CANCELED_EXCHANGE_STATES,
@@ -121,14 +120,12 @@ class Order(ModelBase):
@property
def order_date_utc(self) -> datetime:
"""Order-date with UTC timezoneinfo"""
return self.order_date.replace(tzinfo=timezone.utc)
return self.order_date.replace(tzinfo=UTC)
@property
def order_filled_utc(self) -> datetime | None:
"""last order-date with UTC timezoneinfo"""
return (
self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
)
return self.order_filled_date.replace(tzinfo=UTC) if self.order_filled_date else None
@property
def safe_amount(self) -> float:
@@ -229,7 +226,7 @@ class Order(ModelBase):
self.order_filled_date = dt_from_ts(
safe_value_fallback(order, "lastTradeTimestamp", default_value=dt_ts())
)
self.order_update_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(UTC)
def to_ccxt_object(self, stopPriceName: str = "stopPrice") -> dict[str, Any]:
order: dict[str, Any] = {
@@ -286,7 +283,7 @@ class Order(ModelBase):
self.order_date.strftime(DATETIME_PRINT_FORMAT) if self.order_date else None
),
"order_timestamp": (
int(self.order_date.replace(tzinfo=timezone.utc).timestamp() * 1000)
int(self.order_date.replace(tzinfo=UTC).timestamp() * 1000)
if self.order_date
else None
),
@@ -533,7 +530,7 @@ class LocalTrade:
@property
def open_date_utc(self):
return self.open_date.replace(tzinfo=timezone.utc)
return self.open_date.replace(tzinfo=UTC)
@property
def stoploss_last_update_utc(self):
@@ -543,7 +540,7 @@ class LocalTrade:
@property
def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc) if self.close_date else None
return self.close_date.replace(tzinfo=UTC) if self.close_date else None
@property
def entry_side(self) -> str:
@@ -1056,7 +1053,7 @@ class LocalTrade:
return zero
open_date = self.open_date.replace(tzinfo=None)
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
now = (self.close_date or datetime.now(UTC)).replace(tzinfo=None)
sec_per_hour = FtPrecise(3600)
total_seconds = FtPrecise((now - open_date).total_seconds())
hours = total_seconds / sec_per_hour or zero
@@ -1572,12 +1569,12 @@ class LocalTrade:
fee_close=data["fee_close"],
fee_close_cost=data.get("fee_close_cost"),
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=UTC),
open_rate=data["open_rate"],
open_rate_requested=data.get("open_rate_requested", data["open_rate"]),
open_trade_value=data.get("open_trade_value"),
close_date=(
datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc)
datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=UTC)
if data["close_timestamp"]
else None
),
@@ -1622,7 +1619,7 @@ class LocalTrade:
if order.get("order_date")
else None,
order_filled_date=(
datetime.fromtimestamp(order["order_filled_timestamp"] // 1000, tz=timezone.utc)
datetime.fromtimestamp(order["order_filled_timestamp"] // 1000, tz=UTC)
if order["order_filled_timestamp"]
else None
),
@@ -2093,32 +2090,34 @@ class Trade(ModelBase, LocalTrade):
return resp
@staticmethod
def get_best_pair(start_date: datetime | None = None):
def get_best_pair(trade_filter: list | None = None):
"""
Get best pair with closed trade.
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
filters: list = [Trade.is_open.is_(False)]
if start_date:
filters.append(Trade.close_date >= start_date)
if not trade_filter:
trade_filter = []
trade_filter.append(Trade.is_open.is_(False))
pair_rates_query = Trade._generic_performance_query([Trade.pair], filters)
pair_rates_query = Trade._generic_performance_query([Trade.pair], trade_filter)
best_pair = Trade.session.execute(pair_rates_query).first()
# returns pair, profit_ratio, abs_profit, count
return best_pair
@staticmethod
def get_trading_volume(start_date: datetime | None = None) -> float:
def get_trading_volume(trade_filter: list | None = None) -> float:
"""
Get Trade volume based on Orders
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
filters = [Order.status == "closed"]
if start_date:
filters.append(Order.order_filled_date >= start_date)
if not trade_filter:
trade_filter = []
trade_filter.append(Order.status == "closed")
trading_volume = Trade.session.execute(
select(func.sum(Order.cost).label("volume")).filter(*filters)
select(func.sum(Order.cost).label("volume"))
.join(Order._trade_live)
.filter(*trade_filter)
).scalar_one()
return trading_volume or 0.0

View File

@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from pathlib import Path
import pandas as pd
@@ -638,7 +638,7 @@ def load_and_plot_trades(config: Config):
exchange = ExchangeResolver.load_exchange(config)
IStrategy.dp = DataProvider(config, exchange)
strategy.ft_bot_start()
strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(timezone.utc))
strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(UTC))
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements["timerange"]
trades = plot_elements["trades"]

View File

@@ -3,7 +3,7 @@ Protection manager class
"""
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from freqtrade.constants import Config, LongShort
@@ -49,7 +49,7 @@ class ProtectionManager:
def global_stop(self, now: datetime | None = None, side: LongShort = "long") -> PairLock | None:
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
result = None
for protection_handler in self._protection_handlers:
if protection_handler.has_global_stop:
@@ -65,7 +65,7 @@ class ProtectionManager:
self, pair, now: datetime | None = None, side: LongShort = "long"
) -> PairLock | None:
if not now:
now = datetime.now(timezone.utc)
now = datetime.now(UTC)
result = None
for protection_handler in self._protection_handlers:
if protection_handler.has_local_stop:

View File

@@ -1,7 +1,7 @@
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any
from freqtrade.constants import Config, LongShort
@@ -127,7 +127,7 @@ class IProtection(LoggingMixin, ABC):
max_date: datetime = max([trade.close_date for trade in trades if trade.close_date])
# coming from Database, tzinfo is not set.
if max_date.tzinfo is None:
max_date = max_date.replace(tzinfo=timezone.utc)
max_date = max_date.replace(tzinfo=UTC)
if self._unlock_at is not None:
# unlock_at case with fixed hour of the day

View File

@@ -54,7 +54,7 @@ class StrategyResolver(IResolver):
strategy.ft_load_params_from_file()
# Set attributes
# Check if we need to override configuration
# (Attribute name, default, subkey)
# (Attribute name, default, subkey)
attributes = [
("minimal_roi", {"0": 10.0}),
("timeframe", None),

View File

@@ -1,6 +1,6 @@
import logging
import secrets
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from typing import Any
import jwt
@@ -89,15 +89,15 @@ async def validate_ws_token(
def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: # noqa: S107
to_encode = data.copy()
if token_type == "access": # noqa: S105
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
expire = datetime.now(UTC) + timedelta(minutes=15)
elif token_type == "refresh": # noqa: S105
expire = datetime.now(timezone.utc) + timedelta(days=30)
expire = datetime.now(UTC) + timedelta(days=30)
else:
raise ValueError()
to_encode.update(
{
"exp": expire,
"iat": datetime.now(timezone.utc),
"iat": datetime.now(UTC),
"type": token_type,
}
)

View File

@@ -163,11 +163,22 @@ class Profit(BaseModel):
max_drawdown_start_timestamp: int
max_drawdown_end: str
max_drawdown_end_timestamp: int
current_drawdown: float
current_drawdown_abs: float
current_drawdown_high: float
current_drawdown_start: str
current_drawdown_start_timestamp: int
trading_volume: float | None = None
bot_start_timestamp: int
bot_start_date: str
class ProfitAll(BaseModel):
all: Profit
long: Profit | None = None
short: Profit | None = None
class SellReason(BaseModel):
wins: int
losses: int

View File

@@ -43,6 +43,7 @@ from freqtrade.rpc.api_server.api_schemas import (
Ping,
PlotConfig,
Profit,
ProfitAll,
ResultMsg,
ShowConfig,
Stats,
@@ -89,7 +90,8 @@ logger = logging.getLogger(__name__)
# 2.40: Add hyperopt-loss endpoint
# 2.41: Add download-data endpoint
# 2.42: Add /pair_history endpoint with live data
API_VERSION = 2.42
# 2.43: Add /profit_all endpoint
API_VERSION = 2.43
# Public API, requires no auth.
router_public = APIRouter()
@@ -148,6 +150,24 @@ def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
return rpc._rpc_trade_statistics(config["stake_currency"], config.get("fiat_display_currency"))
@router.get("/profit_all", response_model=ProfitAll, tags=["info"])
def profit_all(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
response = {
"all": rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency")
),
}
if config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
response["long"] = rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency"), direction="long"
)
response["short"] = rpc._rpc_trade_statistics(
config["stake_currency"], config.get("fiat_display_currency"), direction="short"
)
return response
@router.get("/stats", response_model=Stats, tags=["info"])
def stats(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stats()

View File

@@ -1,7 +1,7 @@
from typing import Any, Literal
from typing import Any, Literal, NotRequired
from uuid import uuid4
from typing_extensions import NotRequired, TypedDict
from typing_extensions import TypedDict
from freqtrade.exchange.exchange import Exchange

View File

@@ -102,7 +102,7 @@ class WebSocketChannel:
self._send_times.append(total_time)
self._calc_send_limit()
except asyncio.TimeoutError:
except TimeoutError:
logger.info(f"Connection for {self} timed out, disconnecting")
raise
@@ -201,8 +201,8 @@ class WebSocketChannel:
try:
await task
except (
TimeoutError,
asyncio.CancelledError,
asyncio.TimeoutError,
WebSocketDisconnect,
ConnectionClosed,
RuntimeError,

View File

@@ -266,7 +266,7 @@ class ExternalMessageConsumer:
except Exception as e:
logger.exception(f"Error handling producer message: {e}")
except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosed):
except (TimeoutError, websockets.exceptions.ConnectionClosed):
# We haven't received data yet. Check the connection and continue.
try:
# ping

View File

@@ -5,7 +5,7 @@ This module contains class to define a RPC communications
import logging
from abc import abstractmethod
from collections.abc import Generator, Sequence
from datetime import date, datetime, timedelta, timezone
from datetime import UTC, date, datetime, timedelta
from typing import TYPE_CHECKING, Any
import psutil
@@ -34,7 +34,7 @@ from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msec
from freqtrade.exchange.exchange_utils import price_to_precision
from freqtrade.ft_types import AnnotationType
from freqtrade.loggers import bufferHandler
from freqtrade.persistence import CustomDataWrapper, KeyValueStore, PairLocks, Trade
from freqtrade.persistence import CustomDataWrapper, KeyValueStore, Order, PairLocks, Trade
from freqtrade.persistence.models import PairLock, custom_data_rpc_wrapper
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@@ -375,7 +375,7 @@ class RPC:
"""
:param timeunit: Valid entries are 'days', 'weeks', 'months'
"""
start_date = datetime.now(timezone.utc).date()
start_date = datetime.now(UTC).date()
if timeunit == "weeks":
# weekly
start_date = start_date - timedelta(days=start_date.weekday()) # Monday
@@ -502,20 +502,13 @@ class RPC:
durations = {"wins": wins_dur, "draws": draws_dur, "losses": losses_dur}
return {"exit_reasons": exit_reasons, "durations": durations}
def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str, start_date: datetime | None = None
def _collect_trade_statistics_data(
self,
trades: Sequence["Trade"],
stake_currency: str,
fiat_display_currency: str,
) -> dict[str, Any]:
"""Returns cumulative profit statistics"""
start_date = datetime.fromtimestamp(0) if start_date is None else start_date
trade_filter = (
Trade.is_open.is_(False) & (Trade.close_date >= start_date)
) | Trade.is_open.is_(True)
trades: Sequence[Trade] = Trade.session.scalars(
Trade.get_trades_query(trade_filter, include_orders=False).order_by(Trade.id)
).all()
"""Iterate trades, calculate various statistics, and return intermediate results."""
profit_all_coin = []
profit_all_ratio = []
profit_closed_coin = []
@@ -544,7 +537,7 @@ class RPC:
losing_trades += 1
losing_profit += profit_abs
else:
# Get current rate
# Get current rate for open trades
if len(trade.select_filled_orders(trade.entry_side)) == 0:
# Skip trades with no filled orders
continue
@@ -558,17 +551,74 @@ class RPC:
profit_abs = nan
else:
_profit = trade.calculate_profit(trade.close_rate or current_rate)
profit_ratio = _profit.profit_ratio
profit_abs = _profit.total_profit
profit_all_coin.append(profit_abs)
profit_all_ratio.append(profit_ratio)
return {
"profit_all_coin": profit_all_coin,
"profit_all_ratio": profit_all_ratio,
"profit_closed_coin": profit_closed_coin,
"profit_closed_ratio": profit_closed_ratio,
"durations": durations,
"winning_trades": winning_trades,
"losing_trades": losing_trades,
"winning_profit": winning_profit,
"losing_profit": losing_profit,
}
def _rpc_trade_statistics(
self,
stake_currency: str,
fiat_display_currency: str,
start_date: datetime | None = None,
direction: str | None = None,
) -> dict[str, Any]:
"""
Returns cumulative profit statistics, with optional direction filter (long/short)
"""
start_date = datetime.fromtimestamp(0) if start_date is None else start_date
trade_filter = (
Trade.is_open.is_(False) & (Trade.close_date >= start_date)
) | Trade.is_open.is_(True)
if direction == "long":
dir_filter = Trade.is_short.is_(False)
trade_filter = trade_filter & dir_filter
elif direction == "short":
dir_filter = Trade.is_short.is_(True)
trade_filter = trade_filter & dir_filter
trades: Sequence[Trade] = Trade.session.scalars(
Trade.get_trades_query(trade_filter, include_orders=False).order_by(Trade.id)
).all()
stats = self._collect_trade_statistics_data(trades, stake_currency, fiat_display_currency)
profit_all_coin = stats["profit_all_coin"]
profit_all_ratio = stats["profit_all_ratio"]
profit_closed_coin = stats["profit_closed_coin"]
profit_closed_ratio = stats["profit_closed_ratio"]
durations = stats["durations"]
winning_trades = stats["winning_trades"]
losing_trades = stats["losing_trades"]
winning_profit = stats["winning_profit"]
losing_profit = stats["losing_profit"]
closed_trade_count = len([t for t in trades if not t.is_open])
best_pair = Trade.get_best_pair(start_date)
trading_volume = Trade.get_trading_volume(start_date)
best_pair_filters = [Trade.close_date > start_date]
trading_volume_filters = [Order.order_filled_date >= start_date]
if direction:
best_pair_filters.append(dir_filter)
trading_volume_filters.append(dir_filter)
best_pair = Trade.get_best_pair(best_pair_filters)
trading_volume = Trade.get_trading_volume(trading_volume_filters)
# Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
@@ -681,6 +731,11 @@ class RPC:
"max_drawdown_end_timestamp": dt_ts_def(drawdown.low_date),
"drawdown_high": drawdown.high_value,
"drawdown_low": drawdown.low_value,
"current_drawdown": drawdown.current_relative_account_drawdown,
"current_drawdown_abs": drawdown.current_drawdown_abs,
"current_drawdown_high": drawdown.current_high_value,
"current_drawdown_start": format_date(drawdown.current_high_date),
"current_drawdown_start_timestamp": dt_ts_def(drawdown.current_high_date),
"trading_volume": trading_volume,
"bot_start_timestamp": dt_ts_def(bot_start, 0),
"bot_start_date": format_date(bot_start),
@@ -1094,7 +1149,7 @@ class RPC:
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
if not trade:
logger.warning("delete trade: Invalid argument received")
raise RPCException("invalid argument")
raise RPCException(f"Trade with id '{trade_id}' not found.")
# Try cancelling regular order if that exists
for open_order in trade.open_orders:
@@ -1115,13 +1170,16 @@ class RPC:
c_count += 1
except ExchangeError:
pass
trade_pair = trade.pair
trade.delete()
self._freqtrade.wallets.update()
return {
"result": "success",
"trade_id": trade_id,
"result_msg": f"Deleted trade {trade_id}. Closed {c_count} open orders.",
"result_msg": (
f"Deleted trade #{trade_id} for pair {trade_pair}. "
f"Closed {c_count} open orders."
),
"cancel_order_count": c_count,
}
@@ -1259,7 +1317,7 @@ class RPC:
for lock in locks:
lock.active = False
lock.lock_end_time = datetime.now(timezone.utc)
lock.lock_end_time = datetime.now(UTC)
Trade.commit()

View File

@@ -56,7 +56,8 @@ class __RPCEntryExitMsgBase(RPCSendMsgBase):
quote_currency: str
leverage: float | None
direction: str
limit: float
limit: float # Deprecated, use order_rate instead
order_rate: float
open_rate: float
order_type: str
stake_amount: float
@@ -87,7 +88,6 @@ class RPCExitMsg(__RPCEntryExitMsgBase):
exit_reason: str | None
close_date: datetime
# current_rate: float | None
order_rate: float | None
final_profit_ratio: float | None
is_final_exit: bool

View File

@@ -191,8 +191,8 @@ class Telegram(RPCHandler):
r"/mix_tags",
r"/daily$",
r"/daily \d+$",
r"/profit$",
r"/profit \d+",
r"/profit([_ ]long|[_ ]short)?$",
r"/profit([_ ]long|[_ ]short)? \d+$",
r"/stats$",
r"/count$",
r"/locks$",
@@ -305,13 +305,17 @@ class Telegram(RPCHandler):
CommandHandler("order", self._order),
CommandHandler("list_custom_data", self._list_custom_data),
CommandHandler("tg_info", self._tg_info),
CommandHandler("profit_long", self._profit_long),
CommandHandler("profit_short", self._profit_short),
]
callbacks = [
CallbackQueryHandler(self._status_table, pattern="update_status_table"),
CallbackQueryHandler(self._daily, pattern="update_daily"),
CallbackQueryHandler(self._weekly, pattern="update_weekly"),
CallbackQueryHandler(self._monthly, pattern="update_monthly"),
CallbackQueryHandler(self._profit, pattern="update_profit"),
CallbackQueryHandler(self._profit_long, pattern="update_profit_long"),
CallbackQueryHandler(self._profit_short, pattern="update_profit_short"),
CallbackQueryHandler(self._profit, pattern=r"update_profit$"),
CallbackQueryHandler(self._balance, pattern="update_balance"),
CallbackQueryHandler(self._performance, pattern="update_performance"),
CallbackQueryHandler(
@@ -995,29 +999,25 @@ class Telegram(RPCHandler):
"""
await self._timeunit_stats(update, context, "months")
@authorized_only
async def _profit(self, update: Update, context: CallbackContext) -> None:
def _format_profit_message(
self,
stats: dict,
stake_cur: str,
fiat_disp_cur: str,
timescale: int | None = None,
direction: str | None = None,
) -> str:
"""
Handler for /profit.
Returns a cumulative profit statistics.
:param bot: telegram bot
:param update: message update
:return: None
Format profit statistics message for telegram.
:param stats: Trade statistics dictionary
:param stake_cur: Stake currency
:param fiat_disp_cur: Fiat display currency
:param timescale: Optional timescale filter
:param direction: Optional direction filter ('long', 'short', or None for all)
:return: Formatted markdown message
"""
stake_cur = self._config["stake_currency"]
fiat_disp_cur = self._config.get("fiat_display_currency", "")
start_date = datetime.fromtimestamp(0)
timescale = None
try:
if context.args:
timescale = int(context.args[0]) - 1
today_start = datetime.combine(date.today(), datetime.min.time())
start_date = today_start - timedelta(days=timescale)
except (TypeError, ValueError, IndexError):
pass
stats = self._rpc._rpc_trade_statistics(stake_cur, fiat_disp_cur, start_date)
# Extract common variables
profit_closed_coin = stats["profit_closed_coin"]
profit_closed_ratio_mean = stats["profit_closed_ratio_mean"]
profit_closed_percent = stats["profit_closed_percent"]
@@ -1037,62 +1037,153 @@ class Telegram(RPCHandler):
expectancy = stats["expectancy"]
expectancy_ratio = stats["expectancy_ratio"]
# Direction-specific labels
direction_label = f" {direction}" if direction else ""
no_trades_msg = (
f"No{direction_label} trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
)
no_closed_msg = f"`No closed{direction_label} trade` \n"
closed_roi_label = f"*ROI:* Closed{direction_label} trades"
all_roi_label = f"*ROI:* All{direction_label} trades"
if stats["trade_count"] == 0:
markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
return no_trades_msg
# Build message
if stats["closed_trade_count"] > 0:
fiat_closed_trades = (
f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
)
markdown_msg = (
f"{closed_roi_label}\n"
f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} "
f"({profit_closed_ratio_mean:.2%}) "
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_closed_trades}"
)
else:
# Message to display
if stats["closed_trade_count"] > 0:
fiat_closed_trades = (
f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
)
markdown_msg = (
"*ROI:* Closed trades\n"
f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} "
f"({profit_closed_ratio_mean:.2%}) "
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_closed_trades}"
)
else:
markdown_msg = "`No closed trade` \n"
fiat_all_trades = (
f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
)
markdown_msg = no_closed_msg
fiat_all_trades = (
f"∙ `{fmt_coin(profit_all_fiat, fiat_disp_cur)}`\n" if fiat_disp_cur else ""
)
markdown_msg += (
f"{all_roi_label}\n"
f"∙ `{fmt_coin(profit_all_coin, stake_cur)} "
f"({profit_all_ratio_mean:.2%}) "
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_all_trades}"
f"*Total Trade Count:* `{trade_count}`\n"
f"*Bot started:* `{stats['bot_start_date']}`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}`\n"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\n"
f"*Winrate:* `{winrate:.2%}`\n"
f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`"
)
if stats["closed_trade_count"] > 0:
markdown_msg += (
f"*ROI:* All trades\n"
f"∙ `{fmt_coin(profit_all_coin, stake_cur)} "
f"({profit_all_ratio_mean:.2%}) "
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"{fiat_all_trades}"
f"*Total Trade Count:* `{trade_count}`\n"
f"*Bot started:* `{stats['bot_start_date']}`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}`\n"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\n"
f"*Winrate:* `{winrate:.2%}`\n"
f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`"
f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} "
f"({best_pair_profit_ratio:.2%})`\n"
f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['max_drawdown_start']} "
f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n"
f" to `{stats['max_drawdown_end']} "
f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n"
f"*Current Drawdown:* `{stats['current_drawdown']:.2%} "
f"({fmt_coin(stats['current_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['current_drawdown_start']} "
f"({fmt_coin(stats['current_drawdown_high'], stake_cur)})`\n"
)
if stats["closed_trade_count"] > 0:
markdown_msg += (
f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_pair_profit_abs} "
f"({best_pair_profit_ratio:.2%})`\n"
f"*Trading volume:* `{fmt_coin(stats['trading_volume'], stake_cur)}`\n"
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
f"({fmt_coin(stats['max_drawdown_abs'], stake_cur)})`\n"
f" from `{stats['max_drawdown_start']} "
f"({fmt_coin(stats['drawdown_high'], stake_cur)})`\n"
f" to `{stats['max_drawdown_end']} "
f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n"
)
return markdown_msg
async def _profit_handler(
self,
update: Update,
context: CallbackContext,
direction: str | None = None,
) -> None:
"""
Common handler for profit commands.
:param update: Telegram update
:param context: Callback context
:param direction: Trade direction filter ('long', 'short', or None)
:param callback_path: Callback path for message updates
"""
stake_cur = self._config["stake_currency"]
fiat_disp_cur = self._config.get("fiat_display_currency", "")
start_date = datetime.fromtimestamp(0)
timescale = None
try:
if context.args:
if not direction:
arg = context.args[0].lower()
if arg in ("short", "long"):
direction = arg
context.args.pop(0) # Remove direction from args
timescale = int(context.args[0]) - 1
today_start = datetime.combine(date.today(), datetime.min.time())
start_date = today_start - timedelta(days=timescale)
except (TypeError, ValueError, IndexError):
pass
# Get stats with optional direction filter
stats_kwargs = {
"stake_currency": stake_cur,
"fiat_display_currency": fiat_disp_cur,
"start_date": start_date,
}
if direction:
stats_kwargs["direction"] = direction
stats = self._rpc._rpc_trade_statistics(**stats_kwargs)
markdown_msg = self._format_profit_message(
stats, stake_cur, fiat_disp_cur, timescale, direction
)
await self._send_msg(
markdown_msg,
reload_able=True,
callback_path="update_profit",
callback_path="update_profit" if not direction else f"update_profit_{direction}",
query=update.callback_query,
)
@authorized_only
async def _profit(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
:param bot: telegram bot
:param update: message update
:return: None
"""
await self._profit_handler(update, context)
@authorized_only
async def _profit_long(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /profit_long.
Returns cumulative profit statistics for long trades.
"""
await self._profit_handler(update, context, direction="long")
@authorized_only
async def _profit_short(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /profit_short.
Returns cumulative profit statistics for short trades.
"""
await self._profit_handler(update, context, direction="short")
@authorized_only
async def _stats(self, update: Update, context: CallbackContext) -> None:
"""
@@ -1484,7 +1575,7 @@ class Telegram(RPCHandler):
trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id)
await self._send_msg(
f"`{msg['result_msg']}`\n"
f"{msg['result_msg']}\n"
"Please make sure to take care of this asset on the exchange manually."
)
@@ -1865,6 +1956,10 @@ class Telegram(RPCHandler):
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n"
"*/profit_long [<n>]:* `Lists cumulative profit from all finished long trades, "
"over the last n days`\n"
"*/profit_short [<n>]:* `Lists cumulative profit from all finished short trades, "
"over the last n days`\n"
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
"*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n"

View File

@@ -5,7 +5,7 @@ This module defines the interface to apply for strategies
import logging
from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from math import isinf, isnan
from pandas import DataFrame
@@ -1149,7 +1149,7 @@ class IStrategy(ABC, HyperStrategyMixin):
manually from within the strategy, to allow an easy way to unlock pairs.
:param pair: Unlock pair to allow trading again
"""
PairLocks.unlock_pair(pair, datetime.now(timezone.utc))
PairLocks.unlock_pair(pair, datetime.now(UTC))
def unlock_reason(self, reason: str) -> None:
"""
@@ -1158,7 +1158,7 @@ class IStrategy(ABC, HyperStrategyMixin):
manually from within the strategy, to allow an easy way to unlock pairs.
:param reason: Unlock pairs to allow trading again
"""
PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
PairLocks.unlock_reason(reason, datetime.now(UTC))
def is_pair_locked(
self, pair: str, *, candle_date: datetime | None = None, side: str = "*"

View File

@@ -39,6 +39,17 @@ class StrategyUpdater:
"sell": "exit",
}
# Update function names.
# example: `np.NaN` was removed in the NumPy 2.0 release. Use `np.nan` instead.
module_replacements = {
"numpy": {
"aliases": set(),
"replacements": [
("NaN", "nan"),
],
}
}
# create a dictionary that maps the old column names to the new ones
rename_dict = {"buy": "enter_long", "sell": "exit_long", "buy_tag": "enter_tag"}
@@ -153,16 +164,24 @@ class NameUpdater(ast_comments.NodeTransformer):
def visit_Name(self, node):
# if the name is in the mapping, update it
node.id = self.check_dict(StrategyUpdater.name_mapping, node.id)
for mod, info in StrategyUpdater.module_replacements.items():
for old_attr, new_attr in info["replacements"]:
if node.id == old_attr:
node.id = new_attr
return node
def visit_Import(self, node):
# do not update the names in import statements
for alias in node.names:
if alias.name in StrategyUpdater.module_replacements:
as_name = alias.asname or alias.name
StrategyUpdater.module_replacements[alias.name]["aliases"].add(as_name)
return node
def visit_ImportFrom(self, node):
# if hasattr(node, "module"):
# if node.module == "freqtrade.strategy.hyper":
# node.module = "freqtrade.strategy"
if node.module in StrategyUpdater.module_replacements:
mod = node.module
StrategyUpdater.module_replacements[node.module]["aliases"].add(mod)
return node
def visit_If(self, node: ast_comments.If):
@@ -182,6 +201,12 @@ class NameUpdater(ast_comments.NodeTransformer):
and node.attr == "nr_of_successful_buys"
):
node.attr = "nr_of_successful_entries"
if isinstance(node.value, ast_comments.Name):
for mod, info in StrategyUpdater.module_replacements.items():
if node.value.id in info["aliases"]:
for old_attr, new_attr in info["replacements"]:
if node.attr == old_attr:
node.attr = new_attr
return node
def visit_ClassDef(self, node):

View File

@@ -1,5 +1,5 @@
import re
from datetime import datetime, timezone
from datetime import UTC, datetime
from time import time
import humanize
@@ -9,7 +9,7 @@ from freqtrade.constants import DATETIME_PRINT_FORMAT
def dt_now() -> datetime:
"""Return the current datetime in UTC."""
return datetime.now(timezone.utc)
return datetime.now(UTC)
def dt_utc(
@@ -22,7 +22,7 @@ def dt_utc(
microsecond: int = 0,
) -> datetime:
"""Return a datetime in UTC."""
return datetime(year, month, day, hour, minute, second, microsecond, tzinfo=timezone.utc)
return datetime(year, month, day, hour, minute, second, microsecond, tzinfo=UTC)
def dt_ts(dt: datetime | None = None) -> int:
@@ -68,7 +68,7 @@ def dt_from_ts(timestamp: float) -> datetime:
if timestamp > 1e10:
# Timezone in ms - convert to seconds
timestamp /= 1000
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
return datetime.fromtimestamp(timestamp, tz=UTC)
def shorten_date(_date: str) -> str:

View File

@@ -9,4 +9,4 @@ def get_dry_run_wallet(config: Config) -> int | float:
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
return _start_cap
else:
return _start_cap.get("stake_currency")
return _start_cap.get(config["stake_currency"], 0.0)

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import UTC, datetime
from cachetools import TTLCache
@@ -11,7 +11,7 @@ class PeriodicCache(TTLCache):
def __init__(self, maxsize, ttl, getsizeof=None):
def local_timer():
ts = datetime.now(timezone.utc).timestamp()
ts = datetime.now(UTC).timestamp()
offset = ts % ttl
return ts - offset

View File

@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.6"
__version__ = "2025.7"
if "dev" in __version__:
from pathlib import Path

View File

@@ -13,14 +13,13 @@ authors = [
description = "Freqtrade - Client scripts"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
license = {text = "GPLv3"}
# license = "GPLv3"
classifiers = [
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",

View File

@@ -1,3 +1,3 @@
# Requirements for freqtrade client library
requests==2.32.4
python-rapidjson==1.20
python-rapidjson==1.21

View File

@@ -13,13 +13,12 @@ authors = [
description = "Freqtrade - Crypto Trading Bot"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.11"
license = {text = "GPLv3"}
classifiers = [
"Environment :: Console",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
@@ -111,6 +110,7 @@ develop = [
"pytest-xdist",
"pytest",
"ruff",
"scipy-stubs",
"time-machine",
"types-cachetools",
"types-filelock",

View File

@@ -6,16 +6,16 @@
-r requirements-freqai-rl.txt
-r docs/requirements-docs.txt
ruff==0.12.1
mypy==1.16.1
ruff==0.12.5
mypy==1.17.0
pre-commit==4.2.0
pytest==8.4.1
pytest-asyncio==1.0.0
pytest-asyncio==1.1.0
pytest-cov==6.2.1
pytest-mock==3.14.1
pytest-random-order==1.2.0
pytest-timeout==2.4.0
pytest-xdist==3.7.0
pytest-xdist==3.8.0
isort==6.0.1
# For datetime mocking
time-machine==2.16.0
@@ -24,8 +24,9 @@ time-machine==2.16.0
nbconvert==7.16.6
# mypy types
types-cachetools==6.0.0.20250525
scipy-stubs==1.16.0.2 # keep in sync with `scipy` in `requirements-hyperopt.txt`
types-cachetools==6.1.0.20250717
types-filelock==3.2.7
types-requests==2.32.4.20250611
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20250516
types-python-dateutil==2.9.0.20250708

View File

@@ -5,7 +5,7 @@
torch==2.7.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
gymnasium==0.29.1
# SB3 >=2.5.0 depends on torch 2.3.0 - which implies it dropped support x86 macos
stable_baselines3==2.6.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
stable_baselines3==2.7.0; sys_platform != 'darwin' or platform_machine != 'x86_64'
sb3_contrib>=2.2.1
# Progress bar for stable-baselines3 and sb3-contrib
tqdm==4.67.1

View File

@@ -3,10 +3,10 @@
-r requirements-plot.txt
# Required for freqai
scikit-learn==1.7.0
scikit-learn==1.7.1
joblib==1.5.1
catboost==1.2.8; 'arm' not in platform_machine
lightgbm==4.6.0
xgboost==3.0.2
tensorboard==2.19.0
tensorboard==2.20.0
datasieve==0.1.9

View File

@@ -2,8 +2,8 @@
-r requirements.txt
# Required for hyperopt
scipy==1.15.3
scikit-learn==1.7.0
scipy==1.16.1
scikit-learn==1.7.1
filelock==3.18.0
optuna==4.4.0
cmaes==0.11.1
cmaes==0.12.0

View File

@@ -1,42 +1,42 @@
numpy==2.2.6
pandas==2.3.0
numpy==2.3.2
pandas==2.3.1
bottleneck==1.5.0
numexpr==2.11.0
# Indicator libraries
ft-pandas-ta==0.3.15
ta-lib==0.5.5
technical==1.5.1
technical==1.5.2
ccxt==4.4.91
cryptography==45.0.4
aiohttp==3.12.13
ccxt==4.4.96
cryptography==45.0.5
aiohttp==3.12.14
SQLAlchemy==2.0.41
python-telegram-bot==22.2
python-telegram-bot==22.3
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.12.3
cachetools==6.1.0
requests==2.32.4
urllib3==2.5.0
certifi==2025.6.15
jsonschema==4.24.0
certifi==2025.7.14
jsonschema==4.25.0
tabulate==0.9.0
pycoingecko==3.2.0
jinja2==3.1.6
joblib==1.5.1
rich==14.0.0
pyarrow==20.0.0; platform_machine != 'armv7l'
rich==14.1.0
pyarrow==21.0.0; platform_machine != 'armv7l'
# Load ticker files 30% faster
python-rapidjson==1.20
python-rapidjson==1.21
# Properly format api responses
orjson==3.10.18
orjson==3.11.1
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.115.14
fastapi==0.116.1
pydantic==2.11.7
uvicorn==0.35.0
pyjwt==2.10.1

View File

@@ -234,7 +234,7 @@ async def create_client(
await protocol.on_message(ws, name, message)
except (asyncio.TimeoutError, websockets.exceptions.WebSocketException):
except (TimeoutError, websockets.exceptions.WebSocketException):
# Try pinging
try:
pong = await ws.ping()
@@ -244,7 +244,7 @@ async def create_client(
continue
except asyncio.TimeoutError:
except TimeoutError:
logger.error(f"Ping timed out, retrying in {sleep_time}s")
await asyncio.sleep(sleep_time)

View File

@@ -153,16 +153,13 @@ function Find-PythonExecutable {
"python3.13",
"python3.12",
"python3.11",
"python3.10",
"python3",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
"C:\Python313\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe"
"C:\Python311\python.exe"
)
@@ -178,10 +175,10 @@ function Main {
"Starting the operations..." | Out-File $LogFilePath -Append
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
# Exit on lower versions than Python 3.10 or when Python executable not found
# Exit on lower versions than Python 3.11 or when Python executable not found
$PythonExecutable = Find-PythonExecutable
if ($null -eq $PythonExecutable) {
Write-Log "No suitable Python executable found. Please ensure that Python 3.10 or higher is installed and available in the system PATH." -Level 'ERROR'
Write-Log "No suitable Python executable found. Please ensure that Python 3.11 or higher is installed and available in the system PATH." -Level 'ERROR'
Exit 1
}

View File

@@ -25,7 +25,7 @@ function check_installed_python() {
exit 2
fi
for v in 13 12 11 10
for v in 13 12 11
do
PYTHON="python3.${v}"
which $PYTHON
@@ -36,7 +36,7 @@ function check_installed_python() {
fi
done
echo "No usable python found. Please make sure to have python3.10 or newer installed."
echo "No usable python found. Please make sure to have python3.11 or newer installed."
exit 1
}
@@ -257,7 +257,7 @@ function install() {
install_redhat
else
echo "This script does not support your OS."
echo "If you have Python version 3.10 - 3.13, pip, virtualenv, ta-lib you can continue."
echo "If you have Python version 3.11 - 3.13, pip, virtualenv, ta-lib you can continue."
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
sleep 10
fi
@@ -284,7 +284,7 @@ function help() {
echo " -p,--plot Install dependencies for Plotting scripts."
}
# Verify if 3.10+ is installed
# Verify if 3.11+ is installed
check_installed_python
case $* in

View File

@@ -4,7 +4,7 @@ import logging
import platform
import re
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock, Mock, PropertyMock
@@ -126,7 +126,7 @@ def get_args(args):
def generate_trades_history(n_rows, start_date: datetime | None = None, days=5):
np.random.seed(42)
if not start_date:
start_date = datetime(2020, 1, 1, tzinfo=timezone.utc)
start_date = datetime(2020, 1, 1, tzinfo=UTC)
# Generate random data
end_date = start_date + timedelta(days=days)
@@ -258,6 +258,7 @@ def patch_exchange(
"._supported_trading_mode_margin_pairs",
PropertyMock(
return_value=[
(TradingMode.SPOT, MarginMode.NONE),
(TradingMode.MARGIN, MarginMode.CROSS),
(TradingMode.MARGIN, MarginMode.ISOLATED),
(TradingMode.FUTURES, MarginMode.CROSS),
@@ -3405,4 +3406,35 @@ def leverage_tiers():
"maintAmt": 654500.0,
},
],
"TIA/USDT:USDT": [
# Okx tier - these have a gap between maxNotional and the next minNotional
{
"minNotional": 0.0,
"maxNotional": 6500.0,
"maintenanceMarginRate": 0.0065,
"maxLeverage": 50.0,
"maintAmt": None,
},
{
"minNotional": 6501.0,
"maxNotional": 12000.0,
"maintenanceMarginRate": 0.01,
"maxLeverage": 40.0,
"maintAmt": None,
},
{
"minNotional": 12001.0,
"maxNotional": 25000.0,
"maintenanceMarginRate": 0.015,
"maxLeverage": 20.0,
"maintAmt": None,
},
{
"minNotional": 25001.0,
"maxNotional": 50000.0,
"maintenanceMarginRate": 0.02,
"maxLeverage": 18.18,
"maintAmt": None,
},
],
}

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from freqtrade.persistence.models import Order, Trade
@@ -43,7 +43,7 @@ def mock_trade_1(fee, is_short: bool):
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_date=datetime.now(tz=UTC) - timedelta(minutes=17),
open_rate=0.123,
exchange="binance",
strategy="StrategyTestV3",
@@ -106,8 +106,8 @@ def mock_trade_2(fee, is_short: bool):
timeframe=5,
enter_tag="TEST1",
exit_reason="sell_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=UTC) - timedelta(minutes=2),
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_2(is_short), "ETC/BTC", entry_side(is_short))
@@ -168,8 +168,8 @@ def mock_trade_3(fee, is_short: bool):
strategy="StrategyTestV3",
timeframe=5,
exit_reason="roi",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=UTC),
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_3(is_short), "XRP/BTC", entry_side(is_short))
@@ -205,7 +205,7 @@ def mock_trade_4(fee, is_short: bool):
amount_requested=124.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14),
open_date=datetime.now(tz=UTC) - timedelta(minutes=14),
is_open=True,
open_rate=0.123,
exchange="binance",
@@ -260,7 +260,7 @@ def mock_trade_5(fee, is_short: bool):
amount_requested=124.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12),
open_date=datetime.now(tz=UTC) - timedelta(minutes=12),
is_open=True,
open_rate=0.123,
exchange="binance",
@@ -316,7 +316,7 @@ def mock_trade_6(fee, is_short: bool):
stake_amount=0.001,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
open_date=datetime.now(tz=UTC) - timedelta(minutes=5),
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
@@ -410,7 +410,7 @@ def short_trade(fee):
strategy="DefaultStrategy",
timeframe=5,
exit_reason="sell_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
# close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
is_short=True,
)
@@ -500,8 +500,8 @@ def leverage_trade(fee):
strategy="DefaultStrategy",
timeframe=5,
exit_reason="sell_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300),
close_date=datetime.now(tz=timezone.utc),
open_date=datetime.now(tz=UTC) - timedelta(minutes=300),
close_date=datetime.now(tz=UTC),
interest_rate=0.0005,
)
o = Order.parse_from_ccxt_object(leverage_order(), "DOGE/BTC", "sell")

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from freqtrade.persistence.models import Order, Trade
@@ -55,8 +55,8 @@ def mock_trade_usdt_1(fee, is_short: bool):
stake_amount=20.0,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5),
open_date=datetime.now(tz=UTC) - timedelta(days=2, minutes=20),
close_date=datetime.now(tz=UTC) - timedelta(days=2, minutes=5),
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=False,
@@ -127,8 +127,8 @@ def mock_trade_usdt_2(fee, is_short: bool):
timeframe=5,
enter_tag="TEST1",
exit_reason="exit_signal",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=UTC) - timedelta(minutes=2),
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), "NEO/USDT", entry_side(is_short))
@@ -190,8 +190,8 @@ def mock_trade_usdt_3(fee, is_short: bool):
timeframe=5,
enter_tag="TEST3",
exit_reason="roi",
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc),
open_date=datetime.now(tz=UTC) - timedelta(minutes=20),
close_date=datetime.now(tz=UTC),
is_short=is_short,
)
o = Order.parse_from_ccxt_object(mock_order_usdt_3(is_short), "XRP/USDT", entry_side(is_short))
@@ -228,7 +228,7 @@ def mock_trade_usdt_4(fee, is_short: bool):
amount_requested=10.01,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14),
open_date=datetime.now(tz=UTC) - timedelta(minutes=14),
is_open=True,
open_rate=2.0,
exchange="binance",
@@ -280,7 +280,7 @@ def mock_trade_usdt_5(fee, is_short: bool):
amount_requested=10.01,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12),
open_date=datetime.now(tz=UTC) - timedelta(minutes=12),
is_open=True,
open_rate=2.0,
exchange="binance",
@@ -332,7 +332,7 @@ def mock_trade_usdt_6(fee, is_short: bool):
stake_amount=20.0,
amount=2.0,
amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
open_date=datetime.now(tz=UTC) - timedelta(minutes=5),
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
@@ -374,7 +374,7 @@ def mock_trade_usdt_7(fee, is_short: bool):
fee_open=fee.return_value,
fee_close=fee.return_value,
is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_date=datetime.now(tz=UTC) - timedelta(minutes=17),
open_rate=2.0,
exchange="binance",
strategy="StrategyTestV2",

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import MagicMock
from zipfile import ZipFile
@@ -182,19 +182,19 @@ def test_extract_trades_of_period(testdatadir):
"profit_abs": [0.0, 1, -2, -5],
"open_date": to_datetime(
[
datetime(2017, 11, 13, 15, 40, 0, tzinfo=timezone.utc),
datetime(2017, 11, 14, 9, 41, 0, tzinfo=timezone.utc),
datetime(2017, 11, 14, 14, 20, 0, tzinfo=timezone.utc),
datetime(2017, 11, 15, 3, 40, 0, tzinfo=timezone.utc),
datetime(2017, 11, 13, 15, 40, 0, tzinfo=UTC),
datetime(2017, 11, 14, 9, 41, 0, tzinfo=UTC),
datetime(2017, 11, 14, 14, 20, 0, tzinfo=UTC),
datetime(2017, 11, 15, 3, 40, 0, tzinfo=UTC),
],
utc=True,
),
"close_date": to_datetime(
[
datetime(2017, 11, 13, 16, 40, 0, tzinfo=timezone.utc),
datetime(2017, 11, 14, 10, 41, 0, tzinfo=timezone.utc),
datetime(2017, 11, 14, 15, 25, 0, tzinfo=timezone.utc),
datetime(2017, 11, 15, 3, 55, 0, tzinfo=timezone.utc),
datetime(2017, 11, 13, 16, 40, 0, tzinfo=UTC),
datetime(2017, 11, 14, 10, 41, 0, tzinfo=UTC),
datetime(2017, 11, 14, 15, 25, 0, tzinfo=UTC),
datetime(2017, 11, 15, 3, 55, 0, tzinfo=UTC),
],
utc=True,
),
@@ -203,10 +203,10 @@ def test_extract_trades_of_period(testdatadir):
trades1 = extract_trades_of_period(data, trades)
# First and last trade are dropped as they are out of range
assert len(trades1) == 2
assert trades1.iloc[0].open_date == datetime(2017, 11, 14, 9, 41, 0, tzinfo=timezone.utc)
assert trades1.iloc[0].close_date == datetime(2017, 11, 14, 10, 41, 0, tzinfo=timezone.utc)
assert trades1.iloc[-1].open_date == datetime(2017, 11, 14, 14, 20, 0, tzinfo=timezone.utc)
assert trades1.iloc[-1].close_date == datetime(2017, 11, 14, 15, 25, 0, tzinfo=timezone.utc)
assert trades1.iloc[0].open_date == datetime(2017, 11, 14, 9, 41, 0, tzinfo=UTC)
assert trades1.iloc[0].close_date == datetime(2017, 11, 14, 10, 41, 0, tzinfo=UTC)
assert trades1.iloc[-1].open_date == datetime(2017, 11, 14, 14, 20, 0, tzinfo=UTC)
assert trades1.iloc[-1].close_date == datetime(2017, 11, 14, 15, 25, 0, tzinfo=UTC)
def test_analyze_trade_parallelism(testdatadir):
@@ -293,7 +293,7 @@ def test_combined_dataframes_with_rel_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"]
data = load_data(datadir=testdatadir, pairs=pairs, timeframe="5m")
df = combined_dataframes_with_rel_mean(
data, datetime(2018, 1, 12, tzinfo=timezone.utc), datetime(2018, 1, 28, tzinfo=timezone.utc)
data, datetime(2018, 1, 12, tzinfo=UTC), datetime(2018, 1, 28, tzinfo=UTC)
)
assert isinstance(df, DataFrame)
assert "ETH/BTC" not in df.columns
@@ -596,7 +596,7 @@ def test_calculate_max_drawdown_abs(profits, relative, highd, lowdays, result, r
[1000, 500, 1000, 11000, 10000] # absolute results
[1000, 50%, 0%, 0%, ~9%] # Relative drawdowns
"""
init_date = datetime(2020, 1, 1, tzinfo=timezone.utc)
init_date = datetime(2020, 1, 1, tzinfo=UTC)
dates = [init_date + timedelta(days=i) for i in range(len(profits))]
df = DataFrame(zip(profits, dates, strict=False), columns=["profit_abs", "open_date"])
# sort by profit and reset index

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