Compare commits

...

523 Commits

Author SHA1 Message Date
Matthias
a55691ea7f Merge pull request #10383 from freqtrade/new_release
New release 2024.6
2024-07-01 10:45:09 +02:00
Matthias
d9b588fe59 Version bump 2024.6 2024-06-30 09:06:53 +00:00
Matthias
f20fefffa0 Merge branch 'stable' into new_release 2024-06-30 09:03:46 +00:00
Matthias
60232ca85b Merge pull request #10382 from konradbeck/patch-2
Update telegram /help formatting
2024-06-29 12:09:41 +02:00
konradbeck
1c4e809f84 Update telegram.py
The help command doesn't have consistent formatting.

- /stop: "Description" doesn't conform to the other formatting.
- Statistics header isn't on it's own line.
2024-06-29 09:58:37 +02:00
Matthias
81224cbd44 Merge pull request #10375 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-06-27 07:43:55 +02:00
xmatthias
5fa24163f5 chore: update pre-commit hooks 2024-06-27 03:12:21 +00:00
Matthias
055426c24e Merge pull request #10374 from freqtrade/frog-setup-1
Fix setup.sh pip version
2024-06-26 18:35:38 +02:00
Robert Davey
5effd62599 Fix setup.sh pip version 2024-06-26 16:25:40 +01:00
Matthias
eb8f7666df Merge pull request #10361 from freqtrade/dependabot/pip/develop/types-53b9298882
Bump types-requests from 2.32.0.20240602 to 2.32.0.20240622 in the types group
2024-06-25 14:49:20 +02:00
Matthias
58d6abe15d Update .pre-commit-config.yaml 2024-06-25 14:09:53 +02:00
Matthias
488d149b16 Update precommit config 2024-06-25 14:07:51 +02:00
Matthias
286e8849b5 Merge pull request #10372 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-06-25 14:00:35 +02:00
xmatthias
7644c097b4 chore: update pre-commit hooks 2024-06-25 03:02:45 +00:00
Matthias
7aebd407c0 Merge pull request #10365 from freqtrade/dependabot/pip/develop/sqlalchemy-2.0.31
Bump sqlalchemy from 2.0.30 to 2.0.31
2024-06-24 16:20:07 +02:00
Matthias
9ac7f90cd1 update precommit 2024-06-24 15:51:16 +02:00
Matthias
6c78932d1d Merge pull request #10369 from freqtrade/dependabot/pip/develop/numexpr-2.10.1
Bump numexpr from 2.10.0 to 2.10.1
2024-06-24 14:47:02 +02:00
dependabot[bot]
ead057d6c0 Bump sqlalchemy from 2.0.30 to 2.0.31
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.30 to 2.0.31.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 12:01:20 +00:00
dependabot[bot]
5d13a22499 Bump numexpr from 2.10.0 to 2.10.1
Bumps [numexpr](https://github.com/pydata/numexpr) from 2.10.0 to 2.10.1.
- [Release notes](https://github.com/pydata/numexpr/releases)
- [Changelog](https://github.com/pydata/numexpr/blob/master/RELEASE_NOTES.rst)
- [Commits](https://github.com/pydata/numexpr/compare/v2.10.0...v2.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 12:01:13 +00:00
Matthias
5b7cd49b9c Merge pull request #10364 from freqtrade/dependabot/pip/develop/ccxt-4.3.50
Bump ccxt from 4.3.46 to 4.3.50
2024-06-24 13:59:25 +02:00
Matthias
bfe8548041 Merge pull request #10363 from freqtrade/dependabot/pip/develop/psutil-6.0.0
Bump psutil from 5.9.8 to 6.0.0
2024-06-24 12:49:51 +02:00
Matthias
e2ee8de739 Merge pull request #10368 from freqtrade/dependabot/pip/develop/filelock-3.15.4
Bump filelock from 3.15.1 to 3.15.4
2024-06-24 08:54:00 +02:00
Matthias
71e1b27d68 Merge pull request #10362 from freqtrade/dependabot/pip/develop/bottleneck-1.4.0
Bump bottleneck from 1.3.8 to 1.4.0
2024-06-24 08:53:26 +02:00
Matthias
f3fede99d3 Merge pull request #10367 from freqtrade/dependabot/pip/develop/ruff-0.4.10
Bump ruff from 0.4.9 to 0.4.10
2024-06-24 08:53:00 +02:00
dependabot[bot]
6b84a2907f Bump filelock from 3.15.1 to 3.15.4
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.15.1 to 3.15.4.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.15.1...3.15.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 03:26:29 +00:00
dependabot[bot]
b2376c41d1 Bump ruff from 0.4.9 to 0.4.10
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.9 to 0.4.10.
- [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/v0.4.9...v0.4.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 03:26:22 +00:00
dependabot[bot]
7de5e88dfd Bump ccxt from 4.3.46 to 4.3.50
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.46 to 4.3.50.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.3.46...4.3.50)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 03:25:49 +00:00
dependabot[bot]
9f892e2e47 Bump psutil from 5.9.8 to 6.0.0
Bumps [psutil](https://github.com/giampaolo/psutil) from 5.9.8 to 6.0.0.
- [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/release-5.9.8...release-6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 03:25:42 +00:00
dependabot[bot]
56f2a77c72 Bump bottleneck from 1.3.8 to 1.4.0
Bumps [bottleneck](https://github.com/pydata/bottleneck) from 1.3.8 to 1.4.0.
- [Release notes](https://github.com/pydata/bottleneck/releases)
- [Changelog](https://github.com/pydata/bottleneck/blob/master/RELEASE.rst)
- [Commits](https://github.com/pydata/bottleneck/compare/v1.3.8...v1.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 03:25:34 +00:00
dependabot[bot]
40068dfedb Bump types-requests in the types group
Bumps the types group with 1 update: [types-requests](https://github.com/python/typeshed).


Updates `types-requests` from 2.32.0.20240602 to 2.32.0.20240622
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 03:24:52 +00:00
Matthias
0b9e4f68c7 Merge pull request #10353 from netcan/patch-1
Update pairlists.md
2024-06-21 18:08:35 +02:00
Netcan
b3cc761d8c Update pairlists.md 2024-06-21 23:18:48 +08:00
Matthias
b7f180ab3f fix: Improve safety of custom_stop return validation
If the return is inf or NaN freqtrade should not fail
closes #10349
2024-06-21 16:43:07 +02:00
Matthias
4f43e59643 Add test showing behavior of #10349 2024-06-21 16:41:59 +02:00
Matthias
93ed61a623 Improve stoploss test accuracy 2024-06-21 16:41:25 +02:00
Matthias
f117e66f53 Pin pip from updating 2024-06-21 16:13:06 +02:00
Matthias
9e9aacc102 Pin pip to 24.0 for the moment 2024-06-21 14:45:08 +02:00
Matthias
02c38f7396 Prevent data-downloads for exchanges that don't support this. 2024-06-20 18:29:17 +02:00
Matthias
776a8e43cd Add trades_has_history attribute 2024-06-20 18:24:43 +02:00
Matthias
8ac5fce06b Improve note wording 2024-06-20 06:54:55 +02:00
Matthias
226f907726 Add deprecation note to plot modules 2024-06-20 06:52:25 +02:00
Matthias
27e80b47ae Merge pull request #10342 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-06-20 06:43:32 +02:00
xmatthias
d5bad0ed45 chore: update pre-commit hooks 2024-06-20 03:14:42 +00:00
Matthias
44a37d1120 Merge pull request #10337 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-06-18 18:25:48 +02:00
xmatthias
8867f8ddc1 chore: update pre-commit hooks 2024-06-18 17:53:20 +02:00
Matthias
d06eb09e6e Merge pull request #10330 from freqtrade/dependabot/pip/develop/lightgbm-4.4.0
Bump lightgbm from 4.3.0 to 4.4.0
2024-06-18 16:07:29 +02:00
dependabot[bot]
fd9814df3c Bump lightgbm from 4.3.0 to 4.4.0
Bumps [lightgbm](https://github.com/microsoft/LightGBM) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/microsoft/LightGBM/releases)
- [Commits](https://github.com/microsoft/LightGBM/compare/v4.3.0...v4.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 13:27:45 +00:00
Matthias
d4dbf672ba Merge pull request #10329 from freqtrade/dependabot/pip/develop/pydantic-2.7.4
Bump pydantic from 2.7.3 to 2.7.4
2024-06-18 14:26:53 +02:00
Matthias
6f6e2f1541 Merge pull request #10328 from freqtrade/dependabot/pip/develop/orjson-3.10.5
Bump orjson from 3.10.3 to 3.10.5
2024-06-18 14:03:17 +02:00
Matthias
8ef07503e4 Merge pull request #10335 from freqtrade/dependabot/pip/develop/urllib3-2.2.2
Bump urllib3 from 2.2.1 to 2.2.2
2024-06-18 13:39:37 +02:00
dependabot[bot]
42c1d9a2ef Bump pydantic from 2.7.3 to 2.7.4
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.3 to 2.7.4.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.3...v2.7.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 10:21:55 +00:00
dependabot[bot]
b072a5343b Bump orjson from 3.10.3 to 3.10.5
Bumps [orjson](https://github.com/ijl/orjson) from 3.10.3 to 3.10.5.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.10.3...3.10.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 10:19:52 +00:00
Matthias
a20abfc3c7 Merge pull request #10269 from freqtrade/frog-rest-client-1
Add force_enter optional args and tests
2024-06-18 12:19:38 +02:00
dependabot[bot]
9804443a82 Bump urllib3 from 2.2.1 to 2.2.2
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 10:18:08 +00:00
Matthias
9fa9085b6c Merge pull request #10334 from freqtrade/dependabot/pip/develop/ta-lib-0.4.31
Bump ta-lib from 0.4.30 to 0.4.31
2024-06-18 12:17:15 +02:00
Matthias
bcf01bd9a8 Update TA-lib binaries 2024-06-18 11:42:03 +02:00
dependabot[bot]
0b4ce6e16c Bump ta-lib from 0.4.30 to 0.4.31
Bumps [ta-lib](https://github.com/ta-lib/ta-lib-python) from 0.4.30 to 0.4.31.
- [Changelog](https://github.com/TA-Lib/ta-lib-python/blob/master/CHANGELOG)
- [Commits](https://github.com/ta-lib/ta-lib-python/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 16:13:03 +00:00
Matthias
8ed2bbfa2d Merge pull request #10325 from freqtrade/dependabot/pip/develop/ccxt-4.3.46
Bump ccxt from 4.3.42 to 4.3.46
2024-06-17 06:55:05 +02:00
Matthias
befab6939a Merge pull request #10323 from freqtrade/dependabot/pip/develop/ruff-0.4.9
Bump ruff from 0.4.8 to 0.4.9
2024-06-17 06:53:30 +02:00
Matthias
f65d6f6e75 Merge pull request #10322 from freqtrade/dependabot/pip/develop/mkdocs-7bec7d3824
Bump mkdocs-material from 9.5.26 to 9.5.27 in the mkdocs group
2024-06-17 06:53:14 +02:00
Matthias
f67a4eb097 Merge pull request #10321 from freqtrade/dependabot/github_actions/develop/pypa/gh-action-pypi-publish-1.9.0
Bump pypa/gh-action-pypi-publish from 1.8.14 to 1.9.0
2024-06-17 06:52:44 +02:00
Matthias
3fc116144d Merge pull request #10326 from freqtrade/dependabot/pip/develop/filelock-3.15.1
Bump filelock from 3.14.0 to 3.15.1
2024-06-17 06:50:13 +02:00
dependabot[bot]
d3baade447 Bump filelock from 3.14.0 to 3.15.1
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.14.0 to 3.15.1.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.14.0...3.15.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 03:32:16 +00:00
dependabot[bot]
35e476c473 Bump ccxt from 4.3.42 to 4.3.46
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.42 to 4.3.46.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.3.42...4.3.46)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 03:32:11 +00:00
dependabot[bot]
f4f6dad060 Bump ruff from 0.4.8 to 0.4.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.8 to 0.4.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.8...v0.4.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 03:31:55 +00:00
dependabot[bot]
d7c0ae2256 Bump mkdocs-material from 9.5.26 to 9.5.27 in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 03:31:41 +00:00
dependabot[bot]
c1c4a3844e Bump pypa/gh-action-pypi-publish from 1.8.14 to 1.9.0
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.14 to 1.9.0.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.14...v1.9.0)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 03:25:08 +00:00
Matthias
a9ebefdc37 Prevent warning on __del__ during tests 2024-06-16 09:56:03 +02:00
Matthias
2223c16d00 Load hyperparameters when calling plot_config 2024-06-16 09:52:25 +02:00
Matthias
df47d154f9 Mock config loading for ft-client test 2024-06-16 08:09:35 +02:00
Matthias
3979801a86 Update documentation about keyword arguments 2024-06-15 09:49:08 +02:00
Matthias
6ec4907271 Improve docstring 2024-06-15 09:45:34 +02:00
Matthias
c5b4d6bced Add teset for kwarg splitting 2024-06-15 09:44:58 +02:00
Matthias
5a8838aec7 Additional test-cases 2024-06-15 09:24:07 +02:00
Matthias
1b491e9e15 Update tests to support kwargs 2024-06-15 09:21:35 +02:00
Matthias
a03528406f Split client arguments to accept kwarguments 2024-06-15 09:16:38 +02:00
Matthias
9d3e435162 Improve error for rest client 2024-06-15 09:13:57 +02:00
Matthias
61971f3949 chore: ftclient - Update naming of argument in main method 2024-06-15 09:12:23 +02:00
Matthias
619484a4fd feat: Make the new arguments kwargs only 2024-06-15 09:12:21 +02:00
Matthias
e11295a042 Merge pull request #10305 from freqtrade/dependabot/pip/develop/torch-2.3.1
Bump torch from 2.2.2 to 2.3.1
2024-06-15 07:25:11 +02:00
Matthias
fec0439479 Merge pull request #10315 from freqtrade/ci_ubuntu_24.04
Run CI against ubuntu 24.04
2024-06-13 19:45:49 +02:00
Matthias
eac7d71199 Run CI against ubuntu 24.04 2024-06-13 17:34:08 +02:00
Matthias
03d2d5dc5d Update bt_output types 2024-06-13 06:43:31 +02:00
Matthias
dd469944c9 Extract per-tag subresults from main backtest_result method 2024-06-13 06:43:31 +02:00
Matthias
156eeb90b9 Output mixed tags table 2024-06-13 06:43:31 +02:00
Matthias
68b8b29089 Calculated mixed tags results 2024-06-13 06:43:31 +02:00
Matthias
79cfa6d0d8 Merge pull request #10312 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-06-13 06:32:11 +02:00
Matthias
af6e7f5ec6 Skip publisher check for pester
> The built-in version is signed by Microsoft while newer versions are community-maintained and signed with a different certificate, causing Install-Module to sometimes throw a error requiring us to accept the new publisher certificate.
source: https://pester.dev/docs/introduction/installation#windows
2024-06-13 06:31:43 +02:00
xmatthias
7106ff6923 chore: update pre-commit hooks 2024-06-13 03:02:25 +00:00
Matthias
8dc766c0e2 Merge pull request #10307 from freqtrade/ci_3.12
Revert "Workaround macos CI fails"
2024-06-12 09:39:01 +02:00
Matthias
1b2cfc9857 Simplify generate_tag_metrics logic 2024-06-11 19:53:22 +02:00
Matthias
b8a4752636 Use proper type for exit_reason in tests 2024-06-11 19:51:38 +02:00
Matthias
2ec4449558 Use better column header for backtest output 2024-06-11 19:08:24 +02:00
Matthias
9e3be765d0 Fix table style 2024-06-11 07:11:20 +02:00
Matthias
12d7fbb379 Update docs for new wording 2024-06-11 07:06:09 +02:00
Matthias
09b1b1ab94 Use "trades" wording in backtest tables 2024-06-11 07:03:47 +02:00
Matthias
90efd04617 Improve typing in backtesting 2024-06-11 06:47:23 +02:00
Matthias
21710aeca8 use kwargs in example for clarity
closes #10308
2024-06-11 06:35:22 +02:00
Matthias
e5baa554d4 Merge pull request #10309 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-06-11 06:21:06 +02:00
xmatthias
6aba413aa2 chore: update pre-commit hooks 2024-06-11 03:03:12 +00:00
Matthias
594bb3278a Merge pull request #10302 from freqtrade/dependabot/pip/develop/tensorboard-2.17.0
Bump tensorboard from 2.16.2 to 2.17.0
2024-06-10 19:29:42 +02:00
Matthias
1b66ad4603 Keep torch at 2.2.2 for macos x86 versions 2024-06-10 19:11:17 +02:00
Matthias
33a4d5596f Revert "Workaround macos CI fails"
This reverts commit d2da23f5d1.
2024-06-10 18:01:01 +02:00
dependabot[bot]
03e7151c37 Bump tensorboard from 2.16.2 to 2.17.0
Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.16.2 to 2.17.0.
- [Release notes](https://github.com/tensorflow/tensorboard/releases)
- [Changelog](https://github.com/tensorflow/tensorboard/blob/2.17.0/RELEASE.md)
- [Commits](https://github.com/tensorflow/tensorboard/compare/2.16.2...2.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 07:35:56 +00:00
Matthias
ead1b2c398 Merge pull request #10306 from freqtrade/dependabot/pip/develop/cryptography-42.0.8
Bump cryptography from 42.0.7 to 42.0.8
2024-06-10 09:35:10 +02:00
dependabot[bot]
ac5e687c8f Bump torch from 2.2.2 to 2.3.1
Bumps [torch](https://github.com/pytorch/pytorch) from 2.2.2 to 2.3.1.
- [Release notes](https://github.com/pytorch/pytorch/releases)
- [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md)
- [Commits](https://github.com/pytorch/pytorch/compare/v2.2.2...v2.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 06:17:36 +00:00
dependabot[bot]
0972c213e4 Bump cryptography from 42.0.7 to 42.0.8
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.7 to 42.0.8.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.7...42.0.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 06:17:13 +00:00
Matthias
d2da23f5d1 Workaround macos CI fails 2024-06-10 08:16:21 +02:00
Matthias
014898e019 Merge pull request #10304 from freqtrade/dependabot/pip/develop/ruff-0.4.8
Bump ruff from 0.4.7 to 0.4.8
2024-06-10 07:58:30 +02:00
dependabot[bot]
afdb1f66b3 Bump ruff from 0.4.7 to 0.4.8
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.7 to 0.4.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.7...v0.4.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 05:20:13 +00:00
Matthias
5c5779a765 Merge pull request #10297 from freqtrade/dependabot/pip/develop/packaging-24.1
Bump packaging from 24.0 to 24.1
2024-06-10 07:17:58 +02:00
Matthias
4a78521d90 Merge pull request #10296 from freqtrade/dependabot/pip/develop/mkdocs-454b70509d
Bump mkdocs-material from 9.5.25 to 9.5.26 in the mkdocs group
2024-06-10 07:17:42 +02:00
Matthias
a5187728e0 Merge pull request #10295 from freqtrade/dependabot/pip/develop/pytest-42e73233fd
Bump pytest from 8.2.1 to 8.2.2 in the pytest group
2024-06-10 07:17:20 +02:00
Matthias
b3a91e3d4d Merge pull request #10303 from freqtrade/dependabot/docker/python-3.12.4-slim-bookworm
Bump python from 3.12.3-slim-bookworm to 3.12.4-slim-bookworm
2024-06-10 07:07:09 +02:00
Matthias
e2e2f0d454 Merge pull request #10300 from freqtrade/dependabot/pip/develop/python-telegram-bot-21.3
Bump python-telegram-bot from 21.2 to 21.3
2024-06-10 06:54:59 +02:00
Matthias
d992000343 Merge pull request #10299 from freqtrade/dependabot/pip/develop/pydantic-2.7.3
Bump pydantic from 2.7.2 to 2.7.3
2024-06-10 06:54:34 +02:00
Matthias
0be9490ee7 Merge pull request #10298 from freqtrade/dependabot/pip/develop/ccxt-4.3.42
Bump ccxt from 4.3.38 to 4.3.42
2024-06-10 06:53:59 +02:00
dependabot[bot]
77038011c1 Bump python from 3.12.3-slim-bookworm to 3.12.4-slim-bookworm
Bumps python from 3.12.3-slim-bookworm to 3.12.4-slim-bookworm.

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 03:35:13 +00:00
dependabot[bot]
1340412c99 Bump python-telegram-bot from 21.2 to 21.3
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 21.2 to 21.3.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v21.2...v21.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 03:35:01 +00:00
dependabot[bot]
db18f8ce64 Bump pydantic from 2.7.2 to 2.7.3
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.2 to 2.7.3.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.2...v2.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 03:34:50 +00:00
dependabot[bot]
40cea6d28a Bump ccxt from 4.3.38 to 4.3.42
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.38 to 4.3.42.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.3.38...4.3.42)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 03:34:37 +00:00
dependabot[bot]
48ae99283c Bump packaging from 24.0 to 24.1
Bumps [packaging](https://github.com/pypa/packaging) from 24.0 to 24.1.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/24.0...24.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 03:34:28 +00:00
dependabot[bot]
0c6d3fd675 Bump mkdocs-material from 9.5.25 to 9.5.26 in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 03:34:24 +00:00
dependabot[bot]
b56ea4f637 Bump pytest from 8.2.1 to 8.2.2 in the pytest group
Bumps the pytest group with 1 update: [pytest](https://github.com/pytest-dev/pytest).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 03:34:02 +00:00
Matthias
f314607bb6 Update pairlists to use *args **kwargs init 2024-06-09 08:55:03 +02:00
Matthias
29e23dfdb9 Use self._ for pairlist inits 2024-06-09 08:55:03 +02:00
Matthias
2cb89996d2 Remove unused imports 2024-06-09 08:44:26 +02:00
Matthias
3b86e3e66e Fix deprecated "abstractproperty" 2024-06-09 08:44:04 +02:00
Matthias
598e461892 Remove unused __init__ method 2024-06-09 08:42:51 +02:00
Matthias
0d6109211f Fix further windows tests 2024-06-08 20:26:50 +02:00
Matthias
35700d1452 Fix some windows tests 2024-06-08 17:41:05 +02:00
Matthias
36ad3bff62 Fix /tmp usage in tests 2024-06-08 09:42:01 +02:00
Matthias
2087974520 Fix some direct usages of "/tmp" 2024-06-08 09:40:32 +02:00
Matthias
e3b8e21b76 chore: Enable ruff "S" rule (bandit) 2024-06-08 09:33:15 +02:00
Matthias
de5a5d0967 Don't use assert in non-test code. 2024-06-08 09:32:54 +02:00
Matthias
cef9c45f68 don't use plain eval 2024-06-08 09:31:50 +02:00
Matthias
2f83ff73e2 Further bandid noqa's 2024-06-08 09:27:40 +02:00
Matthias
50e4d273f4 noqa empty passes on version detection 2024-06-08 09:23:02 +02:00
Matthias
6b932133ea Log during cleanup 2024-06-08 09:20:23 +02:00
Matthias
bd8b8e8b8b Add a few bandid noqa's on acceptable use 2024-06-08 09:19:54 +02:00
Matthias
a5d6417434 chore: use nan instead of NaN (numpy 2.x compat) 2024-06-08 08:56:41 +02:00
Matthias
6d40246764 Merge pull request #10288 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-06-06 06:51:08 +02:00
xmatthias
779905a8f2 chore: update pre-commit hooks 2024-06-06 03:02:43 +00:00
Matthias
133dc1d343 api_async is mandatory ... 2024-06-04 19:42:04 +02:00
Matthias
269135c2c9 Fix trading_fees test 2024-06-04 19:12:02 +02:00
Matthias
b294318d0f Update tests for simplified init 2024-06-04 19:05:27 +02:00
Matthias
7c6a5a34f5 Move async_mock into conditional 2024-06-04 19:01:21 +02:00
Matthias
a2251d045c Only load markets once
Increases startup speed by 6s on binance (from 9 to 3s).
2024-06-04 19:01:00 +02:00
Matthias
fbee48a106 Minor test comment fix 2024-06-04 07:24:56 +02:00
Matthias
d79fb8663e Update exchange tests 2024-06-04 07:21:42 +02:00
Matthias
b516a0827d Update hyperopt test mocks 2024-06-04 07:21:42 +02:00
Matthias
ab4c9ccfbc Update bot test 2024-06-04 07:21:42 +02:00
Matthias
5a08d1acf9 combine _load_markets and reload_markets 2024-06-04 07:21:42 +02:00
Matthias
7c3e8071af Merge pull request #10286 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-06-04 06:28:36 +02:00
xmatthias
d5fdaf26cf chore: update pre-commit hooks 2024-06-04 03:03:41 +00:00
Matthias
0f080a871a Control pytest log formatting 2024-06-03 21:00:22 +02:00
Matthias
29b4febfe4 Merge pull request #10282 from freqtrade/dependabot/pip/develop/ta-lib-0.4.30
Bump ta-lib from 0.4.29 to 0.4.30
2024-06-03 12:00:11 +02:00
Matthias
7824c6c690 Merge pull request #10283 from freqtrade/dependabot/pip/develop/pydantic-2.7.2
Bump pydantic from 2.7.1 to 2.7.2
2024-06-03 10:03:08 +02:00
Matthias
cd3f083cde Update ta-lib pre-built binaries 2024-06-03 10:02:31 +02:00
Matthias
a42b48ac57 Merge pull request #10275 from freqtrade/dependabot/github_actions/develop/docker/setup-buildx-action-3
Bump docker/setup-buildx-action from 1 to 3
2024-06-03 09:27:06 +02:00
Matthias
64c38bf32c Merge pull request #10277 from freqtrade/dependabot/pip/develop/types-e2110a637b
Bump types-requests from 2.32.0.20240523 to 2.32.0.20240602 in the types group
2024-06-03 08:53:57 +02:00
Matthias
3a1712a130 Merge pull request #10285 from freqtrade/dependabot/pip/develop/ccxt-4.3.38
Bump ccxt from 4.3.35 to 4.3.38
2024-06-03 08:17:56 +02:00
Matthias
3098221718 Merge pull request #10281 from freqtrade/dependabot/pip/develop/ruff-0.4.7
Bump ruff from 0.4.5 to 0.4.7
2024-06-03 08:15:59 +02:00
dependabot[bot]
c40834eda5 Bump pydantic from 2.7.1 to 2.7.2
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.1 to 2.7.2.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.1...v2.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 05:57:03 +00:00
Matthias
891a29cac8 Merge pull request #10280 from freqtrade/dependabot/pip/develop/uvicorn-0.30.1
Bump uvicorn from 0.29.0 to 0.30.1
2024-06-03 07:55:35 +02:00
Matthias
0461afa8e2 Merge pull request #10278 from freqtrade/dependabot/pip/develop/mkdocs-da0789ad88
Bump mkdocs-material from 9.5.24 to 9.5.25 in the mkdocs group
2024-06-03 07:36:33 +02:00
dependabot[bot]
a0a869e8f4 Bump ta-lib from 0.4.29 to 0.4.30
Bumps [ta-lib](https://github.com/ta-lib/ta-lib-python) from 0.4.29 to 0.4.30.
- [Changelog](https://github.com/TA-Lib/ta-lib-python/blob/master/CHANGELOG)
- [Commits](https://github.com/ta-lib/ta-lib-python/compare/TA_Lib-0.4.29...TA_Lib-0.4.30)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 05:17:24 +00:00
Matthias
05efe203d8 Merge pull request #10276 from freqtrade/dependabot/github_actions/develop/docker/setup-qemu-action-3
Bump docker/setup-qemu-action from 1 to 3
2024-06-03 07:16:12 +02:00
Matthias
4b59ebd2f5 Merge pull request #10279 from freqtrade/dependabot/pip/develop/requests-2.32.3
Bump requests from 2.32.2 to 2.32.3
2024-06-03 07:14:58 +02:00
Matthias
eb7047c68d Add site_description to docs for better SEO 2024-06-03 06:50:56 +02:00
Matthias
0115a7f296 pre-commit types-requests update 2024-06-03 06:26:44 +02:00
dependabot[bot]
5144925b82 Bump ccxt from 4.3.35 to 4.3.38
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.35 to 4.3.38.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.3.35...4.3.38)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 03:44:23 +00:00
dependabot[bot]
a7e9808177 Bump ruff from 0.4.5 to 0.4.7
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.5 to 0.4.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.5...v0.4.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 03:43:58 +00:00
dependabot[bot]
a2b746f2a5 Bump uvicorn from 0.29.0 to 0.30.1
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.29.0 to 0.30.1.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.29.0...0.30.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 03:43:48 +00:00
dependabot[bot]
d97b19db1d Bump requests from 2.32.2 to 2.32.3
Bumps [requests](https://github.com/psf/requests) from 2.32.2 to 2.32.3.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 03:43:43 +00:00
dependabot[bot]
23b5298f07 Bump mkdocs-material from 9.5.24 to 9.5.25 in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 03:43:32 +00:00
dependabot[bot]
4cdfd6a028 Bump types-requests in the types group
Bumps the types group with 1 update: [types-requests](https://github.com/python/typeshed).


Updates `types-requests` from 2.32.0.20240523 to 2.32.0.20240602
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 03:42:51 +00:00
dependabot[bot]
5ea7008b2b Bump docker/setup-qemu-action from 1 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v1...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 03:32:51 +00:00
dependabot[bot]
7f70035c62 Bump docker/setup-buildx-action from 1 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 1 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v1...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 03:32:43 +00:00
Matthias
8eda43f68d Pin numexpr - it's installed as floating dependency anyway 2024-06-02 14:19:21 +02:00
Matthias
a05450c547 Add bottleneck dependency
as per pandas recommendation

https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
2024-06-02 14:17:55 +02:00
Matthias
5d62954602 Merge pull request #10235 from simwai/feature/setup-win
Add Windows Setup Script and Pester Unit Tests for Freqtrade
2024-06-02 14:05:50 +02:00
Matthias
e9fb645b98 Exit-1 if invoke-pester created error entries 2024-06-02 13:42:51 +02:00
Matthias
c324981a17 Simplify setup.tests, avoid error log 2024-06-02 13:42:00 +02:00
Matthias
49a6a18881 Fix setup.ps1 syntax error 2024-06-02 11:48:14 +02:00
Matthias
f6649314a8 use pwsh, not powershell shell 2024-06-02 09:44:38 +02:00
Matthias
e7559cc62c Update pester command 2024-06-02 09:26:15 +02:00
Matthias
7b6864b991 Pester should fail "automatically" ... 2024-06-02 08:53:38 +02:00
Matthias
a2d5b4b2fe include 3.12 in all methods 2024-06-01 20:19:33 +02:00
Matthias
d116952fe0 Don't use overly long lines 2024-06-01 20:19:01 +02:00
Matthias
86e50b1764 Don't take assumptions about the install location of git 2024-06-01 20:17:55 +02:00
Matthias
a306f5a245 Improve wording in setup script 2024-06-01 20:10:24 +02:00
Matthias
93b64e7db6 Update documentation wording 2024-06-01 20:04:57 +02:00
Matthias
69faabb3b4 freqai tests mostly assume backtest runmode 2024-06-01 11:52:20 +02:00
Matthias
0e44cd91d8 StrEnum was only introduced in 3.11 . . . 2024-06-01 08:43:04 +02:00
Matthias
5a0e0263d8 use StrEnum for RunMode 2024-05-31 20:36:18 +02:00
Matthias
e6a562f74a Ensure pairlist tests use proper mode 2024-05-31 20:31:56 +02:00
simwai
2ff6e96255 Hopefully, fixed the failing GitHub action 2024-05-30 23:54:30 +02:00
simwai
39bae749b5 Changed freqUI installation behaviour to auto installing 2024-05-30 19:52:41 +02:00
simwai
9c7bc374bc Fixed the doc 2024-05-30 19:45:41 +02:00
Matthias
8d51a801ad Merge pull request #10271 from freqtrade/new_release
New release 2024.5
2024-05-30 18:01:26 +02:00
simwai
055293db7c Updated Windows installation doc 2024-05-30 17:06:34 +02:00
simwai
c9d67999ee Updated Windows installation doc, refined logging 2024-05-30 10:28:53 +02:00
Simon Waiblinger
2831318a95 Merge branch 'freqtrade:develop' into feature/setup-win 2024-05-30 10:10:16 +02:00
simwai
074434e83c Renamed freqtrade UI to freqUI 2024-05-30 10:08:41 +02:00
Matthias
a02ef7dce1 Bump dev version to 2024.6-dev 2024-05-30 06:40:15 +02:00
Matthias
8fc7056086 Bump version to 2024.5 2024-05-30 06:36:29 +02:00
Matthias
a1cfeb9a29 Merge branch 'stable' into new_release 2024-05-30 06:36:07 +02:00
Matthias
9b1792745e Merge pull request #10270 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-05-30 06:26:21 +02:00
xmatthias
d983572358 chore: update pre-commit hooks 2024-05-30 03:02:44 +00:00
Matthias
d69bb27105 Merge pull request #10268 from freqtrade/frog-rest-docs-1
Update rest-api.md
2024-05-29 17:54:56 +02:00
Robert Davey
7ca96beca7 Amend rate to amount 2024-05-29 15:30:41 +01:00
froggleston
8dc70d15db ruff formetting 2024-05-29 14:59:32 +01:00
Robert Davey
3da18c3443 Add force_enter optional args and tests 2024-05-29 14:45:49 +01:00
Robert Davey
b27e52b272 Update rest-api.md
Update docs to reflect force exit order_type and rate options
2024-05-29 13:35:31 +01:00
Matthias
7f210ab3b5 Merge pull request #10267 from freqtrade/dependabot/pip/develop/ccxt-4.3.35
Bump ccxt from 4.3.30 to 4.3.35
2024-05-29 07:01:26 +02:00
Matthias
efe6101081 Remove bitmart online test skip 2024-05-29 06:34:52 +02:00
dependabot[bot]
d9f48f2ca6 Bump ccxt from 4.3.30 to 4.3.35
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.30 to 4.3.35.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.3.30...4.3.35)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-29 04:33:34 +00:00
Matthias
9d432baf3d Don't hard-pin ta-lib in armhf image 2024-05-28 17:47:39 +02:00
simwai
bad1d83cee Fixed some bugs, added unit tests 2024-05-28 13:10:56 +02:00
Matthias
a4bbf39bb2 Merge pull request #10265 from freqtrade/buildx_update
Update buildx CI setup to supported action combination
2024-05-28 09:36:09 +02:00
Matthias
4b4d2b551b Merge pull request #10264 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-05-28 07:40:54 +02:00
Matthias
0e253cb070 Update buildx CI setup to supported action combination 2024-05-28 07:20:22 +02:00
Matthias
89d8f27d22 Add fix for bitmart failing test 2024-05-28 07:14:25 +02:00
Matthias
6952bac91f fix: codespell skip's to also work correctly on pre-commit hooks 2024-05-28 07:01:53 +02:00
xmatthias
6f7c82c9ae chore: update pre-commit hooks 2024-05-28 04:39:48 +00:00
Matthias
72d33070d4 Fix a few codespell typos 2024-05-28 06:37:54 +02:00
Matthias
9705f40cb5 Exclude leverage tier cache from codespell 2024-05-28 06:37:44 +02:00
Matthias
5e0f64ef55 Pre-commit action should not run pre-commit
Regular CI will take care of this and point out potential problems
2024-05-28 06:36:13 +02:00
Matthias
ddc8999060 Merge pull request #10257 from freqtrade/dependabot/pip/develop/ta-lib-0.4.29
Bump ta-lib from 0.4.28 to 0.4.29
2024-05-27 19:35:56 +02:00
Matthias
0812fe7a9a Merge pull request #10251 from freqtrade/dependabot/pip/develop/types-30a7864252
Bump types-requests from 2.31.0.20240406 to 2.32.0.20240523 in the types group
2024-05-27 19:05:30 +02:00
Matthias
49f580ce24 Upgrade ta-lib wheels 2024-05-27 15:27:30 +02:00
Matthias
81250c1ba4 Merge pull request #10259 from freqtrade/dependabot/pip/develop/scipy-1.13.1
Bump scipy from 1.13.0 to 1.13.1
2024-05-27 10:10:57 +02:00
Matthias
d5ed64582a Merge pull request #10258 from freqtrade/dependabot/pip/develop/schedule-1.2.2
Bump schedule from 1.2.1 to 1.2.2
2024-05-27 09:11:29 +02:00
Matthias
f632823fa6 Merge pull request #10255 from freqtrade/dependabot/pip/develop/python-telegram-bot-21.2
Bump python-telegram-bot from 21.1.1 to 21.2
2024-05-27 08:04:53 +02:00
dependabot[bot]
20d7ccf86a Bump scipy from 1.13.0 to 1.13.1
Bumps [scipy](https://github.com/scipy/scipy) from 1.13.0 to 1.13.1.
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](https://github.com/scipy/scipy/compare/v1.13.0...v1.13.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 05:34:25 +00:00
Matthias
d24899da3d Merge pull request #10254 from freqtrade/dependabot/pip/develop/ruff-0.4.5
Bump ruff from 0.4.4 to 0.4.5
2024-05-27 07:33:40 +02:00
Matthias
15ac68475f Merge pull request #10253 from freqtrade/dependabot/pip/develop/scikit-learn-1.5.0
Bump scikit-learn from 1.4.2 to 1.5.0
2024-05-27 07:33:03 +02:00
Matthias
2509cce29a Merge pull request #10252 from freqtrade/dependabot/pip/develop/mkdocs-eef9345e43
Bump mkdocs-material from 9.5.23 to 9.5.24 in the mkdocs group
2024-05-27 07:32:33 +02:00
Matthias
b12d5b4cb5 Update pre-commit types-requests 2024-05-27 06:27:13 +02:00
dependabot[bot]
ce4211d226 Bump schedule from 1.2.1 to 1.2.2
Bumps [schedule](https://github.com/dbader/schedule) from 1.2.1 to 1.2.2.
- [Changelog](https://github.com/dbader/schedule/blob/master/HISTORY.rst)
- [Commits](https://github.com/dbader/schedule/compare/1.2.1...1.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 03:46:49 +00:00
dependabot[bot]
a7adb67218 Bump ta-lib from 0.4.28 to 0.4.29
Bumps [ta-lib](https://github.com/ta-lib/ta-lib-python) from 0.4.28 to 0.4.29.
- [Changelog](https://github.com/TA-Lib/ta-lib-python/blob/master/CHANGELOG)
- [Commits](https://github.com/ta-lib/ta-lib-python/compare/TA_Lib-0.4.28...TA_Lib-0.4.29)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 03:46:33 +00:00
dependabot[bot]
b2f2048558 Bump python-telegram-bot from 21.1.1 to 21.2
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 21.1.1 to 21.2.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v21.1.1...v21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 03:46:20 +00:00
dependabot[bot]
5c2a1dce7b Bump ruff from 0.4.4 to 0.4.5
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.4 to 0.4.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/v0.4.4...v0.4.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 03:46:14 +00:00
dependabot[bot]
1bfa40a2ce Bump scikit-learn from 1.4.2 to 1.5.0
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.4.2 to 1.5.0.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.4.2...1.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 03:46:04 +00:00
dependabot[bot]
2cc9fe604a Bump mkdocs-material from 9.5.23 to 9.5.24 in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 03:45:59 +00:00
dependabot[bot]
3b8aa4677a Bump types-requests in the types group
Bumps the types group with 1 update: [types-requests](https://github.com/python/typeshed).


Updates `types-requests` from 2.31.0.20240406 to 2.32.0.20240523
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 03:45:19 +00:00
Matthias
917f70892c Merge pull request #10249 from freqtrade/bingx
Add Support for Bingx
2024-05-26 19:36:51 +02:00
Matthias
83bb65132b Bump ccxt to the required version for bingx 2024-05-26 18:17:54 +02:00
Matthias
30ad4ca9a9 Add bingx to list of supported exchanges 2024-05-26 16:37:21 +02:00
Matthias
34d7d530a1 chore(tests): Filled orders should have an average. 2024-05-26 16:37:21 +02:00
Matthias
c6d132376a Add Binance filled order 2024-05-26 16:37:21 +02:00
Matthias
89e3fc1c64 Test bingx order parsing 2024-05-26 16:37:21 +02:00
Matthias
64c7f6b06a Improve bingx file formatting 2024-05-26 16:37:21 +02:00
Matthias
71cb2ded79 Add Bingx stoploss documentation 2024-05-26 16:37:21 +02:00
Matthias
7f990e7df6 Enable bingx stoploss 2024-05-26 16:37:21 +02:00
Simon Waiblinger
26aabafe04 Merge branch 'freqtrade:develop' into feature/setup-win 2024-05-26 15:59:21 +02:00
simwai
6174a49aa5 Implemented the changes to pass the review, implemented consistent variable naming, adjusted unit tests 2024-05-26 15:54:52 +02:00
Matthias
46e97e5806 fix htx: Reduce amount of data downloaded on higher timeframes
closes #10247
2024-05-26 15:49:48 +02:00
Matthias
32ff3ebb99 Improve handling for immediately canceled orders 2024-05-26 09:42:28 +02:00
Matthias
9d3073d930 Add test for new "fully cancel" logic 2024-05-26 08:36:08 +02:00
Matthias
edd92194b0 have handle_onexchange_order delete trades if no order filled. 2024-05-26 08:36:02 +02:00
Matthias
ec0f6cb246 Add Properties for canceled orders to trade_model 2024-05-26 08:35:57 +02:00
Matthias
dc92787f1d Fix gone-wrong hyperopt fix
closes #10192
2024-05-25 11:52:41 +02:00
Simon Waiblinger
9c816045f1 Merge branch 'freqtrade:develop' into feature/setup-win 2024-05-24 20:47:21 +02:00
simwai
b9fd8d2ee7 Fixed log level of one log statement 2024-05-23 19:02:53 +02:00
simwai
e29fcb45ac Removed admin permissions, because it seems not necessary. Improved error messages. Increase speed of requirements installation by introducing new merging strategy. 2024-05-23 18:58:09 +02:00
simwai
670c5d0067 Added powershell unit tests to CI config 2024-05-23 18:16:37 +02:00
simwai
1352240ec7 Formatted setup.ps1 2024-05-23 12:08:10 +02:00
simwai
6d261b828e Applied fixed from last PR review 2024-05-23 12:07:40 +02:00
Matthias
2c740059d7 Merge pull request #10237 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-05-23 06:04:05 +02:00
xmatthias
d59422159a chore: update pre-commit hooks 2024-05-23 03:02:28 +00:00
Matthias
c3fa8a4c45 feat: Allow empty fiat_display_currency
(instead of completely deleting that key)
2024-05-22 20:30:35 +02:00
Matthias
23aef6e054 Bump requests to 2.32.2
2.32.0 was yanked.
2024-05-22 20:16:04 +02:00
simwai
d124716196 Added unit tests 2024-05-21 22:32:03 +02:00
simwai
12b5376cb6 Revert "Updated gitignore file"
This reverts commit 5110c14d35.
2024-05-21 08:04:09 +02:00
Matthias
ea27a1ec13 Merge pull request #10232 from freqtrade/dependabot/pip/requests-2.32.0
Bump requests from 2.31.0 to 2.32.0
2024-05-21 06:30:41 +02:00
dependabot[bot]
d52431c581 ---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 21:58:49 +00:00
simwai
ccb1588048 Added setup.ps1 for installation/updates on Windows 2024-05-20 21:08:58 +02:00
Matthias
aecb86d3f9 Merge pull request #10229 from freqtrade/feat/coingecko_apikey
Support Coingecko api keys
2024-05-20 18:04:53 +02:00
Matthias
531843ebcb Improve message for fiat_display_currency
allow leaving empty for new-config
2024-05-20 17:02:00 +02:00
Matthias
1588a4253d Update tests for coinGecko updates 2024-05-20 15:29:22 +02:00
Matthias
468d0f8cf0 use FtCoinGeckoApi for marketCapPairlist, too 2024-05-20 15:22:13 +02:00
Matthias
2cd3089b3a Update fiat-convert test cases 2024-05-20 15:16:12 +02:00
Matthias
94e0a808b7 Add test, invert logic 2024-05-20 15:14:15 +02:00
Matthias
9e0ccb1cf4 Rename coingecko wrapper file 2024-05-20 15:11:43 +02:00
Matthias
8d1285bb21 Set session params instead of headers 2024-05-20 14:44:25 +02:00
Matthias
5fd76a79fe Add coingecko API documentation 2024-05-20 14:39:57 +02:00
Matthias
3729daf082 Add type check for coingecko settings 2024-05-20 14:34:18 +02:00
Matthias
1ff162cf17 Use coingecko api keys 2024-05-20 14:32:08 +02:00
Matthias
773940e05c Update mocks to FtCoinGeckoApi 2024-05-20 14:15:53 +02:00
Matthias
62166e23f6 Improve singleton pattern 2024-05-20 14:15:20 +02:00
Matthias
cb1600d7b0 Update fiat_convert to use FtCoinGeckoApi 2024-05-20 14:08:44 +02:00
Matthias
c1f780794a Add CoinGeckoApi Wrapper 2024-05-20 14:02:09 +02:00
Matthias
7a309d6927 Add explicit "fiat convert singleton" code 2024-05-20 13:58:15 +02:00
Matthias
95077f4752 Remove unused "mocker" fixtures in fiat_convert 2024-05-20 13:57:06 +02:00
Matthias
0c16a45999 Fix odd bug related to singleton usage 2024-05-20 13:56:28 +02:00
Simon Waiblinger
3bfae7c530 Merge branch 'freqtrade:develop' into develop 2024-05-20 11:10:58 +02:00
Matthias
4d2db33445 Add support for ipv6
closes #10222
2024-05-20 10:39:08 +02:00
Matthias
05765f3479 Merge pull request #10228 from freqtrade/dependabot/pip/develop/pyarrow-16.1.0
Bump pyarrow from 16.0.0 to 16.1.0
2024-05-20 10:32:38 +02:00
Matthias
f47272162c Update pyarrow wheels for 16.1.0 2024-05-20 10:12:08 +02:00
Matthias
1a86d81200 Initial config for Bingx stop orders 2024-05-20 09:53:52 +02:00
Matthias
1717733b0f Merge pull request #10221 from freqtrade/hyp/profit-drawdown
improve MaxDrawDownHyperOptLoss
2024-05-20 09:01:21 +02:00
Matthias
b0987b3c03 Merge pull request #10226 from freqtrade/dependabot/pip/develop/coveralls-4.0.1
Bump coveralls from 4.0.0 to 4.0.1
2024-05-20 07:39:07 +02:00
Matthias
bc7fa52fc0 Merge pull request #10227 from freqtrade/dependabot/pip/develop/ccxt-4.3.27
Bump ccxt from 4.3.24 to 4.3.27
2024-05-20 07:11:41 +02:00
dependabot[bot]
1ae134e94a Bump pyarrow from 16.0.0 to 16.1.0
Bumps [pyarrow](https://github.com/apache/arrow) from 16.0.0 to 16.1.0.
- [Commits](https://github.com/apache/arrow/compare/go/v16.0.0...go/v16.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 04:42:34 +00:00
dependabot[bot]
02a131821a Bump coveralls from 4.0.0 to 4.0.1
Bumps [coveralls](https://github.com/TheKevJames/coveralls-python) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/TheKevJames/coveralls-python/releases)
- [Changelog](https://github.com/TheKevJames/coveralls-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TheKevJames/coveralls-python/compare/4.0.0...4.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 04:42:21 +00:00
Matthias
71b3459d07 Merge pull request #10225 from freqtrade/dependabot/pip/develop/python-rapidjson-1.17
Bump python-rapidjson from 1.16 to 1.17
2024-05-20 06:41:46 +02:00
Matthias
4eb8da2126 Merge pull request #10223 from freqtrade/dependabot/pip/develop/pytest-581622832d
Bump the pytest group with 2 updates
2024-05-20 06:41:06 +02:00
Matthias
20a68ff923 Merge pull request #10224 from freqtrade/dependabot/pip/develop/mkdocs-157145164a
Bump mkdocs-material from 9.5.22 to 9.5.23 in the mkdocs group
2024-05-20 06:39:27 +02:00
dependabot[bot]
0d19176902 Bump ccxt from 4.3.24 to 4.3.27
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.24 to 4.3.27.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.3.24...4.3.27)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 03:27:20 +00:00
dependabot[bot]
70f847b0a6 Bump python-rapidjson from 1.16 to 1.17
Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.16 to 1.17.
- [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.16...v1.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 03:26:55 +00:00
dependabot[bot]
79e522162c Bump mkdocs-material from 9.5.22 to 9.5.23 in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 03:26:41 +00:00
dependabot[bot]
6174817bc0 Bump the pytest group with 2 updates
Bumps the pytest group with 2 updates: [pytest](https://github.com/pytest-dev/pytest) and [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio).


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

Updates `pytest-asyncio` from 0.23.6 to 0.23.7
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.6...v0.23.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 03:26:28 +00:00
Matthias
e49fec5533 Simplify conftest setup (1000 fewer lines!)
default hyperopt results demo should not be a fixture

(it's only used in one place)
2024-05-19 18:15:21 +02:00
Matthias
cdf42604ce Update conftest hyperopt result 2024-05-19 18:04:31 +02:00
Matthias
c1d26d0330 Don't calculate the "legacy" version of drawdown anymore. 2024-05-19 17:57:21 +02:00
Matthias
3bf02c8a64 Simplify hyperopt drawdown logic
Reduces tons of fallback logic
2024-05-19 17:57:05 +02:00
Matthias
a9f13d29fd Fix test type errors 2024-05-19 17:48:36 +02:00
Simon Waiblinger
8e0d686c95 Merge branch 'freqtrade:develop' into develop 2024-05-19 13:53:38 +02:00
Matthias
480477d17a Improve profitdrawdownhyperopt balancing 2024-05-19 10:12:50 +02:00
Matthias
2a1ff7f9b3 Try improve profit-drawdown hyperopt 2024-05-19 09:45:32 +02:00
Matthias
acae6e75f4 Improve drawdown test case 2024-05-19 09:44:36 +02:00
Matthias
e35ad64d6c Move SQL Cheat-sheet into Advanced section
most options are better suited in other ways now.
2024-05-19 09:03:31 +02:00
Matthias
b2cce5ccdf update download data docs 2024-05-18 20:24:40 +02:00
Matthias
c6a5134815 Improve wording of log message 2024-05-18 20:20:58 +02:00
Matthias
c0d43f6d03 Improve line formatting 2024-05-18 20:16:25 +02:00
Matthias
2237410154 Upadate test for new download-data functionality 2024-05-18 20:15:02 +02:00
Matthias
aa0f90bb68 Don't convert trades to OHLCV unless explicitly specified 2024-05-18 20:14:52 +02:00
Matthias
e6d5aa1349 add --convert-trades argument to download-data 2024-05-18 20:14:08 +02:00
Matthias
9b031490cc Update all CI build stuff to 3.12 2024-05-18 15:23:22 +02:00
Matthias
9ebdbed215 Update CI workflows to use 3.12 2024-05-18 15:22:46 +02:00
Matthias
968f74edbd Update docs for full 3.12 support 2024-05-18 15:22:03 +02:00
Matthias
1e0782b626 Add support for python 3.12 in setup.sh
closes #10220
2024-05-18 15:05:14 +02:00
Matthias
8d93f27185 Add simple test for "fetch_my_trades" parsing quality 2024-05-17 18:27:07 +02:00
Matthias
34b06cd9aa Bump ccxt min-version 2024-05-16 19:25:40 +02:00
Matthias
1e04140fff Partially revert bybit leverage-tiers workaround 2024-05-16 19:25:19 +02:00
Matthias
a92178dd60 load_cached_leverage_tiers should allow a remote cache period 2024-05-16 19:11:51 +02:00
Matthias
e17afb2554 Bump ccxt to 4.3.24 2024-05-16 19:08:47 +02:00
Matthias
1e2662b627 Greatly simplify leverage tier loading for binance 2024-05-16 18:20:14 +02:00
Matthias
ac9dccb6d5 Merge pull request #10215 from freqtrade/fix/catboostworkaround
Remove catboost stdout workaround
2024-05-16 17:33:01 +02:00
Matthias
c06ae41fed Remove catboost stdout workaround
https://github.com/catboost/catboost/issues/2195 is fixed, so this SHOULD work
2024-05-16 07:25:49 +02:00
Matthias
0b03e4c46c Move sql cheatsheet to advanced options
it shouldn't be highlighted, as for most operations, there's better alternatives now
2024-05-16 07:11:47 +02:00
Matthias
fe9258a208 Update site-URL mkdocs config in stable
Adds support for proper error pages
2024-05-16 07:07:50 +02:00
Matthias
19d6ce5446 Remove custom build process 2024-05-16 07:04:50 +02:00
Matthias
c955aa02df No extra styling ... 2024-05-16 06:59:12 +02:00
Matthias
2a8cfd2e92 Add extra rtd rules ... 2024-05-16 06:54:20 +02:00
Matthias
5fba44abe7 use dynamic build process for RTD 2024-05-16 06:54:19 +02:00
Matthias
9c8b6babdf Merge pull request #10214 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-05-16 06:43:30 +02:00
xmatthias
fb73e23e64 chore: update pre-commit hooks 2024-05-16 03:02:21 +00:00
Matthias
d318c20d82 Bump ccxt.pro to 4.3.23
closes #10211
2024-05-15 18:14:06 +02:00
Matthias
c91e1d80c2 Merge pull request #10212 from freqtrade/refactor/max_drawdown
Refactor calculate_max_drawdown
2024-05-15 18:04:21 +02:00
Matthias
702ac14f27 Fix using wrong type 2024-05-15 07:04:36 +02:00
Matthias
a6b07ec96f Remove compatibility layer for calculate_max_drawdown 2024-05-15 06:54:17 +02:00
Matthias
c79b75ff9a Update remaining tests 2024-05-15 06:46:30 +02:00
Matthias
a6050cb771 Update tests for new interface 2024-05-14 19:57:46 +02:00
Matthias
bcb59265b5 Use default parameters for DrawdownResult 2024-05-14 19:50:35 +02:00
Matthias
94786454b7 Use calc_drawdown method throughout the bot 2024-05-14 19:37:41 +02:00
Matthias
0aa3ec2845 Have hyperopt-loss function use calc_max_drawdown 2024-05-14 19:28:48 +02:00
Matthias
c8eb22dcbd Add typed max_drawdown function 2024-05-14 19:28:33 +02:00
Matthias
3b0036368d Merge pull request #10210 from stash86/bt-metrics
modify MeasureTime log message to include time limit and 's' suffix
2024-05-14 18:23:50 +02:00
Stefano Ariestasia
75965cd50f modify MeasureTime log message to include time limit and 's' suffix 2024-05-14 16:20:20 +09:00
Matthias
b1fd79d720 Schedule devcontainer pre-built to Sunday morning 2024-05-14 06:37:10 +02:00
Matthias
3cd2b7c163 Merge pull request #10208 from freqtrade/ruff
Add ruff formatting
2024-05-14 06:25:06 +02:00
Matthias
c1fea31437 Merge pull request #10209 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-05-14 06:23:12 +02:00
xmatthias
b36428c2e3 chore: update pre-commit hooks 2024-05-14 03:03:37 +00:00
Matthias
9291698561 A few more formatting updates 2024-05-13 19:49:15 +02:00
Matthias
6a802f5624 Add vscode extensions recommendation file 2024-05-13 19:37:13 +02:00
Matthias
33b95e27de Update documentation to reflect ruff 2024-05-13 19:36:34 +02:00
Matthias
9ec46496f0 Merge pull request #10204 from freqtrade/dependabot/pip/develop/ccxt-4.3.21
Bump ccxt from 4.3.16 to 4.3.21
2024-05-13 07:29:15 +02:00
Matthias
e848c6494e Add ruff format check to github CI 2024-05-13 07:10:25 +02:00
Matthias
58edb0a54a Update misspellings that are being detected now 2024-05-13 07:10:25 +02:00
Matthias
18e03f398e Partially revert odd formatting decisions 2024-05-13 07:10:25 +02:00
Matthias
ccb395c6c5 Ruff rule (commented for now) 2024-05-13 07:10:25 +02:00
Matthias
5d4a930188 ruff format: Update setup 2024-05-13 07:10:25 +02:00
Matthias
b97ff77d65 Update a few missed ruff format updates 2024-05-13 07:10:25 +02:00
Matthias
9d6e4ae67d A few more minor fixes 2024-05-13 07:10:25 +02:00
Matthias
a9732c6195 Fix odd formatting by ruff format 2024-05-13 07:10:25 +02:00
Matthias
876a8f9e3e ruff format: remaining files 2024-05-13 07:10:25 +02:00
Matthias
fea1653e31 ruff format: freqtrade.data 2024-05-13 07:10:25 +02:00
Matthias
801ab4acc9 ruff format: optimize 2024-05-13 07:10:25 +02:00
Matthias
2c60985e2d ruff format: optimize analysis 2024-05-13 07:10:25 +02:00
Matthias
da7addcd98 ruff format: hyperopt 2024-05-13 07:10:25 +02:00
Matthias
f1ef537dfa ruff format: hyperopt-loss 2024-05-13 07:10:25 +02:00
Matthias
ab3dbb7fbc Allow flake E203 -
Incompatible with ruff ...
https://github.com/astral-sh/ruff/issues/8752
2024-05-13 07:10:25 +02:00
Matthias
d1db43dee0 ruff format: freqai 2024-05-13 07:10:25 +02:00
Matthias
e4e8c3967c ruff format: exchange class 2024-05-13 07:10:25 +02:00
Matthias
53eefb9442 ruff format: exchange classes 2024-05-13 07:10:25 +02:00
Matthias
7ea5e40919 ruff format: util 2024-05-13 07:10:25 +02:00
Matthias
5f64cc8e76 ruff format: rpc modules 2024-05-13 07:10:25 +02:00
Matthias
cebbe0121e ruff format: update persistence 2024-05-13 07:10:25 +02:00
Matthias
5783a44c86 ruff format: template directory 2024-05-13 07:10:25 +02:00
Matthias
439b8a0320 ruff format: freqtrade/strategies 2024-05-13 07:10:25 +02:00
Matthias
6bfe7aa72d ruff format: plugins/protections 2024-05-13 07:10:25 +02:00
Matthias
700b7acb6f ruff format: pairlist plugins 2024-05-13 07:10:25 +02:00
Matthias
c9d301e4f9 Ruff format: more random files 2024-05-13 07:10:25 +02:00
Matthias
73e182260e ruff format: more files 2024-05-13 07:10:25 +02:00
Matthias
f8f9ac38b2 ruff format: loggers 2024-05-13 07:10:25 +02:00
Matthias
9303ae29d3 ruff format: freqtrade/configuration 2024-05-13 07:10:25 +02:00
Matthias
8ffc48e4f0 ruff format: constants 2024-05-13 07:10:25 +02:00
Matthias
3c9be47236 ruff format: commands 2024-05-13 07:10:25 +02:00
Matthias
5eb4ad2208 Ruff format edge 2024-05-13 07:10:25 +02:00
Matthias
794e30fedb ruff format: update enums 2024-05-13 07:10:25 +02:00
Matthias
1a4bff7fb8 ruff format freqtrade/resolvers 2024-05-13 07:10:25 +02:00
Matthias
9121d3af65 ruff format: update scripts 2024-05-13 07:10:25 +02:00
Matthias
dc3a3d1cf9 ruff format: udpate build_helpers 2024-05-13 07:10:25 +02:00
Matthias
15f32be176 ruff format: update ft_client 2024-05-13 07:10:25 +02:00
Matthias
4f5bf632fc ruff format: remaining tests 2024-05-13 07:10:25 +02:00
Matthias
d761bd8cec ruff format: tests/freqtradebot 2024-05-13 07:10:25 +02:00
Matthias
644f120ab2 ruff format: tests/hyperopt 2024-05-13 07:10:25 +02:00
Matthias
02075b15e3 ruff format: update more tests 2024-05-13 07:10:24 +02:00
Matthias
40e161a5b9 ruff format: freqai tests 2024-05-13 07:10:24 +02:00
Matthias
ffd49e0e59 ruff format: tests/data 2024-05-13 07:10:24 +02:00
Matthias
d8a8b5c125 ruff format: Update more test files 2024-05-13 07:10:24 +02:00
Matthias
ca1fe06035 ruff format: tests/plugins 2024-05-13 07:10:24 +02:00
Matthias
5a94817721 ruff format: tests/exchange 2024-05-13 07:10:24 +02:00
Matthias
c8626d9412 ruff format: Update tests/exchange 2024-05-13 07:10:24 +02:00
Matthias
e4796fd85b ruff format: update testcommands 2024-05-13 07:10:24 +02:00
Matthias
adeb93dc9c ruff format: update strategy tests 2024-05-13 07:10:24 +02:00
Matthias
1cbd49fd4e ruff format: rpc tests 2024-05-13 07:10:24 +02:00
Matthias
8c7d80b78e ruff format: Update test strategies 2024-05-13 07:10:24 +02:00
Matthias
099b1fc8c4 ruff format: More updates to tests 2024-05-13 07:10:24 +02:00
Matthias
23427bec08 ruff format: Update tests/ base directory 2024-05-13 07:10:24 +02:00
Matthias
53947732a0 ruff format: Update conftest_trades files 2024-05-13 07:10:24 +02:00
Matthias
7090950db6 ruff format: Update a few test files 2024-05-13 07:10:24 +02:00
Matthias
baa15f6ed6 Setup known first party modules 2024-05-13 07:10:24 +02:00
Matthias
a8eabd0b2e Update remaining files with new import sorting 2024-05-13 07:10:24 +02:00
Matthias
7767ad9d6e Update imports in test directory 2024-05-13 07:10:24 +02:00
Matthias
38c69e9258 Update isort configuration 2024-05-13 07:10:24 +02:00
Matthias
c8ebaef936 Update isort config 2024-05-13 07:10:24 +02:00
Matthias
bc0f0b4845 Merge pull request #10206 from freqtrade/dependabot/pip/develop/pre-commit-3.7.1
Bump pre-commit from 3.7.0 to 3.7.1
2024-05-13 07:09:03 +02:00
dependabot[bot]
462dff67d7 Bump ccxt from 4.3.16 to 4.3.21
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.16 to 4.3.21.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.3.16...4.3.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 04:25:02 +00:00
dependabot[bot]
0b64eca9df Bump pre-commit from 3.7.0 to 3.7.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.7.0 to 3.7.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.7.0...v3.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 04:24:33 +00:00
Matthias
acddcbe4a9 Merge pull request #10205 from freqtrade/dependabot/pip/develop/cryptography-42.0.7
Bump cryptography from 42.0.5 to 42.0.7
2024-05-13 06:24:15 +02:00
Matthias
993190eade Merge pull request #10203 from freqtrade/dependabot/pip/develop/ruff-0.4.4
Bump ruff from 0.4.3 to 0.4.4
2024-05-13 06:23:29 +02:00
Matthias
0f551a1df7 Merge pull request #10202 from freqtrade/dependabot/pip/develop/mkdocs-7fb61c50a2
Bump mkdocs-material from 9.5.21 to 9.5.22 in the mkdocs group
2024-05-13 06:23:17 +02:00
dependabot[bot]
feb398ecf1 Bump cryptography from 42.0.5 to 42.0.7
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.5 to 42.0.7.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.5...42.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 03:02:27 +00:00
dependabot[bot]
891b436b03 Bump ruff from 0.4.3 to 0.4.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.3 to 0.4.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/v0.4.3...v0.4.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 03:02:03 +00:00
dependabot[bot]
01b00ba375 Bump mkdocs-material from 9.5.21 to 9.5.22 in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 03:01:53 +00:00
Matthias
63c8eae4a5 Remove unused fixtures 2024-05-12 09:33:39 +02:00
Matthias
eb8ce5b304 Split too long strings in test 2024-05-12 09:32:51 +02:00
Matthias
f52c3677ca Move test comment out of the test data 2024-05-12 09:30:34 +02:00
Matthias
e4881580fd Slightly extend background jobs api 2024-05-12 09:12:53 +02:00
Matthias
0279cf5fed Improved API endpoint ordering 2024-05-12 09:04:03 +02:00
Matthias
1989973439 Merge pull request #10199 from freqtrade/fix/classifier-bug
fix: allow classifiers to work
2024-05-12 08:29:01 +02:00
robcaulk
2d069d6156 fix: allow classifiers to work 2024-05-11 16:21:15 +02:00
Matthias
42705374d0 Merge pull request #10198 from stash86/bt-metrics
remove duplicate stat from BT table
2024-05-11 08:32:25 +02:00
Stefano Ariestasia
4c2586b3aa remove duplicate stat from BT table 2024-05-11 10:24:55 +09:00
Matthias
15c56e55c1 Fix test directory pollution 2024-05-09 20:40:43 +02:00
Matthias
b8d6221f51 Merge pull request #10195 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-05-09 20:04:12 +02:00
Matthias
15bcba9c7e Skip load_leverage_tiers test from bybit 2024-05-09 19:49:52 +02:00
Matthias
e86a0736f3 Add workaround for bybit's changed markets endpoint
closes #10196
2024-05-09 19:42:20 +02:00
xmatthias
ce6445f6b5 chore: update pre-commit hooks 2024-05-09 03:02:41 +00:00
Matthias
f7e691548c Merge pull request #10189 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-05-07 06:21:12 +02:00
xmatthias
6d668d52fd chore: update pre-commit hooks 2024-05-07 03:04:53 +00:00
Matthias
9fd61b7677 Merge pull request #10185 from freqtrade/dependabot/pip/develop/sqlalchemy-2.0.30
Bump sqlalchemy from 2.0.29 to 2.0.30
2024-05-06 13:49:40 +02:00
Matthias
8343c50fe7 Merge pull request #10182 from freqtrade/dependabot/pip/develop/jinja2-3.1.4
Bump jinja2 from 3.1.3 to 3.1.4
2024-05-06 11:35:41 +02:00
Matthias
bab7f5584e Merge pull request #10177 from freqtrade/dependabot/pip/develop/coveralls-4.0.0
Bump coveralls from 3.3.1 to 4.0.0
2024-05-06 11:15:41 +02:00
Matthias
fa1c3b6f3b Merge pull request #10176 from freqtrade/dependabot/pip/develop/plotly-5.22.0
Bump plotly from 5.21.0 to 5.22.0
2024-05-06 10:32:41 +02:00
Matthias
bb3084d868 Merge pull request #10187 from freqtrade/dependabot/pip/develop/nbconvert-7.16.4
Bump nbconvert from 7.16.3 to 7.16.4
2024-05-06 10:09:00 +02:00
Matthias
f961eb62e2 Merge pull request #10186 from freqtrade/dependabot/pip/develop/tqdm-4.66.4
Bump tqdm from 4.66.3 to 4.66.4
2024-05-06 09:44:43 +02:00
Matthias
9bc3049af9 Bump SQLAlchemy pre-commit 2024-05-06 09:18:10 +02:00
dependabot[bot]
bd5bf255e6 Bump sqlalchemy from 2.0.29 to 2.0.30
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.29 to 2.0.30.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 07:11:09 +00:00
Matthias
de3e53b17a Merge pull request #10184 from freqtrade/dependabot/pip/develop/ccxt-4.3.16
Bump ccxt from 4.3.11 to 4.3.16
2024-05-06 09:08:30 +02:00
Matthias
6e2f020ad2 Merge pull request #10183 from freqtrade/dependabot/pip/develop/fastapi-0.111.0
Bump fastapi from 0.110.2 to 0.111.0
2024-05-06 08:30:19 +02:00
Matthias
b9997b7024 Merge pull request #10181 from freqtrade/dependabot/pip/develop/filelock-3.14.0
Bump filelock from 3.13.4 to 3.14.0
2024-05-06 08:08:15 +02:00
Matthias
906b566eff Merge pull request #10180 from freqtrade/dependabot/pip/develop/jsonschema-4.22.0
Bump jsonschema from 4.21.1 to 4.22.0
2024-05-06 08:00:42 +02:00
Matthias
56b40c7294 Merge pull request #10179 from freqtrade/dependabot/pip/develop/orjson-3.10.3
Bump orjson from 3.10.1 to 3.10.3
2024-05-06 07:45:55 +02:00
dependabot[bot]
72e53eee5c Bump jinja2 from 3.1.3 to 3.1.4
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 05:35:12 +00:00
Matthias
9248b24053 Merge pull request #10175 from freqtrade/dependabot/pip/develop/joblib-1.4.2
Bump joblib from 1.4.0 to 1.4.2
2024-05-06 07:34:30 +02:00
Matthias
c2592cf65c Merge pull request #10174 from freqtrade/dependabot/pip/develop/mkdocs-8d79d83046
Bump mkdocs-material from 9.5.19 to 9.5.21 in the mkdocs group
2024-05-06 07:34:04 +02:00
dependabot[bot]
d4755bd7c0 Bump coveralls from 3.3.1 to 4.0.0
Bumps [coveralls](https://github.com/TheKevJames/coveralls-python) from 3.3.1 to 4.0.0.
- [Release notes](https://github.com/TheKevJames/coveralls-python/releases)
- [Changelog](https://github.com/TheKevJames/coveralls-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TheKevJames/coveralls-python/compare/3.3.1...4.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 04:37:24 +00:00
Matthias
6a2022fc5b Merge pull request #10178 from freqtrade/dependabot/pip/develop/ruff-0.4.3
Bump ruff from 0.4.2 to 0.4.3
2024-05-06 06:35:48 +02:00
dependabot[bot]
aaa190e7d9 Bump nbconvert from 7.16.3 to 7.16.4
Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.16.3 to 7.16.4.
- [Release notes](https://github.com/jupyter/nbconvert/releases)
- [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter/nbconvert/compare/v7.16.3...v7.16.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:08:56 +00:00
dependabot[bot]
beffebcbbe Bump tqdm from 4.66.3 to 4.66.4
Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.3 to 4.66.4.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.66.3...v4.66.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:08:52 +00:00
dependabot[bot]
a65a601b1e Bump ccxt from 4.3.11 to 4.3.16
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.3.11 to 4.3.16.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.3.11...4.3.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:08:27 +00:00
dependabot[bot]
b81bf59997 Bump fastapi from 0.110.2 to 0.111.0
Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.110.2 to 0.111.0.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.110.2...0.111.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:08:20 +00:00
dependabot[bot]
2e2949555f Bump filelock from 3.13.4 to 3.14.0
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.13.4 to 3.14.0.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.13.4...3.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:08:10 +00:00
dependabot[bot]
43c327148a Bump jsonschema from 4.21.1 to 4.22.0
Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.21.1 to 4.22.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.21.1...v4.22.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:08:05 +00:00
dependabot[bot]
a9a04ba3ba Bump orjson from 3.10.1 to 3.10.3
Bumps [orjson](https://github.com/ijl/orjson) from 3.10.1 to 3.10.3.
- [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.1...3.10.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:07:58 +00:00
dependabot[bot]
96fbe160df Bump ruff from 0.4.2 to 0.4.3
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.2 to 0.4.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/v0.4.2...v0.4.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:07:52 +00:00
dependabot[bot]
955f5792c7 Bump plotly from 5.21.0 to 5.22.0
Bumps [plotly](https://github.com/plotly/plotly.py) from 5.21.0 to 5.22.0.
- [Release notes](https://github.com/plotly/plotly.py/releases)
- [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.py/compare/v5.21.0...v5.22.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:07:39 +00:00
dependabot[bot]
a1c4be1e3b Bump joblib from 1.4.0 to 1.4.2
Bumps [joblib](https://github.com/joblib/joblib) from 1.4.0 to 1.4.2.
- [Release notes](https://github.com/joblib/joblib/releases)
- [Changelog](https://github.com/joblib/joblib/blob/main/CHANGES.rst)
- [Commits](https://github.com/joblib/joblib/compare/1.4.0...1.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:07:34 +00:00
dependabot[bot]
187397540e Bump mkdocs-material from 9.5.19 to 9.5.21 in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 03:07:28 +00:00
Matthias
39613c0785 no suspect function calls in function headers . . . 2024-05-05 19:55:21 +02:00
Matthias
3f9019a1ad Don't use coro directly 2024-05-05 19:55:02 +02:00
Matthias
c3516dbba6 Simplify trade_statistics function 2024-05-05 16:56:00 +02:00
Matthias
fa79c48c8f Exclude unfilled Trades from "all" /profit
These are not actual profits, as it's unclear if the order
will be filled or will be canceled.

Discovered as part of #10165
2024-05-05 16:48:42 +02:00
Matthias
28449f551a Don't show "0" when fiat_currency is empty 2024-05-05 16:48:42 +02:00
Matthias
566add7a8b Rename variable to show it's just a temporary variable 2024-05-05 16:12:22 +02:00
Matthias
7ba285fbbb Fix bad link 2024-05-05 09:45:25 +02:00
Matthias
936a1b73db Merge pull request #10169 from freqtrade/fix/issue_10166
Improve backtest behavior with adjust_trade_position
2024-05-05 09:40:29 +02:00
Matthias
a31be687d1 Merge pull request #10171 from freqtrade/robcaulk-patch-1
Bring back PCA doc
2024-05-04 18:03:43 +02:00
Robert Caulk
93e65a583f Update freqai-feature-engineering.md 2024-05-04 17:14:36 +02:00
Matthias
643bfa065c Add documentation for freqUI backtest mode 2024-05-04 17:11:46 +02:00
Matthias
8309d92cef Improve freqUI docs 2024-05-04 17:11:46 +02:00
Matthias
acb6dacf2f Add light and dark Screenshots of freqUI 2024-05-04 17:11:46 +02:00
Matthias
ccb1d59a22 Add main header about UI 2024-05-04 17:11:46 +02:00
Matthias
4e5a620364 Add a few screenshots of freqUI 2024-05-04 17:11:46 +02:00
Matthias
9f1ebf0c50 Extract section about CORS to it's own icnlude section 2024-05-04 17:11:46 +02:00
Matthias
8dd6b52be2 Make sure freqUI is visible in the menu 2024-05-04 17:11:46 +02:00
Matthias
74732537b8 Add explicit documentation page for freqUI 2024-05-04 17:11:46 +02:00
Matthias
866f059d6a Use FtPrecise to avoid rounding errors 2024-05-04 11:25:07 +02:00
Matthias
ab93fd3be4 Enhance trade to verify #10166 2024-05-04 11:21:25 +02:00
Matthias
ee7be1cd5a move "add_bt_trade" call for entries into enter_trade function 2024-05-04 09:14:56 +02:00
Matthias
c81c07c24a Add docstring for process_exit_order 2024-05-04 09:07:56 +02:00
Matthias
67636abb30 Fix #10166 with fewer side-effects 2024-05-04 09:01:05 +02:00
Matthias
e5b79eee5a Extract _process_exit_order to separate function 2024-05-04 09:00:46 +02:00
Matthias
62a3ed6f8d partial exit order should not close immediately
closes #10166
2024-05-04 08:41:24 +02:00
Matthias
19edee9123 Merge pull request #10167 from freqtrade/dependabot/pip/tqdm-4.66.3
Bump tqdm from 4.66.2 to 4.66.3
2024-05-04 08:02:31 +02:00
dependabot[bot]
dd42fba7dc Bump tqdm from 4.66.2 to 4.66.3
Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.2 to 4.66.3.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.66.2...v4.66.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-03 21:59:46 +00:00
Matthias
e2a9bc9c64 Merge pull request #10164 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-05-02 06:31:14 +02:00
xmatthias
569e8a74b0 chore: update pre-commit hooks 2024-05-02 03:02:33 +00:00
Matthias
c534d47c12 Merge pull request #10138 from freqtrade/backtest_max_fee
Backtest max fee
2024-04-30 15:33:42 +02:00
Matthias
7bb4b5003d Bump version to 2024.5-dev 2024-04-30 11:46:43 +02:00
Matthias
997db6c706 Type-ignore
we can't type variables of the list-comprehension ...
2024-04-27 19:59:53 +02:00
Matthias
f259270e9c Update tests to properly mock fee 2024-04-27 19:52:48 +02:00
Matthias
3a2e3215b9 Ensure get_fee returns something in tests 2024-04-27 18:26:43 +02:00
Matthias
3f2f2a1dbd Use worst case of maker / taker fee for backtest 2024-04-27 18:26:23 +02:00
Matthias
935e8f49de Type-check fee from configuration ... 2024-04-27 15:36:26 +02:00
Simon Waiblinger
060198c04c Merge branch 'freqtrade:develop' into develop 2024-01-25 22:28:05 +01:00
simwai
44856eedb2 Merge branch 'develop' of https://github.com/simwai/freqtrade into develop 2024-01-08 13:40:34 +01:00
simwai
5110c14d35 Updated gitignore file 2024-01-08 13:40:32 +01:00
440 changed files with 48794 additions and 38051 deletions

View File

@@ -18,15 +18,22 @@
"editor.insertSpaces": true, "editor.insertSpaces": true,
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"[markdown]": { "[markdown]": {
"files.trimTrailingWhitespace": false, "files.trimTrailingWhitespace": false
}, },
"python.pythonPath": "/usr/local/bin/python", "python.pythonPath": "/usr/local/bin/python",
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
}
}, },
// Add the IDs of extensions you want installed when the container is created. // Add the IDs of extensions you want installed when the container is created.
"extensions": [ "extensions": [
"ms-python.python", "ms-python.python",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"ms-python.isort", "charliermarsh.ruff",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker", "ms-azuretools.vscode-docker",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",

View File

@@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Install ccxt - name: Install ccxt
run: pip install ccxt run: pip install ccxt

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ ubuntu-20.04, ubuntu-22.04 ] os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
python-version: ["3.9", "3.10", "3.11", "3.12"] python-version: ["3.9", "3.10", "3.11", "3.12"]
steps: steps:
@@ -55,7 +55,7 @@ jobs:
- name: Installation - *nix - name: Installation - *nix
run: | run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade "pip<=24.0" wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
@@ -111,7 +111,11 @@ jobs:
- name: Run Ruff - name: Run Ruff
run: | run: |
ruff check --output-format=github . ruff check --output-format=github
- name: Run Ruff format check
run: |
ruff format --check
- name: Mypy - name: Mypy
run: | run: |
@@ -188,7 +192,7 @@ jobs:
- name: Installation (python) - name: Installation (python)
run: | run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade "pip<=24.0" wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
@@ -230,7 +234,11 @@ jobs:
- name: Run Ruff - name: Run Ruff
run: | run: |
ruff check --output-format=github . ruff check --output-format=github
- name: Run Ruff format check
run: |
ruff format --check
- name: Mypy - name: Mypy
run: | run: |
@@ -300,12 +308,27 @@ jobs:
- name: Run Ruff - name: Run Ruff
run: | run: |
ruff check --output-format=github . ruff check --output-format=github
- name: Run Ruff format check
run: |
ruff format --check
- name: Mypy - name: Mypy
run: | run: |
mypy freqtrade scripts tests mypy freqtrade scripts tests
- name: Run Pester tests (PowerShell)
run: |
$PSVersionTable
Set-PSRepository psgallery -InstallationPolicy trusted
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
$Error.clear()
Invoke-Pester -Path "tests" -CI
if ($Error.Length -gt 0) {exit 1}
shell: powershell
- name: Discord notification - name: Discord notification
uses: rjstone/discord-webhook-notify@v1 uses: rjstone/discord-webhook-notify@v1
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
@@ -322,7 +345,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.12"
- name: pre-commit dependencies - name: pre-commit dependencies
run: | run: |
@@ -336,7 +359,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.10" python-version: "3.12"
- uses: pre-commit/action@v3.0.1 - uses: pre-commit/action@v3.0.1
docs-check: docs-check:
@@ -351,7 +374,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Documentation build - name: Documentation build
run: | run: |
@@ -377,7 +400,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Cache_dependencies - name: Cache_dependencies
uses: actions/cache@v4 uses: actions/cache@v4
@@ -399,7 +422,7 @@ jobs:
- name: Installation - *nix - name: Installation - *nix
run: | run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade "pip<=24.0" wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
@@ -459,7 +482,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Build distribution - name: Build distribution
run: | run: |
@@ -510,12 +533,12 @@ jobs:
- name: Publish to PyPI (Test) - name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.14 uses: pypa/gh-action-pypi-publish@v1.9.0
with: with:
repository-url: https://test.pypi.org/legacy/ repository-url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.14 uses: pypa/gh-action-pypi-publish@v1.9.0
deploy-docker: deploy-docker:
@@ -530,7 +553,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Extract branch name - name: Extract branch name
id: extract-branch id: extract-branch
@@ -553,12 +576,12 @@ jobs:
sudo systemctl restart docker sudo systemctl restart docker
docker version -f '{{.Server.Experimental}}' docker version -f '{{.Server.Experimental}}'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: crazy-max/ghaction-docker-buildx@v3.3.1 uses: docker/setup-buildx-action@v3
with:
buildx-version: latest
qemu-version: latest
- name: Available platforms - name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }} run: echo ${{ steps.buildx.outputs.platforms }}

View File

@@ -2,6 +2,8 @@ name: Devcontainer Pre-Build
on: on:
workflow_dispatch: workflow_dispatch:
schedule:
- cron: "0 3 * * 0"
# push: # push:
# branches: # branches:
# - "master" # - "master"

View File

@@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Install pre-commit - name: Install pre-commit
@@ -26,9 +26,6 @@ jobs:
- name: Run auto-update - name: Run auto-update
run: pre-commit autoupdate run: pre-commit autoupdate
- name: Run pre-commit
run: pre-commit run --all-files
- uses: peter-evans/create-pull-request@v6 - uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.REPO_SCOPED_TOKEN }} token: ${{ secrets.REPO_SCOPED_TOKEN }}

View File

@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: "7.0.0" rev: "7.1.0"
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [Flake8-pyproject] additional_dependencies: [Flake8-pyproject]
@@ -16,10 +16,10 @@ repos:
additional_dependencies: additional_dependencies:
- types-cachetools==5.3.0.7 - types-cachetools==5.3.0.7
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.31.0.20240406 - types-requests==2.32.0.20240622
- types-tabulate==0.9.0.20240106 - types-tabulate==0.9.0.20240106
- types-python-dateutil==2.9.0.20240316 - types-python-dateutil==2.9.0.20240316
- SQLAlchemy==2.0.29 - SQLAlchemy==2.0.31
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
@@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.4.2' rev: 'v0.4.10'
hooks: hooks:
- id: ruff - id: ruff
@@ -56,7 +56,7 @@ repos:
)$ )$
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.2.6 rev: v2.3.0
hooks: hooks:
- id: codespell - id: codespell
additional_dependencies: additional_dependencies:

11
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker",
"vscode-icons-team.vscode-icons",
"github.vscode-github-actions",
]
}

View File

@@ -72,12 +72,12 @@ you can manually run pre-commit with `pre-commit run -a`.
mypy freqtrade mypy freqtrade
``` ```
### 4. Ensure all imports are correct ### 4. Ensure formatting is correct
#### Run isort #### Run ruff
``` bash ``` bash
isort . ruff format .
``` ```
## (Core)-Committer Guide ## (Core)-Committer Guide

View File

@@ -1,4 +1,4 @@
FROM python:3.12.3-slim-bookworm as base FROM python:3.12.4-slim-bookworm as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
@@ -25,7 +25,7 @@ FROM base as python-deps
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \ && apt-get clean \
&& pip install --upgrade pip wheel && pip install --upgrade "pip<=24.0" wheel
# Install TA-lib # Install TA-lib
COPY build_helpers/* /tmp/ COPY build_helpers/* /tmp/
@@ -35,7 +35,7 @@ ENV LD_LIBRARY_PATH /usr/local/lib
# Install dependencies # Install dependencies
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
USER ftuser USER ftuser
RUN pip install --user --no-cache-dir numpy \ RUN pip install --user --no-cache-dir "numpy<2.0" \
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt && pip install --user --no-cache-dir -r requirements-hyperopt.txt
# Copy dependencies to runtime-image # Copy dependencies to runtime-image

View File

@@ -29,6 +29,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/)
- [X] [Bitmart](https://bitmart.com/) - [X] [Bitmart](https://bitmart.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi) - [X] [HTX](https://www.htx.com/) (Former Huobi)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,21 +6,18 @@ from pathlib import Path
import ccxt import ccxt
key = os.environ.get('FREQTRADE__EXCHANGE__KEY') key = os.environ.get("FREQTRADE__EXCHANGE__KEY")
secret = os.environ.get('FREQTRADE__EXCHANGE__SECRET') secret = os.environ.get("FREQTRADE__EXCHANGE__SECRET")
proxy = os.environ.get('CI_WEB_PROXY') proxy = os.environ.get("CI_WEB_PROXY")
exchange = ccxt.binance({ exchange = ccxt.binance(
'apiKey': key, {"apiKey": key, "secret": secret, "httpsProxy": proxy, "options": {"defaultType": "swap"}}
'secret': secret, )
'httpsProxy': proxy,
'options': {'defaultType': 'swap'}
})
_ = exchange.load_markets() _ = exchange.load_markets()
lev_tiers = exchange.fetch_leverage_tiers() lev_tiers = exchange.fetch_leverage_tiers()
# Assumes this is running in the root of the repository. # Assumes this is running in the root of the repository.
file = Path('freqtrade/exchange/binance_leverage_tiers.json') file = Path("freqtrade/exchange/binance_leverage_tiers.json")
json.dump(dict(sorted(lev_tiers.items())), file.open('w'), indent=2) json.dump(dict(sorted(lev_tiers.items())), file.open("w"), indent=2)

View File

@@ -1,18 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from freqtrade_client import __version__ as client_version
from freqtrade import __version__ as ft_version from freqtrade import __version__ as ft_version
from freqtrade_client import __version__ as client_version
def main(): def main():
if ft_version != client_version: if ft_version != client_version:
print(f"Versions do not match: \n" print(f"Versions do not match: \nft: {ft_version} \nclient: {client_version}")
f"ft: {ft_version} \n"
f"client: {client_version}")
exit(1) exit(1)
print(f"Versions match: ft: {ft_version}, client: {client_version}") print(f"Versions match: ft: {ft_version}, client: {client_version}")
exit(0) exit(0)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -1,6 +1,6 @@
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040 # vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
python -m pip install --upgrade pip wheel python -m pip install --upgrade "pip<=24.0" wheel
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"

View File

@@ -6,28 +6,30 @@ from pathlib import Path
import yaml import yaml
pre_commit_file = Path('.pre-commit-config.yaml') pre_commit_file = Path(".pre-commit-config.yaml")
require_dev = Path('requirements-dev.txt') require_dev = Path("requirements-dev.txt")
require = Path('requirements.txt') require = Path("requirements.txt")
with require_dev.open('r') as rfile: with require_dev.open("r") as rfile:
requirements = rfile.readlines() requirements = rfile.readlines()
with require.open('r') as rfile: with require.open("r") as rfile:
requirements.extend(rfile.readlines()) requirements.extend(rfile.readlines())
# Extract types only # Extract types only
type_reqs = [r.strip('\n') for r in requirements if r.startswith( type_reqs = [
'types-') or r.startswith('SQLAlchemy')] r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
]
with pre_commit_file.open('r') as file: with pre_commit_file.open("r") as file:
f = yaml.load(file, Loader=yaml.SafeLoader) f = yaml.load(file, Loader=yaml.SafeLoader)
mypy_repo = [repo for repo in f['repos'] if repo['repo'] mypy_repo = [
== 'https://github.com/pre-commit/mirrors-mypy'] repo for repo in f["repos"] if repo["repo"] == "https://github.com/pre-commit/mirrors-mypy"
]
hooks = mypy_repo[0]['hooks'][0]['additional_dependencies'] hooks = mypy_repo[0]["hooks"][0]["additional_dependencies"]
errors = [] errors = []
for hook in hooks: for hook in hooks:

View File

@@ -17,7 +17,7 @@ RUN mkdir /freqtrade \
&& chown ftuser:ftuser /freqtrade \ && chown ftuser:ftuser /freqtrade \
# Allow sudoers # Allow sudoers
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \ && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
&& pip install --upgrade pip && pip install --upgrade "pip<=24.0"
WORKDIR /freqtrade WORKDIR /freqtrade
@@ -35,7 +35,7 @@ COPY build_helpers/* /tmp/
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/ COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
USER ftuser USER ftuser
RUN pip install --user --no-cache-dir numpy \ RUN pip install --user --no-cache-dir numpy \
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib==0.4.28 \ && pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
&& pip install --user --no-cache-dir -r requirements.txt && pip install --user --no-cache-dir -r requirements.txt
# Copy dependencies to runtime-image # Copy dependencies to runtime-image

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -253,36 +253,36 @@ A backtesting result will look like that:
``` ```
================================================ BACKTESTING REPORT ================================================= ================================================ BACKTESTING REPORT =================================================
| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | | Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|:---------|--------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| |----------+--------+----------------+------------------+----------------+--------------+--------------------------|
| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | | ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | | ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | | BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | | DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | | ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | | EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | | ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | | ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | | IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | | LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | | LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | | NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | | NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | | REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | | XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | | XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | | XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | | ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | | TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
============================================= LEFT OPEN TRADES REPORT ============================================= ============================================= LEFT OPEN TRADES REPORT =============================================
| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | | Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|:---------|---------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| |----------+---------+----------------+------------------+----------------+----------------+---------------------|
| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | | ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | | LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | | TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
==================== EXIT REASON STATS ==================== ==================== EXIT REASON STATS ====================
| Exit Reason | Exits | Wins | Draws | Losses | | Exit Reason | Exits | Wins | Draws | Losses |
|:-------------------|--------:|------:|-------:|--------:| |--------------------+---------+-------+--------+---------|
| trailing_stop_loss | 205 | 150 | 0 | 55 | | trailing_stop_loss | 205 | 150 | 0 | 55 |
| stop_loss | 166 | 0 | 0 | 166 | | stop_loss | 166 | 0 | 0 | 166 |
| exit_signal | 56 | 36 | 0 | 20 | | exit_signal | 56 | 36 | 0 | 20 |
@@ -631,10 +631,10 @@ Detailed output for all strategies one after the other will be available, so mak
``` ```
================================================== STRATEGY SUMMARY =================================================================== ================================================== STRATEGY SUMMARY ===================================================================
| Strategy | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | | Strategy | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % |
|:------------|---------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| |-------------+---------+----------------+------------------+----------------+----------------+-------+--------+--------+------------|
| Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | | Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 |
| Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | | Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
``` ```
## Next step ## Next step

View File

@@ -568,7 +568,14 @@ The possible values are: `GTC` (default), `FOK` or `IOC`.
This is ongoing work. For now, it is supported only for binance, gate and kucoin. This is ongoing work. For now, it is supported only for binance, gate and kucoin.
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange. Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
### What values can be used for fiat_display_currency? ### Fiat conversion
Freqtrade uses the Coingecko API to convert the coin value to it's corresponding fiat value for the Telegram reports.
The FIAT currency can be set in the configuration file as `fiat_display_currency`.
Removing `fiat_display_currency` completely from the configuration will skip initializing coingecko, and will not show any FIAT currency conversion. This has no importance for the correct functioning of the bot.
#### What values can be used for fiat_display_currency?
The `fiat_display_currency` configuration parameter sets the base currency to use for the The `fiat_display_currency` configuration parameter sets the base currency to use for the
conversion from coin to fiat in the bot Telegram reports. conversion from coin to fiat in the bot Telegram reports.
@@ -587,7 +594,25 @@ The valid values are:
"BTC", "ETH", "XRP", "LTC", "BCH", "BNB" "BTC", "ETH", "XRP", "LTC", "BCH", "BNB"
``` ```
Removing `fiat_display_currency` completely from the configuration will skip initializing coingecko, and will not show any FIAT currency conversion. This has no importance for the correct functioning of the bot. #### Coingecko Rate limit problems
On some IP ranges, coingecko is heavily rate-limiting.
In such cases, you may want to add your coingecko API key to the configuration.
``` json
{
"fiat_display_currency": "USD",
"coingecko": {
"api_key": "your-api",
"is_demo": true
}
}
```
Freqtrade supports both Demo and Pro coingecko API keys.
The Coingecko API key is NOT required for the bot to function correctly.
It is only used for the conversion of coin to fiat in the Telegram reports, which usually also work without API key.
## Using Dry-run mode ## Using Dry-run mode

View File

@@ -24,10 +24,10 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--days INT] [--new-pairs-days INT] [--days INT] [--new-pairs-days INT]
[--include-inactive-pairs] [--include-inactive-pairs]
[--timerange TIMERANGE] [--dl-trades] [--timerange TIMERANGE] [--dl-trades]
[--exchange EXCHANGE] [--convert] [--exchange EXCHANGE]
[-t TIMEFRAMES [TIMEFRAMES ...]] [--erase] [-t TIMEFRAMES [TIMEFRAMES ...]] [--erase]
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}] [--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
[--data-format-trades {json,jsongz,hdf5,feather}] [--data-format-trades {json,jsongz,hdf5,feather,parquet}]
[--trading-mode {spot,margin,futures}] [--trading-mode {spot,margin,futures}]
[--prepend] [--prepend]
@@ -48,6 +48,11 @@ options:
--dl-trades Download trades instead of OHLCV data. The bot will --dl-trades Download trades instead of OHLCV data. The bot will
resample trades to the desired timeframe as specified resample trades to the desired timeframe as specified
as --timeframes/-t. as --timeframes/-t.
--convert Convert downloaded trades to OHLCV data. Only
applicable in combination with `--dl-trades`. Will be
automatic for exchanges which don't have historic
OHLCV (e.g. Kraken). If not provided, use `trades-to-
ohlcv` to convert trades data to OHLCV data.
--exchange EXCHANGE Exchange name. Only valid if no config is provided. --exchange EXCHANGE Exchange name. Only valid if no config is provided.
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...] -t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
Specify which tickers to download. Space-separated Specify which tickers to download. Space-separated
@@ -57,7 +62,7 @@ options:
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet} --data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded candle (OHLCV) data. Storage format for downloaded candle (OHLCV) data.
(default: `feather`). (default: `feather`).
--data-format-trades {json,jsongz,hdf5,feather} --data-format-trades {json,jsongz,hdf5,feather,parquet}
Storage format for downloaded trades data. (default: Storage format for downloaded trades data. (default:
`feather`). `feather`).
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures} --trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
@@ -471,15 +476,20 @@ ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
## Trades (tick) data ## Trades (tick) data
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API. By default, `download-data` sub-command downloads Candles (OHLCV) data. Most exchanges also provide historic trade-data via their API.
This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes. This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes.
Since this data is large by default, the files use the feather fileformat by default. They are stored in your data-directory with the naming convention of `<pair>-trades.feather` (`ETH_BTC-trades.feather`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository. Since this data is large by default, the files use the feather file format by default. They are stored in your data-directory with the naming convention of `<pair>-trades.feather` (`ETH_BTC-trades.feather`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository.
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally. To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades.
If `--convert` is also provided, the resample step will happen automatically and overwrite eventually existing OHLCV data for the given pair/timeframe combinations.
!!! Warning "do not use" !!! Warning "Do not use"
You should not use this unless you're a kraken user. Most other exchanges provide OHLCV data with sufficient history. You should not use this unless you're a kraken user (Kraken does not provide historic OHLCV data).
Most other exchanges provide OHLCV data with sufficient history, so downloading multiple timeframes through that method will still proof to be a lot faster than downloading trades data.
!!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
Example call: Example call:
@@ -490,12 +500,6 @@ freqtrade download-data --exchange kraken --pairs XRP/EUR ETH/EUR --days 20 --dl
!!! Note !!! Note
While this method uses async calls, it will be slow, since it requires the result of the previous call to generate the next request to the exchange. While this method uses async calls, it will be slow, since it requires the result of the previous call to generate the next request to the exchange.
!!! Warning
The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading.
!!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
## Next step ## Next step
Great, you now have backtest data downloaded, so you can now start [backtesting](backtesting.md) your strategy. Great, you now have some data downloaded, so you can now start [backtesting](backtesting.md) your strategy.

View File

@@ -127,6 +127,13 @@ These settings will be checked on startup, and freqtrade will show an error if t
Freqtrade will not attempt to change these settings. Freqtrade will not attempt to change these settings.
## Bingx
BingX supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
!!! Tip "Stoploss on Exchange"
Bingx supports `stoploss_on_exchange` and can use both stop-limit and stop-market orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
## Kraken ## Kraken
Kraken supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "IOC" (immediate-or-cancel) and "PO" (Post only) settings. Kraken supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.

85
docs/freq-ui.md Normal file
View File

@@ -0,0 +1,85 @@
# FreqUI
Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade frontend.
By default, the UI is automatically installed as part of the installation (script, docker).
freqUI can also be manually installed by using the `freqtrade install-ui` command.
This same command can also be used to update freqUI to new new releases.
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured API port (by default `http://127.0.0.1:8080`).
??? Note "Looking to contribute to freqUI?"
Developers should not use this method, but instead clone the corresponding use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. A working installation of node will be required to build the frontend.
!!! tip "freqUI is not required to run freqtrade"
freqUI is an optional component of freqtrade, and is not required to run the bot.
It is a frontend that can be used to monitor the bot and to interact with it - but freqtrade itself will work perfectly fine without it.
## Configuration
FreqUI does not have it's own configuration file - but assumes a working setup for the [rest-api](rest-api.md) is available.
Please refer to the corresponding documentation page to get setup with freqUI
## UI
FreqUI is a modern, responsive web application that can be used to monitor and interact with your bot.
FreqUI provides a light, as well as a dark theme.
Themes can be easily switched via a prominent button at the top of the page.
The theme of the screenshots on this page will adapt to the selected documentation Theme, so to see the dark (or light) version, please switch the theme of the Documentation.
### Login
The below screenshot shows the login screen of freqUI.
![FreqUI - login](assets/frequi-login-CORS.png#only-dark)
![FreqUI - login](assets/frequi-login-CORS-light.png#only-light)
!!! Hint "CORS"
The Cors error shown in this screenshot is due to the fact that the UI is running on a different port than the API, and [CORS](#cors) has not been setup correctly yet.
### Trade view
The trade view allows you to visualize the trades that the bot is making and to interact with the bot.
On this page, you can also interact with the bot by starting and stopping it and - if configured - force trade entries and exits.
![FreqUI - trade view](assets/freqUI-trade-pane-dark.png#only-dark)
![FreqUI - trade view](assets/freqUI-trade-pane-light.png#only-light)
### Plot Configurator
FreqUI Plots can be configured either via a `plot_config` configuration object in the strategy (which can be loaded via "from strategy" button) or via the UI.
Multiple plot configurations can be created and switched at will - allowing for flexible, different views into your charts.
The plot configuration can be accessed via the "Plot Configurator" (Cog icon) button in the top right corner of the trade view.
![FreqUI - plot configuration](assets/freqUI-plot-configurator-dark.png#only-dark)
![FreqUI - plot configuration](assets/freqUI-plot-configurator-light.png#only-light)
### Settings
Several UI related settings can be changed by accessing the settings page.
Things you can change (among others):
* Timezone of the UI
* Visualization of open trades as part of the favicon (browser tab)
* Candle colors (up/down -> red/green)
* Enable / disable in-app notification types
![FreqUI - Settings view](assets/frequi-settings-dark.png#only-dark)
![FreqUI - Settings view](assets/frequi-settings-light.png#only-light)
## Backtesting
When freqtrade is started in [webserver mode](utils.md#webserver-mode) (freqtrade started with `freqtrade webserver`), the backtesting view becomes available.
This view allows you to backtest strategies and visualize the results.
You can also load and visualize previous backtest results, as well as compare the results with each other.
![FreqUI - Backtesting](assets/freqUI-backtesting-dark.png#only-dark)
![FreqUI - Backtesting](assets/freqUI-backtesting-light.png#only-light)
--8<-- "includes/cors.md"

View File

@@ -224,7 +224,7 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points. B
## Building the data pipeline ## Building the data pipeline
By default, FreqAI builds a dynamic pipeline based on user congfiguration settings. The default settings are robust and designed to work with a variety of methods. These two steps are a `MinMaxScaler(-1,1)` and a `VarianceThreshold` which removes any column that has 0 variance. Users can activate other steps with more configuration parameters. For example if users add `use_SVM_to_remove_outliers: true` to the `freqai` config, then FreqAI will automatically add the [`SVMOutlierExtractor`](#identifying-outliers-using-a-support-vector-machine-svm) to the pipeline. Likewise, users can add `principal_component_analysis: true` to the `freqai` config to activate PCA. The [DissimilarityIndex](#identifying-outliers-with-the-dissimilarity-index-di) is activated with `DI_threshold: 1`. Finally, noise can also be added to the data with `noise_standard_deviation: 0.1`. Finally, users can add [DBSCAN](#identifying-outliers-with-dbscan) outlier removal with `use_DBSCAN_to_remove_outliers: true`. By default, FreqAI builds a dynamic pipeline based on user configuration settings. The default settings are robust and designed to work with a variety of methods. These two steps are a `MinMaxScaler(-1,1)` and a `VarianceThreshold` which removes any column that has 0 variance. Users can activate other steps with more configuration parameters. For example if users add `use_SVM_to_remove_outliers: true` to the `freqai` config, then FreqAI will automatically add the [`SVMOutlierExtractor`](#identifying-outliers-using-a-support-vector-machine-svm) to the pipeline. Likewise, users can add `principal_component_analysis: true` to the `freqai` config to activate PCA. The [DissimilarityIndex](#identifying-outliers-with-the-dissimilarity-index-di) is activated with `DI_threshold: 1`. Finally, noise can also be added to the data with `noise_standard_deviation: 0.1`. Finally, users can add [DBSCAN](#identifying-outliers-with-dbscan) outlier removal with `use_DBSCAN_to_remove_outliers: true`.
!!! note "More information available" !!! note "More information available"
Please review the [parameter table](freqai-parameter-table.md) for more information on these parameters. Please review the [parameter table](freqai-parameter-table.md) for more information on these parameters.
@@ -391,3 +391,18 @@ Given a number of data points $N$, and a distance $\varepsilon$, DBSCAN clusters
![dbscan](assets/freqai_dbscan.jpg) ![dbscan](assets/freqai_dbscan.jpg)
FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set. FreqAI uses `sklearn.cluster.DBSCAN` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html) (external website)) with `min_samples` ($N$) taken as 1/4 of the no. of time points (candles) in the feature set. `eps` ($\varepsilon$) is computed automatically as the elbow point in the *k-distance graph* computed from the nearest neighbors in the pairwise distances of all data points in the feature set.
### Data dimensionality reduction with Principal Component Analysis
You can reduce the dimensionality of your features by activating the principal_component_analysis in the config:
```json
"freqai": {
"feature_parameters" : {
"principal_component_analysis": true
}
}
```
This will perform PCA on the features and reduce their dimensionality so that the explained variance of the data set is >= 0.999. Reducing data dimensionality makes training the model faster and hence allows for more up-to-date models.

View File

@@ -36,7 +36,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1). | `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `feature_engineering_*()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer. | `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `feature_engineering_*()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer.
| `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers. | `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers.
| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis) <br> **Datatype:** Boolean. <br> Default: `False`. | `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) <br> **Datatype:** Boolean. <br> Default: `False`.
| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`. | `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`.
| `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Positive float (typically < 1). | `DI_threshold` | Activates the use of the Dissimilarity Index for outlier detection when set to > 0. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Positive float (typically < 1).
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean. | `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean.

43
docs/includes/cors.md Normal file
View File

@@ -0,0 +1,43 @@
## CORS
This whole section is only necessary in cross-origin cases (where you multiple bot API's running on `localhost:8081`, `localhost:8082`, ...), and want to combine them into one FreqUI instance.
??? info "Technical explanation"
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
Users can allow access from different origin URL's to the bot API via the `CORS_origins` configuration setting.
It consists of a list of allowed URL's that are allowed to consume resources from the bot's API.
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"CORS_origins": ["https://frequi.freqtrade.io"],
//...
}
```
In the following (pretty common) case, FreqUI is accessible on `http://localhost:8080/trade` (this is what you see in your navbar when navigating to freqUI).
![freqUI url](assets/frequi_url.png)
The correct configuration for this case is `http://localhost:8080` - the main part of the URL including the port.
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"CORS_origins": ["http://localhost:8080"],
//...
}
```
!!! Tip "trailing Slash"
The trailing slash is not allowed in the `CORS_origins` configuration (e.g. `"http://localhots:8080/"`).
Such a configuration will not take effect, and the cors errors will remain.
!!! Note
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.

View File

@@ -373,7 +373,7 @@ Filters low-value coins which would not allow setting stoplosses.
Namely, pairs are blacklisted if a variance of one percent or more in the stop price would be caused by precision rounding on the exchange, i.e. `rounded(stop_price) <= rounded(stop_price * 0.99)`. The idea is to avoid coins with a value VERY close to their lower trading boundary, not allowing setting of proper stoploss. Namely, pairs are blacklisted if a variance of one percent or more in the stop price would be caused by precision rounding on the exchange, i.e. `rounded(stop_price) <= rounded(stop_price * 0.99)`. The idea is to avoid coins with a value VERY close to their lower trading boundary, not allowing setting of proper stoploss.
!!! Tip "PerformanceFilter is pointless for futures trading" !!! Tip "PrecisionFilter is pointless for futures trading"
The above does not apply to shorts. And for longs, in theory the trade will be liquidated first. The above does not apply to shorts. And for longs, in theory the trade will be liquidated first.
!!! Warning "Backtesting" !!! Warning "Backtesting"

View File

@@ -41,6 +41,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/)
- [X] [Bitmart](https://bitmart.com/) - [X] [Bitmart](https://bitmart.com/)
- [X] [BingX](https://bingx.com/invite/0EM9RX)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi) - [X] [HTX](https://www.htx.com/) (Former Huobi)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)

View File

@@ -286,7 +286,7 @@ cd freqtrade
#### Freqtrade install: Conda Environment #### Freqtrade install: Conda Environment
```bash ```bash
conda create --name freqtrade python=3.11 conda create --name freqtrade python=3.12
``` ```
!!! Note "Creating Conda Environment" !!! Note "Creating Conda Environment"

View File

@@ -2,6 +2,14 @@
This page explains how to plot prices, indicators and profits. This page explains how to plot prices, indicators and profits.
!!! Warning "Deprecated"
The commands described in this page (`plot-dataframe`, `plot-profit`) should be considered deprecated and are in maintenance mode.
This is mostly for the performance problems even medium sized plots can cause, but also because "store a file and open it in a browser" isn't very intuitive from a UI perspective.
While there are no immediate plans to remove them, they are not actively maintained - and may be removed short-term should major changes be required to keep them working.
Please use [FreqUI](freq-ui.md) for plotting needs, which doesn't struggle with the same performance problems.
## Installation / Setup ## Installation / Setup
Plotting modules use the Plotly library. You can install / upgrade this by running the following command: Plotting modules use the Plotly library. You can install / upgrade this by running the following command:

View File

@@ -1,6 +1,6 @@
markdown==3.6 markdown==3.6
mkdocs==1.6.0 mkdocs==1.6.0
mkdocs-material==9.5.19 mkdocs-material==9.5.27
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.8.1 pymdown-extensions==10.8.1
jinja2==3.1.3 jinja2==3.1.4

View File

@@ -1,16 +1,8 @@
# REST API & FreqUI # REST API
## FreqUI ## FreqUI
Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade UI. FreqUI now has it's own dedicated [documentation section](frequi.md) - please refer to that section for all information regarding the FreqUI.
By default, the UI is not included in the installation (except for docker images), and must be installed explicitly with `freqtrade install-ui`.
This same command can also be used to update freqUI, should there be a new release.
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
!!! Note "developers"
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
## Configuration ## Configuration
@@ -126,6 +118,14 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
freqtrade-client --config rest_config.json <command> [optional parameters] freqtrade-client --config rest_config.json <command> [optional parameters]
``` ```
Commands with many arguments may require keyword arguments (for clarity) - which can be provided as follows:
``` bash
freqtrade-client --config rest_config.json forceenter BTC/USDT long enter_tag=GutFeeling
```
This method will work for all arguments - check the "show" command for a list of available parameters.
??? Note "Programmatic use" ??? Note "Programmatic use"
The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API. The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API.
to do so, please use the following: to do so, please use the following:
@@ -169,7 +169,7 @@ freqtrade-client --config rest_config.json <command> [optional parameters]
| `delete_lock <lock_id>` | Deletes (disables) the lock by id. | `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `locks add <pair>, <until>, [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe). | `locks add <pair>, <until>, [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe).
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`). | `forceexit <trade_id> [order_type] [amount]` | Instantly exits the given trade (ignoring `minimum_roi`), using the given order type ("market" or "limit", uses your config setting if not specified), and the chosen amount (full sell if not specified).
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`force_entry_enable` must be set to True) | `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`force_entry_enable` must be set to True)
| `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`force_entry_enable` must be set to True) | `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`force_entry_enable` must be set to True)
@@ -488,42 +488,4 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"}
``` ```
### CORS --8<-- "includes/cors.md"
This whole section is only necessary in cross-origin cases (where you multiple bot API's running on `localhost:8081`, `localhost:8082`, ...), and want to combine them into one FreqUI instance.
??? info "Technical explanation"
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
Users can allow access from different origin URL's to the bot API via the `CORS_origins` configuration setting.
It consists of a list of allowed URL's that are allowed to consume resources from the bot's API.
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"CORS_origins": ["https://frequi.freqtrade.io"],
//...
}
```
In the following (pretty common) case, FreqUI is accessible on `http://localhost:8080/trade` (this is what you see in your navbar when navigating to freqUI).
![freqUI url](assets/frequi_url.png)
The correct configuration for this case is `http://localhost:8080` - the main part of the URL including the port.
```jsonc
{
//...
"jwt_secret_key": "somethingrandom",
"CORS_origins": ["http://localhost:8080"],
//...
}
```
!!! Note
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.

View File

@@ -30,6 +30,7 @@ The Order-type will be ignored if only one mode is available.
|----------|-------------| |----------|-------------|
| Binance | limit | | Binance | limit |
| Binance Futures | market, limit | | Binance Futures | market, limit |
| Bingx | market, limit |
| HTX (former Huobi) | limit | | HTX (former Huobi) | limit |
| kraken | market, limit | | kraken | market, limit |
| Gate | limit | | Gate | limit |

View File

@@ -165,7 +165,9 @@ E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoplo
During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades). During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades).
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss. Returning `None` will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
`NaN` and `inf` values are considered invalid and will be ignored (identical to `None`).
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)). Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)).
@@ -467,7 +469,7 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
??? Example "Returning a stoploss using absolute price from the custom stoploss function" ??? Example "Returning a stoploss using absolute price from the custom stoploss function"
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate, is_short=trade.is_short, leverage=trade.leverage)`. If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate=current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement. For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement.
``` python ``` python
@@ -492,7 +494,8 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
candle = dataframe.iloc[-1].squeeze() candle = dataframe.iloc[-1].squeeze()
side = 1 if trade.is_short else -1 side = 1 if trade.is_short else -1
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
current_rate, is_short=trade.is_short, current_rate=current_rate,
is_short=trade.is_short,
leverage=trade.leverage) leverage=trade.leverage)
``` ```

View File

@@ -13,28 +13,28 @@ The following attributes / properties are available for each individual trade -
| Attribute | DataType | Description | | Attribute | DataType | Description |
|------------|-------------|-------------| |------------|-------------|-------------|
`pair`| string | Pair of this trade | `pair` | string | Pair of this trade. |
`is_open`| boolean | Is the trade currently open, or has it been concluded | `is_open` | boolean | Is the trade currently open, or has it been concluded. |
`open_rate`| float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments) | `open_rate` | float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments). |
`close_rate`| float | Close rate - only set when is_open = False | `close_rate` | float | Close rate - only set when is_open = False. |
`stake_amount`| float | Amount in Stake (or Quote) currency. | `stake_amount` | float | Amount in Stake (or Quote) currency. |
`amount`| float | Amount in Asset / Base currency that is currently owned. | `amount` | float | Amount in Asset / Base currency that is currently owned. |
`open_date`| datetime | Timestamp when trade was opened **use `open_date_utc` instead** | `open_date` | datetime | Timestamp when trade was opened **use `open_date_utc` instead** |
`open_date_utc`| datetime | Timestamp when trade was opened - in UTC | `open_date_utc` | datetime | Timestamp when trade was opened - in UTC. |
`close_date`| datetime | Timestamp when trade was closed **use `close_date_utc` instead** | `close_date` | datetime | Timestamp when trade was closed **use `close_date_utc` instead** |
`close_date_utc`| datetime | Timestamp when trade was closed - in UTC | `close_date_utc` | datetime | Timestamp when trade was closed - in UTC. |
`close_profit`| float | Relative profit at the time of trade closure. `0.01` == 1% | `close_profit` | float | Relative profit at the time of trade closure. `0.01` == 1% |
`close_profit_abs`| float | Absolute profit (in stake currency) at the time of trade closure. | `close_profit_abs` | float | Absolute profit (in stake currency) at the time of trade closure. |
`leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets. | `leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets. |
`enter_tag`| string | Tag provided on entry via the `enter_tag` column in the dataframe | `enter_tag` | string | Tag provided on entry via the `enter_tag` column in the dataframe. |
`is_short` | boolean | True for short trades, False otherwise | `is_short` | boolean | True for short trades, False otherwise. |
`orders` | Order[] | List of order objects attached to this trade (includes both filled and cancelled orders) | `orders` | Order[] | List of order objects attached to this trade (includes both filled and cancelled orders). |
`date_last_filled_utc` | datetime | Time of the last filled order | `date_last_filled_utc` | datetime | Time of the last filled order. |
`entry_side` | "buy" / "sell" | Order Side the trade was entered | `entry_side` | "buy" / "sell" | Order Side the trade was entered. |
`exit_side` | "buy" / "sell" | Order Side that will result in a trade exit / position reduction. | `exit_side` | "buy" / "sell" | Order Side that will result in a trade exit / position reduction. |
`trade_direction` | "long" / "short" | Trade direction in text - long or short. | `trade_direction` | "long" / "short" | Trade direction in text - long or short. |
`nr_of_successful_entries` | int | Number of successful (filled) entry orders | `nr_of_successful_entries` | int | Number of successful (filled) entry orders. |
`nr_of_successful_exits` | int | Number of successful (filled) exit orders | `nr_of_successful_exits` | int | Number of successful (filled) exit orders. |
## Class methods ## Class methods

View File

@@ -5,6 +5,30 @@ 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. 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. Otherwise, please follow the instructions below.
All instructions assume that python 3.9+ is installed and available.
## Clone the git repository
First of all clone the repository by running:
``` powershell
git clone https://github.com/freqtrade/freqtrade.git
```
Now, choose your installation method, either automatically via script (recommended) or manually following the corresponding instructions.
## Install freqtrade automatically
### Run the installation script
The script will ask you a few questions to determine which parts should be installed.
```powershell
Set-ExecutionPolicy -ExecutionPolicy Bypass
cd freqtrade
. .\setup.ps1
```
## Install freqtrade manually ## Install freqtrade manually
!!! Note "64bit Python version" !!! Note "64bit Python version"
@@ -14,17 +38,11 @@ Otherwise, please follow the instructions below.
!!! Hint !!! Hint
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information. Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
### 1. Clone the git repository ### Install ta-lib
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
### 2. Install ta-lib
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows). 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.9, 3.10 and 3.11) 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.9, 3.10, 3.11 and 3.12) and for 64bit Windows.
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade. 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. Other versions must be downloaded from the above link.

View File

@@ -1,22 +1,34 @@
""" Freqtrade bot """ """Freqtrade bot"""
__version__ = '2024.4'
if 'dev' in __version__: __version__ = "2024.6"
if "dev" in __version__:
from pathlib import Path from pathlib import Path
try: try:
import subprocess import subprocess
freqtrade_basedir = Path(__file__).parent freqtrade_basedir = Path(__file__).parent
__version__ = __version__ + '-' + subprocess.check_output( __version__ = (
['git', 'log', '--format="%h"', '-n 1'], __version__
stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"') + "-"
+ subprocess.check_output(
["git", "log", '--format="%h"', "-n 1"],
stderr=subprocess.DEVNULL,
cwd=freqtrade_basedir,
)
.decode("utf-8")
.rstrip()
.strip('"')
)
except Exception: # pragma: no cover except Exception: # pragma: no cover
# git not available, ignore # git not available, ignore
try: try:
# Try Fallback to freqtrade_commit file (created by CI while building docker image) # Try Fallback to freqtrade_commit file (created by CI while building docker image)
versionfile = Path('./freqtrade_commit') versionfile = Path("./freqtrade_commit")
if versionfile.is_file(): if versionfile.is_file():
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}" __version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
except Exception: except Exception: # noqa: S110
pass pass

View File

@@ -9,5 +9,5 @@ To launch Freqtrade as a module
from freqtrade import main from freqtrade import main
if __name__ == '__main__': if __name__ == "__main__":
main.main() main.main()

View File

@@ -6,22 +6,39 @@ Contains all start-commands, subcommands and CLI Interface creation.
Note: Be careful with file-scoped imports in these subfiles. Note: Be careful with file-scoped imports in these subfiles.
as they are parsed on startup, nothing containing optional modules should be loaded. as they are parsed on startup, nothing containing optional modules should be loaded.
""" """
from freqtrade.commands.analyze_commands import start_analysis_entries_exits from freqtrade.commands.analyze_commands import start_analysis_entries_exits
from freqtrade.commands.arguments import Arguments from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config, start_show_config from freqtrade.commands.build_config_commands import start_new_config, start_show_config
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades, from freqtrade.commands.data_commands import (
start_download_data, start_list_data) start_convert_data,
start_convert_trades,
start_download_data,
start_list_data,
)
from freqtrade.commands.db_commands import start_convert_db from freqtrade.commands.db_commands import start_convert_db
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, from freqtrade.commands.deploy_commands import (
start_new_strategy) start_create_userdir,
start_install_ui,
start_new_strategy,
)
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_freqAI_models, from freqtrade.commands.list_commands import (
start_list_markets, start_list_strategies, start_list_exchanges,
start_list_timeframes, start_show_trades) start_list_freqAI_models,
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show, start_list_markets,
start_edge, start_hyperopt, start_list_strategies,
start_lookahead_analysis, start_list_timeframes,
start_recursive_analysis) start_show_trades,
)
from freqtrade.commands.optimize_commands import (
start_backtesting,
start_backtesting_show,
start_edge,
start_hyperopt,
start_lookahead_analysis,
start_recursive_analysis,
)
from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.pairlist_commands import start_test_pairlist
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
from freqtrade.commands.strategy_utils_commands import start_strategy_update from freqtrade.commands.strategy_utils_commands import start_strategy_update

View File

@@ -20,25 +20,25 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
config = setup_utils_configuration(args, method) config = setup_utils_configuration(args, method)
no_unlimited_runmodes = { no_unlimited_runmodes = {
RunMode.BACKTEST: 'backtesting', RunMode.BACKTEST: "backtesting",
} }
if method in no_unlimited_runmodes.keys(): if method in no_unlimited_runmodes.keys():
from freqtrade.data.btanalysis import get_latest_backtest_filename from freqtrade.data.btanalysis import get_latest_backtest_filename
if 'exportfilename' in config: if "exportfilename" in config:
if config['exportfilename'].is_dir(): if config["exportfilename"].is_dir():
btfile = Path(get_latest_backtest_filename(config['exportfilename'])) btfile = Path(get_latest_backtest_filename(config["exportfilename"]))
signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl" signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl"
else: else:
if config['exportfilename'].exists(): if config["exportfilename"].exists():
btfile = Path(config['exportfilename']) btfile = Path(config["exportfilename"])
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl" signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
else: else:
raise ConfigurationError(f"{config['exportfilename']} does not exist.") raise ConfigurationError(f"{config['exportfilename']} does not exist.")
else: else:
raise ConfigurationError('exportfilename not in config.') raise ConfigurationError("exportfilename not in config.")
if (not Path(signals_file).exists()): if not Path(signals_file).exists():
raise OperationalException( raise OperationalException(
f"Cannot find latest backtest signals file: {signals_file}." f"Cannot find latest backtest signals file: {signals_file}."
"Run backtesting with `--export signals`." "Run backtesting with `--export signals`."
@@ -58,6 +58,6 @@ def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
# Initialize configuration # Initialize configuration
config = setup_analyze_configuration(args, RunMode.BACKTEST) config = setup_analyze_configuration(args, RunMode.BACKTEST)
logger.info('Starting freqtrade in analysis mode') logger.info("Starting freqtrade in analysis mode")
process_entry_exit_reasons(config) process_entry_exit_reasons(config)

View File

@@ -1,6 +1,7 @@
""" """
This module contains the argument manager class This module contains the argument manager class
""" """
import argparse import argparse
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
@@ -12,35 +13,72 @@ from freqtrade.constants import DEFAULT_CONFIG
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search", "freqaimodel", ARGS_STRATEGY = [
"freqaimodel_path"] "strategy",
"strategy_path",
"recursive_strategy_search",
"freqaimodel",
"freqaimodel_path",
]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
ARGS_WEBSERVER: List[str] = [] ARGS_WEBSERVER: List[str] = []
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", ARGS_COMMON_OPTIMIZE = [
"max_open_trades", "stake_amount", "fee", "pairs"] "timeframe",
"timerange",
"dataformat_ohlcv",
"max_open_trades",
"stake_amount",
"fee",
"pairs",
]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + [
"enable_protections", "dry_run_wallet", "timeframe_detail", "position_stacking",
"strategy_list", "export", "exportfilename", "use_max_market_positions",
"backtest_breakdown", "backtest_cache", "enable_protections",
"freqai_backtest_live_models"] "dry_run_wallet",
"timeframe_detail",
"strategy_list",
"export",
"exportfilename",
"backtest_breakdown",
"backtest_cache",
"freqai_backtest_live_models",
]
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + [
"position_stacking", "use_max_market_positions", "hyperopt",
"enable_protections", "dry_run_wallet", "timeframe_detail", "hyperopt_path",
"epochs", "spaces", "print_all", "position_stacking",
"print_colorized", "print_json", "hyperopt_jobs", "use_max_market_positions",
"hyperopt_random_state", "hyperopt_min_trades", "enable_protections",
"hyperopt_loss", "disableparamexport", "dry_run_wallet",
"hyperopt_ignore_missing_space", "analyze_per_epoch"] "timeframe_detail",
"epochs",
"spaces",
"print_all",
"print_colorized",
"print_json",
"hyperopt_jobs",
"hyperopt_random_state",
"hyperopt_min_trades",
"hyperopt_loss",
"disableparamexport",
"hyperopt_ignore_missing_space",
"analyze_per_epoch",
]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized", ARGS_LIST_STRATEGIES = [
"recursive_strategy_search"] "strategy_path",
"print_one_column",
"print_colorized",
"recursive_strategy_search",
]
ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_colorized"] ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_colorized"]
@@ -52,12 +90,27 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", ARGS_LIST_PAIRS = [
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all", "exchange",
"trading_mode"] "print_list",
"list_pairs_print_json",
"print_one_column",
"print_csv",
"base_currencies",
"quote_currencies",
"list_pairs_all",
"trading_mode",
]
ARGS_TEST_PAIRLIST = ["user_data_dir", "verbosity", "config", "quote_currencies", ARGS_TEST_PAIRLIST = [
"print_one_column", "list_pairs_print_json", "exchange"] "user_data_dir",
"verbosity",
"config",
"quote_currencies",
"print_one_column",
"list_pairs_print_json",
"exchange",
]
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
@@ -70,22 +123,59 @@ ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase",
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades", ARGS_CONVERT_TRADES = [
"trading_mode"] "pairs",
"timeframes",
"exchange",
"dataformat_ohlcv",
"dataformat_trades",
"trading_mode",
]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive", ARGS_DOWNLOAD_DATA = [
"timerange", "download_trades", "exchange", "timeframes", "pairs",
"erase", "dataformat_ohlcv", "dataformat_trades", "trading_mode", "pairs_file",
"prepend_data"] "days",
"new_pairs_days",
"include_inactive",
"timerange",
"download_trades",
"convert_trades",
"exchange",
"timeframes",
"erase",
"dataformat_ohlcv",
"dataformat_trades",
"trading_mode",
"prepend_data",
]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_DATAFRAME = [
"db_url", "trade_source", "export", "exportfilename", "pairs",
"timerange", "timeframe", "no_trades"] "indicators1",
"indicators2",
"plot_limit",
"db_url",
"trade_source",
"export",
"exportfilename",
"timerange",
"timeframe",
"no_trades",
]
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", ARGS_PLOT_PROFIT = [
"trade_source", "timeframe", "plot_auto_open", ] "pairs",
"timerange",
"export",
"exportfilename",
"db_url",
"trade_source",
"timeframe",
"plot_auto_open",
]
ARGS_CONVERT_DB = ["db_url", "db_url_from"] ARGS_CONVERT_DB = ["db_url", "db_url_from"]
@@ -93,36 +183,76 @@ ARGS_INSTALL_UI = ["erase_ui_only", "ui_version"]
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", ARGS_HYPEROPT_LIST = [
"hyperopt_list_min_trades", "hyperopt_list_max_trades", "hyperopt_list_best",
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time", "hyperopt_list_profitable",
"hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit", "hyperopt_list_min_trades",
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit", "hyperopt_list_max_trades",
"hyperopt_list_min_objective", "hyperopt_list_max_objective", "hyperopt_list_min_avg_time",
"print_colorized", "print_json", "hyperopt_list_no_details", "hyperopt_list_max_avg_time",
"hyperoptexportfilename", "export_csv"] "hyperopt_list_min_avg_profit",
"hyperopt_list_max_avg_profit",
"hyperopt_list_min_total_profit",
"hyperopt_list_max_total_profit",
"hyperopt_list_min_objective",
"hyperopt_list_max_objective",
"print_colorized",
"print_json",
"hyperopt_list_no_details",
"hyperoptexportfilename",
"export_csv",
]
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index", ARGS_HYPEROPT_SHOW = [
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header", "hyperopt_list_best",
"disableparamexport", "backtest_breakdown"] "hyperopt_list_profitable",
"hyperopt_show_index",
"print_json",
"hyperoptexportfilename",
"hyperopt_show_no_header",
"disableparamexport",
"backtest_breakdown",
]
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list", ARGS_ANALYZE_ENTRIES_EXITS = [
"exit_reason_list", "indicator_list", "timerange", "exportfilename",
"analysis_rejected", "analysis_to_csv", "analysis_csv_path"] "analysis_groups",
"enter_reason_list",
"exit_reason_list",
"indicator_list",
"timerange",
"analysis_rejected",
"analysis_to_csv",
"analysis_csv_path",
]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", NO_CONF_REQURIED = [
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels", "convert-data",
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter", "convert-trade-data",
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv", "download-data",
"strategy-updater"] "list-timeframes",
"list-markets",
"list-pairs",
"list-strategies",
"list-freqaimodels",
"list-data",
"hyperopt-list",
"hyperopt-show",
"backtest-filter",
"plot-dataframe",
"plot-profit",
"show-trades",
"trades-to-ohlcv",
"strategy-updater",
]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"] ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"]
ARGS_LOOKAHEAD_ANALYSIS = [ ARGS_LOOKAHEAD_ANALYSIS = [
a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", 'cache') a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", "cache")
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"] ] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"] ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
@@ -156,14 +286,14 @@ class Arguments:
# Workaround issue in argparse with action='append' and default value # Workaround issue in argparse with action='append' and default value
# (see https://bugs.python.org/issue16399) # (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting) # Allow no-config for certain commands (like downloading / plotting)
if ('config' in parsed_arg and parsed_arg.config is None): 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) 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: if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
user_dir = parsed_arg.user_data_dir user_dir = parsed_arg.user_data_dir
else: else:
# Default case # Default case
user_dir = 'user_data' user_dir = "user_data"
# Try loading from "user_data/config.json" # Try loading from "user_data/config.json"
cfgfile = Path(user_dir) / DEFAULT_CONFIG cfgfile = Path(user_dir) / DEFAULT_CONFIG
if cfgfile.is_file(): if cfgfile.is_file():
@@ -177,7 +307,6 @@ class Arguments:
return parsed_arg return parsed_arg
def _build_args(self, optionlist, parser): def _build_args(self, optionlist, parser):
for val in optionlist: for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val] opt = AVAILABLE_CLI_OPTIONS[val]
parser.add_argument(*opt.cli, dest=val, **opt.kwargs) parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
@@ -198,43 +327,61 @@ class Arguments:
# Build main command # Build main command
self.parser = argparse.ArgumentParser( self.parser = argparse.ArgumentParser(
prog="freqtrade", prog="freqtrade", description="Free, open source crypto trading bot"
description='Free, open source crypto trading bot'
) )
self._build_args(optionlist=['version'], parser=self.parser) self._build_args(optionlist=["version"], parser=self.parser)
from freqtrade.commands import (start_analysis_entries_exits, start_backtesting, from freqtrade.commands import (
start_backtesting_show, start_convert_data, start_analysis_entries_exits,
start_convert_db, start_convert_trades, start_backtesting,
start_create_userdir, start_download_data, start_edge, start_backtesting_show,
start_hyperopt, start_hyperopt_list, start_hyperopt_show, start_convert_data,
start_install_ui, start_list_data, start_list_exchanges, start_convert_db,
start_list_freqAI_models, start_list_markets, start_convert_trades,
start_list_strategies, start_list_timeframes, start_create_userdir,
start_lookahead_analysis, start_new_config, start_download_data,
start_new_strategy, start_plot_dataframe, start_plot_profit, start_edge,
start_recursive_analysis, start_show_config, start_hyperopt,
start_show_trades, start_strategy_update, start_hyperopt_list,
start_test_pairlist, start_trading, start_webserver) start_hyperopt_show,
start_install_ui,
start_list_data,
start_list_exchanges,
start_list_freqAI_models,
start_list_markets,
start_list_strategies,
start_list_timeframes,
start_lookahead_analysis,
start_new_config,
start_new_strategy,
start_plot_dataframe,
start_plot_profit,
start_recursive_analysis,
start_show_config,
start_show_trades,
start_strategy_update,
start_test_pairlist,
start_trading,
start_webserver,
)
subparsers = self.parser.add_subparsers(dest='command', subparsers = self.parser.add_subparsers(
# Use custom message when no subhandler is added dest="command",
# shown from `main.py` # Use custom message when no subhandler is added
# required=True # shown from `main.py`
) # required=True
)
# Add trade subcommand # Add trade subcommand
trade_cmd = subparsers.add_parser( trade_cmd = subparsers.add_parser(
'trade', "trade", help="Trade module.", parents=[_common_parser, _strategy_parser]
help='Trade module.',
parents=[_common_parser, _strategy_parser]
) )
trade_cmd.set_defaults(func=start_trading) trade_cmd.set_defaults(func=start_trading)
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd) self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
# add create-userdir subcommand # add create-userdir subcommand
create_userdir_cmd = subparsers.add_parser( create_userdir_cmd = subparsers.add_parser(
'create-userdir', "create-userdir",
help="Create user-data directory.", help="Create user-data directory.",
) )
create_userdir_cmd.set_defaults(func=start_create_userdir) create_userdir_cmd.set_defaults(func=start_create_userdir)
@@ -242,7 +389,7 @@ class Arguments:
# add new-config subcommand # add new-config subcommand
build_config_cmd = subparsers.add_parser( build_config_cmd = subparsers.add_parser(
'new-config', "new-config",
help="Create new config", help="Create new config",
) )
build_config_cmd.set_defaults(func=start_new_config) build_config_cmd.set_defaults(func=start_new_config)
@@ -250,7 +397,7 @@ class Arguments:
# add show-config subcommand # add show-config subcommand
show_config_cmd = subparsers.add_parser( show_config_cmd = subparsers.add_parser(
'show-config', "show-config",
help="Show resolved config", help="Show resolved config",
) )
show_config_cmd.set_defaults(func=start_show_config) show_config_cmd.set_defaults(func=start_show_config)
@@ -258,7 +405,7 @@ class Arguments:
# add new-strategy subcommand # add new-strategy subcommand
build_strategy_cmd = subparsers.add_parser( build_strategy_cmd = subparsers.add_parser(
'new-strategy', "new-strategy",
help="Create new strategy", help="Create new strategy",
) )
build_strategy_cmd.set_defaults(func=start_new_strategy) build_strategy_cmd.set_defaults(func=start_new_strategy)
@@ -266,8 +413,8 @@ class Arguments:
# Add download-data subcommand # Add download-data subcommand
download_data_cmd = subparsers.add_parser( download_data_cmd = subparsers.add_parser(
'download-data', "download-data",
help='Download backtesting data.', help="Download backtesting data.",
parents=[_common_parser], parents=[_common_parser],
) )
download_data_cmd.set_defaults(func=start_download_data) download_data_cmd.set_defaults(func=start_download_data)
@@ -275,8 +422,8 @@ class Arguments:
# Add convert-data subcommand # Add convert-data subcommand
convert_data_cmd = subparsers.add_parser( convert_data_cmd = subparsers.add_parser(
'convert-data', "convert-data",
help='Convert candle (OHLCV) data from one format to another.', help="Convert candle (OHLCV) data from one format to another.",
parents=[_common_parser], parents=[_common_parser],
) )
convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True)) convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True))
@@ -284,8 +431,8 @@ class Arguments:
# Add convert-trade-data subcommand # Add convert-trade-data subcommand
convert_trade_data_cmd = subparsers.add_parser( convert_trade_data_cmd = subparsers.add_parser(
'convert-trade-data', "convert-trade-data",
help='Convert trade data from one format to another.', help="Convert trade data from one format to another.",
parents=[_common_parser], parents=[_common_parser],
) )
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
@@ -293,8 +440,8 @@ class Arguments:
# Add trades-to-ohlcv subcommand # Add trades-to-ohlcv subcommand
convert_trade_data_cmd = subparsers.add_parser( convert_trade_data_cmd = subparsers.add_parser(
'trades-to-ohlcv', "trades-to-ohlcv",
help='Convert trade data to OHLCV data.', help="Convert trade data to OHLCV data.",
parents=[_common_parser], parents=[_common_parser],
) )
convert_trade_data_cmd.set_defaults(func=start_convert_trades) convert_trade_data_cmd.set_defaults(func=start_convert_trades)
@@ -302,8 +449,8 @@ class Arguments:
# Add list-data subcommand # Add list-data subcommand
list_data_cmd = subparsers.add_parser( list_data_cmd = subparsers.add_parser(
'list-data', "list-data",
help='List downloaded data.', help="List downloaded data.",
parents=[_common_parser], parents=[_common_parser],
) )
list_data_cmd.set_defaults(func=start_list_data) list_data_cmd.set_defaults(func=start_list_data)
@@ -311,17 +458,15 @@ class Arguments:
# Add backtesting subcommand # Add backtesting subcommand
backtesting_cmd = subparsers.add_parser( backtesting_cmd = subparsers.add_parser(
'backtesting', "backtesting", help="Backtesting module.", parents=[_common_parser, _strategy_parser]
help='Backtesting module.',
parents=[_common_parser, _strategy_parser]
) )
backtesting_cmd.set_defaults(func=start_backtesting) backtesting_cmd.set_defaults(func=start_backtesting)
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
# Add backtesting-show subcommand # Add backtesting-show subcommand
backtesting_show_cmd = subparsers.add_parser( backtesting_show_cmd = subparsers.add_parser(
'backtesting-show', "backtesting-show",
help='Show past Backtest results', help="Show past Backtest results",
parents=[_common_parser], parents=[_common_parser],
) )
backtesting_show_cmd.set_defaults(func=start_backtesting_show) backtesting_show_cmd.set_defaults(func=start_backtesting_show)
@@ -329,26 +474,22 @@ class Arguments:
# Add backtesting analysis subcommand # Add backtesting analysis subcommand
analysis_cmd = subparsers.add_parser( analysis_cmd = subparsers.add_parser(
'backtesting-analysis', "backtesting-analysis", help="Backtest Analysis module.", parents=[_common_parser]
help='Backtest Analysis module.',
parents=[_common_parser]
) )
analysis_cmd.set_defaults(func=start_analysis_entries_exits) analysis_cmd.set_defaults(func=start_analysis_entries_exits)
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd) self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
# Add edge subcommand # Add edge subcommand
edge_cmd = subparsers.add_parser( edge_cmd = subparsers.add_parser(
'edge', "edge", help="Edge module.", parents=[_common_parser, _strategy_parser]
help='Edge module.',
parents=[_common_parser, _strategy_parser]
) )
edge_cmd.set_defaults(func=start_edge) edge_cmd.set_defaults(func=start_edge)
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
# Add hyperopt subcommand # Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser( hyperopt_cmd = subparsers.add_parser(
'hyperopt', "hyperopt",
help='Hyperopt module.', help="Hyperopt module.",
parents=[_common_parser, _strategy_parser], parents=[_common_parser, _strategy_parser],
) )
hyperopt_cmd.set_defaults(func=start_hyperopt) hyperopt_cmd.set_defaults(func=start_hyperopt)
@@ -356,8 +497,8 @@ class Arguments:
# Add hyperopt-list subcommand # Add hyperopt-list subcommand
hyperopt_list_cmd = subparsers.add_parser( hyperopt_list_cmd = subparsers.add_parser(
'hyperopt-list', "hyperopt-list",
help='List Hyperopt results', help="List Hyperopt results",
parents=[_common_parser], parents=[_common_parser],
) )
hyperopt_list_cmd.set_defaults(func=start_hyperopt_list) hyperopt_list_cmd.set_defaults(func=start_hyperopt_list)
@@ -365,8 +506,8 @@ class Arguments:
# Add hyperopt-show subcommand # Add hyperopt-show subcommand
hyperopt_show_cmd = subparsers.add_parser( hyperopt_show_cmd = subparsers.add_parser(
'hyperopt-show', "hyperopt-show",
help='Show details of Hyperopt results', help="Show details of Hyperopt results",
parents=[_common_parser], parents=[_common_parser],
) )
hyperopt_show_cmd.set_defaults(func=start_hyperopt_show) hyperopt_show_cmd.set_defaults(func=start_hyperopt_show)
@@ -374,8 +515,8 @@ class Arguments:
# Add list-exchanges subcommand # Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser( list_exchanges_cmd = subparsers.add_parser(
'list-exchanges', "list-exchanges",
help='Print available exchanges.', help="Print available exchanges.",
parents=[_common_parser], parents=[_common_parser],
) )
list_exchanges_cmd.set_defaults(func=start_list_exchanges) list_exchanges_cmd.set_defaults(func=start_list_exchanges)
@@ -383,8 +524,8 @@ class Arguments:
# Add list-markets subcommand # Add list-markets subcommand
list_markets_cmd = subparsers.add_parser( list_markets_cmd = subparsers.add_parser(
'list-markets', "list-markets",
help='Print markets on exchange.', help="Print markets on exchange.",
parents=[_common_parser], parents=[_common_parser],
) )
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False)) list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
@@ -392,8 +533,8 @@ class Arguments:
# Add list-pairs subcommand # Add list-pairs subcommand
list_pairs_cmd = subparsers.add_parser( list_pairs_cmd = subparsers.add_parser(
'list-pairs', "list-pairs",
help='Print pairs on exchange.', help="Print pairs on exchange.",
parents=[_common_parser], parents=[_common_parser],
) )
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True)) list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
@@ -401,8 +542,8 @@ class Arguments:
# Add list-strategies subcommand # Add list-strategies subcommand
list_strategies_cmd = subparsers.add_parser( list_strategies_cmd = subparsers.add_parser(
'list-strategies', "list-strategies",
help='Print available strategies.', help="Print available strategies.",
parents=[_common_parser], parents=[_common_parser],
) )
list_strategies_cmd.set_defaults(func=start_list_strategies) list_strategies_cmd.set_defaults(func=start_list_strategies)
@@ -410,8 +551,8 @@ class Arguments:
# Add list-freqAI Models subcommand # Add list-freqAI Models subcommand
list_freqaimodels_cmd = subparsers.add_parser( list_freqaimodels_cmd = subparsers.add_parser(
'list-freqaimodels', "list-freqaimodels",
help='Print available freqAI models.', help="Print available freqAI models.",
parents=[_common_parser], parents=[_common_parser],
) )
list_freqaimodels_cmd.set_defaults(func=start_list_freqAI_models) list_freqaimodels_cmd.set_defaults(func=start_list_freqAI_models)
@@ -419,8 +560,8 @@ class Arguments:
# Add list-timeframes subcommand # Add list-timeframes subcommand
list_timeframes_cmd = subparsers.add_parser( list_timeframes_cmd = subparsers.add_parser(
'list-timeframes', "list-timeframes",
help='Print available timeframes for the exchange.', help="Print available timeframes for the exchange.",
parents=[_common_parser], parents=[_common_parser],
) )
list_timeframes_cmd.set_defaults(func=start_list_timeframes) list_timeframes_cmd.set_defaults(func=start_list_timeframes)
@@ -428,8 +569,8 @@ class Arguments:
# Add show-trades subcommand # Add show-trades subcommand
show_trades = subparsers.add_parser( show_trades = subparsers.add_parser(
'show-trades', "show-trades",
help='Show trades.', help="Show trades.",
parents=[_common_parser], parents=[_common_parser],
) )
show_trades.set_defaults(func=start_show_trades) show_trades.set_defaults(func=start_show_trades)
@@ -437,8 +578,8 @@ class Arguments:
# Add test-pairlist subcommand # Add test-pairlist subcommand
test_pairlist_cmd = subparsers.add_parser( test_pairlist_cmd = subparsers.add_parser(
'test-pairlist', "test-pairlist",
help='Test your pairlist configuration.', help="Test your pairlist configuration.",
) )
test_pairlist_cmd.set_defaults(func=start_test_pairlist) test_pairlist_cmd.set_defaults(func=start_test_pairlist)
self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd) self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd)
@@ -453,16 +594,16 @@ class Arguments:
# Add install-ui subcommand # Add install-ui subcommand
install_ui_cmd = subparsers.add_parser( install_ui_cmd = subparsers.add_parser(
'install-ui', "install-ui",
help='Install FreqUI', help="Install FreqUI",
) )
install_ui_cmd.set_defaults(func=start_install_ui) install_ui_cmd.set_defaults(func=start_install_ui)
self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd) self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd)
# Add Plotting subcommand # Add Plotting subcommand
plot_dataframe_cmd = subparsers.add_parser( plot_dataframe_cmd = subparsers.add_parser(
'plot-dataframe', "plot-dataframe",
help='Plot candles with indicators.', help="Plot candles with indicators.",
parents=[_common_parser, _strategy_parser], parents=[_common_parser, _strategy_parser],
) )
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe) plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
@@ -470,8 +611,8 @@ class Arguments:
# Plot profit # Plot profit
plot_profit_cmd = subparsers.add_parser( plot_profit_cmd = subparsers.add_parser(
'plot-profit', "plot-profit",
help='Generate plot showing profits.', help="Generate plot showing profits.",
parents=[_common_parser, _strategy_parser], parents=[_common_parser, _strategy_parser],
) )
plot_profit_cmd.set_defaults(func=start_plot_profit) plot_profit_cmd.set_defaults(func=start_plot_profit)
@@ -479,40 +620,36 @@ class Arguments:
# Add webserver subcommand # Add webserver subcommand
webserver_cmd = subparsers.add_parser( webserver_cmd = subparsers.add_parser(
'webserver', "webserver", help="Webserver module.", parents=[_common_parser]
help='Webserver module.',
parents=[_common_parser]
) )
webserver_cmd.set_defaults(func=start_webserver) webserver_cmd.set_defaults(func=start_webserver)
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd) self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
# Add strategy_updater subcommand # Add strategy_updater subcommand
strategy_updater_cmd = subparsers.add_parser( strategy_updater_cmd = subparsers.add_parser(
'strategy-updater', "strategy-updater",
help='updates outdated strategy files to the current version', help="updates outdated strategy files to the current version",
parents=[_common_parser] parents=[_common_parser],
) )
strategy_updater_cmd.set_defaults(func=start_strategy_update) strategy_updater_cmd.set_defaults(func=start_strategy_update)
self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd) self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd)
# Add lookahead_analysis subcommand # Add lookahead_analysis subcommand
lookahead_analayis_cmd = subparsers.add_parser( lookahead_analayis_cmd = subparsers.add_parser(
'lookahead-analysis', "lookahead-analysis",
help="Check for potential look ahead bias.", help="Check for potential look ahead bias.",
parents=[_common_parser, _strategy_parser] parents=[_common_parser, _strategy_parser],
) )
lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis) lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis)
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS, self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS, parser=lookahead_analayis_cmd)
parser=lookahead_analayis_cmd)
# Add recursive_analysis subcommand # Add recursive_analysis subcommand
recursive_analayis_cmd = subparsers.add_parser( recursive_analayis_cmd = subparsers.add_parser(
'recursive-analysis', "recursive-analysis",
help="Check for potential recursive formula issue.", help="Check for potential recursive formula issue.",
parents=[_common_parser, _strategy_parser] parents=[_common_parser, _strategy_parser],
) )
recursive_analayis_cmd.set_defaults(func=start_recursive_analysis) recursive_analayis_cmd.set_defaults(func=start_recursive_analysis)
self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS, self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS, parser=recursive_analayis_cmd)
parser=recursive_analayis_cmd)

View File

@@ -45,7 +45,7 @@ def ask_user_overwrite(config_path: Path) -> bool:
}, },
] ]
answers = prompt(questions) answers = prompt(questions)
return answers['overwrite'] return answers["overwrite"]
def ask_user_config() -> Dict[str, Any]: def ask_user_config() -> Dict[str, Any]:
@@ -65,7 +65,7 @@ def ask_user_config() -> Dict[str, Any]:
"type": "text", "type": "text",
"name": "stake_currency", "name": "stake_currency",
"message": "Please insert your stake currency:", "message": "Please insert your stake currency:",
"default": 'USDT', "default": "USDT",
}, },
{ {
"type": "text", "type": "text",
@@ -73,36 +73,38 @@ def ask_user_config() -> Dict[str, Any]:
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):", "message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
"default": "unlimited", "default": "unlimited",
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val), "validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"' "filter": lambda val: (
if val == UNLIMITED_STAKE_AMOUNT '"' + UNLIMITED_STAKE_AMOUNT + '"' if val == UNLIMITED_STAKE_AMOUNT else val
else val ),
}, },
{ {
"type": "text", "type": "text",
"name": "max_open_trades", "name": "max_open_trades",
"message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):", "message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):",
"default": "3", "default": "3",
"validate": lambda val: validate_is_int(val) "validate": lambda val: validate_is_int(val),
}, },
{ {
"type": "select", "type": "select",
"name": "timeframe_in_config", "name": "timeframe_in_config",
"message": "Time", "message": "Time",
"choices": ["Have the strategy define timeframe.", "Override in configuration."] "choices": ["Have the strategy define timeframe.", "Override in configuration."],
}, },
{ {
"type": "text", "type": "text",
"name": "timeframe", "name": "timeframe",
"message": "Please insert your desired timeframe (e.g. 5m):", "message": "Please insert your desired timeframe (e.g. 5m):",
"default": "5m", "default": "5m",
"when": lambda x: x["timeframe_in_config"] == 'Override in configuration.' "when": lambda x: x["timeframe_in_config"] == "Override in configuration.",
}, },
{ {
"type": "text", "type": "text",
"name": "fiat_display_currency", "name": "fiat_display_currency",
"message": "Please insert your display Currency (for reporting):", "message": (
"default": 'USD', "Please insert your display Currency for reporting "
"(leave empty to disable FIAT conversion):"
),
"default": "USD",
}, },
{ {
"type": "select", "type": "select",
@@ -111,6 +113,7 @@ def ask_user_config() -> Dict[str, Any]:
"choices": [ "choices": [
"binance", "binance",
"binanceus", "binanceus",
"bingx",
"gate", "gate",
"htx", "htx",
"kraken", "kraken",
@@ -125,33 +128,33 @@ def ask_user_config() -> Dict[str, Any]:
"name": "trading_mode", "name": "trading_mode",
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?", "message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
"default": False, "default": False,
"filter": lambda val: 'futures' if val else 'spot', "filter": lambda val: "futures" if val else "spot",
"when": lambda x: x["exchange_name"] in ['binance', 'gate', 'okx'], "when": lambda x: x["exchange_name"] in ["binance", "gate", "okx", "bybit"],
}, },
{ {
"type": "autocomplete", "type": "autocomplete",
"name": "exchange_name", "name": "exchange_name",
"message": "Type your exchange name (Must be supported by ccxt)", "message": "Type your exchange name (Must be supported by ccxt)",
"choices": available_exchanges(), "choices": available_exchanges(),
"when": lambda x: x["exchange_name"] == 'other' "when": lambda x: x["exchange_name"] == "other",
}, },
{ {
"type": "password", "type": "password",
"name": "exchange_key", "name": "exchange_key",
"message": "Insert Exchange Key", "message": "Insert Exchange Key",
"when": lambda x: not x['dry_run'] "when": lambda x: not x["dry_run"],
}, },
{ {
"type": "password", "type": "password",
"name": "exchange_secret", "name": "exchange_secret",
"message": "Insert Exchange Secret", "message": "Insert Exchange Secret",
"when": lambda x: not x['dry_run'] "when": lambda x: not x["dry_run"],
}, },
{ {
"type": "password", "type": "password",
"name": "exchange_key_password", "name": "exchange_key_password",
"message": "Insert Exchange API Key password", "message": "Insert Exchange API Key password",
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okx') "when": lambda x: not x["dry_run"] and x["exchange_name"] in ("kucoin", "okx"),
}, },
{ {
"type": "confirm", "type": "confirm",
@@ -163,13 +166,13 @@ def ask_user_config() -> Dict[str, Any]:
"type": "password", "type": "password",
"name": "telegram_token", "name": "telegram_token",
"message": "Insert Telegram token", "message": "Insert Telegram token",
"when": lambda x: x['telegram'] "when": lambda x: x["telegram"],
}, },
{ {
"type": "password", "type": "password",
"name": "telegram_chat_id", "name": "telegram_chat_id",
"message": "Insert Telegram chat id", "message": "Insert Telegram chat id",
"when": lambda x: x['telegram'] "when": lambda x: x["telegram"],
}, },
{ {
"type": "confirm", "type": "confirm",
@@ -180,23 +183,25 @@ def ask_user_config() -> Dict[str, Any]:
{ {
"type": "text", "type": "text",
"name": "api_server_listen_addr", "name": "api_server_listen_addr",
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, " "message": (
"otherwise best left untouched)"), "Insert Api server Listen Address (0.0.0.0 for docker, "
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", "otherwise best left untouched)"
"when": lambda x: x['api_server'] ),
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", # noqa: S104
"when": lambda x: x["api_server"],
}, },
{ {
"type": "text", "type": "text",
"name": "api_server_username", "name": "api_server_username",
"message": "Insert api-server username", "message": "Insert api-server username",
"default": "freqtrader", "default": "freqtrader",
"when": lambda x: x['api_server'] "when": lambda x: x["api_server"],
}, },
{ {
"type": "password", "type": "password",
"name": "api_server_password", "name": "api_server_password",
"message": "Insert api-server password", "message": "Insert api-server password",
"when": lambda x: x['api_server'] "when": lambda x: x["api_server"],
}, },
] ]
answers = prompt(questions) answers = prompt(questions)
@@ -205,15 +210,11 @@ def ask_user_config() -> Dict[str, Any]:
# Interrupted questionary sessions return an empty dict. # Interrupted questionary sessions return an empty dict.
raise OperationalException("User interrupted interactive questions.") raise OperationalException("User interrupted interactive questions.")
# Ensure default is set for non-futures exchanges # Ensure default is set for non-futures exchanges
answers['trading_mode'] = answers.get('trading_mode', "spot") answers["trading_mode"] = answers.get("trading_mode", "spot")
answers['margin_mode'] = ( answers["margin_mode"] = "isolated" if answers.get("trading_mode") == "futures" else ""
'isolated'
if answers.get('trading_mode') == 'futures'
else ''
)
# Force JWT token to be a random string # Force JWT token to be a random string
answers['api_server_jwt_key'] = secrets.token_hex() answers["api_server_jwt_key"] = secrets.token_hex()
answers['api_server_ws_token'] = secrets.token_urlsafe(25) answers["api_server_ws_token"] = secrets.token_urlsafe(25)
return answers return answers
@@ -225,26 +226,26 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
:param selections: Dict containing selections taken by the user. :param selections: Dict containing selections taken by the user.
""" """
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
try: try:
exchange_template = MAP_EXCHANGE_CHILDCLASS.get( exchange_template = MAP_EXCHANGE_CHILDCLASS.get(
selections['exchange_name'], selections['exchange_name']) selections["exchange_name"], selections["exchange_name"]
)
selections['exchange'] = render_template( selections["exchange"] = render_template(
templatefile=f"subtemplates/exchange_{exchange_template}.j2", templatefile=f"subtemplates/exchange_{exchange_template}.j2", arguments=selections
arguments=selections
) )
except TemplateNotFound: except TemplateNotFound:
selections['exchange'] = render_template( selections["exchange"] = render_template(
templatefile="subtemplates/exchange_generic.j2", templatefile="subtemplates/exchange_generic.j2", arguments=selections
arguments=selections
) )
config_text = render_template(templatefile='base_config.json.j2', config_text = render_template(templatefile="base_config.json.j2", arguments=selections)
arguments=selections)
logger.info(f"Writing config to `{config_path}`.") logger.info(f"Writing config to `{config_path}`.")
logger.info( logger.info(
"Please make sure to check the configuration contents and adjust settings to your needs.") "Please make sure to check the configuration contents and adjust settings to your needs."
)
config_path.write_text(config_text) config_path.write_text(config_text)
@@ -255,7 +256,7 @@ def start_new_config(args: Dict[str, Any]) -> None:
Asking the user questions to fill out the template accordingly. Asking the user questions to fill out the template accordingly.
""" """
config_path = Path(args['config'][0]) config_path = Path(args["config"][0])
chown_user_directory(config_path.parent) chown_user_directory(config_path.parent)
if config_path.exists(): if config_path.exists():
overwrite = ask_user_overwrite(config_path) overwrite = ask_user_overwrite(config_path)
@@ -264,22 +265,22 @@ def start_new_config(args: Dict[str, Any]) -> None:
else: else:
raise OperationalException( raise OperationalException(
f"Configuration file `{config_path}` already exists. " f"Configuration file `{config_path}` already exists. "
"Please delete it or use a different configuration file name.") "Please delete it or use a different configuration file name."
)
selections = ask_user_config() selections = ask_user_config()
deploy_new_config(config_path, selections) deploy_new_config(config_path, selections)
def start_show_config(args: Dict[str, Any]) -> None: def start_show_config(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False)
# TODO: Sanitize from sensitive info before printing # TODO: Sanitize from sensitive info before printing
print("Your combined configuration is:") print("Your combined configuration is:")
config_sanitized = sanitize_config( config_sanitized = sanitize_config(
config['original_config'], config["original_config"], show_sensitive=args.get("show_sensitive", False)
show_sensitive=args.get('show_sensitive', False)
) )
from rich import print_json from rich import print_json
print_json(data=config_sanitized) print_json(data=config_sanitized)

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,11 @@ from typing import Any, Dict
from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.configuration import TimeRange, setup_utils_configuration
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, from freqtrade.data.converter import (
convert_trades_to_ohlcv) convert_ohlcv_format,
convert_trades_format,
convert_trades_to_ohlcv,
)
from freqtrade.data.history import download_data_main from freqtrade.data.history import download_data_main
from freqtrade.enums import CandleType, RunMode, TradingMode from freqtrade.enums import CandleType, RunMode, TradingMode
from freqtrade.exceptions import ConfigurationError from freqtrade.exceptions import ConfigurationError
@@ -20,14 +23,17 @@ logger = logging.getLogger(__name__)
def _check_data_config_download_sanity(config: Config) -> None: def _check_data_config_download_sanity(config: Config) -> None:
if 'days' in config and 'timerange' in config: if "days" in config and "timerange" in config:
raise ConfigurationError("--days and --timerange are mutually exclusive. " raise ConfigurationError(
"You can only specify one or the other.") "--days and --timerange are mutually exclusive. "
"You can only specify one or the other."
)
if 'pairs' not in config: if "pairs" not in config:
raise ConfigurationError( raise ConfigurationError(
"Downloading data requires a list of pairs. " "Downloading data requires a list of pairs. "
"Please check the documentation on how to configure this.") "Please check the documentation on how to configure this."
)
def start_download_data(args: Dict[str, Any]) -> None: def start_download_data(args: Dict[str, Any]) -> None:
@@ -46,38 +52,41 @@ def start_download_data(args: Dict[str, Any]) -> None:
def start_convert_trades(args: Dict[str, Any]) -> None: def start_convert_trades(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
timerange = TimeRange() timerange = TimeRange()
# Remove stake-currency to skip checks which are not relevant for datadownload # Remove stake-currency to skip checks which are not relevant for datadownload
config['stake_currency'] = '' config["stake_currency"] = ""
if 'timeframes' not in config: if "timeframes" not in config:
config['timeframes'] = DL_DATA_TIMEFRAMES config["timeframes"] = DL_DATA_TIMEFRAMES
# Init exchange # Init exchange
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
# Manual validations of relevant settings # Manual validations of relevant settings
for timeframe in config['timeframes']: for timeframe in config["timeframes"]:
exchange.validate_timeframes(timeframe) exchange.validate_timeframes(timeframe)
available_pairs = [ available_pairs = [
p for p in exchange.get_markets( p
tradable_only=True, active_only=not config.get('include_inactive') for p in exchange.get_markets(
).keys() tradable_only=True, active_only=not config.get("include_inactive")
).keys()
] ]
expanded_pairs = dynamic_expand_pairlist(config, available_pairs) expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
# Convert downloaded trade data to different timeframes # Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv( convert_trades_to_ohlcv(
pairs=expanded_pairs, timeframes=config['timeframes'], pairs=expanded_pairs,
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), timeframes=config["timeframes"],
data_format_ohlcv=config['dataformat_ohlcv'], datadir=config["datadir"],
data_format_trades=config['dataformat_trades'], timerange=timerange,
candle_type=config.get('candle_type_def', CandleType.SPOT) erase=bool(config.get("erase")),
data_format_ohlcv=config["dataformat_ohlcv"],
data_format_trades=config["dataformat_trades"],
candle_type=config.get("candle_type_def", CandleType.SPOT),
) )
@@ -88,14 +97,19 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if ohlcv: if ohlcv:
migrate_data(config) migrate_data(config)
convert_ohlcv_format(config, convert_ohlcv_format(
convert_from=args['format_from'], config,
convert_to=args['format_to'], convert_from=args["format_from"],
erase=args['erase']) convert_to=args["format_to"],
erase=args["erase"],
)
else: else:
convert_trades_format(config, convert_trades_format(
convert_from=args['format_from_trades'], convert_to=args['format_to'], config,
erase=args['erase']) convert_from=args["format_from_trades"],
convert_to=args["format_to"],
erase=args["erase"],
)
def start_list_data(args: Dict[str, Any]) -> None: def start_list_data(args: Dict[str, Any]) -> None:
@@ -108,45 +122,59 @@ def start_list_data(args: Dict[str, Any]) -> None:
from tabulate import tabulate from tabulate import tabulate
from freqtrade.data.history import get_datahandler from freqtrade.data.history import get_datahandler
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
dhc = get_datahandler(config["datadir"], config["dataformat_ohlcv"])
paircombs = dhc.ohlcv_get_available_data( paircombs = dhc.ohlcv_get_available_data(
config['datadir'], config["datadir"], config.get("trading_mode", TradingMode.SPOT)
config.get('trading_mode', TradingMode.SPOT) )
)
if args['pairs']: if args["pairs"]:
paircombs = [comb for comb in paircombs if comb[0] in args['pairs']] paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]]
print(f"Found {len(paircombs)} pair / timeframe combinations.") print(f"Found {len(paircombs)} pair / timeframe combinations.")
if not config.get('show_timerange'): if not config.get("show_timerange"):
groupedpair = defaultdict(list) groupedpair = defaultdict(list)
for pair, timeframe, candle_type in sorted( for pair, timeframe, candle_type in sorted(
paircombs, paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
): ):
groupedpair[(pair, candle_type)].append(timeframe) groupedpair[(pair, candle_type)].append(timeframe)
if groupedpair: if groupedpair:
print(tabulate([ print(
(pair, ', '.join(timeframes), candle_type) tabulate(
for (pair, candle_type), timeframes in groupedpair.items() [
], (pair, ", ".join(timeframes), candle_type)
headers=("Pair", "Timeframe", "Type"), for (pair, candle_type), timeframes in groupedpair.items()
tablefmt='psql', stralign='right')) ],
headers=("Pair", "Timeframe", "Type"),
tablefmt="psql",
stralign="right",
)
)
else: else:
paircombs1 = [( paircombs1 = [
pair, timeframe, candle_type, (pair, timeframe, candle_type, *dhc.ohlcv_data_min_max(pair, timeframe, candle_type))
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type) for pair, timeframe, candle_type in paircombs
) for pair, timeframe, candle_type in paircombs] ]
print(tabulate([ print(
(pair, timeframe, candle_type, tabulate(
start.strftime(DATETIME_PRINT_FORMAT), [
end.strftime(DATETIME_PRINT_FORMAT), length) (
for pair, timeframe, candle_type, start, end, length in sorted( pair,
paircombs1, timeframe,
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])) candle_type,
], start.strftime(DATETIME_PRINT_FORMAT),
headers=("Pair", "Timeframe", "Type", 'From', 'To', 'Candles'), end.strftime(DATETIME_PRINT_FORMAT),
tablefmt='psql', stralign='right')) length,
)
for pair, timeframe, candle_type, start, end, length in sorted(
paircombs1, key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
)
],
headers=("Pair", "Timeframe", "Type", "From", "To", "Candles"),
tablefmt="psql",
stralign="right",
)
)

View File

@@ -19,9 +19,9 @@ def start_convert_db(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
init_db(config['db_url']) init_db(config["db_url"])
session_target = Trade.session session_target = Trade.session
init_db(config['db_url_from']) init_db(config["db_url_from"])
logger.info("Starting db migration.") logger.info("Starting db migration.")
trade_count = 0 trade_count = 0
@@ -47,9 +47,11 @@ def start_convert_db(args: Dict[str, Any]) -> None:
max_order_id = session_target.scalar(select(func.max(Order.id))) max_order_id = session_target.scalar(select(func.max(Order.id)))
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id))) max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
set_sequence_ids(session_target.get_bind(), set_sequence_ids(
trade_id=max_trade_id, session_target.get_bind(),
order_id=max_order_id, trade_id=max_trade_id,
pairlock_id=max_pairlock_id) order_id=max_order_id,
pairlock_id=max_pairlock_id,
)
logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.") logger.info(f"Migrated {trade_count} Trades, and {pairlock_count} Pairlocks.")

View File

@@ -38,7 +38,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
""" """
Deploy new strategy from template to strategy_path Deploy new strategy from template to strategy_path
""" """
fallback = 'full' fallback = "full"
attributes = render_template_with_fallback( attributes = render_template_with_fallback(
templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2", templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2",
templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2", templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2",
@@ -64,33 +64,35 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
templatefallbackfile="strategy_subtemplates/strategy_methods_empty.j2", templatefallbackfile="strategy_subtemplates/strategy_methods_empty.j2",
) )
strategy_text = render_template(templatefile='base_strategy.py.j2', strategy_text = render_template(
arguments={"strategy": strategy_name, templatefile="base_strategy.py.j2",
"attributes": attributes, arguments={
"indicators": indicators, "strategy": strategy_name,
"buy_trend": buy_trend, "attributes": attributes,
"sell_trend": sell_trend, "indicators": indicators,
"plot_config": plot_config, "buy_trend": buy_trend,
"additional_methods": additional_methods, "sell_trend": sell_trend,
}) "plot_config": plot_config,
"additional_methods": additional_methods,
},
)
logger.info(f"Writing strategy to `{strategy_path}`.") logger.info(f"Writing strategy to `{strategy_path}`.")
strategy_path.write_text(strategy_text) strategy_path.write_text(strategy_text)
def start_new_strategy(args: Dict[str, Any]) -> None: def start_new_strategy(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if "strategy" in args and args["strategy"]: if "strategy" in args and args["strategy"]:
new_path = config["user_data_dir"] / USERPATH_STRATEGIES / (args["strategy"] + ".py")
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py')
if new_path.exists(): if new_path.exists():
raise OperationalException(f"`{new_path}` already exists. " raise OperationalException(
"Please choose another Strategy Name.") f"`{new_path}` already exists. Please choose another Strategy Name."
)
deploy_new_strategy(args['strategy'], new_path, args['template']) deploy_new_strategy(args["strategy"], new_path, args["template"])
else: else:
raise ConfigurationError("`new-strategy` requires --strategy to be set.") raise ConfigurationError("`new-strategy` requires --strategy to be set.")
@@ -100,8 +102,8 @@ def clean_ui_subdir(directory: Path):
if directory.is_dir(): if directory.is_dir():
logger.info("Removing UI directory content.") logger.info("Removing UI directory content.")
for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root for p in reversed(list(directory.glob("**/*"))): # iterate contents from leaves to root
if p.name in ('.gitkeep', 'fallback_file.html'): if p.name in (".gitkeep", "fallback_file.html"):
continue continue
if p.is_file(): if p.is_file():
p.unlink() p.unlink()
@@ -110,11 +112,11 @@ def clean_ui_subdir(directory: Path):
def read_ui_version(dest_folder: Path) -> Optional[str]: def read_ui_version(dest_folder: Path) -> Optional[str]:
file = dest_folder / '.uiversion' file = dest_folder / ".uiversion"
if not file.is_file(): if not file.is_file():
return None return None
with file.open('r') as f: with file.open("r") as f:
return f.read() return f.read()
@@ -133,12 +135,12 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
destfile.mkdir(exist_ok=True) destfile.mkdir(exist_ok=True)
else: else:
destfile.write_bytes(x.read()) destfile.write_bytes(x.read())
with (dest_folder / '.uiversion').open('w') as f: with (dest_folder / ".uiversion").open("w") as f:
f.write(version) f.write(version)
def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]: def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
base_url = 'https://api.github.com/repos/freqtrade/frequi/' base_url = "https://api.github.com/repos/freqtrade/frequi/"
# Get base UI Repo path # Get base UI Repo path
resp = requests.get(f"{base_url}releases", timeout=req_timeout) resp = requests.get(f"{base_url}releases", timeout=req_timeout)
@@ -146,42 +148,41 @@ def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
r = resp.json() r = resp.json()
if version: if version:
tmp = [x for x in r if x['name'] == version] tmp = [x for x in r if x["name"] == version]
if tmp: if tmp:
latest_version = tmp[0]['name'] latest_version = tmp[0]["name"]
assets = tmp[0].get('assets', []) assets = tmp[0].get("assets", [])
else: else:
raise ValueError("UI-Version not found.") raise ValueError("UI-Version not found.")
else: else:
latest_version = r[0]['name'] latest_version = r[0]["name"]
assets = r[0].get('assets', []) assets = r[0].get("assets", [])
dl_url = '' dl_url = ""
if assets and len(assets) > 0: if assets and len(assets) > 0:
dl_url = assets[0]['browser_download_url'] dl_url = assets[0]["browser_download_url"]
# URL not found - try assets url # URL not found - try assets url
if not dl_url: if not dl_url:
assets = r[0]['assets_url'] assets = r[0]["assets_url"]
resp = requests.get(assets, timeout=req_timeout) resp = requests.get(assets, timeout=req_timeout)
r = resp.json() r = resp.json()
dl_url = r[0]['browser_download_url'] dl_url = r[0]["browser_download_url"]
return dl_url, latest_version return dl_url, latest_version
def start_install_ui(args: Dict[str, Any]) -> None: def start_install_ui(args: Dict[str, Any]) -> None:
dest_folder = Path(__file__).parents[1] / "rpc/api_server/ui/installed/"
dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/'
# First make sure the assets are removed. # First make sure the assets are removed.
dl_url, latest_version = get_ui_download_url(args.get('ui_version')) dl_url, latest_version = get_ui_download_url(args.get("ui_version"))
curr_version = read_ui_version(dest_folder) curr_version = read_ui_version(dest_folder)
if curr_version == latest_version and not args.get('erase_ui_only'): if curr_version == latest_version and not args.get("erase_ui_only"):
logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.") logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.")
return return
clean_ui_subdir(dest_folder) clean_ui_subdir(dest_folder)
if args.get('erase_ui_only'): if args.get("erase_ui_only"):
logger.info("Erased UI directory content. Not downloading new version.") logger.info("Erased UI directory content. Not downloading new version.")
else: else:
# Download a new version # Download a new version

View File

@@ -22,15 +22,15 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
print_colorized = config.get('print_colorized', False) print_colorized = config.get("print_colorized", False)
print_json = config.get('print_json', False) print_json = config.get("print_json", False)
export_csv = config.get('export_csv') export_csv = config.get("export_csv")
no_details = config.get('hyperopt_list_no_details', False) no_details = config.get("hyperopt_list_no_details", False)
no_header = False no_header = False
results_file = get_latest_hyperopt_file( results_file = get_latest_hyperopt_file(
config['user_data_dir'] / 'hyperopt_results', config["user_data_dir"] / "hyperopt_results", config.get("hyperoptexportfilename")
config.get('hyperoptexportfilename')) )
# Previous evaluations # Previous evaluations
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config) epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
@@ -40,21 +40,26 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if not export_csv: if not export_csv:
try: try:
print(HyperoptTools.get_result_table(config, epochs, total_epochs, print(
not config.get('hyperopt_list_best', False), HyperoptTools.get_result_table(
print_colorized, 0)) config,
epochs,
total_epochs,
not config.get("hyperopt_list_best", False),
print_colorized,
0,
)
)
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print("User interrupted..")
if epochs and not no_details: if epochs and not no_details:
sorted_epochs = sorted(epochs, key=itemgetter('loss')) sorted_epochs = sorted(epochs, key=itemgetter("loss"))
results = sorted_epochs[0] results = sorted_epochs[0]
HyperoptTools.show_epoch_details(results, total_epochs, print_json, no_header) HyperoptTools.show_epoch_details(results, total_epochs, print_json, no_header)
if epochs and export_csv: if epochs and export_csv:
HyperoptTools.export_csv_file( HyperoptTools.export_csv_file(config, epochs, export_csv)
config, epochs, export_csv
)
def start_hyperopt_show(args: Dict[str, Any]) -> None: def start_hyperopt_show(args: Dict[str, Any]) -> None:
@@ -65,13 +70,13 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
print_json = config.get('print_json', False) print_json = config.get("print_json", False)
no_header = config.get('hyperopt_show_no_header', False) no_header = config.get("hyperopt_show_no_header", False)
results_file = get_latest_hyperopt_file( results_file = get_latest_hyperopt_file(
config['user_data_dir'] / 'hyperopt_results', config["user_data_dir"] / "hyperopt_results", config.get("hyperoptexportfilename")
config.get('hyperoptexportfilename')) )
n = config.get('hyperopt_show_index', -1) n = config.get("hyperopt_show_index", -1)
# Previous evaluations # Previous evaluations
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config) epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
@@ -80,10 +85,12 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if n > filtered_epochs: if n > filtered_epochs:
raise OperationalException( raise OperationalException(
f"The index of the epoch to show should be less than {filtered_epochs + 1}.") f"The index of the epoch to show should be less than {filtered_epochs + 1}."
)
if n < -filtered_epochs: if n < -filtered_epochs:
raise OperationalException( raise OperationalException(
f"The index of the epoch to show should be greater than {-filtered_epochs - 1}.") f"The index of the epoch to show should be greater than {-filtered_epochs - 1}."
)
# Translate epoch index from human-readable format to pythonic # Translate epoch index from human-readable format to pythonic
if n > 0: if n > 0:
@@ -92,13 +99,18 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if epochs: if epochs:
val = epochs[n] val = epochs[n]
metrics = val['results_metrics'] metrics = val["results_metrics"]
if 'strategy_name' in metrics: if "strategy_name" in metrics:
strategy_name = metrics['strategy_name'] strategy_name = metrics["strategy_name"]
show_backtest_result(strategy_name, metrics, show_backtest_result(
metrics['stake_currency'], config.get('backtest_breakdown', [])) strategy_name,
metrics,
metrics["stake_currency"],
config.get("backtest_breakdown", []),
)
HyperoptTools.try_export_params(config, strategy_name, val) HyperoptTools.try_export_params(config, strategy_name, val)
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header, HyperoptTools.show_epoch_details(
header_str="Epoch details") val, total_epochs, print_json, no_header, header_str="Epoch details"
)

View File

@@ -26,42 +26,47 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
:return: None :return: None
""" """
exchanges = list_available_exchanges(args['list_exchanges_all']) exchanges = list_available_exchanges(args["list_exchanges_all"])
if args['print_one_column']: if args["print_one_column"]:
print('\n'.join([e['name'] for e in exchanges])) print("\n".join([e["name"] for e in exchanges]))
else: else:
headers = { headers = {
'name': 'Exchange name', "name": "Exchange name",
'supported': 'Supported', "supported": "Supported",
'trade_modes': 'Markets', "trade_modes": "Markets",
'comment': 'Reason', "comment": "Reason",
} }
headers.update({'valid': 'Valid'} if args['list_exchanges_all'] else {}) headers.update({"valid": "Valid"} if args["list_exchanges_all"] else {})
def build_entry(exchange: ValidExchangesType, valid: bool): def build_entry(exchange: ValidExchangesType, valid: bool):
valid_entry = {'valid': exchange['valid']} if valid else {} valid_entry = {"valid": exchange["valid"]} if valid else {}
result: Dict[str, Union[str, bool]] = { result: Dict[str, Union[str, bool]] = {
'name': exchange['name'], "name": exchange["name"],
**valid_entry, **valid_entry,
'supported': 'Official' if exchange['supported'] else '', "supported": "Official" if exchange["supported"] else "",
'trade_modes': ', '.join( "trade_modes": ", ".join(
(f"{a['margin_mode']} " if a['margin_mode'] else '') + a['trading_mode'] (f"{a['margin_mode']} " if a["margin_mode"] else "") + a["trading_mode"]
for a in exchange['trade_modes'] for a in exchange["trade_modes"]
), ),
'comment': exchange['comment'], "comment": exchange["comment"],
} }
return result return result
if args['list_exchanges_all']: if args["list_exchanges_all"]:
print("All exchanges supported by the ccxt library:") print("All exchanges supported by the ccxt library:")
exchanges = [build_entry(e, True) for e in exchanges] exchanges = [build_entry(e, True) for e in exchanges]
else: else:
print("Exchanges available for Freqtrade:") print("Exchanges available for Freqtrade:")
exchanges = [build_entry(e, False) for e in exchanges if e['valid'] is not False] exchanges = [build_entry(e, False) for e in exchanges if e["valid"] is not False]
print(tabulate(exchanges, headers=headers, )) print(
tabulate(
exchanges,
headers=headers,
)
)
def _print_objs_tabular(objs: List, print_colorized: bool) -> None: def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
@@ -71,26 +76,35 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
yellow = Fore.YELLOW yellow = Fore.YELLOW
reset = Style.RESET_ALL reset = Style.RESET_ALL
else: else:
red = '' red = ""
yellow = '' yellow = ""
reset = '' reset = ""
names = [s['name'] for s in objs] names = [s["name"] for s in objs]
objs_to_print = [{ objs_to_print = [
'name': s['name'] if s['name'] else "--", {
'location': s['location_rel'], "name": s["name"] if s["name"] else "--",
'status': (red + "LOAD FAILED" + reset if s['class'] is None "location": s["location_rel"],
else "OK" if names.count(s['name']) == 1 "status": (
else yellow + "DUPLICATE NAME" + reset) red + "LOAD FAILED" + reset
} for s in objs] if s["class"] is None
else "OK"
if names.count(s["name"]) == 1
else yellow + "DUPLICATE NAME" + reset
),
}
for s in objs
]
for idx, s in enumerate(objs): for idx, s in enumerate(objs):
if 'hyperoptable' in s: if "hyperoptable" in s:
objs_to_print[idx].update({ objs_to_print[idx].update(
'hyperoptable': "Yes" if s['hyperoptable']['count'] > 0 else "No", {
'buy-Params': len(s['hyperoptable'].get('buy', [])), "hyperoptable": "Yes" if s["hyperoptable"]["count"] > 0 else "No",
'sell-Params': len(s['hyperoptable'].get('sell', [])), "buy-Params": len(s["hyperoptable"].get("buy", [])),
}) "sell-Params": len(s["hyperoptable"].get("sell", [])),
print(tabulate(objs_to_print, headers='keys', tablefmt='psql', stralign='right')) }
)
print(tabulate(objs_to_print, headers="keys", tablefmt="psql", stralign="right"))
def start_list_strategies(args: Dict[str, Any]) -> None: def start_list_strategies(args: Dict[str, Any]) -> None:
@@ -100,19 +114,20 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
strategy_objs = StrategyResolver.search_all_objects( strategy_objs = StrategyResolver.search_all_objects(
config, not args['print_one_column'], config.get('recursive_strategy_search', False)) config, not args["print_one_column"], config.get("recursive_strategy_search", False)
)
# Sort alphabetically # Sort alphabetically
strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
for obj in strategy_objs: for obj in strategy_objs:
if obj['class']: if obj["class"]:
obj['hyperoptable'] = obj['class'].detect_all_parameters() obj["hyperoptable"] = obj["class"].detect_all_parameters()
else: else:
obj['hyperoptable'] = {'count': 0} obj["hyperoptable"] = {"count": 0}
if args['print_one_column']: if args["print_one_column"]:
print('\n'.join([s['name'] for s in strategy_objs])) print("\n".join([s["name"] for s in strategy_objs]))
else: else:
_print_objs_tabular(strategy_objs, config.get('print_colorized', False)) _print_objs_tabular(strategy_objs, config.get("print_colorized", False))
def start_list_freqAI_models(args: Dict[str, Any]) -> None: def start_list_freqAI_models(args: Dict[str, Any]) -> None:
@@ -121,13 +136,14 @@ def start_list_freqAI_models(args: Dict[str, Any]) -> None:
""" """
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
model_objs = FreqaiModelResolver.search_all_objects(config, not args['print_one_column'])
model_objs = FreqaiModelResolver.search_all_objects(config, not args["print_one_column"])
# Sort alphabetically # Sort alphabetically
model_objs = sorted(model_objs, key=lambda x: x['name']) model_objs = sorted(model_objs, key=lambda x: x["name"])
if args['print_one_column']: if args["print_one_column"]:
print('\n'.join([s['name'] for s in model_objs])) print("\n".join([s["name"] for s in model_objs]))
else: else:
_print_objs_tabular(model_objs, config.get('print_colorized', False)) _print_objs_tabular(model_objs, config.get("print_colorized", False))
def start_list_timeframes(args: Dict[str, Any]) -> None: def start_list_timeframes(args: Dict[str, Any]) -> None:
@@ -136,16 +152,18 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
""" """
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
# Do not use timeframe set in the config # Do not use timeframe set in the config
config['timeframe'] = None config["timeframe"] = None
# Init exchange # Init exchange
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
if args['print_one_column']: if args["print_one_column"]:
print('\n'.join(exchange.timeframes)) print("\n".join(exchange.timeframes))
else: else:
print(f"Timeframes available for the exchange `{exchange.name}`: " print(
f"{', '.join(exchange.timeframes)}") f"Timeframes available for the exchange `{exchange.name}`: "
f"{', '.join(exchange.timeframes)}"
)
def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
@@ -161,51 +179,75 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
# By default only active pairs/markets are to be shown # By default only active pairs/markets are to be shown
active_only = not args.get('list_pairs_all', False) active_only = not args.get("list_pairs_all", False)
base_currencies = args.get('base_currencies', []) base_currencies = args.get("base_currencies", [])
quote_currencies = args.get('quote_currencies', []) quote_currencies = args.get("quote_currencies", [])
try: try:
pairs = exchange.get_markets(base_currencies=base_currencies, pairs = exchange.get_markets(
quote_currencies=quote_currencies, base_currencies=base_currencies,
tradable_only=pairs_only, quote_currencies=quote_currencies,
active_only=active_only) tradable_only=pairs_only,
active_only=active_only,
)
# Sort the pairs/markets by symbol # Sort the pairs/markets by symbol
pairs = dict(sorted(pairs.items())) pairs = dict(sorted(pairs.items()))
except Exception as e: except Exception as e:
raise OperationalException(f"Cannot get markets. Reason: {e}") from e raise OperationalException(f"Cannot get markets. Reason: {e}") from e
else: else:
summary_str = ((f"Exchange {exchange.name} has {len(pairs)} ") + summary_str = (
("active " if active_only else "") + (f"Exchange {exchange.name} has {len(pairs)} ")
(plural(len(pairs), "pair" if pairs_only else "market")) + + ("active " if active_only else "")
(f" with {', '.join(base_currencies)} as base " + (plural(len(pairs), "pair" if pairs_only else "market"))
f"{plural(len(base_currencies), 'currency', 'currencies')}" + (
if base_currencies else "") + f" with {', '.join(base_currencies)} as base "
(" and" if base_currencies and quote_currencies else "") + f"{plural(len(base_currencies), 'currency', 'currencies')}"
(f" with {', '.join(quote_currencies)} as quote " if base_currencies
f"{plural(len(quote_currencies), 'currency', 'currencies')}" else ""
if quote_currencies else "")) )
+ (" and" if base_currencies and quote_currencies else "")
+ (
f" with {', '.join(quote_currencies)} as quote "
f"{plural(len(quote_currencies), 'currency', 'currencies')}"
if quote_currencies
else ""
)
)
headers = ["Id", "Symbol", "Base", "Quote", "Active", headers = [
"Spot", "Margin", "Future", "Leverage"] "Id",
"Symbol",
"Base",
"Quote",
"Active",
"Spot",
"Margin",
"Future",
"Leverage",
]
tabular_data = [{ tabular_data = [
'Id': v['id'], {
'Symbol': v['symbol'], "Id": v["id"],
'Base': v['base'], "Symbol": v["symbol"],
'Quote': v['quote'], "Base": v["base"],
'Active': market_is_active(v), "Quote": v["quote"],
'Spot': 'Spot' if exchange.market_is_spot(v) else '', "Active": market_is_active(v),
'Margin': 'Margin' if exchange.market_is_margin(v) else '', "Spot": "Spot" if exchange.market_is_spot(v) else "",
'Future': 'Future' if exchange.market_is_future(v) else '', "Margin": "Margin" if exchange.market_is_margin(v) else "",
'Leverage': exchange.get_max_leverage(v['symbol'], 20) "Future": "Future" if exchange.market_is_future(v) else "",
} for _, v in pairs.items()] "Leverage": exchange.get_max_leverage(v["symbol"], 20),
}
for _, v in pairs.items()
]
if (args.get('print_one_column', False) or if (
args.get('list_pairs_print_json', False) or args.get("print_one_column", False)
args.get('print_csv', False)): or args.get("list_pairs_print_json", False)
or args.get("print_csv", False)
):
# Print summary string in the log in case of machine-readable # Print summary string in the log in case of machine-readable
# regular formats. # regular formats.
logger.info(f"{summary_str}.") logger.info(f"{summary_str}.")
@@ -215,24 +257,26 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
print() print()
if pairs: if pairs:
if args.get('print_list', False): if args.get("print_list", False):
# print data as a list, with human-readable summary # print data as a list, with human-readable summary
print(f"{summary_str}: {', '.join(pairs.keys())}.") print(f"{summary_str}: {', '.join(pairs.keys())}.")
elif args.get('print_one_column', False): elif args.get("print_one_column", False):
print('\n'.join(pairs.keys())) print("\n".join(pairs.keys()))
elif args.get('list_pairs_print_json', False): elif args.get("list_pairs_print_json", False):
print(rapidjson.dumps(list(pairs.keys()), default=str)) print(rapidjson.dumps(list(pairs.keys()), default=str))
elif args.get('print_csv', False): elif args.get("print_csv", False):
writer = csv.DictWriter(sys.stdout, fieldnames=headers) writer = csv.DictWriter(sys.stdout, fieldnames=headers)
writer.writeheader() writer.writeheader()
writer.writerows(tabular_data) writer.writerows(tabular_data)
else: else:
# print data as a table, with the human-readable summary # print data as a table, with the human-readable summary
print(f"{summary_str}:") print(f"{summary_str}:")
print(tabulate(tabular_data, headers='keys', tablefmt='psql', stralign='right')) print(tabulate(tabular_data, headers="keys", tablefmt="psql", stralign="right"))
elif not (args.get('print_one_column', False) or elif not (
args.get('list_pairs_print_json', False) or args.get("print_one_column", False)
args.get('print_csv', False)): or args.get("list_pairs_print_json", False)
or args.get("print_csv", False)
):
print(f"{summary_str}.") print(f"{summary_str}.")
@@ -243,21 +287,22 @@ def start_show_trades(args: Dict[str, Any]) -> None:
import json import json
from freqtrade.persistence import Trade, init_db from freqtrade.persistence import Trade, init_db
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if 'db_url' not in config: if "db_url" not in config:
raise ConfigurationError("--db-url is required for this command.") raise ConfigurationError("--db-url is required for this command.")
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
init_db(config['db_url']) init_db(config["db_url"])
tfilter = [] tfilter = []
if config.get('trade_ids'): if config.get("trade_ids"):
tfilter.append(Trade.id.in_(config['trade_ids'])) tfilter.append(Trade.id.in_(config["trade_ids"]))
trades = Trade.get_trades(tfilter).all() trades = Trade.get_trades(tfilter).all()
logger.info(f"Printing {len(trades)} Trades: ") logger.info(f"Printing {len(trades)} Trades: ")
if config.get('print_json', False): if config.get("print_json", False):
print(json.dumps([trade.to_json() for trade in trades], indent=4)) print(json.dumps([trade.to_json() for trade in trades], indent=4))
else: else:
for trade in trades: for trade in trades:

View File

@@ -21,20 +21,22 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
config = setup_utils_configuration(args, method) config = setup_utils_configuration(args, method)
no_unlimited_runmodes = { no_unlimited_runmodes = {
RunMode.BACKTEST: 'backtesting', RunMode.BACKTEST: "backtesting",
RunMode.HYPEROPT: 'hyperoptimization', RunMode.HYPEROPT: "hyperoptimization",
} }
if method in no_unlimited_runmodes.keys(): if method in no_unlimited_runmodes.keys():
wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio'] wallet_size = config["dry_run_wallet"] * config["tradable_balance_ratio"]
# tradable_balance_ratio # tradable_balance_ratio
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT if (
and config['stake_amount'] > wallet_size): config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT
wallet = fmt_coin(wallet_size, config['stake_currency']) and config["stake_amount"] > wallet_size
stake = fmt_coin(config['stake_amount'], config['stake_currency']) ):
wallet = fmt_coin(wallet_size, config["stake_currency"])
stake = fmt_coin(config["stake_amount"], config["stake_currency"])
raise ConfigurationError( raise ConfigurationError(
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. " f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`." f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
) )
return config return config
@@ -51,7 +53,7 @@ def start_backtesting(args: Dict[str, Any]) -> None:
# Initialize configuration # Initialize configuration
config = setup_optimize_configuration(args, RunMode.BACKTEST) config = setup_optimize_configuration(args, RunMode.BACKTEST)
logger.info('Starting freqtrade in Backtesting mode') logger.info("Starting freqtrade in Backtesting mode")
# Initialize backtesting object # Initialize backtesting object
backtesting = Backtesting(config) backtesting = Backtesting(config)
@@ -68,7 +70,7 @@ def start_backtesting_show(args: Dict[str, Any]) -> None:
from freqtrade.data.btanalysis import load_backtest_stats from freqtrade.data.btanalysis import load_backtest_stats
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
results = load_backtest_stats(config['exportfilename']) results = load_backtest_stats(config["exportfilename"])
show_backtest_results(config, results) show_backtest_results(config, results)
show_sorted_pairlist(config, results) show_sorted_pairlist(config, results)
@@ -87,20 +89,20 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt import Hyperopt
except ImportError as e: except ImportError as e:
raise OperationalException( raise OperationalException(
f"{e}. Please ensure that the hyperopt dependencies are installed.") from e f"{e}. Please ensure that the hyperopt dependencies are installed."
) from e
# Initialize configuration # Initialize configuration
config = setup_optimize_configuration(args, RunMode.HYPEROPT) config = setup_optimize_configuration(args, RunMode.HYPEROPT)
logger.info('Starting freqtrade in Hyperopt mode') logger.info("Starting freqtrade in Hyperopt mode")
lock = FileLock(Hyperopt.get_lock_filename(config)) lock = FileLock(Hyperopt.get_lock_filename(config))
try: try:
with lock.acquire(timeout=1): with lock.acquire(timeout=1):
# Remove noisy log messages # Remove noisy log messages
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING) logging.getLogger("hyperopt.tpe").setLevel(logging.WARNING)
logging.getLogger('filelock').setLevel(logging.WARNING) logging.getLogger("filelock").setLevel(logging.WARNING)
# Initialize backtesting object # Initialize backtesting object
hyperopt = Hyperopt(config) hyperopt = Hyperopt(config)
@@ -108,9 +110,11 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
except Timeout: except Timeout:
logger.info("Another running instance of freqtrade Hyperopt detected.") logger.info("Another running instance of freqtrade Hyperopt detected.")
logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " logger.info(
"Hyperopt module is resource hungry. Please run your Hyperopt sequentially " "Simultaneous execution of multiple Hyperopt commands is not supported. "
"or on separate machines.") "Hyperopt module is resource hungry. Please run your Hyperopt sequentially "
"or on separate machines."
)
logger.info("Quitting now.") logger.info("Quitting now.")
# TODO: return False here in order to help freqtrade to exit # TODO: return False here in order to help freqtrade to exit
# with non-zero exit code... # with non-zero exit code...
@@ -127,7 +131,7 @@ def start_edge(args: Dict[str, Any]) -> None:
# Initialize configuration # Initialize configuration
config = setup_optimize_configuration(args, RunMode.EDGE) config = setup_optimize_configuration(args, RunMode.EDGE)
logger.info('Starting freqtrade in Edge mode') logger.info("Starting freqtrade in Edge mode")
# Initialize Edge object # Initialize Edge object
edge_cli = EdgeCli(config) edge_cli = EdgeCli(config)

View File

@@ -17,28 +17,29 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
""" """
from freqtrade.persistence import FtNoDBContext from freqtrade.persistence import FtNoDBContext
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
quote_currencies = args.get('quote_currencies') quote_currencies = args.get("quote_currencies")
if not quote_currencies: if not quote_currencies:
quote_currencies = [config.get('stake_currency')] quote_currencies = [config.get("stake_currency")]
results = {} results = {}
with FtNoDBContext(): with FtNoDBContext():
for curr in quote_currencies: for curr in quote_currencies:
config['stake_currency'] = curr config["stake_currency"] = curr
pairlists = PairListManager(exchange, config) pairlists = PairListManager(exchange, config)
pairlists.refresh_pairlist() pairlists.refresh_pairlist()
results[curr] = pairlists.whitelist results[curr] = pairlists.whitelist
for curr, pairlist in results.items(): for curr, pairlist in results.items():
if not args.get('print_one_column', False) and not args.get('list_pairs_print_json', False): if not args.get("print_one_column", False) and not args.get("list_pairs_print_json", False):
print(f"Pairs for {curr}: ") print(f"Pairs for {curr}: ")
if args.get('print_one_column', False): if args.get("print_one_column", False):
print('\n'.join(pairlist)) print("\n".join(pairlist))
elif args.get('list_pairs_print_json', False): elif args.get("list_pairs_print_json", False):
print(rapidjson.dumps(list(pairlist), default=str)) print(rapidjson.dumps(list(pairlist), default=str))
else: else:
print(pairlist) print(pairlist)

View File

@@ -6,10 +6,11 @@ from freqtrade.exceptions import ConfigurationError
def validate_plot_args(args: Dict[str, Any]) -> None: def validate_plot_args(args: Dict[str, Any]) -> None:
if not args.get('datadir') and not args.get('config'): if not args.get("datadir") and not args.get("config"):
raise ConfigurationError( raise ConfigurationError(
"You need to specify either `--datadir` or `--config` " "You need to specify either `--datadir` or `--config` "
"for plot-profit and plot-dataframe.") "for plot-profit and plot-dataframe."
)
def start_plot_dataframe(args: Dict[str, Any]) -> None: def start_plot_dataframe(args: Dict[str, Any]) -> None:
@@ -18,6 +19,7 @@ def start_plot_dataframe(args: Dict[str, Any]) -> None:
""" """
# Import here to avoid errors if plot-dependencies are not installed. # Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import load_and_plot_trades from freqtrade.plot.plotting import load_and_plot_trades
validate_plot_args(args) validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT) config = setup_utils_configuration(args, RunMode.PLOT)
@@ -30,6 +32,7 @@ def start_plot_profit(args: Dict[str, Any]) -> None:
""" """
# Import here to avoid errors if plot-dependencies are not installed. # Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import plot_profit from freqtrade.plot.plotting import plot_profit
validate_plot_args(args) validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT) config = setup_utils_configuration(args, RunMode.PLOT)

View File

@@ -26,13 +26,15 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
strategy_objs = StrategyResolver.search_all_objects( strategy_objs = StrategyResolver.search_all_objects(
config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) config, enum_failed=False, recursive=config.get("recursive_strategy_search", False)
)
filtered_strategy_objs = [] filtered_strategy_objs = []
if args['strategy_list']: if args["strategy_list"]:
filtered_strategy_objs = [ filtered_strategy_objs = [
strategy_obj for strategy_obj in strategy_objs strategy_obj
if strategy_obj['name'] in args['strategy_list'] for strategy_obj in strategy_objs
if strategy_obj["name"] in args["strategy_list"]
] ]
else: else:
@@ -41,8 +43,8 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
processed_locations = set() processed_locations = set()
for strategy_obj in filtered_strategy_objs: for strategy_obj in filtered_strategy_objs:
if strategy_obj['location'] not in processed_locations: if strategy_obj["location"] not in processed_locations:
processed_locations.add(strategy_obj['location']) processed_locations.add(strategy_obj["location"])
start_conversion(strategy_obj, config) start_conversion(strategy_obj, config)

View File

@@ -24,13 +24,13 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
] ]
config = deepcopy(config) config = deepcopy(config)
for key in keys_to_remove: for key in keys_to_remove:
if '.' in key: if "." in key:
nested_keys = key.split('.') nested_keys = key.split(".")
nested_config = config nested_config = config
for nested_key in nested_keys[:-1]: for nested_key in nested_keys[:-1]:
nested_config = nested_config.get(nested_key, {}) nested_config = nested_config.get(nested_key, {})
nested_config[nested_keys[-1]] = 'REDACTED' nested_config[nested_keys[-1]] = "REDACTED"
else: else:
config[key] = 'REDACTED' config[key] = "REDACTED"
return config return config

View File

@@ -11,7 +11,8 @@ logger = logging.getLogger(__name__)
def setup_utils_configuration( def setup_utils_configuration(
args: Dict[str, Any], method: RunMode, *, set_dry: bool = True) -> Dict[str, Any]: args: Dict[str, Any], method: RunMode, *, set_dry: bool = True
) -> Dict[str, Any]:
""" """
Prepare the configuration for utils subcommands Prepare the configuration for utils subcommands
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
@@ -23,7 +24,7 @@ def setup_utils_configuration(
# Ensure these modes are using Dry-run # Ensure these modes are using Dry-run
if set_dry: if set_dry:
config['dry_run'] = True config["dry_run"] = True
validate_config_consistency(config, preliminary=True) validate_config_consistency(config, preliminary=True)
return config return config

View File

@@ -20,18 +20,16 @@ def _extend_validator(validator_class):
Extended validator for the Freqtrade configuration JSON Schema. Extended validator for the Freqtrade configuration JSON Schema.
Currently it only handles defaults for subschemas. Currently it only handles defaults for subschemas.
""" """
validate_properties = validator_class.VALIDATORS['properties'] validate_properties = validator_class.VALIDATORS["properties"]
def set_defaults(validator, properties, instance, schema): def set_defaults(validator, properties, instance, schema):
for prop, subschema in properties.items(): for prop, subschema in properties.items():
if 'default' in subschema: if "default" in subschema:
instance.setdefault(prop, subschema['default']) instance.setdefault(prop, subschema["default"])
yield from validate_properties(validator, properties, instance, schema) yield from validate_properties(validator, properties, instance, schema)
return validators.extend( return validators.extend(validator_class, {"properties": set_defaults})
validator_class, {'properties': set_defaults}
)
FreqtradeValidator = _extend_validator(Draft4Validator) FreqtradeValidator = _extend_validator(Draft4Validator)
@@ -44,27 +42,23 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
:return: Returns the config if valid, otherwise throw an exception :return: Returns the config if valid, otherwise throw an exception
""" """
conf_schema = deepcopy(constants.CONF_SCHEMA) conf_schema = deepcopy(constants.CONF_SCHEMA)
if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE): if conf.get("runmode", RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE):
conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED conf_schema["required"] = constants.SCHEMA_TRADE_REQUIRED
elif conf.get('runmode', RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT): elif conf.get("runmode", RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT):
if preliminary: if preliminary:
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED conf_schema["required"] = constants.SCHEMA_BACKTEST_REQUIRED
else: else:
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL conf_schema["required"] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
elif conf.get('runmode', RunMode.OTHER) == RunMode.WEBSERVER: elif conf.get("runmode", RunMode.OTHER) == RunMode.WEBSERVER:
conf_schema['required'] = constants.SCHEMA_MINIMAL_WEBSERVER conf_schema["required"] = constants.SCHEMA_MINIMAL_WEBSERVER
else: else:
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED conf_schema["required"] = constants.SCHEMA_MINIMAL_REQUIRED
try: try:
FreqtradeValidator(conf_schema).validate(conf) FreqtradeValidator(conf_schema).validate(conf)
return conf return conf
except ValidationError as e: except ValidationError as e:
logger.critical( logger.critical(f"Invalid configuration. Reason: {e}")
f"Invalid configuration. Reason: {e}" raise ValidationError(best_match(Draft4Validator(conf_schema).iter_errors(conf)).message)
)
raise ValidationError(
best_match(Draft4Validator(conf_schema).iter_errors(conf)).message
)
def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None: def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None:
@@ -91,7 +85,7 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
validate_migrated_strategy_settings(conf) validate_migrated_strategy_settings(conf)
# validate configuration before returning # validate configuration before returning
logger.info('Validating configuration ...') logger.info("Validating configuration ...")
validate_config_schema(conf, preliminary=preliminary) validate_config_schema(conf, preliminary=preliminary)
@@ -100,9 +94,11 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
If edge is disabled, either max_open_trades or stake_amount need to be set. If edge is disabled, either max_open_trades or stake_amount need to be set.
:raise: ConfigurationError if config validation failed :raise: ConfigurationError if config validation failed
""" """
if (not conf.get('edge', {}).get('enabled') if (
and conf.get('max_open_trades') == float('inf') not conf.get("edge", {}).get("enabled")
and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT): and conf.get("max_open_trades") == float("inf")
and conf.get("stake_amount") == constants.UNLIMITED_STAKE_AMOUNT
):
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.") raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
@@ -111,45 +107,47 @@ def _validate_price_config(conf: Dict[str, Any]) -> None:
When using market orders, price sides must be using the "other" side of the price When using market orders, price sides must be using the "other" side of the price
""" """
# TODO: The below could be an enforced setting when using market orders # TODO: The below could be an enforced setting when using market orders
if (conf.get('order_types', {}).get('entry') == 'market' if conf.get("order_types", {}).get("entry") == "market" and conf.get("entry_pricing", {}).get(
and conf.get('entry_pricing', {}).get('price_side') not in ('ask', 'other')): "price_side"
raise ConfigurationError( ) not in ("ask", "other"):
'Market entry orders require entry_pricing.price_side = "other".') raise ConfigurationError('Market entry orders require entry_pricing.price_side = "other".')
if (conf.get('order_types', {}).get('exit') == 'market' if conf.get("order_types", {}).get("exit") == "market" and conf.get("exit_pricing", {}).get(
and conf.get('exit_pricing', {}).get('price_side') not in ('bid', 'other')): "price_side"
) not in ("bid", "other"):
raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".') raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".')
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if conf.get("stoploss") == 0.0:
if conf.get('stoploss') == 0.0:
raise ConfigurationError( raise ConfigurationError(
'The config stoploss needs to be different from 0 to avoid problems with sell orders.' "The config stoploss needs to be different from 0 to avoid problems with sell orders."
) )
# Skip if trailing stoploss is not activated # Skip if trailing stoploss is not activated
if not conf.get('trailing_stop', False): if not conf.get("trailing_stop", False):
return return
tsl_positive = float(conf.get('trailing_stop_positive', 0)) tsl_positive = float(conf.get("trailing_stop_positive", 0))
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) tsl_offset = float(conf.get("trailing_stop_positive_offset", 0))
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) tsl_only_offset = conf.get("trailing_only_offset_is_reached", False)
if tsl_only_offset: if tsl_only_offset:
if tsl_positive == 0.0: if tsl_positive == 0.0:
raise ConfigurationError( raise ConfigurationError(
'The config trailing_only_offset_is_reached needs ' "The config trailing_only_offset_is_reached needs "
'trailing_stop_positive_offset to be more than 0 in your config.') "trailing_stop_positive_offset to be more than 0 in your config."
)
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
raise ConfigurationError( raise ConfigurationError(
'The config trailing_stop_positive_offset needs ' "The config trailing_stop_positive_offset needs "
'to be greater than trailing_stop_positive in your config.') "to be greater than trailing_stop_positive in your config."
)
# Fetch again without default # Fetch again without default
if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0: if "trailing_stop_positive" in conf and float(conf["trailing_stop_positive"]) == 0.0:
raise ConfigurationError( raise ConfigurationError(
'The config trailing_stop_positive needs to be different from 0 ' "The config trailing_stop_positive needs to be different from 0 "
'to avoid problems with sell orders.' "to avoid problems with sell orders."
) )
@@ -158,10 +156,10 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists. Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
""" """
if not conf.get('edge', {}).get('enabled'): if not conf.get("edge", {}).get("enabled"):
return return
if not conf.get('use_exit_signal', True): if not conf.get("use_exit_signal", True):
raise ConfigurationError( raise ConfigurationError(
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen." "Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
) )
@@ -171,13 +169,20 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
""" """
Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does. Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does.
""" """
if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT, if conf.get("runmode", RunMode.OTHER) in [
RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]: RunMode.OTHER,
RunMode.PLOT,
RunMode.UTIL_NO_EXCHANGE,
RunMode.UTIL_EXCHANGE,
]:
return return
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]): for pl in conf.get("pairlists", [{"method": "StaticPairList"}]):
if (isinstance(pl, dict) and pl.get('method') == 'StaticPairList' if (
and not conf.get('exchange', {}).get('pair_whitelist')): isinstance(pl, dict)
and pl.get("method") == "StaticPairList"
and not conf.get("exchange", {}).get("pair_whitelist")
):
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.") raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
@@ -186,14 +191,14 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
Validate protection configuration validity Validate protection configuration validity
""" """
for prot in conf.get('protections', []): for prot in conf.get("protections", []):
if ('stop_duration' in prot and 'stop_duration_candles' in prot): if "stop_duration" in prot and "stop_duration_candles" in prot:
raise ConfigurationError( raise ConfigurationError(
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n" "Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}" f"Please fix the protection {prot.get('method')}"
) )
if ('lookback_period' in prot and 'lookback_period_candles' in prot): if "lookback_period" in prot and "lookback_period_candles" in prot:
raise ConfigurationError( raise ConfigurationError(
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n" "Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
f"Please fix the protection {prot.get('method')}" f"Please fix the protection {prot.get('method')}"
@@ -201,10 +206,10 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None: def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
ask_strategy = conf.get('exit_pricing', {}) ask_strategy = conf.get("exit_pricing", {})
ob_min = ask_strategy.get('order_book_min') ob_min = ask_strategy.get("order_book_min")
ob_max = ask_strategy.get('order_book_max') ob_max = ask_strategy.get("order_book_max")
if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'): if ob_min is not None and ob_max is not None and ask_strategy.get("use_order_book"):
if ob_min != ob_max: if ob_min != ob_max:
raise ConfigurationError( raise ConfigurationError(
"Using order_book_max != order_book_min in exit_pricing is no longer supported." "Using order_book_max != order_book_min in exit_pricing is no longer supported."
@@ -212,7 +217,7 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
) )
else: else:
# Move value to order_book_top # Move value to order_book_top
ask_strategy['order_book_top'] = ob_min ask_strategy["order_book_top"] = ob_min
logger.warning( logger.warning(
"DEPRECATED: " "DEPRECATED: "
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` " "Please use `order_book_top` instead of `order_book_min` and `order_book_max` "
@@ -221,7 +226,6 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None: def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
_validate_time_in_force(conf) _validate_time_in_force(conf)
_validate_order_types(conf) _validate_order_types(conf)
_validate_unfilledtimeout(conf) _validate_unfilledtimeout(conf)
@@ -230,119 +234,129 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
def _validate_time_in_force(conf: Dict[str, Any]) -> None: def _validate_time_in_force(conf: Dict[str, Any]) -> None:
time_in_force = conf.get("order_time_in_force", {})
time_in_force = conf.get('order_time_in_force', {}) if "buy" in time_in_force or "sell" in time_in_force:
if 'buy' in time_in_force or 'sell' in time_in_force: if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise ConfigurationError( raise ConfigurationError(
"Please migrate your time_in_force settings to use 'entry' and 'exit'.") "Please migrate your time_in_force settings to use 'entry' and 'exit'."
)
else: else:
logger.warning( logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated." "DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated."
"Please migrate your time_in_force settings to use 'entry' and 'exit'." "Please migrate your time_in_force settings to use 'entry' and 'exit'."
) )
process_deprecated_setting( process_deprecated_setting(
conf, 'order_time_in_force', 'buy', 'order_time_in_force', 'entry') conf, "order_time_in_force", "buy", "order_time_in_force", "entry"
)
process_deprecated_setting( process_deprecated_setting(
conf, 'order_time_in_force', 'sell', 'order_time_in_force', 'exit') conf, "order_time_in_force", "sell", "order_time_in_force", "exit"
)
def _validate_order_types(conf: Dict[str, Any]) -> None: def _validate_order_types(conf: Dict[str, Any]) -> None:
order_types = conf.get("order_types", {})
order_types = conf.get('order_types', {}) old_order_types = [
old_order_types = ['buy', 'sell', 'emergencysell', 'forcebuy', "buy",
'forcesell', 'emergencyexit', 'forceexit', 'forceentry'] "sell",
"emergencysell",
"forcebuy",
"forcesell",
"emergencyexit",
"forceexit",
"forceentry",
]
if any(x in order_types for x in old_order_types): if any(x in order_types for x in old_order_types):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
raise ConfigurationError( raise ConfigurationError(
"Please migrate your order_types settings to use the new wording.") "Please migrate your order_types settings to use the new wording."
)
else: else:
logger.warning( logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for order_types is deprecated." "DEPRECATED: Using 'buy' and 'sell' for order_types is deprecated."
"Please migrate your order_types settings to use 'entry' and 'exit' wording." "Please migrate your order_types settings to use 'entry' and 'exit' wording."
) )
for o, n in [ for o, n in [
('buy', 'entry'), ("buy", "entry"),
('sell', 'exit'), ("sell", "exit"),
('emergencysell', 'emergency_exit'), ("emergencysell", "emergency_exit"),
('forcesell', 'force_exit'), ("forcesell", "force_exit"),
('forcebuy', 'force_entry'), ("forcebuy", "force_entry"),
('emergencyexit', 'emergency_exit'), ("emergencyexit", "emergency_exit"),
('forceexit', 'force_exit'), ("forceexit", "force_exit"),
('forceentry', 'force_entry'), ("forceentry", "force_entry"),
]: ]:
process_deprecated_setting(conf, "order_types", o, "order_types", n)
process_deprecated_setting(conf, 'order_types', o, 'order_types', n)
def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None: def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
unfilledtimeout = conf.get('unfilledtimeout', {}) unfilledtimeout = conf.get("unfilledtimeout", {})
if any(x in unfilledtimeout for x in ['buy', 'sell']): if any(x in unfilledtimeout for x in ["buy", "sell"]):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
raise ConfigurationError( raise ConfigurationError(
"Please migrate your unfilledtimeout settings to use the new wording.") "Please migrate your unfilledtimeout settings to use the new wording."
)
else: else:
logger.warning( logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated." "DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated."
"Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording." "Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording."
) )
for o, n in [ for o, n in [
('buy', 'entry'), ("buy", "entry"),
('sell', 'exit'), ("sell", "exit"),
]: ]:
process_deprecated_setting(conf, "unfilledtimeout", o, "unfilledtimeout", n)
process_deprecated_setting(conf, 'unfilledtimeout', o, 'unfilledtimeout', n)
def _validate_pricing_rules(conf: Dict[str, Any]) -> None: def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
if conf.get("ask_strategy") or conf.get("bid_strategy"):
if conf.get('ask_strategy') or conf.get('bid_strategy'): if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: raise ConfigurationError("Please migrate your pricing settings to use the new wording.")
raise ConfigurationError(
"Please migrate your pricing settings to use the new wording.")
else: else:
logger.warning( logger.warning(
"DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is deprecated." "DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is deprecated."
"Please migrate your settings to use 'entry_pricing' and 'exit_pricing'." "Please migrate your settings to use 'entry_pricing' and 'exit_pricing'."
) )
conf['entry_pricing'] = {} conf["entry_pricing"] = {}
for obj in list(conf.get('bid_strategy', {}).keys()): for obj in list(conf.get("bid_strategy", {}).keys()):
if obj == 'ask_last_balance': if obj == "ask_last_balance":
process_deprecated_setting(conf, 'bid_strategy', obj, process_deprecated_setting(
'entry_pricing', 'price_last_balance') conf, "bid_strategy", obj, "entry_pricing", "price_last_balance"
)
else: else:
process_deprecated_setting(conf, 'bid_strategy', obj, 'entry_pricing', obj) process_deprecated_setting(conf, "bid_strategy", obj, "entry_pricing", obj)
del conf['bid_strategy'] del conf["bid_strategy"]
conf['exit_pricing'] = {} conf["exit_pricing"] = {}
for obj in list(conf.get('ask_strategy', {}).keys()): for obj in list(conf.get("ask_strategy", {}).keys()):
if obj == 'bid_last_balance': if obj == "bid_last_balance":
process_deprecated_setting(conf, 'ask_strategy', obj, process_deprecated_setting(
'exit_pricing', 'price_last_balance') conf, "ask_strategy", obj, "exit_pricing", "price_last_balance"
)
else: else:
process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj) process_deprecated_setting(conf, "ask_strategy", obj, "exit_pricing", obj)
del conf['ask_strategy'] del conf["ask_strategy"]
def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None: def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
freqai_enabled = conf.get('freqai', {}).get('enabled', False) freqai_enabled = conf.get("freqai", {}).get("enabled", False)
analyze_per_epoch = conf.get('analyze_per_epoch', False) analyze_per_epoch = conf.get("analyze_per_epoch", False)
if analyze_per_epoch and freqai_enabled: if analyze_per_epoch and freqai_enabled:
raise ConfigurationError( raise ConfigurationError(
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') "Using analyze-per-epoch parameter is not supported with a FreqAI strategy."
)
def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None: def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None:
freqai_enabled = conf.get('freqai', {}).get('enabled', False) freqai_enabled = conf.get("freqai", {}).get("enabled", False)
if freqai_enabled: if freqai_enabled:
main_tf = conf.get('timeframe', '5m') main_tf = conf.get("timeframe", "5m")
freqai_include_timeframes = conf.get('freqai', {}).get('feature_parameters', {} freqai_include_timeframes = (
).get('include_timeframes', []) conf.get("freqai", {}).get("feature_parameters", {}).get("include_timeframes", [])
)
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
main_tf_s = timeframe_to_seconds(main_tf) main_tf_s = timeframe_to_seconds(main_tf)
offending_lines = [] offending_lines = []
for tf in freqai_include_timeframes: for tf in freqai_include_timeframes:
@@ -352,57 +366,65 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool)
if offending_lines: if offending_lines:
raise ConfigurationError( raise ConfigurationError(
f"Main timeframe of {main_tf} must be smaller or equal to FreqAI " f"Main timeframe of {main_tf} must be smaller or equal to FreqAI "
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}") f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}"
)
# Ensure that the base timeframe is included in the include_timeframes list # Ensure that the base timeframe is included in the include_timeframes list
if not preliminary and main_tf not in freqai_include_timeframes: if not preliminary and main_tf not in freqai_include_timeframes:
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {}) feature_parameters = conf.get("freqai", {}).get("feature_parameters", {})
include_timeframes = [main_tf] + freqai_include_timeframes include_timeframes = [main_tf] + freqai_include_timeframes
conf.get('freqai', {}).get('feature_parameters', {}) \ conf.get("freqai", {}).get("feature_parameters", {}).update(
.update({**feature_parameters, 'include_timeframes': include_timeframes}) {**feature_parameters, "include_timeframes": include_timeframes}
)
def _validate_freqai_backtest(conf: Dict[str, Any]) -> None: def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
if conf.get('runmode', RunMode.OTHER) == RunMode.BACKTEST: if conf.get("runmode", RunMode.OTHER) == RunMode.BACKTEST:
freqai_enabled = conf.get('freqai', {}).get('enabled', False) freqai_enabled = conf.get("freqai", {}).get("enabled", False)
timerange = conf.get('timerange') timerange = conf.get("timerange")
freqai_backtest_live_models = conf.get('freqai_backtest_live_models', False) freqai_backtest_live_models = conf.get("freqai_backtest_live_models", False)
if freqai_backtest_live_models and freqai_enabled and timerange: if freqai_backtest_live_models and freqai_enabled and timerange:
raise ConfigurationError( raise ConfigurationError(
'Using timerange parameter is not supported with ' "Using timerange parameter is not supported with "
'--freqai-backtest-live-models parameter.') "--freqai-backtest-live-models parameter."
)
if freqai_backtest_live_models and not freqai_enabled: if freqai_backtest_live_models and not freqai_enabled:
raise ConfigurationError( raise ConfigurationError(
'Using --freqai-backtest-live-models parameter is only ' "Using --freqai-backtest-live-models parameter is only "
'supported with a FreqAI strategy.') "supported with a FreqAI strategy."
)
if freqai_enabled and not freqai_backtest_live_models and not timerange: if freqai_enabled and not freqai_backtest_live_models and not timerange:
raise ConfigurationError( raise ConfigurationError(
'Please pass --timerange if you intend to use FreqAI for backtesting.') "Please pass --timerange if you intend to use FreqAI for backtesting."
)
def _validate_consumers(conf: Dict[str, Any]) -> None: def _validate_consumers(conf: Dict[str, Any]) -> None:
emc_conf = conf.get('external_message_consumer', {}) emc_conf = conf.get("external_message_consumer", {})
if emc_conf.get('enabled', False): if emc_conf.get("enabled", False):
if len(emc_conf.get('producers', [])) < 1: if len(emc_conf.get("producers", [])) < 1:
raise ConfigurationError("You must specify at least 1 Producer to connect to.") raise ConfigurationError("You must specify at least 1 Producer to connect to.")
producer_names = [p['name'] for p in emc_conf.get('producers', [])] producer_names = [p["name"] for p in emc_conf.get("producers", [])]
duplicates = [item for item, count in Counter(producer_names).items() if count > 1] duplicates = [item for item, count in Counter(producer_names).items() if count > 1]
if duplicates: if duplicates:
raise ConfigurationError( raise ConfigurationError(
f"Producer names must be unique. Duplicate: {', '.join(duplicates)}") f"Producer names must be unique. Duplicate: {', '.join(duplicates)}"
if conf.get('process_only_new_candles', True): )
if conf.get("process_only_new_candles", True):
# Warning here or require it? # Warning here or require it?
logger.warning("To receive best performance with external data, " logger.warning(
"please set `process_only_new_candles` to False") "To receive best performance with external data, "
"please set `process_only_new_candles` to False"
)
def _strategy_settings(conf: Dict[str, Any]) -> None: def _strategy_settings(conf: Dict[str, Any]) -> None:
process_deprecated_setting(conf, None, "use_sell_signal", None, "use_exit_signal")
process_deprecated_setting(conf, None, 'use_sell_signal', None, 'use_exit_signal') process_deprecated_setting(conf, None, "sell_profit_only", None, "exit_profit_only")
process_deprecated_setting(conf, None, 'sell_profit_only', None, 'exit_profit_only') process_deprecated_setting(conf, None, "sell_profit_offset", None, "exit_profit_offset")
process_deprecated_setting(conf, None, 'sell_profit_offset', None, 'exit_profit_offset') process_deprecated_setting(
process_deprecated_setting(conf, None, 'ignore_roi_if_buy_signal', conf, None, "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
None, 'ignore_roi_if_entry_signal') )

View File

@@ -1,6 +1,8 @@
""" """
This module contains the configuration class This module contains the configuration class
""" """
import ast
import logging import logging
import warnings import warnings
from copy import deepcopy from copy import deepcopy
@@ -56,7 +58,7 @@ class Configuration:
:return: configuration dictionary :return: configuration dictionary
""" """
# Keep this method as staticmethod, so it can be used from interactive environments # Keep this method as staticmethod, so it can be used from interactive environments
c = Configuration({'config': files}, RunMode.OTHER) c = Configuration({"config": files}, RunMode.OTHER)
return c.get_config() return c.get_config()
def load_config(self) -> Dict[str, Any]: def load_config(self) -> Dict[str, Any]:
@@ -69,19 +71,20 @@ class Configuration:
# Load environment variables # Load environment variables
from freqtrade.commands.arguments import NO_CONF_ALLOWED from freqtrade.commands.arguments import NO_CONF_ALLOWED
if self.args.get('command') not in NO_CONF_ALLOWED:
if self.args.get("command") not in NO_CONF_ALLOWED:
env_data = enironment_vars_to_dict() env_data = enironment_vars_to_dict()
config = deep_merge_dicts(env_data, config) config = deep_merge_dicts(env_data, config)
# Normalize config # Normalize config
if 'internals' not in config: if "internals" not in config:
config['internals'] = {} config["internals"] = {}
if 'pairlists' not in config: if "pairlists" not in config:
config['pairlists'] = [] config["pairlists"] = []
# Keep a copy of the original configuration file # Keep a copy of the original configuration file
config['original_config'] = deepcopy(config) config["original_config"] = deepcopy(config)
self._process_logging_options(config) self._process_logging_options(config)
@@ -105,7 +108,7 @@ class Configuration:
from freqtrade.exchange.check_exchange import check_exchange from freqtrade.exchange.check_exchange import check_exchange
# Check if the exchange set by the user is supported # Check if the exchange set by the user is supported
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) check_exchange(config, config.get("experimental", {}).get("block_bad_exchanges", True))
self._resolve_pairs_list(config) self._resolve_pairs_list(config)
@@ -119,52 +122,56 @@ class Configuration:
the -v/--verbose, --logfile options the -v/--verbose, --logfile options
""" """
# Log level # Log level
config.update({'verbosity': self.args.get('verbosity', 0)}) config.update({"verbosity": self.args.get("verbosity", 0)})
if 'logfile' in self.args and self.args['logfile']: if "logfile" in self.args and self.args["logfile"]:
config.update({'logfile': self.args['logfile']}) config.update({"logfile": self.args["logfile"]})
setup_logging(config) setup_logging(config)
def _process_trading_options(self, config: Config) -> None: def _process_trading_options(self, config: Config) -> None:
if config['runmode'] not in TRADE_MODES: if config["runmode"] not in TRADE_MODES:
return return
if config.get('dry_run', False): if config.get("dry_run", False):
logger.info('Dry run is enabled') logger.info("Dry run is enabled")
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: if config.get("db_url") in [None, constants.DEFAULT_DB_PROD_URL]:
# Default to in-memory db for dry_run if not specified # Default to in-memory db for dry_run if not specified
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL config["db_url"] = constants.DEFAULT_DB_DRYRUN_URL
else: else:
if not config.get('db_url'): if not config.get("db_url"):
config['db_url'] = constants.DEFAULT_DB_PROD_URL config["db_url"] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled') logger.info("Dry run is disabled")
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
def _process_common_options(self, config: Config) -> None: def _process_common_options(self, config: Config) -> None:
# Set strategy if not specified in config and or if it's non default # Set strategy if not specified in config and or if it's non default
if self.args.get('strategy') or not config.get('strategy'): if self.args.get("strategy") or not config.get("strategy"):
config.update({'strategy': self.args.get('strategy')}) config.update({"strategy": self.args.get("strategy")})
self._args_to_config(config, argname='strategy_path', self._args_to_config(
logstring='Using additional Strategy lookup path: {}') config, argname="strategy_path", logstring="Using additional Strategy lookup path: {}"
)
if ('db_url' in self.args and self.args['db_url'] and if (
self.args['db_url'] != constants.DEFAULT_DB_PROD_URL): "db_url" in self.args
config.update({'db_url': self.args['db_url']}) and self.args["db_url"]
logger.info('Parameter --db-url detected ...') and self.args["db_url"] != constants.DEFAULT_DB_PROD_URL
):
config.update({"db_url": self.args["db_url"]})
logger.info("Parameter --db-url detected ...")
self._args_to_config(config, argname='db_url_from', self._args_to_config(
logstring='Parameter --db-url-from detected ...') config, argname="db_url_from", logstring="Parameter --db-url-from detected ..."
)
if config.get('force_entry_enable', False): if config.get("force_entry_enable", False):
logger.warning('`force_entry_enable` RPC message enabled.') logger.warning("`force_entry_enable` RPC message enabled.")
# Support for sd_notify # Support for sd_notify
if 'sd_notify' in self.args and self.args['sd_notify']: if "sd_notify" in self.args and self.args["sd_notify"]:
config['internals'].update({'sd_notify': True}) config["internals"].update({"sd_notify": True})
def _process_datadir_options(self, config: Config) -> None: def _process_datadir_options(self, config: Config) -> None:
""" """
@@ -172,245 +179,275 @@ class Configuration:
--user-data, --datadir --user-data, --datadir
""" """
# Check exchange parameter here - otherwise `datadir` might be wrong. # Check exchange parameter here - otherwise `datadir` might be wrong.
if 'exchange' in self.args and self.args['exchange']: if "exchange" in self.args and self.args["exchange"]:
config['exchange']['name'] = self.args['exchange'] config["exchange"]["name"] = self.args["exchange"]
logger.info(f"Using exchange {config['exchange']['name']}") logger.info(f"Using exchange {config['exchange']['name']}")
if 'pair_whitelist' not in config['exchange']: if "pair_whitelist" not in config["exchange"]:
config['exchange']['pair_whitelist'] = [] config["exchange"]["pair_whitelist"] = []
if 'user_data_dir' in self.args and self.args['user_data_dir']: if "user_data_dir" in self.args and self.args["user_data_dir"]:
config.update({'user_data_dir': self.args['user_data_dir']}) config.update({"user_data_dir": self.args["user_data_dir"]})
elif 'user_data_dir' not in config: elif "user_data_dir" not in config:
# Default to cwd/user_data (legacy option ...) # Default to cwd/user_data (legacy option ...)
config.update({'user_data_dir': str(Path.cwd() / 'user_data')}) config.update({"user_data_dir": str(Path.cwd() / "user_data")})
# reset to user_data_dir so this contains the absolute path. # reset to user_data_dir so this contains the absolute path.
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False) config["user_data_dir"] = create_userdata_dir(config["user_data_dir"], create_dir=False)
logger.info('Using user-data directory: %s ...', config['user_data_dir']) logger.info("Using user-data directory: %s ...", config["user_data_dir"])
config.update({'datadir': create_datadir(config, self.args.get('datadir'))}) config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
logger.info('Using data directory: %s ...', config.get('datadir')) logger.info("Using data directory: %s ...", config.get("datadir"))
if self.args.get('exportfilename'): if self.args.get("exportfilename"):
self._args_to_config(config, argname='exportfilename', self._args_to_config(
logstring='Storing backtest results to {} ...') config, argname="exportfilename", logstring="Storing backtest results to {} ..."
config['exportfilename'] = Path(config['exportfilename']) )
config["exportfilename"] = Path(config["exportfilename"])
else: else:
config['exportfilename'] = (config['user_data_dir'] config["exportfilename"] = config["user_data_dir"] / "backtest_results"
/ 'backtest_results')
if self.args.get('show_sensitive'): if self.args.get("show_sensitive"):
logger.warning( logger.warning(
"Sensitive information will be shown in the upcoming output. " "Sensitive information will be shown in the upcoming output. "
"Please make sure to never share this output without redacting " "Please make sure to never share this output without redacting "
"the information yourself.") "the information yourself."
)
def _process_optimize_options(self, config: Config) -> None: def _process_optimize_options(self, config: Config) -> None:
# This will override the strategy configuration # This will override the strategy configuration
self._args_to_config(config, argname='timeframe', self._args_to_config(
logstring='Parameter -i/--timeframe detected ... ' config,
'Using timeframe: {} ...') argname="timeframe",
logstring="Parameter -i/--timeframe detected ... Using timeframe: {} ...",
self._args_to_config(config, argname='position_stacking', )
logstring='Parameter --enable-position-stacking detected ...')
self._args_to_config( self._args_to_config(
config, argname='enable_protections', config,
logstring='Parameter --enable-protections detected, enabling Protections. ...') argname="position_stacking",
logstring="Parameter --enable-position-stacking detected ...",
)
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: self._args_to_config(
config.update({'use_max_market_positions': False}) config,
logger.info('Parameter --disable-max-market-positions detected ...') argname="enable_protections",
logger.info('max_open_trades set to unlimited ...') logstring="Parameter --enable-protections detected, enabling Protections. ...",
elif 'max_open_trades' in self.args and self.args['max_open_trades']: )
config.update({'max_open_trades': self.args['max_open_trades']})
logger.info('Parameter --max-open-trades detected, ' if "use_max_market_positions" in self.args and not self.args["use_max_market_positions"]:
'overriding max_open_trades to: %s ...', config.get('max_open_trades')) config.update({"use_max_market_positions": False})
elif config['runmode'] in NON_UTIL_MODES: logger.info("Parameter --disable-max-market-positions detected ...")
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) logger.info("max_open_trades set to unlimited ...")
elif "max_open_trades" in self.args and self.args["max_open_trades"]:
config.update({"max_open_trades": self.args["max_open_trades"]})
logger.info(
"Parameter --max-open-trades detected, overriding max_open_trades to: %s ...",
config.get("max_open_trades"),
)
elif config["runmode"] in NON_UTIL_MODES:
logger.info("Using max_open_trades: %s ...", config.get("max_open_trades"))
# Setting max_open_trades to infinite if -1 # Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1: if config.get("max_open_trades") == -1:
config['max_open_trades'] = float('inf') config["max_open_trades"] = float("inf")
if self.args.get('stake_amount'): if self.args.get("stake_amount"):
# Convert explicitly to float to support CLI argument for both unlimited and value # Convert explicitly to float to support CLI argument for both unlimited and value
try: try:
self.args['stake_amount'] = float(self.args['stake_amount']) self.args["stake_amount"] = float(self.args["stake_amount"])
except ValueError: except ValueError:
pass pass
configurations = [ configurations = [
('timeframe_detail', (
'Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...'), "timeframe_detail",
('backtest_show_pair_list', 'Parameter --show-pair-list detected.'), "Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...",
('stake_amount', ),
'Parameter --stake-amount detected, overriding stake_amount to: {} ...'), ("backtest_show_pair_list", "Parameter --show-pair-list detected."),
('dry_run_wallet', (
'Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...'), "stake_amount",
('fee', 'Parameter --fee detected, setting fee to: {} ...'), "Parameter --stake-amount detected, overriding stake_amount to: {} ...",
('timerange', 'Parameter --timerange detected: {} ...'), ),
] (
"dry_run_wallet",
"Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...",
),
("fee", "Parameter --fee detected, setting fee to: {} ..."),
("timerange", "Parameter --timerange detected: {} ..."),
]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
self._process_datadir_options(config) self._process_datadir_options(config)
self._args_to_config(config, argname='strategy_list', self._args_to_config(
logstring='Using strategy list of {} strategies', logfun=len) config,
argname="strategy_list",
logstring="Using strategy list of {} strategies",
logfun=len,
)
configurations = [ configurations = [
('recursive_strategy_search', (
'Recursively searching for a strategy in the strategies folder.'), "recursive_strategy_search",
('timeframe', 'Overriding timeframe with Command line argument'), "Recursively searching for a strategy in the strategies folder.",
('export', 'Parameter --export detected: {} ...'), ),
('backtest_breakdown', 'Parameter --breakdown detected ...'), ("timeframe", "Overriding timeframe with Command line argument"),
('backtest_cache', 'Parameter --cache={} detected ...'), ("export", "Parameter --export detected: {} ..."),
('disableparamexport', 'Parameter --disableparamexport detected: {} ...'), ("backtest_breakdown", "Parameter --breakdown detected ..."),
('freqai_backtest_live_models', ("backtest_cache", "Parameter --cache={} detected ..."),
'Parameter --freqai-backtest-live-models detected ...'), ("disableparamexport", "Parameter --disableparamexport detected: {} ..."),
("freqai_backtest_live_models", "Parameter --freqai-backtest-live-models detected ..."),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
# Edge section: # Edge section:
if 'stoploss_range' in self.args and self.args["stoploss_range"]: if "stoploss_range" in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args["stoploss_range"]) txt_range = ast.literal_eval(self.args["stoploss_range"])
config['edge'].update({'stoploss_range_min': txt_range[0]}) config["edge"].update({"stoploss_range_min": txt_range[0]})
config['edge'].update({'stoploss_range_max': txt_range[1]}) config["edge"].update({"stoploss_range_max": txt_range[1]})
config['edge'].update({'stoploss_range_step': txt_range[2]}) config["edge"].update({"stoploss_range_step": txt_range[2]})
logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) logger.info("Parameter --stoplosses detected: %s ...", self.args["stoploss_range"])
# Hyperopt section # Hyperopt section
configurations = [ configurations = [
('hyperopt', 'Using Hyperopt class name: {}'), ("hyperopt", "Using Hyperopt class name: {}"),
('hyperopt_path', 'Using additional Hyperopt lookup path: {}'), ("hyperopt_path", "Using additional Hyperopt lookup path: {}"),
('hyperoptexportfilename', 'Using hyperopt file: {}'), ("hyperoptexportfilename", "Using hyperopt file: {}"),
('lookahead_analysis_exportfilename', 'Saving lookahead analysis results into {} ...'), ("lookahead_analysis_exportfilename", "Saving lookahead analysis results into {} ..."),
('epochs', 'Parameter --epochs detected ... Will run Hyperopt with for {} epochs ...'), ("epochs", "Parameter --epochs detected ... Will run Hyperopt with for {} epochs ..."),
('spaces', 'Parameter -s/--spaces detected: {}'), ("spaces", "Parameter -s/--spaces detected: {}"),
('analyze_per_epoch', 'Parameter --analyze-per-epoch detected.'), ("analyze_per_epoch", "Parameter --analyze-per-epoch detected."),
('print_all', 'Parameter --print-all detected ...'), ("print_all", "Parameter --print-all detected ..."),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
if 'print_colorized' in self.args and not self.args["print_colorized"]: if "print_colorized" in self.args and not self.args["print_colorized"]:
logger.info('Parameter --no-color detected ...') logger.info("Parameter --no-color detected ...")
config.update({'print_colorized': False}) config.update({"print_colorized": False})
else: else:
config.update({'print_colorized': True}) config.update({"print_colorized": True})
configurations = [ configurations = [
('print_json', 'Parameter --print-json detected ...'), ("print_json", "Parameter --print-json detected ..."),
('export_csv', 'Parameter --export-csv detected: {}'), ("export_csv", "Parameter --export-csv detected: {}"),
('hyperopt_jobs', 'Parameter -j/--job-workers detected: {}'), ("hyperopt_jobs", "Parameter -j/--job-workers detected: {}"),
('hyperopt_random_state', 'Parameter --random-state detected: {}'), ("hyperopt_random_state", "Parameter --random-state detected: {}"),
('hyperopt_min_trades', 'Parameter --min-trades detected: {}'), ("hyperopt_min_trades", "Parameter --min-trades detected: {}"),
('hyperopt_loss', 'Using Hyperopt loss class name: {}'), ("hyperopt_loss", "Using Hyperopt loss class name: {}"),
('hyperopt_show_index', 'Parameter -n/--index detected: {}'), ("hyperopt_show_index", "Parameter -n/--index detected: {}"),
('hyperopt_list_best', 'Parameter --best detected: {}'), ("hyperopt_list_best", "Parameter --best detected: {}"),
('hyperopt_list_profitable', 'Parameter --profitable detected: {}'), ("hyperopt_list_profitable", "Parameter --profitable detected: {}"),
('hyperopt_list_min_trades', 'Parameter --min-trades detected: {}'), ("hyperopt_list_min_trades", "Parameter --min-trades detected: {}"),
('hyperopt_list_max_trades', 'Parameter --max-trades detected: {}'), ("hyperopt_list_max_trades", "Parameter --max-trades detected: {}"),
('hyperopt_list_min_avg_time', 'Parameter --min-avg-time detected: {}'), ("hyperopt_list_min_avg_time", "Parameter --min-avg-time detected: {}"),
('hyperopt_list_max_avg_time', 'Parameter --max-avg-time detected: {}'), ("hyperopt_list_max_avg_time", "Parameter --max-avg-time detected: {}"),
('hyperopt_list_min_avg_profit', 'Parameter --min-avg-profit detected: {}'), ("hyperopt_list_min_avg_profit", "Parameter --min-avg-profit detected: {}"),
('hyperopt_list_max_avg_profit', 'Parameter --max-avg-profit detected: {}'), ("hyperopt_list_max_avg_profit", "Parameter --max-avg-profit detected: {}"),
('hyperopt_list_min_total_profit', 'Parameter --min-total-profit detected: {}'), ("hyperopt_list_min_total_profit", "Parameter --min-total-profit detected: {}"),
('hyperopt_list_max_total_profit', 'Parameter --max-total-profit detected: {}'), ("hyperopt_list_max_total_profit", "Parameter --max-total-profit detected: {}"),
('hyperopt_list_min_objective', 'Parameter --min-objective detected: {}'), ("hyperopt_list_min_objective", "Parameter --min-objective detected: {}"),
('hyperopt_list_max_objective', 'Parameter --max-objective detected: {}'), ("hyperopt_list_max_objective", "Parameter --max-objective detected: {}"),
('hyperopt_list_no_details', 'Parameter --no-details detected: {}'), ("hyperopt_list_no_details", "Parameter --no-details detected: {}"),
('hyperopt_show_no_header', 'Parameter --no-header detected: {}'), ("hyperopt_show_no_header", "Parameter --no-header detected: {}"),
('hyperopt_ignore_missing_space', 'Paramter --ignore-missing-space detected: {}'), ("hyperopt_ignore_missing_space", "Parameter --ignore-missing-space detected: {}"),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
def _process_plot_options(self, config: Config) -> None: def _process_plot_options(self, config: Config) -> None:
configurations = [ configurations = [
('pairs', 'Using pairs {}'), ("pairs", "Using pairs {}"),
('indicators1', 'Using indicators1: {}'), ("indicators1", "Using indicators1: {}"),
('indicators2', 'Using indicators2: {}'), ("indicators2", "Using indicators2: {}"),
('trade_ids', 'Filtering on trade_ids: {}'), ("trade_ids", "Filtering on trade_ids: {}"),
('plot_limit', 'Limiting plot to: {}'), ("plot_limit", "Limiting plot to: {}"),
('plot_auto_open', 'Parameter --auto-open detected.'), ("plot_auto_open", "Parameter --auto-open detected."),
('trade_source', 'Using trades from: {}'), ("trade_source", "Using trades from: {}"),
('prepend_data', 'Prepend detected. Allowing data prepending.'), ("prepend_data", "Prepend detected. Allowing data prepending."),
('erase', 'Erase detected. Deleting existing data.'), ("erase", "Erase detected. Deleting existing data."),
('no_trades', 'Parameter --no-trades detected.'), ("no_trades", "Parameter --no-trades detected."),
('timeframes', 'timeframes --timeframes: {}'), ("timeframes", "timeframes --timeframes: {}"),
('days', 'Detected --days: {}'), ("days", "Detected --days: {}"),
('include_inactive', 'Detected --include-inactive-pairs: {}'), ("include_inactive", "Detected --include-inactive-pairs: {}"),
('download_trades', 'Detected --dl-trades: {}'), ("download_trades", "Detected --dl-trades: {}"),
('dataformat_ohlcv', 'Using "{}" to store OHLCV data.'), ("convert_trades", "Detected --convert: {} - Converting Trade data to OHCV {}"),
('dataformat_trades', 'Using "{}" to store trades data.'), ("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
('show_timerange', 'Detected --show-timerange'), ("dataformat_trades", 'Using "{}" to store trades data.'),
("show_timerange", "Detected --show-timerange"),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
def _process_data_options(self, config: Config) -> None: def _process_data_options(self, config: Config) -> None:
self._args_to_config(config, argname='new_pairs_days', self._args_to_config(
logstring='Detected --new-pairs-days: {}') config, argname="new_pairs_days", logstring="Detected --new-pairs-days: {}"
self._args_to_config(config, argname='trading_mode', )
logstring='Detected --trading-mode: {}') self._args_to_config(
config['candle_type_def'] = CandleType.get_default( config, argname="trading_mode", logstring="Detected --trading-mode: {}"
config.get('trading_mode', 'spot') or 'spot') )
config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot') or 'spot') config["candle_type_def"] = CandleType.get_default(
self._args_to_config(config, argname='candle_types', config.get("trading_mode", "spot") or "spot"
logstring='Detected --candle-types: {}') )
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
self._args_to_config(
config, argname="candle_types", logstring="Detected --candle-types: {}"
)
def _process_analyze_options(self, config: Config) -> None: def _process_analyze_options(self, config: Config) -> None:
configurations = [ configurations = [
('analysis_groups', 'Analysis reason groups: {}'), ("analysis_groups", "Analysis reason groups: {}"),
('enter_reason_list', 'Analysis enter tag list: {}'), ("enter_reason_list", "Analysis enter tag list: {}"),
('exit_reason_list', 'Analysis exit tag list: {}'), ("exit_reason_list", "Analysis exit tag list: {}"),
('indicator_list', 'Analysis indicator list: {}'), ("indicator_list", "Analysis indicator list: {}"),
('timerange', 'Filter trades by timerange: {}'), ("timerange", "Filter trades by timerange: {}"),
('analysis_rejected', 'Analyse rejected signals: {}'), ("analysis_rejected", "Analyse rejected signals: {}"),
('analysis_to_csv', 'Store analysis tables to CSV: {}'), ("analysis_to_csv", "Store analysis tables to CSV: {}"),
('analysis_csv_path', 'Path to store analysis CSVs: {}'), ("analysis_csv_path", "Path to store analysis CSVs: {}"),
# Lookahead analysis results # Lookahead analysis results
('targeted_trade_amount', 'Targeted Trade amount: {}'), ("targeted_trade_amount", "Targeted Trade amount: {}"),
('minimum_trade_amount', 'Minimum Trade amount: {}'), ("minimum_trade_amount", "Minimum Trade amount: {}"),
('lookahead_analysis_exportfilename', 'Path to store lookahead-analysis-results: {}'), ("lookahead_analysis_exportfilename", "Path to store lookahead-analysis-results: {}"),
('startup_candle', 'Startup candle to be used on recursive analysis: {}'), ("startup_candle", "Startup candle to be used on recursive analysis: {}"),
] ]
self._args_to_config_loop(config, configurations) self._args_to_config_loop(config, configurations)
def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None: def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None:
for argname, logstring in configurations: for argname, logstring in configurations:
self._args_to_config(config, argname=argname, logstring=logstring) self._args_to_config(config, argname=argname, logstring=logstring)
def _process_runmode(self, config: Config) -> None: def _process_runmode(self, config: Config) -> None:
self._args_to_config(
self._args_to_config(config, argname='dry_run', config,
logstring='Parameter --dry-run detected, ' argname="dry_run",
'overriding dry_run to: {} ...') logstring="Parameter --dry-run detected, overriding dry_run to: {} ...",
)
if not self.runmode: if not self.runmode:
# Handle real mode, infer dry/live from config # Handle real mode, infer dry/live from config
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE self.runmode = RunMode.DRY_RUN if config.get("dry_run", True) else RunMode.LIVE
logger.info(f"Runmode set to {self.runmode.value}.") logger.info(f"Runmode set to {self.runmode.value}.")
config.update({'runmode': self.runmode}) config.update({"runmode": self.runmode})
def _process_freqai_options(self, config: Config) -> None: def _process_freqai_options(self, config: Config) -> None:
self._args_to_config(
config, argname="freqaimodel", logstring="Using freqaimodel class name: {}"
)
self._args_to_config(config, argname='freqaimodel', self._args_to_config(
logstring='Using freqaimodel class name: {}') config, argname="freqaimodel_path", logstring="Using freqaimodel path: {}"
)
self._args_to_config(config, argname='freqaimodel_path',
logstring='Using freqaimodel path: {}')
return return
def _args_to_config(self, config: Config, argname: str, def _args_to_config(
logstring: str, logfun: Optional[Callable] = None, self,
deprecated_msg: Optional[str] = None) -> None: config: Config,
argname: str,
logstring: str,
logfun: Optional[Callable] = None,
deprecated_msg: Optional[str] = None,
) -> None:
""" """
:param config: Configuration dictionary :param config: Configuration dictionary
:param argname: Argumentname in self.args - will be copied to config dict. :param argname: Argumentname in self.args - will be copied to config dict.
@@ -420,9 +457,11 @@ class Configuration:
sample: logfun=len (prints the length of the found sample: logfun=len (prints the length of the found
configuration instead of the content) configuration instead of the content)
""" """
if (argname in self.args and self.args[argname] is not None if (
and self.args[argname] is not False): argname in self.args
and self.args[argname] is not None
and self.args[argname] is not False
):
config.update({argname: self.args[argname]}) config.update({argname: self.args[argname]})
if logfun: if logfun:
logger.info(logstring.format(logfun(config[argname]))) logger.info(logstring.format(logfun(config[argname])))
@@ -441,7 +480,7 @@ class Configuration:
""" """
if "pairs" in config: if "pairs" in config:
config['exchange']['pair_whitelist'] = config['pairs'] config["exchange"]["pair_whitelist"] = config["pairs"]
return return
if "pairs_file" in self.args and self.args["pairs_file"]: if "pairs_file" in self.args and self.args["pairs_file"]:
@@ -451,19 +490,19 @@ class Configuration:
# or if pairs file is specified explicitly # or if pairs file is specified explicitly
if not pairs_file.exists(): if not pairs_file.exists():
raise OperationalException(f'No pairs file found with path "{pairs_file}".') raise OperationalException(f'No pairs file found with path "{pairs_file}".')
config['pairs'] = load_file(pairs_file) config["pairs"] = load_file(pairs_file)
if isinstance(config['pairs'], list): if isinstance(config["pairs"], list):
config['pairs'].sort() config["pairs"].sort()
return return
if 'config' in self.args and self.args['config']: if "config" in self.args and self.args["config"]:
logger.info("Using pairlist from configuration.") logger.info("Using pairlist from configuration.")
config['pairs'] = config.get('exchange', {}).get('pair_whitelist') config["pairs"] = config.get("exchange", {}).get("pair_whitelist")
else: else:
# Fall back to /dl_path/pairs.json # Fall back to /dl_path/pairs.json
pairs_file = config['datadir'] / 'pairs.json' pairs_file = config["datadir"] / "pairs.json"
if pairs_file.exists(): if pairs_file.exists():
logger.info(f'Reading pairs file "{pairs_file}".') logger.info(f'Reading pairs file "{pairs_file}".')
config['pairs'] = load_file(pairs_file) config["pairs"] = load_file(pairs_file)
if 'pairs' in config and isinstance(config['pairs'], list): if "pairs" in config and isinstance(config["pairs"], list):
config['pairs'].sort() config["pairs"].sort()

View File

@@ -12,9 +12,13 @@ from freqtrade.exceptions import ConfigurationError, OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def check_conflicting_settings(config: Config, def check_conflicting_settings(
section_old: Optional[str], name_old: str, config: Config,
section_new: Optional[str], name_new: str) -> None: section_old: Optional[str],
name_old: str,
section_new: Optional[str],
name_new: str,
) -> None:
section_new_config = config.get(section_new, {}) if section_new else config section_new_config = config.get(section_new, {}) if section_new else config
section_old_config = config.get(section_old, {}) if section_old else config section_old_config = config.get(section_old, {}) if section_old else config
if name_new in section_new_config and name_old in section_old_config: if name_new in section_new_config and name_old in section_old_config:
@@ -29,9 +33,9 @@ def check_conflicting_settings(config: Config,
) )
def process_removed_setting(config: Config, def process_removed_setting(
section1: str, name1: str, config: Config, section1: str, name1: str, section2: Optional[str], name2: str
section2: Optional[str], name2: str) -> None: ) -> None:
""" """
:param section1: Removed section :param section1: Removed section
:param name1: Removed setting name :param name1: Removed setting name
@@ -48,10 +52,13 @@ def process_removed_setting(config: Config,
) )
def process_deprecated_setting(config: Config, def process_deprecated_setting(
section_old: Optional[str], name_old: str, config: Config,
section_new: Optional[str], name_new: str section_old: Optional[str],
) -> None: name_old: str,
section_new: Optional[str],
name_new: str,
) -> None:
check_conflicting_settings(config, section_old, name_old, section_new, name_new) check_conflicting_settings(config, section_old, name_old, section_new, name_new)
section_old_config = config.get(section_old, {}) if section_old else config section_old_config = config.get(section_old, {}) if section_old else config
@@ -71,57 +78,91 @@ def process_deprecated_setting(config: Config,
def process_temporary_deprecated_settings(config: Config) -> None: def process_temporary_deprecated_settings(config: Config) -> None:
# Kept for future deprecated / moved settings # Kept for future deprecated / moved settings
# check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', # check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
# 'experimental', 'use_sell_signal') # 'experimental', 'use_sell_signal')
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after', process_deprecated_setting(
None, 'ignore_buying_expired_candle_after') config,
"ask_strategy",
"ignore_buying_expired_candle_after",
None,
"ignore_buying_expired_candle_after",
)
process_deprecated_setting(config, None, 'forcebuy_enable', None, 'force_entry_enable') process_deprecated_setting(config, None, "forcebuy_enable", None, "force_entry_enable")
# New settings # New settings
if config.get('telegram'): if config.get("telegram"):
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell', process_deprecated_setting(
'notification_settings', 'exit') config["telegram"], "notification_settings", "sell", "notification_settings", "exit"
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_fill', )
'notification_settings', 'exit_fill') process_deprecated_setting(
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_cancel', config["telegram"],
'notification_settings', 'exit_cancel') "notification_settings",
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy', "sell_fill",
'notification_settings', 'entry') "notification_settings",
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_fill', "exit_fill",
'notification_settings', 'entry_fill') )
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_cancel', process_deprecated_setting(
'notification_settings', 'entry_cancel') config["telegram"],
if config.get('webhook'): "notification_settings",
process_deprecated_setting(config, 'webhook', 'webhookbuy', 'webhook', 'webhookentry') "sell_cancel",
process_deprecated_setting(config, 'webhook', 'webhookbuycancel', "notification_settings",
'webhook', 'webhookentrycancel') "exit_cancel",
process_deprecated_setting(config, 'webhook', 'webhookbuyfill', )
'webhook', 'webhookentryfill') process_deprecated_setting(
process_deprecated_setting(config, 'webhook', 'webhooksell', 'webhook', 'webhookexit') config["telegram"], "notification_settings", "buy", "notification_settings", "entry"
process_deprecated_setting(config, 'webhook', 'webhooksellcancel', )
'webhook', 'webhookexitcancel') process_deprecated_setting(
process_deprecated_setting(config, 'webhook', 'webhooksellfill', config["telegram"],
'webhook', 'webhookexitfill') "notification_settings",
"buy_fill",
"notification_settings",
"entry_fill",
)
process_deprecated_setting(
config["telegram"],
"notification_settings",
"buy_cancel",
"notification_settings",
"entry_cancel",
)
if config.get("webhook"):
process_deprecated_setting(config, "webhook", "webhookbuy", "webhook", "webhookentry")
process_deprecated_setting(
config, "webhook", "webhookbuycancel", "webhook", "webhookentrycancel"
)
process_deprecated_setting(
config, "webhook", "webhookbuyfill", "webhook", "webhookentryfill"
)
process_deprecated_setting(config, "webhook", "webhooksell", "webhook", "webhookexit")
process_deprecated_setting(
config, "webhook", "webhooksellcancel", "webhook", "webhookexitcancel"
)
process_deprecated_setting(
config, "webhook", "webhooksellfill", "webhook", "webhookexitfill"
)
# Legacy way - having them in experimental ... # Legacy way - having them in experimental ...
process_removed_setting(config, 'experimental', 'use_sell_signal', None, 'use_exit_signal') process_removed_setting(config, "experimental", "use_sell_signal", None, "use_exit_signal")
process_removed_setting(config, 'experimental', 'sell_profit_only', None, 'exit_profit_only') process_removed_setting(config, "experimental", "sell_profit_only", None, "exit_profit_only")
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal', process_removed_setting(
None, 'ignore_roi_if_entry_signal') config, "experimental", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
)
process_removed_setting(config, 'ask_strategy', 'use_sell_signal', None, 'use_exit_signal') process_removed_setting(config, "ask_strategy", "use_sell_signal", None, "use_exit_signal")
process_removed_setting(config, 'ask_strategy', 'sell_profit_only', None, 'exit_profit_only') process_removed_setting(config, "ask_strategy", "sell_profit_only", None, "exit_profit_only")
process_removed_setting(config, 'ask_strategy', 'sell_profit_offset', process_removed_setting(
None, 'exit_profit_offset') config, "ask_strategy", "sell_profit_offset", None, "exit_profit_offset"
process_removed_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', )
None, 'ignore_roi_if_entry_signal') process_removed_setting(
if (config.get('edge', {}).get('enabled', False) config, "ask_strategy", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
and 'capital_available_percentage' in config.get('edge', {})): )
if config.get("edge", {}).get(
"enabled", False
) and "capital_available_percentage" in config.get("edge", {}):
raise ConfigurationError( raise ConfigurationError(
"DEPRECATED: " "DEPRECATED: "
"Using 'edge.capital_available_percentage' has been deprecated in favor of " "Using 'edge.capital_available_percentage' has been deprecated in favor of "
@@ -129,12 +170,11 @@ def process_temporary_deprecated_settings(config: Config) -> None:
"'tradable_balance_ratio' and remove 'capital_available_percentage' " "'tradable_balance_ratio' and remove 'capital_available_percentage' "
"from the edge configuration." "from the edge configuration."
) )
if 'ticker_interval' in config: if "ticker_interval" in config:
raise ConfigurationError( raise ConfigurationError(
"DEPRECATED: 'ticker_interval' detected. " "DEPRECATED: 'ticker_interval' detected. "
"Please use 'timeframe' instead of 'ticker_interval." "Please use 'timeframe' instead of 'ticker_interval."
) )
if 'protections' in config: if "protections" in config:
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.") logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")

View File

@@ -5,4 +5,4 @@ def running_in_docker() -> bool:
""" """
Check if we are running in a docker container Check if we are running in a docker container
""" """
return os.environ.get('FT_APP_ENV') == 'docker' return os.environ.get("FT_APP_ENV") == "docker"

View File

@@ -4,8 +4,14 @@ from pathlib import Path
from typing import Optional from typing import Optional
from freqtrade.configuration.detect_environment import running_in_docker from freqtrade.configuration.detect_environment import running_in_docker
from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS, from freqtrade.constants import (
USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config) USER_DATA_FILES,
USERPATH_FREQAIMODELS,
USERPATH_HYPEROPTS,
USERPATH_NOTEBOOKS,
USERPATH_STRATEGIES,
Config,
)
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@@ -13,16 +19,15 @@ logger = logging.getLogger(__name__)
def create_datadir(config: Config, datadir: Optional[str] = None) -> Path: def create_datadir(config: Config, datadir: Optional[str] = None) -> Path:
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data") folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
if not datadir: if not datadir:
# set datadir # set datadir
exchange_name = config.get('exchange', {}).get('name', '').lower() exchange_name = config.get("exchange", {}).get("name", "").lower()
folder = folder.joinpath(exchange_name) folder = folder.joinpath(exchange_name)
if not folder.is_dir(): if not folder.is_dir():
folder.mkdir(parents=True) folder.mkdir(parents=True)
logger.info(f'Created data directory: {datadir}') logger.info(f"Created data directory: {datadir}")
return folder return folder
@@ -34,8 +39,8 @@ def chown_user_directory(directory: Path) -> None:
if running_in_docker(): if running_in_docker():
try: try:
import subprocess import subprocess
subprocess.check_output(
['sudo', 'chown', '-R', 'ftuser:', str(directory.resolve())]) subprocess.check_output(["sudo", "chown", "-R", "ftuser:", str(directory.resolve())])
except Exception: except Exception:
logger.warning(f"Could not chown {directory}") logger.warning(f"Could not chown {directory}")
@@ -50,18 +55,28 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
:param create_dir: Create directory if it does not exist. :param create_dir: Create directory if it does not exist.
:return: Path object containing the directory :return: Path object containing the directory
""" """
sub_dirs = ["backtest_results", "data", USERPATH_HYPEROPTS, "hyperopt_results", "logs", sub_dirs = [
USERPATH_NOTEBOOKS, "plot", USERPATH_STRATEGIES, USERPATH_FREQAIMODELS] "backtest_results",
"data",
USERPATH_HYPEROPTS,
"hyperopt_results",
"logs",
USERPATH_NOTEBOOKS,
"plot",
USERPATH_STRATEGIES,
USERPATH_FREQAIMODELS,
]
folder = Path(directory) folder = Path(directory)
chown_user_directory(folder) chown_user_directory(folder)
if not folder.is_dir(): if not folder.is_dir():
if create_dir: if create_dir:
folder.mkdir(parents=True) folder.mkdir(parents=True)
logger.info(f'Created user-data directory: {folder}') logger.info(f"Created user-data directory: {folder}")
else: else:
raise OperationalException( raise OperationalException(
f"Directory `{folder}` does not exist. " f"Directory `{folder}` does not exist. "
"Please use `freqtrade create-userdir` to create a user directory") "Please use `freqtrade create-userdir` to create a user directory"
)
# Create required subdirectories # Create required subdirectories
for f in sub_dirs: for f in sub_dirs:

View File

@@ -16,9 +16,9 @@ def _get_var_typed(val):
try: try:
return float(val) return float(val)
except ValueError: except ValueError:
if val.lower() in ('t', 'true'): if val.lower() in ("t", "true"):
return True return True
elif val.lower() in ('f', 'false'): elif val.lower() in ("f", "false"):
return False return False
# keep as string # keep as string
return val return val
@@ -32,16 +32,21 @@ def _flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str
:param prefix: Prefix to consider (usually FREQTRADE__) :param prefix: Prefix to consider (usually FREQTRADE__)
:return: Nested dict based on available and relevant variables. :return: Nested dict based on available and relevant variables.
""" """
no_convert = ['CHAT_ID', 'PASSWORD'] no_convert = ["CHAT_ID", "PASSWORD"]
relevant_vars: Dict[str, Any] = {} relevant_vars: Dict[str, Any] = {}
for env_var, val in sorted(env_dict.items()): for env_var, val in sorted(env_dict.items()):
if env_var.startswith(prefix): if env_var.startswith(prefix):
logger.info(f"Loading variable '{env_var}'") logger.info(f"Loading variable '{env_var}'")
key = env_var.replace(prefix, '') key = env_var.replace(prefix, "")
for k in reversed(key.split('__')): for k in reversed(key.split("__")):
val = {k.lower(): _get_var_typed(val) val = {
if not isinstance(val, dict) and k not in no_convert else val} k.lower(): (
_get_var_typed(val)
if not isinstance(val, dict) and k not in no_convert
else val
)
}
relevant_vars = deep_merge_dicts(val, relevant_vars) relevant_vars = deep_merge_dicts(val, relevant_vars)
return relevant_vars return relevant_vars

View File

@@ -1,6 +1,7 @@
""" """
This module contain functions to load the configuration file This module contain functions to load the configuration file
""" """
import logging import logging
import re import re
import sys import sys
@@ -25,25 +26,25 @@ def log_config_error_range(path: str, errmsg: str) -> str:
""" """
Parses configuration file and prints range around error Parses configuration file and prints range around error
""" """
if path != '-': if path != "-":
offsetlist = re.findall(r'(?<=Parse\serror\sat\soffset\s)\d+', errmsg) offsetlist = re.findall(r"(?<=Parse\serror\sat\soffset\s)\d+", errmsg)
if offsetlist: if offsetlist:
offset = int(offsetlist[0]) offset = int(offsetlist[0])
text = Path(path).read_text() text = Path(path).read_text()
# Fetch an offset of 80 characters around the error line # Fetch an offset of 80 characters around the error line
subtext = text[offset - min(80, offset):offset + 80] subtext = text[offset - min(80, offset) : offset + 80]
segments = subtext.split('\n') segments = subtext.split("\n")
if len(segments) > 3: if len(segments) > 3:
# Remove first and last lines, to avoid odd truncations # Remove first and last lines, to avoid odd truncations
return '\n'.join(segments[1:-1]) return "\n".join(segments[1:-1])
else: else:
return subtext return subtext
return '' return ""
def load_file(path: Path) -> Dict[str, Any]: def load_file(path: Path) -> Dict[str, Any]:
try: try:
with path.open('r') as file: with path.open("r") as file:
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
except FileNotFoundError: except FileNotFoundError:
raise OperationalException(f'File "{path}" not found!') from None raise OperationalException(f'File "{path}" not found!') from None
@@ -58,25 +59,27 @@ def load_config_file(path: str) -> Dict[str, Any]:
""" """
try: try:
# Read config from stdin if requested in the options # Read config from stdin if requested in the options
with Path(path).open() if path != '-' else sys.stdin as file: with Path(path).open() if path != "-" else sys.stdin as file:
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
except FileNotFoundError: except FileNotFoundError:
raise OperationalException( raise OperationalException(
f'Config file "{path}" not found!' f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.') from None " Please create a config file or check whether it exists."
) from None
except rapidjson.JSONDecodeError as e: except rapidjson.JSONDecodeError as e:
err_range = log_config_error_range(path, str(e)) err_range = log_config_error_range(path, str(e))
raise ConfigurationError( raise ConfigurationError(
f'{e}\n' f"{e}\nPlease verify the following segment of your configuration:\n{err_range}"
f'Please verify the following segment of your configuration:\n{err_range}' if err_range
if err_range else 'Please verify your configuration file for syntax errors.' else "Please verify your configuration file for syntax errors."
) )
return config return config
def load_from_files( def load_from_files(
files: List[str], base_path: Optional[Path] = None, level: int = 0) -> Dict[str, Any]: files: List[str], base_path: Optional[Path] = None, level: int = 0
) -> Dict[str, Any]:
""" """
Recursively load configuration files if specified. Recursively load configuration files if specified.
Sub-files are assumed to be relative to the initial config. Sub-files are assumed to be relative to the initial config.
@@ -90,8 +93,8 @@ def load_from_files(
files_loaded = [] files_loaded = []
# We expect here a list of config filenames # We expect here a list of config filenames
for filename in files: for filename in files:
logger.info(f'Using config: {filename} ...') logger.info(f"Using config: {filename} ...")
if filename == '-': if filename == "-":
# Immediately load stdin and return # Immediately load stdin and return
return load_config_file(filename) return load_config_file(filename)
file = Path(filename) file = Path(filename)
@@ -100,10 +103,11 @@ def load_from_files(
file = base_path / file file = base_path / file
config_tmp = load_config_file(str(file)) config_tmp = load_config_file(str(file))
if 'add_config_files' in config_tmp: if "add_config_files" in config_tmp:
config_sub = load_from_files( config_sub = load_from_files(
config_tmp['add_config_files'], file.resolve().parent, level + 1) config_tmp["add_config_files"], file.resolve().parent, level + 1
files_loaded.extend(config_sub.get('config_files', [])) )
files_loaded.extend(config_sub.get("config_files", []))
config_tmp = deep_merge_dicts(config_tmp, config_sub) config_tmp = deep_merge_dicts(config_tmp, config_sub)
files_loaded.insert(0, str(file)) files_loaded.insert(0, str(file))
@@ -111,6 +115,6 @@ def load_from_files(
# Merge config options, overwriting prior values # Merge config options, overwriting prior values
config = deep_merge_dicts(config_tmp, config) config = deep_merge_dicts(config_tmp, config)
config['config_files'] = files_loaded config["config_files"] = files_loaded
return config return config

View File

@@ -1,6 +1,7 @@
""" """
This module contains the argument manager class This module contains the argument manager class
""" """
import logging import logging
import re import re
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -22,9 +23,13 @@ class TimeRange:
if *type is None, don't use corresponding startvalue. if *type is None, don't use corresponding startvalue.
""" """
def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None, def __init__(
startts: int = 0, stopts: int = 0): self,
starttype: Optional[str] = None,
stoptype: Optional[str] = None,
startts: int = 0,
stopts: int = 0,
):
self.starttype: Optional[str] = starttype self.starttype: Optional[str] = starttype
self.stoptype: Optional[str] = stoptype self.stoptype: Optional[str] = stoptype
self.startts: int = startts self.startts: int = startts
@@ -48,12 +53,12 @@ class TimeRange:
Returns a string representation of the timerange as used by parse_timerange. Returns a string representation of the timerange as used by parse_timerange.
Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set. Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set.
""" """
start = '' start = ""
stop = '' stop = ""
if startdt := self.startdt: if startdt := self.startdt:
start = startdt.strftime('%Y%m%d') start = startdt.strftime("%Y%m%d")
if stopdt := self.stopdt: if stopdt := self.stopdt:
stop = stopdt.strftime('%Y%m%d') stop = stopdt.strftime("%Y%m%d")
return f"{start}-{stop}" return f"{start}-{stop}"
@property @property
@@ -61,7 +66,7 @@ class TimeRange:
""" """
Returns a string representation of the start date Returns a string representation of the start date
""" """
val = 'unbounded' val = "unbounded"
if (startdt := self.startdt) is not None: if (startdt := self.startdt) is not None:
val = startdt.strftime(DATETIME_PRINT_FORMAT) val = startdt.strftime(DATETIME_PRINT_FORMAT)
return val return val
@@ -71,15 +76,19 @@ class TimeRange:
""" """
Returns a string representation of the stop date Returns a string representation of the stop date
""" """
val = 'unbounded' val = "unbounded"
if (stopdt := self.stopdt) is not None: if (stopdt := self.stopdt) is not None:
val = stopdt.strftime(DATETIME_PRINT_FORMAT) val = stopdt.strftime(DATETIME_PRINT_FORMAT)
return val return val
def __eq__(self, other): def __eq__(self, other):
"""Override the default Equals behavior""" """Override the default Equals behavior"""
return (self.starttype == other.starttype and self.stoptype == other.stoptype return (
and self.startts == other.startts and self.stopts == other.stopts) self.starttype == other.starttype
and self.stoptype == other.stoptype
and self.startts == other.startts
and self.stopts == other.stopts
)
def subtract_start(self, seconds: int) -> None: def subtract_start(self, seconds: int) -> None:
""" """
@@ -90,8 +99,9 @@ class TimeRange:
if self.startts: if self.startts:
self.startts = self.startts - seconds self.startts = self.startts - seconds
def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int, def adjust_start_if_necessary(
min_date: datetime) -> None: self, timeframe_secs: int, startup_candles: int, min_date: datetime
) -> None:
""" """
Adjust startts by <startup_candles> candles. Adjust startts by <startup_candles> candles.
Applies only if no startup-candles have been available. Applies only if no startup-candles have been available.
@@ -101,13 +111,13 @@ class TimeRange:
has to be moved has to be moved
:return: None (Modifies the object in place) :return: None (Modifies the object in place)
""" """
if (not self.starttype or (startup_candles if not self.starttype or (startup_candles and min_date.timestamp() >= self.startts):
and min_date.timestamp() >= self.startts)):
# If no startts was defined, or backtest-data starts at the defined backtest-date # If no startts was defined, or backtest-data starts at the defined backtest-date
logger.warning("Moving start-date by %s candles to account for startup time.", logger.warning(
startup_candles) "Moving start-date by %s candles to account for startup time.", startup_candles
)
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles) self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
self.starttype = 'date' self.starttype = "date"
@classmethod @classmethod
def parse_timerange(cls, text: Optional[str]) -> Self: def parse_timerange(cls, text: Optional[str]) -> Self:
@@ -118,16 +128,17 @@ class TimeRange:
""" """
if not text: if not text:
return cls(None, None, 0, 0) return cls(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')), syntax = [
(r'^(\d{8})-$', ('date', None)), (r"^-(\d{8})$", (None, "date")),
(r'^(\d{8})-(\d{8})$', ('date', 'date')), (r"^(\d{8})-$", ("date", None)),
(r'^-(\d{10})$', (None, 'date')), (r"^(\d{8})-(\d{8})$", ("date", "date")),
(r'^(\d{10})-$', ('date', None)), (r"^-(\d{10})$", (None, "date")),
(r'^(\d{10})-(\d{10})$', ('date', 'date')), (r"^(\d{10})-$", ("date", None)),
(r'^-(\d{13})$', (None, 'date')), (r"^(\d{10})-(\d{10})$", ("date", "date")),
(r'^(\d{13})-$', ('date', None)), (r"^-(\d{13})$", (None, "date")),
(r'^(\d{13})-(\d{13})$', ('date', 'date')), (r"^(\d{13})-$", ("date", None)),
] (r"^(\d{13})-(\d{13})$", ("date", "date")),
]
for rex, stype in syntax: for rex, stype in syntax:
# Apply the regular expression to text # Apply the regular expression to text
match = re.match(rex, text) match = re.match(rex, text)
@@ -138,9 +149,12 @@ class TimeRange:
stop: int = 0 stop: int = 0
if stype[0]: if stype[0]:
starts = rvals[index] starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8: if stype[0] == "date" and len(starts) == 8:
start = int(datetime.strptime(starts, '%Y%m%d').replace( start = int(
tzinfo=timezone.utc).timestamp()) datetime.strptime(starts, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
)
elif len(starts) == 13: elif len(starts) == 13:
start = int(starts) // 1000 start = int(starts) // 1000
else: else:
@@ -148,15 +162,19 @@ class TimeRange:
index += 1 index += 1
if stype[1]: if stype[1]:
stops = rvals[index] stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8: if stype[1] == "date" and len(stops) == 8:
stop = int(datetime.strptime(stops, '%Y%m%d').replace( stop = int(
tzinfo=timezone.utc).timestamp()) datetime.strptime(stops, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
)
elif len(stops) == 13: elif len(stops) == 13:
stop = int(stops) // 1000 stop = int(stops) // 1000
else: else:
stop = int(stops) stop = int(stops)
if start > stop > 0: if start > stop > 0:
raise ConfigurationError( raise ConfigurationError(
f'Start date is after stop date for timerange "{text}"') f'Start date is after stop date for timerange "{text}"'
)
return cls(stype[0], stype[1], start, stop) return cls(stype[0], stype[1], start, stop)
raise ConfigurationError(f'Incorrect syntax for timerange "{text}"') raise ConfigurationError(f'Incorrect syntax for timerange "{text}"')

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,4 @@ Module to handle data operations for freqtrade
""" """
# limit what's imported when using `from freqtrade.data import *` # limit what's imported when using `from freqtrade.data import *`
__all__ = [ __all__ = ["converter"]
'converter'
]

View File

@@ -1,6 +1,7 @@
""" """
Helpers when analyzing backtest data Helpers when analyzing backtest data
""" """
import logging import logging
from copy import copy from copy import copy
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -21,14 +22,35 @@ from freqtrade.types import BacktestHistoryEntryType, BacktestResultType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Newest format # Newest format
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'max_stake_amount', 'amount', BT_DATA_COLUMNS = [
'open_date', 'close_date', 'open_rate', 'close_rate', "pair",
'fee_open', 'fee_close', 'trade_duration', "stake_amount",
'profit_ratio', 'profit_abs', 'exit_reason', "max_stake_amount",
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', "amount",
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag', "open_date",
'leverage', 'is_short', 'open_timestamp', 'close_timestamp', 'orders' "close_date",
] "open_rate",
"close_rate",
"fee_open",
"fee_close",
"trade_duration",
"profit_ratio",
"profit_abs",
"exit_reason",
"initial_stop_loss_abs",
"initial_stop_loss_ratio",
"stop_loss_abs",
"stop_loss_ratio",
"min_rate",
"max_rate",
"is_open",
"enter_tag",
"leverage",
"is_short",
"open_timestamp",
"close_timestamp",
"orders",
]
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
@@ -50,15 +72,16 @@ def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> s
if not filename.is_file(): if not filename.is_file():
raise ValueError( raise ValueError(
f"Directory '{directory}' does not seem to contain backtest statistics yet.") f"Directory '{directory}' does not seem to contain backtest statistics yet."
)
with filename.open() as file: with filename.open() as file:
data = json_load(file) data = json_load(file)
if f'latest_{variant}' not in data: if f"latest_{variant}" not in data:
raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.") raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.")
return data[f'latest_{variant}'] return data[f"latest_{variant}"]
def get_latest_backtest_filename(directory: Union[Path, str]) -> str: def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
@@ -71,7 +94,7 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
* `directory/.last_result.json` does not exist * `directory/.last_result.json` does not exist
* `directory/.last_result.json` has the wrong content * `directory/.last_result.json` has the wrong content
""" """
return get_latest_optimize_filename(directory, 'backtest') return get_latest_optimize_filename(directory, "backtest")
def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str: def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
@@ -85,14 +108,15 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
* `directory/.last_result.json` has the wrong content * `directory/.last_result.json` has the wrong content
""" """
try: try:
return get_latest_optimize_filename(directory, 'hyperopt') return get_latest_optimize_filename(directory, "hyperopt")
except ValueError: except ValueError:
# Return default (legacy) pickle filename # Return default (legacy) pickle filename
return 'hyperopt_results.pickle' return "hyperopt_results.pickle"
def get_latest_hyperopt_file( def get_latest_hyperopt_file(
directory: Union[Path, str], predef_filename: Optional[str] = None) -> Path: directory: Union[Path, str], predef_filename: Optional[str] = None
) -> Path:
""" """
Get latest hyperopt export based on '.last_result.json'. Get latest hyperopt export based on '.last_result.json'.
:param directory: Directory to search for last result :param directory: Directory to search for last result
@@ -107,7 +131,8 @@ def get_latest_hyperopt_file(
if predef_filename: if predef_filename:
if Path(predef_filename).is_absolute(): if Path(predef_filename).is_absolute():
raise ConfigurationError( raise ConfigurationError(
"--hyperopt-filename expects only the filename, not an absolute path.") "--hyperopt-filename expects only the filename, not an absolute path."
)
return directory / predef_filename return directory / predef_filename
return directory / get_latest_hyperopt_filename(directory) return directory / get_latest_hyperopt_filename(directory)
@@ -126,7 +151,7 @@ def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
except FileNotFoundError: except FileNotFoundError:
return {} return {}
except Exception as e: except Exception as e:
raise OperationalException('Unexpected error while loading backtest metadata.') from e raise OperationalException("Unexpected error while loading backtest metadata.") from e
def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType: def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
@@ -147,7 +172,7 @@ def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
# Legacy list format does not contain metadata. # Legacy list format does not contain metadata.
if isinstance(data, dict): if isinstance(data, dict):
data['metadata'] = load_backtest_metadata(filename) data["metadata"] = load_backtest_metadata(filename)
return data return data
@@ -159,38 +184,39 @@ def load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
:param results: dict to merge the result to. :param results: dict to merge the result to.
""" """
bt_data = load_backtest_stats(filename) bt_data = load_backtest_stats(filename)
k: Literal['metadata', 'strategy'] k: Literal["metadata", "strategy"]
for k in ('metadata', 'strategy'): # type: ignore for k in ("metadata", "strategy"): # type: ignore
results[k][strategy_name] = bt_data[k][strategy_name] results[k][strategy_name] = bt_data[k][strategy_name]
results['metadata'][strategy_name]['filename'] = filename.stem results["metadata"][strategy_name]["filename"] = filename.stem
comparison = bt_data['strategy_comparison'] comparison = bt_data["strategy_comparison"]
for i in range(len(comparison)): for i in range(len(comparison)):
if comparison[i]['key'] == strategy_name: if comparison[i]["key"] == strategy_name:
results['strategy_comparison'].append(comparison[i]) results["strategy_comparison"].append(comparison[i])
break break
def _get_backtest_files(dirname: Path) -> List[Path]: def _get_backtest_files(dirname: Path) -> List[Path]:
# Weird glob expression here avoids including .meta.json files. # Weird glob expression here avoids including .meta.json files.
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json')))) return list(reversed(sorted(dirname.glob("backtest-result-*-[0-9][0-9].json"))))
def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]: def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
metadata = load_backtest_metadata(filename) metadata = load_backtest_metadata(filename)
return [ return [
{ {
'filename': filename.stem, "filename": filename.stem,
'strategy': s, "strategy": s,
'run_id': v['run_id'], "run_id": v["run_id"],
'notes': v.get('notes', ''), "notes": v.get("notes", ""),
# Backtest "run" time # Backtest "run" time
'backtest_start_time': v['backtest_start_time'], "backtest_start_time": v["backtest_start_time"],
# Backtest timerange # Backtest timerange
'backtest_start_ts': v.get('backtest_start_ts', None), "backtest_start_ts": v.get("backtest_start_ts", None),
'backtest_end_ts': v.get('backtest_end_ts', None), "backtest_end_ts": v.get("backtest_end_ts", None),
'timeframe': v.get('timeframe', None), "timeframe": v.get("timeframe", None),
'timeframe_detail': v.get('timeframe_detail', None), "timeframe_detail": v.get("timeframe_detail", None),
} for s, v in metadata.items() }
for s, v in metadata.items()
] ]
@@ -218,7 +244,7 @@ def delete_backtest_result(file_abs: Path):
""" """
# *.meta.json # *.meta.json
logger.info(f"Deleting backtest result file: {file_abs.name}") logger.info(f"Deleting backtest result file: {file_abs.name}")
file_abs_meta = file_abs.with_suffix('.meta.json') file_abs_meta = file_abs.with_suffix(".meta.json")
file_abs.unlink() file_abs.unlink()
file_abs_meta.unlink() file_abs_meta.unlink()
@@ -244,12 +270,13 @@ def get_backtest_market_change(filename: Path, include_ts: bool = True) -> pd.Da
""" """
df = pd.read_feather(filename) df = pd.read_feather(filename)
if include_ts: if include_ts:
df.loc[:, '__date_ts'] = df.loc[:, 'date'].astype(np.int64) // 1000 // 1000 df.loc[:, "__date_ts"] = df.loc[:, "date"].astype(np.int64) // 1000 // 1000
return df return df
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], def find_existing_backtest_stats(
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]: dirname: Union[Path, str], run_ids: Dict[str, str], min_backtest_date: Optional[datetime] = None
) -> Dict[str, Any]:
""" """
Find existing backtest stats that match specified run IDs and load them. Find existing backtest stats that match specified run IDs and load them.
:param dirname: pathlib.Path object, or string pointing to the file. :param dirname: pathlib.Path object, or string pointing to the file.
@@ -261,9 +288,9 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
run_ids = copy(run_ids) run_ids = copy(run_ids)
dirname = Path(dirname) dirname = Path(dirname)
results: Dict[str, Any] = { results: Dict[str, Any] = {
'metadata': {}, "metadata": {},
'strategy': {}, "strategy": {},
'strategy_comparison': [], "strategy_comparison": [],
} }
for filename in _get_backtest_files(dirname): for filename in _get_backtest_files(dirname):
@@ -280,14 +307,14 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
continue continue
if min_backtest_date is not None: if min_backtest_date is not None:
backtest_date = strategy_metadata['backtest_start_time'] backtest_date = strategy_metadata["backtest_start_time"]
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc) backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
if backtest_date < min_backtest_date: if backtest_date < min_backtest_date:
# Do not use a cached result for this strategy as first result is too old. # Do not use a cached result for this strategy as first result is too old.
del run_ids[strategy_name] del run_ids[strategy_name]
continue continue
if strategy_metadata['run_id'] == run_id: if strategy_metadata["run_id"] == run_id:
del run_ids[strategy_name] del run_ids[strategy_name]
load_and_merge_backtest_result(strategy_name, filename, results) load_and_merge_backtest_result(strategy_name, filename, results)
@@ -300,20 +327,20 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
""" """
Compatibility support for older backtest data. Compatibility support for older backtest data.
""" """
df['open_date'] = pd.to_datetime(df['open_date'], utc=True) df["open_date"] = pd.to_datetime(df["open_date"], utc=True)
df['close_date'] = pd.to_datetime(df['close_date'], utc=True) df["close_date"] = pd.to_datetime(df["close_date"], utc=True)
# Compatibility support for pre short Columns # Compatibility support for pre short Columns
if 'is_short' not in df.columns: if "is_short" not in df.columns:
df['is_short'] = False df["is_short"] = False
if 'leverage' not in df.columns: if "leverage" not in df.columns:
df['leverage'] = 1.0 df["leverage"] = 1.0
if 'enter_tag' not in df.columns: if "enter_tag" not in df.columns:
df['enter_tag'] = df['buy_tag'] df["enter_tag"] = df["buy_tag"]
df = df.drop(['buy_tag'], axis=1) df = df.drop(["buy_tag"], axis=1)
if 'max_stake_amount' not in df.columns: if "max_stake_amount" not in df.columns:
df['max_stake_amount'] = df['stake_amount'] df["max_stake_amount"] = df["stake_amount"]
if 'orders' not in df.columns: if "orders" not in df.columns:
df['orders'] = None df["orders"] = None
return df return df
@@ -329,23 +356,25 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
data = load_backtest_stats(filename) data = load_backtest_stats(filename)
if not isinstance(data, list): if not isinstance(data, list):
# new, nested format # new, nested format
if 'strategy' not in data: if "strategy" not in data:
raise ValueError("Unknown dataformat.") raise ValueError("Unknown dataformat.")
if not strategy: if not strategy:
if len(data['strategy']) == 1: if len(data["strategy"]) == 1:
strategy = list(data['strategy'].keys())[0] strategy = list(data["strategy"].keys())[0]
else: else:
raise ValueError("Detected backtest result with more than one strategy. " raise ValueError(
"Please specify a strategy.") "Detected backtest result with more than one strategy. "
"Please specify a strategy."
)
if strategy not in data['strategy']: if strategy not in data["strategy"]:
raise ValueError( raise ValueError(
f"Strategy {strategy} not available in the backtest result. " f"Strategy {strategy} not available in the backtest result. "
f"Available strategies are '{','.join(data['strategy'].keys())}'" f"Available strategies are '{','.join(data['strategy'].keys())}'"
) )
data = data['strategy'][strategy]['trades'] data = data["strategy"][strategy]["trades"]
df = pd.DataFrame(data) df = pd.DataFrame(data)
if not df.empty: if not df.empty:
df = _load_backtest_data_df_compatibility(df) df = _load_backtest_data_df_compatibility(df)
@@ -353,7 +382,8 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
else: else:
# old format - only with lists. # old format - only with lists.
raise OperationalException( raise OperationalException(
"Backtest-results with only trades data are no longer supported.") "Backtest-results with only trades data are no longer supported."
)
if not df.empty: if not df.empty:
df = df.sort_values("open_date").reset_index(drop=True) df = df.sort_values("open_date").reset_index(drop=True)
return df return df
@@ -368,23 +398,26 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
:return: dataframe with open-counts per time-period in timeframe :return: dataframe with open-counts per time-period in timeframe
""" """
from freqtrade.exchange import timeframe_to_resample_freq from freqtrade.exchange import timeframe_to_resample_freq
timeframe_freq = timeframe_to_resample_freq(timeframe) timeframe_freq = timeframe_to_resample_freq(timeframe)
dates = [pd.Series(pd.date_range(row[1]['open_date'], row[1]['close_date'], dates = [
freq=timeframe_freq)) pd.Series(pd.date_range(row[1]["open_date"], row[1]["close_date"], freq=timeframe_freq))
for row in results[['open_date', 'close_date']].iterrows()] for row in results[["open_date", "close_date"]].iterrows()
]
deltas = [len(x) for x in dates] deltas = [len(x) for x in dates]
dates = pd.Series(pd.concat(dates).values, name='date') dates = pd.Series(pd.concat(dates).values, name="date")
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
df2 = pd.concat([dates, df2], axis=1) df2 = pd.concat([dates, df2], axis=1)
df2 = df2.set_index('date') df2 = df2.set_index("date")
df_final = df2.resample(timeframe_freq)[['pair']].count() df_final = df2.resample(timeframe_freq)[["pair"]].count()
df_final = df_final.rename({'pair': 'open_trades'}, axis=1) df_final = df_final.rename({"pair": "open_trades"}, axis=1)
return df_final return df_final
def evaluate_result_multi(results: pd.DataFrame, timeframe: str, def evaluate_result_multi(
max_open_trades: IntOrInf) -> pd.DataFrame: results: pd.DataFrame, timeframe: str, max_open_trades: IntOrInf
) -> pd.DataFrame:
""" """
Find overlapping trades by expanding each trade once per period it was open Find overlapping trades by expanding each trade once per period it was open
and then counting overlaps and then counting overlaps
@@ -394,7 +427,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
:return: dataframe with open-counts per time-period in freq :return: dataframe with open-counts per time-period in freq
""" """
df_final = analyze_trade_parallelism(results, timeframe) df_final = analyze_trade_parallelism(results, timeframe)
return df_final[df_final['open_trades'] > max_open_trades] return df_final[df_final["open_trades"] > max_open_trades]
def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame: def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
@@ -405,9 +438,9 @@ def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.
""" """
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS) df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
if len(df) > 0: if len(df) > 0:
df['close_date'] = pd.to_datetime(df['close_date'], utc=True) df["close_date"] = pd.to_datetime(df["close_date"], utc=True)
df['open_date'] = pd.to_datetime(df['open_date'], utc=True) df["open_date"] = pd.to_datetime(df["open_date"], utc=True)
df['close_rate'] = df['close_rate'].astype('float64') df["close_rate"] = df["close_rate"].astype("float64")
return df return df
@@ -429,8 +462,13 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
return trades return trades
def load_trades(source: str, db_url: str, exportfilename: Path, def load_trades(
no_trades: bool = False, strategy: Optional[str] = None) -> pd.DataFrame: source: str,
db_url: str,
exportfilename: Path,
no_trades: bool = False,
strategy: Optional[str] = None,
) -> pd.DataFrame:
""" """
Based on configuration option 'trade_source': Based on configuration option 'trade_source':
* loads data from DB (using `db_url`) * loads data from DB (using `db_url`)
@@ -451,8 +489,9 @@ def load_trades(source: str, db_url: str, exportfilename: Path,
return load_backtest_data(exportfilename, strategy) return load_backtest_data(exportfilename, strategy)
def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame, def extract_trades_of_period(
date_index=False) -> pd.DataFrame: dataframe: pd.DataFrame, trades: pd.DataFrame, date_index=False
) -> pd.DataFrame:
""" """
Compare trades and backtested pair DataFrames to get trades performed on backtested period Compare trades and backtested pair DataFrames to get trades performed on backtested period
:return: the DataFrame of a trades of period :return: the DataFrame of a trades of period
@@ -461,8 +500,9 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame,
trades_start = dataframe.index[0] trades_start = dataframe.index[0]
trades_stop = dataframe.index[-1] trades_stop = dataframe.index[-1]
else: else:
trades_start = dataframe.iloc[0]['date'] trades_start = dataframe.iloc[0]["date"]
trades_stop = dataframe.iloc[-1]['date'] trades_stop = dataframe.iloc[-1]["date"]
trades = trades.loc[(trades['open_date'] >= trades_start) & trades = trades.loc[
(trades['close_date'] <= trades_stop)] (trades["open_date"] >= trades_start) & (trades["close_date"] <= trades_stop)
]
return trades return trades

View File

@@ -1,28 +1,38 @@
from freqtrade.data.converter.converter import (clean_ohlcv_dataframe, convert_ohlcv_format, from freqtrade.data.converter.converter import (
ohlcv_fill_up_missing_data, ohlcv_to_dataframe, clean_ohlcv_dataframe,
order_book_to_dataframe, reduce_dataframe_footprint, convert_ohlcv_format,
trim_dataframe, trim_dataframes) ohlcv_fill_up_missing_data,
from freqtrade.data.converter.trade_converter import (convert_trades_format, ohlcv_to_dataframe,
convert_trades_to_ohlcv, trades_convert_types, order_book_to_dataframe,
trades_df_remove_duplicates, reduce_dataframe_footprint,
trades_dict_to_list, trades_list_to_df, trim_dataframe,
trades_to_ohlcv) trim_dataframes,
)
from freqtrade.data.converter.trade_converter import (
convert_trades_format,
convert_trades_to_ohlcv,
trades_convert_types,
trades_df_remove_duplicates,
trades_dict_to_list,
trades_list_to_df,
trades_to_ohlcv,
)
__all__ = [ __all__ = [
'clean_ohlcv_dataframe', "clean_ohlcv_dataframe",
'convert_ohlcv_format', "convert_ohlcv_format",
'ohlcv_fill_up_missing_data', "ohlcv_fill_up_missing_data",
'ohlcv_to_dataframe', "ohlcv_to_dataframe",
'order_book_to_dataframe', "order_book_to_dataframe",
'reduce_dataframe_footprint', "reduce_dataframe_footprint",
'trim_dataframe', "trim_dataframe",
'trim_dataframes', "trim_dataframes",
'convert_trades_format', "convert_trades_format",
'convert_trades_to_ohlcv', "convert_trades_to_ohlcv",
'trades_convert_types', "trades_convert_types",
'trades_df_remove_duplicates', "trades_df_remove_duplicates",
'trades_dict_to_list', "trades_dict_to_list",
'trades_list_to_df', "trades_list_to_df",
'trades_to_ohlcv', "trades_to_ohlcv",
] ]

View File

@@ -1,6 +1,7 @@
""" """
Functions to convert data from one format to another Functions to convert data from one format to another
""" """
import logging import logging
from typing import Dict from typing import Dict
@@ -15,8 +16,14 @@ from freqtrade.enums import CandleType, TradingMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *, def ohlcv_to_dataframe(
fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame: ohlcv: list,
timeframe: str,
pair: str,
*,
fill_missing: bool = True,
drop_incomplete: bool = True,
) -> DataFrame:
""" """
Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv) Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv)
to a Dataframe to a Dataframe
@@ -32,20 +39,28 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
cols = DEFAULT_DATAFRAME_COLUMNS cols = DEFAULT_DATAFRAME_COLUMNS
df = DataFrame(ohlcv, columns=cols) df = DataFrame(ohlcv, columns=cols)
df['date'] = to_datetime(df['date'], unit='ms', utc=True) df["date"] = to_datetime(df["date"], unit="ms", utc=True)
# Some exchanges return int values for Volume and even for OHLC. # Some exchanges return int values for Volume and even for OHLC.
# Convert them since TA-LIB indicators used in the strategy assume floats # Convert them since TA-LIB indicators used in the strategy assume floats
# and fail with exception... # and fail with exception...
df = df.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', df = df.astype(
'volume': 'float'}) dtype={
return clean_ohlcv_dataframe(df, timeframe, pair, "open": "float",
fill_missing=fill_missing, "high": "float",
drop_incomplete=drop_incomplete) "low": "float",
"close": "float",
"volume": "float",
}
)
return clean_ohlcv_dataframe(
df, timeframe, pair, fill_missing=fill_missing, drop_incomplete=drop_incomplete
)
def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *, def clean_ohlcv_dataframe(
fill_missing: bool, drop_incomplete: bool) -> DataFrame: data: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
) -> DataFrame:
""" """
Cleanse a OHLCV dataframe by Cleanse a OHLCV dataframe by
* Grouping it by date (removes duplicate tics) * Grouping it by date (removes duplicate tics)
@@ -60,17 +75,19 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
:return: DataFrame :return: DataFrame
""" """
# group by index and aggregate results to eliminate duplicate ticks # group by index and aggregate results to eliminate duplicate ticks
data = data.groupby(by='date', as_index=False, sort=True).agg({ data = data.groupby(by="date", as_index=False, sort=True).agg(
'open': 'first', {
'high': 'max', "open": "first",
'low': 'min', "high": "max",
'close': 'last', "low": "min",
'volume': 'max', "close": "last",
}) "volume": "max",
}
)
# eliminate partial candle # eliminate partial candle
if drop_incomplete: if drop_incomplete:
data.drop(data.tail(1).index, inplace=True) data.drop(data.tail(1).index, inplace=True)
logger.debug('Dropping last candle') logger.debug("Dropping last candle")
if fill_missing: if fill_missing:
return ohlcv_fill_up_missing_data(data, timeframe, pair) return ohlcv_fill_up_missing_data(data, timeframe, pair)
@@ -81,37 +98,35 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame: def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
""" """
Fills up missing data with 0 volume rows, Fills up missing data with 0 volume rows,
using the previous close as price for "open", "high" "low" and "close", volume is set to 0 using the previous close as price for "open", "high", "low" and "close", volume is set to 0
""" """
from freqtrade.exchange import timeframe_to_resample_freq from freqtrade.exchange import timeframe_to_resample_freq
ohlcv_dict = { ohlcv_dict = {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}
resample_interval = timeframe_to_resample_freq(timeframe) resample_interval = timeframe_to_resample_freq(timeframe)
# Resample to create "NAN" values # Resample to create "NAN" values
df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict) df = dataframe.resample(resample_interval, on="date").agg(ohlcv_dict)
# Forwardfill close for missing columns # Forwardfill close for missing columns
df['close'] = df['close'].ffill() df["close"] = df["close"].ffill()
# Use close for "open, high, low" # Use close for "open, high, low"
df.loc[:, ['open', 'high', 'low']] = df[['open', 'high', 'low']].fillna( df.loc[:, ["open", "high", "low"]] = df[["open", "high", "low"]].fillna(
value={'open': df['close'], value={
'high': df['close'], "open": df["close"],
'low': df['close'], "high": df["close"],
}) "low": df["close"],
}
)
df.reset_index(inplace=True) df.reset_index(inplace=True)
len_before = len(dataframe) len_before = len(dataframe)
len_after = len(df) len_after = len(df)
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
if len_before != len_after: if len_before != len_after:
message = (f"Missing data fillup for {pair}, {timeframe}: " message = (
f"before: {len_before} - after: {len_after} - {pct_missing:.2%}") f"Missing data fillup for {pair}, {timeframe}: "
f"before: {len_before} - after: {len_after} - {pct_missing:.2%}"
)
if pct_missing > 0.01: if pct_missing > 0.01:
logger.info(message) logger.info(message)
else: else:
@@ -120,8 +135,9 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
return df return df
def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date', def trim_dataframe(
startup_candles: int = 0) -> DataFrame: df: DataFrame, timerange, *, df_date_col: str = "date", startup_candles: int = 0
) -> DataFrame:
""" """
Trim dataframe based on given timerange Trim dataframe based on given timerange
:param df: Dataframe to trim :param df: Dataframe to trim
@@ -134,15 +150,16 @@ def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date',
# Trim candles instead of timeframe in case of given startup_candle count # Trim candles instead of timeframe in case of given startup_candle count
df = df.iloc[startup_candles:, :] df = df.iloc[startup_candles:, :]
else: else:
if timerange.starttype == 'date': if timerange.starttype == "date":
df = df.loc[df[df_date_col] >= timerange.startdt, :] df = df.loc[df[df_date_col] >= timerange.startdt, :]
if timerange.stoptype == 'date': if timerange.stoptype == "date":
df = df.loc[df[df_date_col] <= timerange.stopdt, :] df = df.loc[df[df_date_col] <= timerange.stopdt, :]
return df return df
def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange, def trim_dataframes(
startup_candles: int) -> Dict[str, DataFrame]: preprocessed: Dict[str, DataFrame], timerange, startup_candles: int
) -> Dict[str, DataFrame]:
""" """
Trim startup period from analyzed dataframes Trim startup period from analyzed dataframes
:param preprocessed: Dict of pair: dataframe :param preprocessed: Dict of pair: dataframe
@@ -157,8 +174,9 @@ def trim_dataframes(preprocessed: Dict[str, DataFrame], timerange,
if not trimed_df.empty: if not trimed_df.empty:
processed[pair] = trimed_df processed[pair] = trimed_df
else: else:
logger.warning(f'{pair} has no data left after adjusting for startup candles, ' logger.warning(
f'skipping.') f"{pair} has no data left after adjusting for startup candles, skipping."
)
return processed return processed
@@ -170,19 +188,28 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
b_sum b_size bids asks a_size a_sum b_sum b_size bids asks a_size a_sum
------------------------------------------------------------------- -------------------------------------------------------------------
""" """
cols = ['bids', 'b_size'] cols = ["bids", "b_size"]
bids_frame = DataFrame(bids, columns=cols) bids_frame = DataFrame(bids, columns=cols)
# add cumulative sum column # add cumulative sum column
bids_frame['b_sum'] = bids_frame['b_size'].cumsum() bids_frame["b_sum"] = bids_frame["b_size"].cumsum()
cols2 = ['asks', 'a_size'] cols2 = ["asks", "a_size"]
asks_frame = DataFrame(asks, columns=cols2) asks_frame = DataFrame(asks, columns=cols2)
# add cumulative sum column # add cumulative sum column
asks_frame['a_sum'] = asks_frame['a_size'].cumsum() asks_frame["a_sum"] = asks_frame["a_size"].cumsum()
frame = pd.concat([bids_frame['b_sum'], bids_frame['b_size'], bids_frame['bids'], frame = pd.concat(
asks_frame['asks'], asks_frame['a_size'], asks_frame['a_sum']], axis=1, [
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum']) bids_frame["b_sum"],
bids_frame["b_size"],
bids_frame["bids"],
asks_frame["asks"],
asks_frame["a_size"],
asks_frame["a_sum"],
],
axis=1,
keys=["b_sum", "b_size", "bids", "asks", "a_size", "a_sum"],
)
# logger.info('order book %s', frame ) # logger.info('order book %s', frame )
return frame return frame
@@ -201,47 +228,51 @@ def convert_ohlcv_format(
:param erase: Erase source data (does not apply if source and target format are identical) :param erase: Erase source data (does not apply if source and target format are identical)
""" """
from freqtrade.data.history import get_datahandler from freqtrade.data.history import get_datahandler
src = get_datahandler(config['datadir'], convert_from)
trg = get_datahandler(config['datadir'], convert_to) src = get_datahandler(config["datadir"], convert_from)
timeframes = config.get('timeframes', [config.get('timeframe')]) trg = get_datahandler(config["datadir"], convert_to)
timeframes = config.get("timeframes", [config.get("timeframe")])
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}") logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', [ candle_types = [
c.value for c in CandleType])] CandleType.from_string(ct)
for ct in config.get("candle_types", [c.value for c in CandleType])
]
logger.info(candle_types) logger.info(candle_types)
paircombs = src.ohlcv_get_available_data(config['datadir'], TradingMode.SPOT) paircombs = src.ohlcv_get_available_data(config["datadir"], TradingMode.SPOT)
paircombs.extend(src.ohlcv_get_available_data(config['datadir'], TradingMode.FUTURES)) paircombs.extend(src.ohlcv_get_available_data(config["datadir"], TradingMode.FUTURES))
if 'pairs' in config: if "pairs" in config:
# Filter pairs # Filter pairs
paircombs = [comb for comb in paircombs if comb[0] in config['pairs']] paircombs = [comb for comb in paircombs if comb[0] in config["pairs"]]
if 'timeframes' in config: if "timeframes" in config:
paircombs = [comb for comb in paircombs if comb[1] in config['timeframes']] paircombs = [comb for comb in paircombs if comb[1] in config["timeframes"]]
paircombs = [comb for comb in paircombs if comb[2] in candle_types] paircombs = [comb for comb in paircombs if comb[2] in candle_types]
paircombs = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value)) paircombs = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value))
formatted_paircombs = '\n'.join([f"{pair}, {timeframe}, {candle_type}" formatted_paircombs = "\n".join(
for pair, timeframe, candle_type in paircombs]) [f"{pair}, {timeframe}, {candle_type}" for pair, timeframe, candle_type in paircombs]
)
logger.info(f"Converting candle (OHLCV) data for the following pair combinations:\n" logger.info(
f"{formatted_paircombs}") f"Converting candle (OHLCV) data for the following pair combinations:\n"
f"{formatted_paircombs}"
)
for pair, timeframe, candle_type in paircombs: for pair, timeframe, candle_type in paircombs:
data = src.ohlcv_load(pair=pair, timeframe=timeframe, data = src.ohlcv_load(
timerange=None, pair=pair,
fill_missing=False, timeframe=timeframe,
drop_incomplete=False, timerange=None,
startup_candles=0, fill_missing=False,
candle_type=candle_type) drop_incomplete=False,
startup_candles=0,
candle_type=candle_type,
)
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}") logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
if len(data) > 0: if len(data) > 0:
trg.ohlcv_store( trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data, candle_type=candle_type)
pair=pair,
timeframe=timeframe,
data=data,
candle_type=candle_type
)
if erase and convert_from != convert_to: if erase and convert_from != convert_to:
logger.info(f"Deleting source data for {pair} / {timeframe}") logger.info(f"Deleting source data for {pair} / {timeframe}")
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type) src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
@@ -254,12 +285,11 @@ def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
:return: Dataframe converted to float/int 32s :return: Dataframe converted to float/int 32s
""" """
logger.debug(f"Memory usage of dataframe is " logger.debug(f"Memory usage of dataframe is {df.memory_usage().sum() / 1024**2:.2f} MB")
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
df_dtypes = df.dtypes df_dtypes = df.dtypes
for column, dtype in df_dtypes.items(): for column, dtype in df_dtypes.items():
if column in ['open', 'high', 'low', 'close', 'volume']: if column in ["open", "high", "low", "close", "volume"]:
continue continue
if dtype == np.float64: if dtype == np.float64:
df_dtypes[column] = np.float32 df_dtypes[column] = np.float32
@@ -267,7 +297,6 @@ def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
df_dtypes[column] = np.int32 df_dtypes[column] = np.int32
df = df.astype(df_dtypes) df = df.astype(df_dtypes)
logger.debug(f"Memory usage after optimization is: " logger.debug(f"Memory usage after optimization is: {df.memory_usage().sum() / 1024**2:.2f} MB")
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
return df return df

View File

@@ -1,6 +1,7 @@
""" """
Functions to convert data from one format to another Functions to convert data from one format to another
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict, List from typing import Dict, List
@@ -9,8 +10,13 @@ import pandas as pd
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES, from freqtrade.constants import (
Config, TradeList) DEFAULT_DATAFRAME_COLUMNS,
DEFAULT_TRADES_COLUMNS,
TRADES_DTYPES,
Config,
TradeList,
)
from freqtrade.enums import CandleType, TradingMode from freqtrade.enums import CandleType, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@@ -25,7 +31,7 @@ def trades_df_remove_duplicates(trades: pd.DataFrame) -> pd.DataFrame:
:param trades: DataFrame with the columns constants.DEFAULT_TRADES_COLUMNS :param trades: DataFrame with the columns constants.DEFAULT_TRADES_COLUMNS
:return: DataFrame with duplicates removed based on the 'timestamp' column :return: DataFrame with duplicates removed based on the 'timestamp' column
""" """
return trades.drop_duplicates(subset=['timestamp', 'id']) return trades.drop_duplicates(subset=["timestamp", "id"])
def trades_dict_to_list(trades: List[Dict]) -> TradeList: def trades_dict_to_list(trades: List[Dict]) -> TradeList:
@@ -42,7 +48,7 @@ def trades_convert_types(trades: DataFrame) -> DataFrame:
Convert Trades dtypes and add 'date' column Convert Trades dtypes and add 'date' column
""" """
trades = trades.astype(TRADES_DTYPES) trades = trades.astype(TRADES_DTYPES)
trades['date'] = to_datetime(trades['timestamp'], unit='ms', utc=True) trades["date"] = to_datetime(trades["timestamp"], unit="ms", utc=True)
return trades return trades
@@ -71,13 +77,14 @@ def trades_to_ohlcv(trades: DataFrame, timeframe: str) -> DataFrame:
:raises: ValueError if no trades are provided :raises: ValueError if no trades are provided
""" """
from freqtrade.exchange import timeframe_to_resample_freq from freqtrade.exchange import timeframe_to_resample_freq
if trades.empty: if trades.empty:
raise ValueError('Trade-list empty.') raise ValueError("Trade-list empty.")
df = trades.set_index('date', drop=True) df = trades.set_index("date", drop=True)
resample_interval = timeframe_to_resample_freq(timeframe) resample_interval = timeframe_to_resample_freq(timeframe)
df_new = df['price'].resample(resample_interval).ohlc() df_new = df["price"].resample(resample_interval).ohlc()
df_new['volume'] = df['amount'].resample(resample_interval).sum() df_new["volume"] = df["amount"].resample(resample_interval).sum()
df_new['date'] = df_new.index df_new["date"] = df_new.index
# Drop 0 volume rows # Drop 0 volume rows
df_new = df_new.dropna() df_new = df_new.dropna()
return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS] return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS]
@@ -97,24 +104,27 @@ def convert_trades_to_ohlcv(
Convert stored trades data to ohlcv data Convert stored trades data to ohlcv data
""" """
from freqtrade.data.history import get_datahandler from freqtrade.data.history import get_datahandler
data_handler_trades = get_datahandler(datadir, data_format=data_format_trades) data_handler_trades = get_datahandler(datadir, data_format=data_format_trades)
data_handler_ohlcv = get_datahandler(datadir, data_format=data_format_ohlcv) data_handler_ohlcv = get_datahandler(datadir, data_format=data_format_ohlcv)
logger.info(f"About to convert pairs: '{', '.join(pairs)}', " logger.info(
f"intervals: '{', '.join(timeframes)}' to {datadir}") f"About to convert pairs: '{', '.join(pairs)}', "
f"intervals: '{', '.join(timeframes)}' to {datadir}"
)
trading_mode = TradingMode.FUTURES if candle_type != CandleType.SPOT else TradingMode.SPOT trading_mode = TradingMode.FUTURES if candle_type != CandleType.SPOT else TradingMode.SPOT
for pair in pairs: for pair in pairs:
trades = data_handler_trades.trades_load(pair, trading_mode) trades = data_handler_trades.trades_load(pair, trading_mode)
for timeframe in timeframes: for timeframe in timeframes:
if erase: if erase:
if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type): if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type):
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.') logger.info(f"Deleting existing data for pair {pair}, interval {timeframe}.")
try: try:
ohlcv = trades_to_ohlcv(trades, timeframe) ohlcv = trades_to_ohlcv(trades, timeframe)
# Store ohlcv # Store ohlcv
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type) data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type)
except ValueError: except ValueError:
logger.warning(f'Could not convert {pair} to OHLCV.') logger.warning(f"Could not convert {pair} to OHLCV.")
def convert_trades_format(config: Config, convert_from: str, convert_to: str, erase: bool): def convert_trades_format(config: Config, convert_from: str, convert_to: str, erase: bool):
@@ -125,25 +135,27 @@ def convert_trades_format(config: Config, convert_from: str, convert_to: str, er
:param convert_to: Target format :param convert_to: Target format
:param erase: Erase source data (does not apply if source and target format are identical) :param erase: Erase source data (does not apply if source and target format are identical)
""" """
if convert_from == 'kraken_csv': if convert_from == "kraken_csv":
if config['exchange']['name'] != 'kraken': if config["exchange"]["name"] != "kraken":
raise OperationalException( raise OperationalException(
'Converting from csv is only supported for kraken.' "Converting from csv is only supported for kraken."
'Please refer to the documentation for details about this special mode.' "Please refer to the documentation for details about this special mode."
) )
from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv
import_kraken_trades_from_csv(config, convert_to) import_kraken_trades_from_csv(config, convert_to)
return return
from freqtrade.data.history import get_datahandler from freqtrade.data.history import get_datahandler
src = get_datahandler(config['datadir'], convert_from)
trg = get_datahandler(config['datadir'], convert_to)
if 'pairs' not in config: src = get_datahandler(config["datadir"], convert_from)
config['pairs'] = src.trades_get_pairs(config['datadir']) trg = get_datahandler(config["datadir"], convert_to)
if "pairs" not in config:
config["pairs"] = src.trades_get_pairs(config["datadir"])
logger.info(f"Converting trades for {config['pairs']}") logger.info(f"Converting trades for {config['pairs']}")
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
for pair in config['pairs']: for pair in config["pairs"]:
data = src.trades_load(pair, trading_mode) data = src.trades_load(pair, trading_mode)
logger.info(f"Converting {len(data)} trades for {pair}") logger.info(f"Converting {len(data)} trades for {pair}")
trg.trades_store(pair, data, trading_mode) trg.trades_store(pair, data, trading_mode)

View File

@@ -4,8 +4,10 @@ from pathlib import Path
import pandas as pd import pandas as pd
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, Config from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, Config
from freqtrade.data.converter.trade_converter import (trades_convert_types, from freqtrade.data.converter.trade_converter import (
trades_df_remove_duplicates) trades_convert_types,
trades_df_remove_duplicates,
)
from freqtrade.data.history import get_datahandler from freqtrade.data.history import get_datahandler
from freqtrade.enums import TradingMode from freqtrade.enums import TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@@ -15,32 +17,33 @@ from freqtrade.resolvers import ExchangeResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
KRAKEN_CSV_TRADE_COLUMNS = ['timestamp', 'price', 'amount'] KRAKEN_CSV_TRADE_COLUMNS = ["timestamp", "price", "amount"]
def import_kraken_trades_from_csv(config: Config, convert_to: str): def import_kraken_trades_from_csv(config: Config, convert_to: str):
""" """
Import kraken trades from csv Import kraken trades from csv
""" """
if config['exchange']['name'] != 'kraken': if config["exchange"]["name"] != "kraken":
raise OperationalException('This function is only for the kraken exchange.') raise OperationalException("This function is only for the kraken exchange.")
datadir: Path = config['datadir'] datadir: Path = config["datadir"]
data_handler = get_datahandler(datadir, data_format=convert_to) data_handler = get_datahandler(datadir, data_format=convert_to)
tradesdir: Path = config['datadir'] / 'trades_csv' tradesdir: Path = config["datadir"] / "trades_csv"
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
# iterate through directories in this directory # iterate through directories in this directory
data_symbols = {p.stem for p in tradesdir.rglob('*.csv')} data_symbols = {p.stem for p in tradesdir.rglob("*.csv")}
# create pair/filename mapping # create pair/filename mapping
markets = { markets = {
(m['symbol'], m['altname']) for m in exchange.markets.values() (m["symbol"], m["altname"])
if m.get('altname') in data_symbols for m in exchange.markets.values()
if m.get("altname") in data_symbols
} }
logger.info(f"Found csv files for {', '.join(data_symbols)}.") logger.info(f"Found csv files for {', '.join(data_symbols)}.")
if pairs_raw := config.get('pairs'): if pairs_raw := config.get("pairs"):
pairs = expand_pairlist(pairs_raw, [m[0] for m in markets]) pairs = expand_pairlist(pairs_raw, [m[0] for m in markets])
markets = {m for m in markets if m[0] in pairs} markets = {m for m in markets if m[0] in pairs}
if not markets: if not markets:
@@ -66,18 +69,20 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str):
trades = pd.concat(dfs, ignore_index=True) trades = pd.concat(dfs, ignore_index=True)
del dfs del dfs
trades.loc[:, 'timestamp'] = trades['timestamp'] * 1e3 trades.loc[:, "timestamp"] = trades["timestamp"] * 1e3
trades.loc[:, 'cost'] = trades['price'] * trades['amount'] trades.loc[:, "cost"] = trades["price"] * trades["amount"]
for col in DEFAULT_TRADES_COLUMNS: for col in DEFAULT_TRADES_COLUMNS:
if col not in trades.columns: if col not in trades.columns:
trades.loc[:, col] = '' trades.loc[:, col] = ""
trades = trades[DEFAULT_TRADES_COLUMNS] trades = trades[DEFAULT_TRADES_COLUMNS]
trades = trades_convert_types(trades) trades = trades_convert_types(trades)
trades_df = trades_df_remove_duplicates(trades) trades_df = trades_df_remove_duplicates(trades)
del trades del trades
logger.info(f"{pair}: {len(trades_df)} trades, from " logger.info(
f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to " f"{pair}: {len(trades_df)} trades, from "
f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}") f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to "
f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}"
)
data_handler.trades_store(pair, trades_df, TradingMode.SPOT) data_handler.trades_store(pair, trades_df, TradingMode.SPOT)

View File

@@ -4,6 +4,7 @@ Responsible to provide data to the bot
including ticker and orderbook data, live and historical candle (OHLCV) data including ticker and orderbook data, live and historical candle (OHLCV) data
Common Interface for bot and strategy to access data. Common Interface for bot and strategy to access data.
""" """
import logging import logging
from collections import deque from collections import deque
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -12,8 +13,12 @@ from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes, from freqtrade.constants import (
PairWithTimeframe) FULL_DATAFRAME_THRESHOLD,
Config,
ListPairsWithTimeframes,
PairWithTimeframe,
)
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType, RPCMessageType, RunMode from freqtrade.enums import CandleType, RPCMessageType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exceptions import ExchangeError, OperationalException
@@ -27,18 +32,17 @@ from freqtrade.util import PeriodicCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
NO_EXCHANGE_EXCEPTION = 'Exchange is not available to DataProvider.' NO_EXCHANGE_EXCEPTION = "Exchange is not available to DataProvider."
MAX_DATAFRAME_CANDLES = 1000 MAX_DATAFRAME_CANDLES = 1000
class DataProvider: class DataProvider:
def __init__( def __init__(
self, self,
config: Config, config: Config,
exchange: Optional[Exchange], exchange: Optional[Exchange],
pairlists=None, pairlists=None,
rpc: Optional[RPCManager] = None rpc: Optional[RPCManager] = None,
) -> None: ) -> None:
self._config = config self._config = config
self._exchange = exchange self._exchange = exchange
@@ -49,18 +53,20 @@ class DataProvider:
self.__slice_date: Optional[datetime] = None self.__slice_date: Optional[datetime] = None
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
self.__producer_pairs_df: Dict[str, self.__producer_pairs_df: Dict[
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {} str, Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]
] = {}
self.__producer_pairs: Dict[str, List[str]] = {} self.__producer_pairs: Dict[str, List[str]] = {}
self._msg_queue: deque = deque() self._msg_queue: deque = deque()
self._default_candle_type = self._config.get('candle_type_def', CandleType.SPOT) self._default_candle_type = self._config.get("candle_type_def", CandleType.SPOT)
self._default_timeframe = self._config.get('timeframe', '1h') self._default_timeframe = self._config.get("timeframe", "1h")
self.__msg_cache = PeriodicCache( self.__msg_cache = PeriodicCache(
maxsize=1000, ttl=timeframe_to_seconds(self._default_timeframe)) maxsize=1000, ttl=timeframe_to_seconds(self._default_timeframe)
)
self.producers = self._config.get('external_message_consumer', {}).get('producers', []) self.producers = self._config.get("external_message_consumer", {}).get("producers", [])
self.external_data_enabled = len(self.producers) > 0 self.external_data_enabled = len(self.producers) > 0
def _set_dataframe_max_index(self, limit_index: int): def _set_dataframe_max_index(self, limit_index: int):
@@ -80,11 +86,7 @@ class DataProvider:
self.__slice_date = limit_date self.__slice_date = limit_date
def _set_cached_df( def _set_cached_df(
self, self, pair: str, timeframe: str, dataframe: DataFrame, candle_type: CandleType
pair: str,
timeframe: str,
dataframe: DataFrame,
candle_type: CandleType
) -> None: ) -> None:
""" """
Store cached Dataframe. Store cached Dataframe.
@@ -96,8 +98,7 @@ class DataProvider:
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
""" """
pair_key = (pair, timeframe, candle_type) pair_key = (pair, timeframe, candle_type)
self.__cached_pairs[pair_key] = ( self.__cached_pairs[pair_key] = (dataframe, datetime.now(timezone.utc))
dataframe, datetime.now(timezone.utc))
# For multiple producers we will want to merge the pairlists instead of overwriting # 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"): def _set_producer_pairs(self, pairlist: List[str], producer_name: str = "default"):
@@ -116,12 +117,7 @@ class DataProvider:
""" """
return self.__producer_pairs.get(producer_name, []).copy() return self.__producer_pairs.get(producer_name, []).copy()
def _emit_df( def _emit_df(self, pair_key: PairWithTimeframe, dataframe: DataFrame, new_candle: bool) -> None:
self,
pair_key: PairWithTimeframe,
dataframe: DataFrame,
new_candle: bool
) -> None:
""" """
Send this dataframe as an ANALYZED_DF message to RPC Send this dataframe as an ANALYZED_DF message to RPC
@@ -131,19 +127,21 @@ class DataProvider:
""" """
if self.__rpc: if self.__rpc:
msg: RPCAnalyzedDFMsg = { msg: RPCAnalyzedDFMsg = {
'type': RPCMessageType.ANALYZED_DF, "type": RPCMessageType.ANALYZED_DF,
'data': { "data": {
'key': pair_key, "key": pair_key,
'df': dataframe.tail(1), "df": dataframe.tail(1),
'la': datetime.now(timezone.utc) "la": datetime.now(timezone.utc),
} },
} }
self.__rpc.send_msg(msg) self.__rpc.send_msg(msg)
if new_candle: if new_candle:
self.__rpc.send_msg({ self.__rpc.send_msg(
'type': RPCMessageType.NEW_CANDLE, {
'data': pair_key, "type": RPCMessageType.NEW_CANDLE,
}) "data": pair_key,
}
)
def _replace_external_df( def _replace_external_df(
self, self,
@@ -152,7 +150,7 @@ class DataProvider:
last_analyzed: datetime, last_analyzed: datetime,
timeframe: str, timeframe: str,
candle_type: CandleType, candle_type: CandleType,
producer_name: str = "default" producer_name: str = "default",
) -> None: ) -> None:
""" """
Add the pair data to this class from an external source. Add the pair data to this class from an external source.
@@ -178,7 +176,7 @@ class DataProvider:
last_analyzed: datetime, last_analyzed: datetime,
timeframe: str, timeframe: str,
candle_type: CandleType, candle_type: CandleType,
producer_name: str = "default" producer_name: str = "default",
) -> Tuple[bool, int]: ) -> Tuple[bool, int]:
""" """
Append a candle to the existing external dataframe. The incoming dataframe Append a candle to the existing external dataframe. The incoming dataframe
@@ -204,12 +202,14 @@ class DataProvider:
last_analyzed=last_analyzed, last_analyzed=last_analyzed,
timeframe=timeframe, timeframe=timeframe,
candle_type=candle_type, candle_type=candle_type,
producer_name=producer_name producer_name=producer_name,
) )
return (True, 0) return (True, 0)
if (producer_name not in self.__producer_pairs_df if (
or pair_key not in self.__producer_pairs_df[producer_name]): producer_name not in self.__producer_pairs_df
or pair_key not in self.__producer_pairs_df[producer_name]
):
# We don't have data from this producer yet, # We don't have data from this producer yet,
# or we don't have data for this pair_key # or we don't have data for this pair_key
# return False and 1000 for the full df # return False and 1000 for the full df
@@ -220,12 +220,12 @@ class DataProvider:
# CHECK FOR MISSING CANDLES # CHECK FOR MISSING CANDLES
# Convert the timeframe to a timedelta for pandas # Convert the timeframe to a timedelta for pandas
timeframe_delta: Timedelta = to_timedelta(timeframe) timeframe_delta: Timedelta = to_timedelta(timeframe)
local_last: Timestamp = existing_df.iloc[-1]['date'] # We want the last date from our copy local_last: Timestamp = existing_df.iloc[-1]["date"] # We want the last date from our copy
# We want the first date from the incoming # We want the first date from the incoming
incoming_first: Timestamp = dataframe.iloc[0]['date'] incoming_first: Timestamp = dataframe.iloc[0]["date"]
# Remove existing candles that are newer than the incoming first candle # Remove existing candles that are newer than the incoming first candle
existing_df1 = existing_df[existing_df['date'] < incoming_first] existing_df1 = existing_df[existing_df["date"] < incoming_first]
candle_difference = (incoming_first - local_last) / timeframe_delta candle_difference = (incoming_first - local_last) / timeframe_delta
@@ -243,13 +243,13 @@ class DataProvider:
# Everything is good, we appended # Everything is good, we appended
self._replace_external_df( self._replace_external_df(
pair, pair,
appended_df, appended_df,
last_analyzed=last_analyzed, last_analyzed=last_analyzed,
timeframe=timeframe, timeframe=timeframe,
candle_type=candle_type, candle_type=candle_type,
producer_name=producer_name producer_name=producer_name,
) )
return (True, 0) return (True, 0)
def get_producer_df( def get_producer_df(
@@ -257,7 +257,7 @@ class DataProvider:
pair: str, pair: str,
timeframe: Optional[str] = None, timeframe: Optional[str] = None,
candle_type: Optional[CandleType] = None, candle_type: Optional[CandleType] = None,
producer_name: str = "default" producer_name: str = "default",
) -> Tuple[DataFrame, datetime]: ) -> Tuple[DataFrame, datetime]:
""" """
Get the pair data from producers. Get the pair data from producers.
@@ -292,64 +292,64 @@ class DataProvider:
""" """
self._pairlists = pairlists self._pairlists = pairlists
def historic_ohlcv( def historic_ohlcv(self, pair: str, timeframe: str, candle_type: str = "") -> DataFrame:
self,
pair: str,
timeframe: str,
candle_type: str = ''
) -> DataFrame:
""" """
Get stored historical candle (OHLCV) data Get stored historical candle (OHLCV) data
:param pair: pair to get the data for :param pair: pair to get the data for
:param timeframe: timeframe to get data for :param timeframe: timeframe to get data for
:param candle_type: '', mark, index, premiumIndex, or funding_rate :param candle_type: '', mark, index, premiumIndex, or funding_rate
""" """
_candle_type = CandleType.from_string( _candle_type = (
candle_type) if candle_type != '' else self._config['candle_type_def'] CandleType.from_string(candle_type)
if candle_type != ""
else self._config["candle_type_def"]
)
saved_pair: PairWithTimeframe = (pair, str(timeframe), _candle_type) saved_pair: PairWithTimeframe = (pair, str(timeframe), _candle_type)
if saved_pair not in self.__cached_pairs_backtesting: if saved_pair not in self.__cached_pairs_backtesting:
timerange = TimeRange.parse_timerange(None if self._config.get( timerange = TimeRange.parse_timerange(
'timerange') is None else str(self._config.get('timerange'))) None
if self._config.get("timerange") is None
else str(self._config.get("timerange"))
)
startup_candles = self.get_required_startup(str(timeframe)) startup_candles = self.get_required_startup(str(timeframe))
tf_seconds = timeframe_to_seconds(str(timeframe)) tf_seconds = timeframe_to_seconds(str(timeframe))
timerange.subtract_start(tf_seconds * startup_candles) timerange.subtract_start(tf_seconds * startup_candles)
logger.info(f"Loading data for {pair} {timeframe} " logger.info(
f"from {timerange.start_fmt} to {timerange.stop_fmt}") f"Loading data for {pair} {timeframe} "
f"from {timerange.start_fmt} to {timerange.stop_fmt}"
)
self.__cached_pairs_backtesting[saved_pair] = load_pair_history( self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
pair=pair, pair=pair,
timeframe=timeframe, timeframe=timeframe,
datadir=self._config['datadir'], datadir=self._config["datadir"],
timerange=timerange, timerange=timerange,
data_format=self._config['dataformat_ohlcv'], data_format=self._config["dataformat_ohlcv"],
candle_type=_candle_type, candle_type=_candle_type,
) )
return self.__cached_pairs_backtesting[saved_pair].copy() return self.__cached_pairs_backtesting[saved_pair].copy()
def get_required_startup(self, timeframe: str) -> int: def get_required_startup(self, timeframe: str) -> int:
freqai_config = self._config.get('freqai', {}) freqai_config = self._config.get("freqai", {})
if not freqai_config.get('enabled', False): if not freqai_config.get("enabled", False):
return self._config.get('startup_candle_count', 0) return self._config.get("startup_candle_count", 0)
else: else:
startup_candles = self._config.get('startup_candle_count', 0) startup_candles = self._config.get("startup_candle_count", 0)
indicator_periods = freqai_config['feature_parameters']['indicator_periods_candles'] indicator_periods = freqai_config["feature_parameters"]["indicator_periods_candles"]
# make sure the startupcandles is at least the set maximum indicator periods # make sure the startupcandles is at least the set maximum indicator periods
self._config['startup_candle_count'] = max(startup_candles, max(indicator_periods)) self._config["startup_candle_count"] = max(startup_candles, max(indicator_periods))
tf_seconds = timeframe_to_seconds(timeframe) tf_seconds = timeframe_to_seconds(timeframe)
train_candles = freqai_config['train_period_days'] * 86400 / tf_seconds train_candles = freqai_config["train_period_days"] * 86400 / tf_seconds
total_candles = int(self._config['startup_candle_count'] + train_candles) total_candles = int(self._config["startup_candle_count"] + train_candles)
logger.info( logger.info(
f'Increasing startup_candle_count for freqai on {timeframe} to {total_candles}') f"Increasing startup_candle_count for freqai on {timeframe} to {total_candles}"
)
return total_candles return total_candles
def get_pair_dataframe( def get_pair_dataframe(
self, self, pair: str, timeframe: Optional[str] = None, candle_type: str = ""
pair: str,
timeframe: Optional[str] = None,
candle_type: str = ''
) -> DataFrame: ) -> DataFrame:
""" """
Return pair candle (OHLCV) data, either live or cached historical -- depending Return pair candle (OHLCV) data, either live or cached historical -- depending
@@ -366,13 +366,13 @@ class DataProvider:
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type) data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
else: else:
# Get historical OHLCV data (cached on disk). # Get historical OHLCV data (cached on disk).
timeframe = timeframe or self._config['timeframe'] timeframe = timeframe or self._config["timeframe"]
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type) data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
# Cut date to timeframe-specific date. # Cut date to timeframe-specific date.
# This is necessary to prevent lookahead bias in callbacks through informative pairs. # This is necessary to prevent lookahead bias in callbacks through informative pairs.
if self.__slice_date: if self.__slice_date:
cutoff_date = timeframe_to_prev_date(timeframe, self.__slice_date) cutoff_date = timeframe_to_prev_date(timeframe, self.__slice_date)
data = data.loc[data['date'] < cutoff_date] data = data.loc[data["date"] < cutoff_date]
if len(data) == 0: if len(data) == 0:
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).") logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
return data return data
@@ -387,7 +387,7 @@ class DataProvider:
combination. combination.
Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached. Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached.
""" """
pair_key = (pair, timeframe, self._config.get('candle_type_def', CandleType.SPOT)) pair_key = (pair, timeframe, self._config.get("candle_type_def", CandleType.SPOT))
if pair_key in self.__cached_pairs: if pair_key in self.__cached_pairs:
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
df, date = self.__cached_pairs[pair_key] df, date = self.__cached_pairs[pair_key]
@@ -395,7 +395,7 @@ class DataProvider:
df, date = self.__cached_pairs[pair_key] df, date = self.__cached_pairs[pair_key]
if self.__slice_index is not None: if self.__slice_index is not None:
max_index = self.__slice_index max_index = self.__slice_index
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES):max_index] df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
return df, date return df, date
else: else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
@@ -406,7 +406,7 @@ class DataProvider:
Get runmode of the bot Get runmode of the bot
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other". can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
""" """
return RunMode(self._config.get('runmode', RunMode.OTHER)) return RunMode(self._config.get("runmode", RunMode.OTHER))
def current_whitelist(self) -> List[str]: def current_whitelist(self) -> List[str]:
""" """
@@ -434,9 +434,11 @@ class DataProvider:
# Exchange functions # Exchange functions
def refresh(self, def refresh(
pairlist: ListPairsWithTimeframes, self,
helping_pairs: Optional[ListPairsWithTimeframes] = None) -> None: pairlist: ListPairsWithTimeframes,
helping_pairs: Optional[ListPairsWithTimeframes] = None,
) -> None:
""" """
Refresh data, called with each cycle Refresh data, called with each cycle
""" """
@@ -456,11 +458,7 @@ class DataProvider:
return list(self._exchange._klines.keys()) return list(self._exchange._klines.keys())
def ohlcv( def ohlcv(
self, self, pair: str, timeframe: Optional[str] = None, copy: bool = True, candle_type: str = ""
pair: str,
timeframe: Optional[str] = None,
copy: bool = True,
candle_type: str = ''
) -> DataFrame: ) -> DataFrame:
""" """
Get candle (OHLCV) data for the given pair as DataFrame Get candle (OHLCV) data for the given pair as DataFrame
@@ -474,11 +472,13 @@ class DataProvider:
if self._exchange is None: if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION) raise OperationalException(NO_EXCHANGE_EXCEPTION)
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
_candle_type = CandleType.from_string( _candle_type = (
candle_type) if candle_type != '' else self._config['candle_type_def'] CandleType.from_string(candle_type)
if candle_type != ""
else self._config["candle_type_def"]
)
return self._exchange.klines( return self._exchange.klines(
(pair, timeframe or self._config['timeframe'], _candle_type), (pair, timeframe or self._config["timeframe"], _candle_type), copy=copy
copy=copy
) )
else: else:
return DataFrame() return DataFrame()

View File

@@ -8,8 +8,11 @@ from tabulate import tabulate
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import Config from freqtrade.constants import Config
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, from freqtrade.data.btanalysis import (
load_backtest_stats) get_latest_backtest_filename,
load_backtest_data,
load_backtest_stats,
)
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@@ -18,9 +21,10 @@ logger = logging.getLogger(__name__)
def _load_backtest_analysis_data(backtest_dir: Path, name: str): def _load_backtest_analysis_data(backtest_dir: Path, name: str):
if backtest_dir.is_dir(): if backtest_dir.is_dir():
scpf = Path(backtest_dir, scpf = Path(
Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl" backtest_dir,
) Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl",
)
else: else:
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl") scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
@@ -53,7 +57,8 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
for pair in pairlist: for pair in pairlist:
if pair in signal_candles[strategy_name]: if pair in signal_candles[strategy_name]:
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators( analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
pair, trades, signal_candles[strategy_name][pair]) pair, trades, signal_candles[strategy_name][pair]
)
except Exception as e: except Exception as e:
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e) print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
@@ -64,28 +69,28 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
buyf = signal_candles buyf = signal_candles
if len(buyf) > 0: if len(buyf) > 0:
buyf = buyf.set_index('date', drop=False) buyf = buyf.set_index("date", drop=False)
trades_red = trades.loc[trades['pair'] == pair].copy() trades_red = trades.loc[trades["pair"] == pair].copy()
trades_inds = pd.DataFrame() trades_inds = pd.DataFrame()
if trades_red.shape[0] > 0 and buyf.shape[0] > 0: if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
for t, v in trades_red.open_date.items(): for t, v in trades_red.open_date.items():
allinds = buyf.loc[(buyf['date'] < v)] allinds = buyf.loc[(buyf["date"] < v)]
if allinds.shape[0] > 0: if allinds.shape[0] > 0:
tmp_inds = allinds.iloc[[-1]] tmp_inds = allinds.iloc[[-1]]
trades_red.loc[t, 'signal_date'] = tmp_inds['date'].values[0] trades_red.loc[t, "signal_date"] = tmp_inds["date"].values[0]
trades_red.loc[t, 'enter_reason'] = trades_red.loc[t, 'enter_tag'] trades_red.loc[t, "enter_reason"] = trades_red.loc[t, "enter_tag"]
tmp_inds.index.rename('signal_date', inplace=True) tmp_inds.index.rename("signal_date", inplace=True)
trades_inds = pd.concat([trades_inds, tmp_inds]) trades_inds = pd.concat([trades_inds, tmp_inds])
if 'signal_date' in trades_red: if "signal_date" in trades_red:
trades_red['signal_date'] = pd.to_datetime(trades_red['signal_date'], utc=True) trades_red["signal_date"] = pd.to_datetime(trades_red["signal_date"], utc=True)
trades_red.set_index('signal_date', inplace=True) trades_red.set_index("signal_date", inplace=True)
try: try:
trades_red = pd.merge(trades_red, trades_inds, on='signal_date', how='outer') trades_red = pd.merge(trades_red, trades_inds, on="signal_date", how="outer")
except Exception as e: except Exception as e:
raise e raise e
return trades_red return trades_red
@@ -93,138 +98,166 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
return pd.DataFrame() return pd.DataFrame()
def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ): def _do_group_table_output(
bigdf,
glist,
csv_path: Path,
to_csv=False,
):
for g in glist: for g in glist:
# 0: summary wins/losses grouped by enter tag # 0: summary wins/losses grouped by enter tag
if g == "0": if g == "0":
group_mask = ['enter_reason'] group_mask = ["enter_reason"]
wins = bigdf.loc[bigdf['profit_abs'] >= 0] \ wins = (
.groupby(group_mask) \ bigdf.loc[bigdf["profit_abs"] >= 0].groupby(group_mask).agg({"profit_abs": ["sum"]})
.agg({'profit_abs': ['sum']}) )
wins.columns = ['profit_abs_wins'] wins.columns = ["profit_abs_wins"]
loss = bigdf.loc[bigdf['profit_abs'] < 0] \ loss = (
.groupby(group_mask) \ bigdf.loc[bigdf["profit_abs"] < 0].groupby(group_mask).agg({"profit_abs": ["sum"]})
.agg({'profit_abs': ['sum']}) )
loss.columns = ['profit_abs_loss'] loss.columns = ["profit_abs_loss"]
new = bigdf.groupby(group_mask).agg({'profit_abs': [ new = bigdf.groupby(group_mask).agg(
'count', {"profit_abs": ["count", lambda x: sum(x > 0), lambda x: sum(x <= 0)]}
lambda x: sum(x > 0), )
lambda x: sum(x <= 0)]})
new = pd.concat([new, wins, loss], axis=1).fillna(0) new = pd.concat([new, wins, loss], axis=1).fillna(0)
new['profit_tot'] = new['profit_abs_wins'] - abs(new['profit_abs_loss']) new["profit_tot"] = new["profit_abs_wins"] - abs(new["profit_abs_loss"])
new['wl_ratio_pct'] = (new.iloc[:, 1] / new.iloc[:, 0] * 100).fillna(0) new["wl_ratio_pct"] = (new.iloc[:, 1] / new.iloc[:, 0] * 100).fillna(0)
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0) new["avg_win"] = (new["profit_abs_wins"] / new.iloc[:, 1]).fillna(0)
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0) new["avg_loss"] = (new["profit_abs_loss"] / new.iloc[:, 2]).fillna(0)
new['exp_ratio'] = ( new["exp_ratio"] = (
( ((1 + (new["avg_win"] / abs(new["avg_loss"]))) * (new["wl_ratio_pct"] / 100)) - 1
(1 + (new['avg_win'] / abs(new['avg_loss']))) * (new['wl_ratio_pct'] / 100) ).fillna(0)
) - 1).fillna(0)
new.columns = ['total_num_buys', 'wins', 'losses', new.columns = [
'profit_abs_wins', 'profit_abs_loss', "total_num_buys",
'profit_tot', 'wl_ratio_pct', "wins",
'avg_win', 'avg_loss', 'exp_ratio'] "losses",
"profit_abs_wins",
"profit_abs_loss",
"profit_tot",
"wl_ratio_pct",
"avg_win",
"avg_loss",
"exp_ratio",
]
sortcols = ['total_num_buys'] sortcols = ["total_num_buys"]
_print_table(new, sortcols, show_index=True, name="Group 0:", _print_table(
to_csv=to_csv, csv_path=csv_path) new, sortcols, show_index=True, name="Group 0:", to_csv=to_csv, csv_path=csv_path
)
else: else:
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'], agg_mask = {
'profit_ratio': ['median', 'mean', 'sum']} "profit_abs": ["count", "sum", "median", "mean"],
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median', "profit_ratio": ["median", "mean", "sum"],
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct', }
'total_profit_pct'] agg_cols = [
sortcols = ['profit_abs_sum', 'enter_reason'] "num_buys",
"profit_abs_sum",
"profit_abs_median",
"profit_abs_mean",
"median_profit_pct",
"mean_profit_pct",
"total_profit_pct",
]
sortcols = ["profit_abs_sum", "enter_reason"]
# 1: profit summaries grouped by enter_tag # 1: profit summaries grouped by enter_tag
if g == "1": if g == "1":
group_mask = ['enter_reason'] group_mask = ["enter_reason"]
# 2: profit summaries grouped by enter_tag and exit_tag # 2: profit summaries grouped by enter_tag and exit_tag
if g == "2": if g == "2":
group_mask = ['enter_reason', 'exit_reason'] group_mask = ["enter_reason", "exit_reason"]
# 3: profit summaries grouped by pair and enter_tag # 3: profit summaries grouped by pair and enter_tag
if g == "3": if g == "3":
group_mask = ['pair', 'enter_reason'] group_mask = ["pair", "enter_reason"]
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large) # 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
if g == "4": if g == "4":
group_mask = ['pair', 'enter_reason', 'exit_reason'] group_mask = ["pair", "enter_reason", "exit_reason"]
# 5: profit summaries grouped by exit_tag # 5: profit summaries grouped by exit_tag
if g == "5": if g == "5":
group_mask = ['exit_reason'] group_mask = ["exit_reason"]
sortcols = ['exit_reason'] sortcols = ["exit_reason"]
if group_mask: if group_mask:
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index() new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
new.columns = group_mask + agg_cols new.columns = group_mask + agg_cols
new['median_profit_pct'] = new['median_profit_pct'] * 100 new["median_profit_pct"] = new["median_profit_pct"] * 100
new['mean_profit_pct'] = new['mean_profit_pct'] * 100 new["mean_profit_pct"] = new["mean_profit_pct"] * 100
new['total_profit_pct'] = new['total_profit_pct'] * 100 new["total_profit_pct"] = new["total_profit_pct"] * 100
_print_table(new, sortcols, name=f"Group {g}:", _print_table(new, sortcols, name=f"Group {g}:", to_csv=to_csv, csv_path=csv_path)
to_csv=to_csv, csv_path=csv_path)
else: else:
logger.warning("Invalid group mask specified.") logger.warning("Invalid group mask specified.")
def _do_rejected_signals_output(rejected_signals_df: pd.DataFrame, def _do_rejected_signals_output(
to_csv: bool = False, csv_path=None) -> None: rejected_signals_df: pd.DataFrame, to_csv: bool = False, csv_path=None
cols = ['pair', 'date', 'enter_tag'] ) -> None:
sortcols = ['date', 'pair', 'enter_tag'] cols = ["pair", "date", "enter_tag"]
_print_table(rejected_signals_df[cols], sortcols = ["date", "pair", "enter_tag"]
sortcols, _print_table(
show_index=False, rejected_signals_df[cols],
name="Rejected Signals:", sortcols,
to_csv=to_csv, show_index=False,
csv_path=csv_path) name="Rejected Signals:",
to_csv=to_csv,
csv_path=csv_path,
)
def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'): def _select_rows_within_dates(df, timerange=None, df_date_col: str = "date"):
if timerange: if timerange:
if timerange.starttype == 'date': if timerange.starttype == "date":
df = df.loc[(df[df_date_col] >= timerange.startdt)] df = df.loc[(df[df_date_col] >= timerange.startdt)]
if timerange.stoptype == 'date': if timerange.stoptype == "date":
df = df.loc[(df[df_date_col] < timerange.stopdt)] df = df.loc[(df[df_date_col] < timerange.stopdt)]
return df return df
def _select_rows_by_tags(df, enter_reason_list, exit_reason_list): def _select_rows_by_tags(df, enter_reason_list, exit_reason_list):
if enter_reason_list and "all" not in enter_reason_list: if enter_reason_list and "all" not in enter_reason_list:
df = df.loc[(df['enter_reason'].isin(enter_reason_list))] df = df.loc[(df["enter_reason"].isin(enter_reason_list))]
if exit_reason_list and "all" not in exit_reason_list: if exit_reason_list and "all" not in exit_reason_list:
df = df.loc[(df['exit_reason'].isin(exit_reason_list))] df = df.loc[(df["exit_reason"].isin(exit_reason_list))]
return df return df
def prepare_results(analysed_trades, stratname, def prepare_results(
enter_reason_list, exit_reason_list, analysed_trades, stratname, enter_reason_list, exit_reason_list, timerange=None
timerange=None): ):
res_df = pd.DataFrame() res_df = pd.DataFrame()
for pair, trades in analysed_trades[stratname].items(): for pair, trades in analysed_trades[stratname].items():
if (trades.shape[0] > 0): if trades.shape[0] > 0:
trades.dropna(subset=['close_date'], inplace=True) trades.dropna(subset=["close_date"], inplace=True)
res_df = pd.concat([res_df, trades], ignore_index=True) res_df = pd.concat([res_df, trades], ignore_index=True)
res_df = _select_rows_within_dates(res_df, timerange) res_df = _select_rows_within_dates(res_df, timerange)
if res_df is not None and res_df.shape[0] > 0 and ('enter_reason' in res_df.columns): if res_df is not None and res_df.shape[0] > 0 and ("enter_reason" in res_df.columns):
res_df = _select_rows_by_tags(res_df, enter_reason_list, exit_reason_list) res_df = _select_rows_by_tags(res_df, enter_reason_list, exit_reason_list)
return res_df return res_df
def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_list: List[str], def print_results(
csv_path: Path, rejected_signals=None, to_csv=False): res_df: pd.DataFrame,
analysis_groups: List[str],
indicator_list: List[str],
csv_path: Path,
rejected_signals=None,
to_csv=False,
):
if res_df.shape[0] > 0: if res_df.shape[0] > 0:
if analysis_groups: if analysis_groups:
_do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path) _do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path)
@@ -237,30 +270,31 @@ def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_li
# NB this can be large for big dataframes! # NB this can be large for big dataframes!
if "all" in indicator_list: if "all" in indicator_list:
_print_table(res_df, _print_table(
show_index=False, res_df, show_index=False, name="Indicators:", to_csv=to_csv, csv_path=csv_path
name="Indicators:", )
to_csv=to_csv,
csv_path=csv_path)
elif indicator_list is not None and indicator_list: elif indicator_list is not None and indicator_list:
available_inds = [] available_inds = []
for ind in indicator_list: for ind in indicator_list:
if ind in res_df: if ind in res_df:
available_inds.append(ind) available_inds.append(ind)
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
_print_table(res_df[ilist], _print_table(
sortcols=['exit_reason'], res_df[ilist],
show_index=False, sortcols=["exit_reason"],
name="Indicators:", show_index=False,
to_csv=to_csv, name="Indicators:",
csv_path=csv_path) to_csv=to_csv,
csv_path=csv_path,
)
else: else:
print("\\No trades to show") print("\\No trades to show")
def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None, def _print_table(
to_csv=False, csv_path: Path): df: pd.DataFrame, sortcols=None, *, show_index=False, name=None, to_csv=False, csv_path: Path
if (sortcols is not None): ):
if sortcols is not None:
data = df.sort_values(sortcols) data = df.sort_values(sortcols)
else: else:
data = df data = df
@@ -273,60 +307,64 @@ def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None
if name is not None: if name is not None:
print(name) print(name)
print( print(tabulate(data, headers="keys", tablefmt="psql", showindex=show_index))
tabulate(
data,
headers='keys',
tablefmt='psql',
showindex=show_index
)
)
def process_entry_exit_reasons(config: Config): def process_entry_exit_reasons(config: Config):
try: try:
analysis_groups = config.get('analysis_groups', []) analysis_groups = config.get("analysis_groups", [])
enter_reason_list = config.get('enter_reason_list', ["all"]) enter_reason_list = config.get("enter_reason_list", ["all"])
exit_reason_list = config.get('exit_reason_list', ["all"]) exit_reason_list = config.get("exit_reason_list", ["all"])
indicator_list = config.get('indicator_list', []) indicator_list = config.get("indicator_list", [])
do_rejected = config.get('analysis_rejected', False) do_rejected = config.get("analysis_rejected", False)
to_csv = config.get('analysis_to_csv', False) to_csv = config.get("analysis_to_csv", False)
csv_path = Path(config.get('analysis_csv_path', config['exportfilename'])) csv_path = Path(config.get("analysis_csv_path", config["exportfilename"]))
if to_csv and not csv_path.is_dir(): if to_csv and not csv_path.is_dir():
raise OperationalException(f"Specified directory {csv_path} does not exist.") raise OperationalException(f"Specified directory {csv_path} does not exist.")
timerange = TimeRange.parse_timerange(None if config.get( timerange = TimeRange.parse_timerange(
'timerange') is None else str(config.get('timerange'))) None if config.get("timerange") is None else str(config.get("timerange"))
)
backtest_stats = load_backtest_stats(config['exportfilename']) backtest_stats = load_backtest_stats(config["exportfilename"])
for strategy_name, results in backtest_stats['strategy'].items(): for strategy_name, results in backtest_stats["strategy"].items():
trades = load_backtest_data(config['exportfilename'], strategy_name) trades = load_backtest_data(config["exportfilename"], strategy_name)
if trades is not None and not trades.empty: if trades is not None and not trades.empty:
signal_candles = _load_signal_candles(config['exportfilename']) signal_candles = _load_signal_candles(config["exportfilename"])
rej_df = None rej_df = None
if do_rejected: if do_rejected:
rejected_signals_dict = _load_rejected_signals(config['exportfilename']) rejected_signals_dict = _load_rejected_signals(config["exportfilename"])
rej_df = prepare_results(rejected_signals_dict, strategy_name, rej_df = prepare_results(
enter_reason_list, exit_reason_list, rejected_signals_dict,
timerange=timerange) strategy_name,
enter_reason_list,
exit_reason_list,
timerange=timerange,
)
analysed_trades_dict = _process_candles_and_indicators( analysed_trades_dict = _process_candles_and_indicators(
config['exchange']['pair_whitelist'], strategy_name, config["exchange"]["pair_whitelist"], strategy_name, trades, signal_candles
trades, signal_candles) )
res_df = prepare_results(analysed_trades_dict, strategy_name, res_df = prepare_results(
enter_reason_list, exit_reason_list, analysed_trades_dict,
timerange=timerange) strategy_name,
enter_reason_list,
exit_reason_list,
timerange=timerange,
)
print_results(res_df, print_results(
analysis_groups, res_df,
indicator_list, analysis_groups,
rejected_signals=rej_df, indicator_list,
to_csv=to_csv, rejected_signals=rej_df,
csv_path=csv_path) to_csv=to_csv,
csv_path=csv_path,
)
except ValueError as e: except ValueError as e:
raise OperationalException(e) from e raise OperationalException(e) from e

View File

@@ -5,8 +5,17 @@ Includes:
* load data for a pair (or a list of pairs) from disk * load data for a pair (or a list of pairs) from disk
* download data from exchange and store to disk * download data from exchange and store to disk
""" """
# flake8: noqa: F401 # flake8: noqa: F401
from .datahandlers import get_datahandler from .datahandlers import get_datahandler
from .history_utils import (convert_trades_to_ohlcv, download_data_main, get_timerange, load_data, from .history_utils import (
load_pair_history, refresh_backtest_ohlcv_data, convert_trades_to_ohlcv,
refresh_backtest_trades_data, refresh_data, validate_backtest_data) download_data_main,
get_timerange,
load_data,
load_pair_history,
refresh_backtest_ohlcv_data,
refresh_backtest_trades_data,
refresh_data,
validate_backtest_data,
)

View File

@@ -14,11 +14,11 @@ logger = logging.getLogger(__name__)
class FeatherDataHandler(IDataHandler): class FeatherDataHandler(IDataHandler):
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
def ohlcv_store( def ohlcv_store(
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None: self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
) -> None:
""" """
Store data in json format "values". Store data in json format "values".
format looks as follows: format looks as follows:
@@ -33,11 +33,12 @@ class FeatherDataHandler(IDataHandler):
self.create_dir_if_needed(filename) self.create_dir_if_needed(filename)
data.reset_index(drop=True).loc[:, self._columns].to_feather( data.reset_index(drop=True).loc[:, self._columns].to_feather(
filename, compression_level=9, compression='lz4') filename, compression_level=9, compression="lz4"
)
def _ohlcv_load(self, pair: str, timeframe: str, def _ohlcv_load(
timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
) -> DataFrame: ) -> DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
Implements the loading and conversion to a Pandas dataframe. Implements the loading and conversion to a Pandas dataframe.
@@ -50,28 +51,31 @@ class FeatherDataHandler(IDataHandler):
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame :return: DataFrame with ohlcv data, or empty DataFrame
""" """
filename = self._pair_data_filename( filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
self._datadir, pair, timeframe, candle_type=candle_type)
if not filename.exists(): if not filename.exists():
# Fallback mode for 1M files # Fallback mode for 1M files
filename = self._pair_data_filename( filename = self._pair_data_filename(
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True) self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True
)
if not filename.exists(): if not filename.exists():
return DataFrame(columns=self._columns) return DataFrame(columns=self._columns)
pairdata = read_feather(filename) pairdata = read_feather(filename)
pairdata.columns = self._columns pairdata.columns = self._columns
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', pairdata = pairdata.astype(
'low': 'float', 'close': 'float', 'volume': 'float'}) dtype={
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) "open": "float",
"high": "float",
"low": "float",
"close": "float",
"volume": "float",
}
)
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
return pairdata return pairdata
def ohlcv_append( def ohlcv_append(
self, self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
pair: str,
timeframe: str,
data: DataFrame,
candle_type: CandleType
) -> None: ) -> None:
""" """
Append data to existing data structures Append data to existing data structures
@@ -92,7 +96,7 @@ class FeatherDataHandler(IDataHandler):
""" """
filename = self._pair_trades_filename(self._datadir, pair, trading_mode) filename = self._pair_trades_filename(self._datadir, pair, trading_mode)
self.create_dir_if_needed(filename) self.create_dir_if_needed(filename)
data.reset_index(drop=True).to_feather(filename, compression_level=9, compression='lz4') data.reset_index(drop=True).to_feather(filename, compression_level=9, compression="lz4")
def trades_append(self, pair: str, data: DataFrame): def trades_append(self, pair: str, data: DataFrame):
""" """
@@ -104,7 +108,7 @@ class FeatherDataHandler(IDataHandler):
raise NotImplementedError() raise NotImplementedError()
def _trades_load( def _trades_load(
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
) -> DataFrame: ) -> DataFrame:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json

View File

@@ -15,11 +15,11 @@ logger = logging.getLogger(__name__)
class HDF5DataHandler(IDataHandler): class HDF5DataHandler(IDataHandler):
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
def ohlcv_store( def ohlcv_store(
self, pair: str, timeframe: str, data: pd.DataFrame, candle_type: CandleType) -> None: self, pair: str, timeframe: str, data: pd.DataFrame, candle_type: CandleType
) -> None:
""" """
Store data in hdf5 file. Store data in hdf5 file.
:param pair: Pair - used to generate filename :param pair: Pair - used to generate filename
@@ -35,13 +35,18 @@ class HDF5DataHandler(IDataHandler):
self.create_dir_if_needed(filename) self.create_dir_if_needed(filename)
_data.loc[:, self._columns].to_hdf( _data.loc[:, self._columns].to_hdf(
filename, key=key, mode='a', complevel=9, complib='blosc', filename,
format='table', data_columns=['date'] key=key,
mode="a",
complevel=9,
complib="blosc",
format="table",
data_columns=["date"],
) )
def _ohlcv_load(self, pair: str, timeframe: str, def _ohlcv_load(
timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
Implements the loading and conversion to a Pandas dataframe. Implements the loading and conversion to a Pandas dataframe.
@@ -55,41 +60,40 @@ class HDF5DataHandler(IDataHandler):
:return: DataFrame with ohlcv data, or empty DataFrame :return: DataFrame with ohlcv data, or empty DataFrame
""" """
key = self._pair_ohlcv_key(pair, timeframe) key = self._pair_ohlcv_key(pair, timeframe)
filename = self._pair_data_filename( filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
self._datadir,
pair,
timeframe,
candle_type=candle_type
)
if not filename.exists(): if not filename.exists():
# Fallback mode for 1M files # Fallback mode for 1M files
filename = self._pair_data_filename( filename = self._pair_data_filename(
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True) self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True
)
if not filename.exists(): if not filename.exists():
return pd.DataFrame(columns=self._columns) return pd.DataFrame(columns=self._columns)
where = [] where = []
if timerange: if timerange:
if timerange.starttype == 'date': if timerange.starttype == "date":
where.append(f"date >= Timestamp({timerange.startts * 1e9})") where.append(f"date >= Timestamp({timerange.startts * 1e9})")
if timerange.stoptype == 'date': if timerange.stoptype == "date":
where.append(f"date <= Timestamp({timerange.stopts * 1e9})") where.append(f"date <= Timestamp({timerange.stopts * 1e9})")
pairdata = pd.read_hdf(filename, key=key, mode="r", where=where) pairdata = pd.read_hdf(filename, key=key, mode="r", where=where)
if list(pairdata.columns) != self._columns: if list(pairdata.columns) != self._columns:
raise ValueError("Wrong dataframe format") raise ValueError("Wrong dataframe format")
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', pairdata = pairdata.astype(
'low': 'float', 'close': 'float', 'volume': 'float'}) dtype={
"open": "float",
"high": "float",
"low": "float",
"close": "float",
"volume": "float",
}
)
pairdata = pairdata.reset_index(drop=True) pairdata = pairdata.reset_index(drop=True)
return pairdata return pairdata
def ohlcv_append( def ohlcv_append(
self, self, pair: str, timeframe: str, data: pd.DataFrame, candle_type: CandleType
pair: str,
timeframe: str,
data: pd.DataFrame,
candle_type: CandleType
) -> None: ) -> None:
""" """
Append data to existing data structures Append data to existing data structures
@@ -111,9 +115,13 @@ class HDF5DataHandler(IDataHandler):
key = self._pair_trades_key(pair) key = self._pair_trades_key(pair)
data.to_hdf( data.to_hdf(
self._pair_trades_filename(self._datadir, pair, trading_mode), key=key, self._pair_trades_filename(self._datadir, pair, trading_mode),
mode='a', complevel=9, complib='blosc', key=key,
format='table', data_columns=['timestamp'] mode="a",
complevel=9,
complib="blosc",
format="table",
data_columns=["timestamp"],
) )
def trades_append(self, pair: str, data: pd.DataFrame): def trades_append(self, pair: str, data: pd.DataFrame):
@@ -142,13 +150,13 @@ class HDF5DataHandler(IDataHandler):
return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS) return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS)
where = [] where = []
if timerange: if timerange:
if timerange.starttype == 'date': if timerange.starttype == "date":
where.append(f"timestamp >= {timerange.startts * 1e3}") where.append(f"timestamp >= {timerange.startts * 1e3}")
if timerange.stoptype == 'date': if timerange.stoptype == "date":
where.append(f"timestamp < {timerange.stopts * 1e3}") where.append(f"timestamp < {timerange.stopts * 1e3}")
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where) trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None}) trades[["id", "type"]] = trades[["id", "type"]].replace({np.nan: None})
return trades return trades
@classmethod @classmethod
@@ -158,7 +166,7 @@ class HDF5DataHandler(IDataHandler):
@classmethod @classmethod
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str: def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
# Escape futures pairs to avoid warnings # Escape futures pairs to avoid warnings
pair_esc = pair.replace(':', '_') pair_esc = pair.replace(":", "_")
return f"{pair_esc}/ohlcv/tf_{timeframe}" return f"{pair_esc}/ohlcv/tf_{timeframe}"
@classmethod @classmethod

View File

@@ -3,6 +3,7 @@ Abstract datahandler interface.
It's subclasses handle and storing data from disk. It's subclasses handle and storing data from disk.
""" """
import logging import logging
import re import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@@ -16,8 +17,12 @@ from pandas import DataFrame
from freqtrade import misc from freqtrade import misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes from freqtrade.constants import DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes
from freqtrade.data.converter import (clean_ohlcv_dataframe, trades_convert_types, from freqtrade.data.converter import (
trades_df_remove_duplicates, trim_dataframe) clean_ohlcv_dataframe,
trades_convert_types,
trades_df_remove_duplicates,
trim_dataframe,
)
from freqtrade.enums import CandleType, TradingMode from freqtrade.enums import CandleType, TradingMode
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
@@ -26,8 +31,7 @@ logger = logging.getLogger(__name__)
class IDataHandler(ABC): class IDataHandler(ABC):
_OHLCV_REGEX = r"^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
_OHLCV_REGEX = r'^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)'
def __init__(self, datadir: Path) -> None: def __init__(self, datadir: Path) -> None:
self._datadir = datadir self._datadir = datadir
@@ -41,7 +45,8 @@ class IDataHandler(ABC):
@classmethod @classmethod
def ohlcv_get_available_data( def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes: cls, datadir: Path, trading_mode: TradingMode
) -> ListPairsWithTimeframes:
""" """
Returns a list of all pairs with ohlcv data available in this datadir Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files :param datadir: Directory to search for ohlcv files
@@ -49,17 +54,20 @@ class IDataHandler(ABC):
:return: List of Tuples of (pair, timeframe, CandleType) :return: List of Tuples of (pair, timeframe, CandleType)
""" """
if trading_mode == TradingMode.FUTURES: if trading_mode == TradingMode.FUTURES:
datadir = datadir.joinpath('futures') datadir = datadir.joinpath("futures")
_tmp = [ _tmp = [
re.search( re.search(cls._OHLCV_REGEX, p.name)
cls._OHLCV_REGEX, p.name for p in datadir.glob(f"*.{cls._get_file_extension()}")
) for p in datadir.glob(f"*.{cls._get_file_extension()}")] ]
return [ return [
( (
cls.rebuild_pair_from_filename(match[1]), cls.rebuild_pair_from_filename(match[1]),
cls.rebuild_timeframe_from_filename(match[2]), cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3]) CandleType.from_string(match[3]),
) for match in _tmp if match and len(match.groups()) > 1] )
for match in _tmp
if match and len(match.groups()) > 1
]
@classmethod @classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]: def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
@@ -73,17 +81,20 @@ class IDataHandler(ABC):
""" """
candle = "" candle = ""
if candle_type != CandleType.SPOT: if candle_type != CandleType.SPOT:
datadir = datadir.joinpath('futures') datadir = datadir.joinpath("futures")
candle = f"-{candle_type}" candle = f"-{candle_type}"
ext = cls._get_file_extension() ext = cls._get_file_extension()
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + candle + f'.{ext})', p.name) _tmp = [
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")] re.search(r"^(\S+)(?=\-" + timeframe + candle + f".{ext})", p.name)
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")
]
# Check if regex found something and only return these results # Check if regex found something and only return these results
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match] return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
@abstractmethod @abstractmethod
def ohlcv_store( def ohlcv_store(
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None: self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
) -> None:
""" """
Store ohlcv data. Store ohlcv data.
:param pair: Pair - used to generate filename :param pair: Pair - used to generate filename
@@ -93,8 +104,9 @@ class IDataHandler(ABC):
:return: None :return: None
""" """
def ohlcv_data_min_max(self, pair: str, timeframe: str, def ohlcv_data_min_max(
candle_type: CandleType) -> Tuple[datetime, datetime, int]: self, pair: str, timeframe: str, candle_type: CandleType
) -> Tuple[datetime, datetime, int]:
""" """
Returns the min and max timestamp for the given pair and timeframe. Returns the min and max timestamp for the given pair and timeframe.
:param pair: Pair to get min/max for :param pair: Pair to get min/max for
@@ -109,12 +121,12 @@ class IDataHandler(ABC):
datetime.fromtimestamp(0, tz=timezone.utc), datetime.fromtimestamp(0, tz=timezone.utc),
0, 0,
) )
return df.iloc[0]['date'].to_pydatetime(), df.iloc[-1]['date'].to_pydatetime(), len(df) return df.iloc[0]["date"].to_pydatetime(), df.iloc[-1]["date"].to_pydatetime(), len(df)
@abstractmethod @abstractmethod
def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange], def _ohlcv_load(
candle_type: CandleType self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
) -> DataFrame: ) -> DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
Implements the loading and conversion to a Pandas dataframe. Implements the loading and conversion to a Pandas dataframe.
@@ -144,11 +156,7 @@ class IDataHandler(ABC):
@abstractmethod @abstractmethod
def ohlcv_append( def ohlcv_append(
self, self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
pair: str,
timeframe: str,
data: DataFrame,
candle_type: CandleType
) -> None: ) -> None:
""" """
Append data to existing data structures Append data to existing data structures
@@ -166,8 +174,10 @@ class IDataHandler(ABC):
:return: List of Pairs :return: List of Pairs
""" """
_ext = cls._get_file_extension() _ext = cls._get_file_extension()
_tmp = [re.search(r'^(\S+)(?=\-trades.' + _ext + ')', p.name) _tmp = [
for p in datadir.glob(f"*trades.{_ext}")] re.search(r"^(\S+)(?=\-trades." + _ext + ")", p.name)
for p in datadir.glob(f"*trades.{_ext}")
]
# Check if regex found something and only return these results to avoid exceptions. # Check if regex found something and only return these results to avoid exceptions.
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match] return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
@@ -227,7 +237,7 @@ class IDataHandler(ABC):
return False return False
def trades_load( def trades_load(
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
) -> DataFrame: ) -> DataFrame:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
@@ -260,7 +270,7 @@ class IDataHandler(ABC):
pair: str, pair: str,
timeframe: str, timeframe: str,
candle_type: CandleType, candle_type: CandleType,
no_timeframe_modify: bool = False no_timeframe_modify: bool = False,
) -> Path: ) -> Path:
pair_s = misc.pair_to_filename(pair) pair_s = misc.pair_to_filename(pair)
candle = "" candle = ""
@@ -268,10 +278,9 @@ class IDataHandler(ABC):
timeframe = cls.timeframe_to_file(timeframe) timeframe = cls.timeframe_to_file(timeframe)
if candle_type != CandleType.SPOT: if candle_type != CandleType.SPOT:
datadir = datadir.joinpath('futures') datadir = datadir.joinpath("futures")
candle = f"-{candle_type}" candle = f"-{candle_type}"
filename = datadir.joinpath( filename = datadir.joinpath(f"{pair_s}-{timeframe}{candle}.{cls._get_file_extension()}")
f'{pair_s}-{timeframe}{candle}.{cls._get_file_extension()}')
return filename return filename
@classmethod @classmethod
@@ -279,14 +288,14 @@ class IDataHandler(ABC):
pair_s = misc.pair_to_filename(pair) pair_s = misc.pair_to_filename(pair)
if trading_mode == TradingMode.FUTURES: if trading_mode == TradingMode.FUTURES:
# Futures pair ... # Futures pair ...
datadir = datadir.joinpath('futures') datadir = datadir.joinpath("futures")
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}') filename = datadir.joinpath(f"{pair_s}-trades.{cls._get_file_extension()}")
return filename return filename
@staticmethod @staticmethod
def timeframe_to_file(timeframe: str): def timeframe_to_file(timeframe: str):
return timeframe.replace('M', 'Mo') return timeframe.replace("M", "Mo")
@staticmethod @staticmethod
def rebuild_timeframe_from_filename(timeframe: str) -> str: def rebuild_timeframe_from_filename(timeframe: str) -> str:
@@ -294,7 +303,7 @@ class IDataHandler(ABC):
converts timeframe from disk to file converts timeframe from disk to file
Replaces mo with M (to avoid problems on case-insensitive filesystems) Replaces mo with M (to avoid problems on case-insensitive filesystems)
""" """
return re.sub('1mo', '1M', timeframe, flags=re.IGNORECASE) return re.sub("1mo", "1M", timeframe, flags=re.IGNORECASE)
@staticmethod @staticmethod
def rebuild_pair_from_filename(pair: str) -> str: def rebuild_pair_from_filename(pair: str) -> str:
@@ -302,18 +311,22 @@ class IDataHandler(ABC):
Rebuild pair name from filename Rebuild pair name from filename
Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names. Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names.
""" """
res = re.sub(r'^(([A-Za-z\d]{1,10})|^([A-Za-z\-]{1,6}))(_)', r'\g<1>/', pair, count=1) res = re.sub(r"^(([A-Za-z\d]{1,10})|^([A-Za-z\-]{1,6}))(_)", r"\g<1>/", pair, count=1)
res = re.sub('_', ':', res, count=1) res = re.sub("_", ":", res, count=1)
return res return res
def ohlcv_load(self, pair, timeframe: str, def ohlcv_load(
candle_type: CandleType, *, self,
timerange: Optional[TimeRange] = None, pair,
fill_missing: bool = True, timeframe: str,
drop_incomplete: bool = False, candle_type: CandleType,
startup_candles: int = 0, *,
warn_no_data: bool = True, timerange: Optional[TimeRange] = None,
) -> DataFrame: fill_missing: bool = True,
drop_incomplete: bool = False,
startup_candles: int = 0,
warn_no_data: bool = True,
) -> DataFrame:
""" """
Load cached candle (OHLCV) data for the given pair. Load cached candle (OHLCV) data for the given pair.
@@ -333,15 +346,12 @@ class IDataHandler(ABC):
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
pairdf = self._ohlcv_load( pairdf = self._ohlcv_load(
pair, pair, timeframe, timerange=timerange_startup, candle_type=candle_type
timeframe,
timerange=timerange_startup,
candle_type=candle_type
) )
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
return pairdf return pairdf
else: else:
enddate = pairdf.iloc[-1]['date'] enddate = pairdf.iloc[-1]["date"]
if timerange_startup: if timerange_startup:
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup) self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
@@ -350,17 +360,25 @@ class IDataHandler(ABC):
return pairdf return pairdf
# incomplete candles should only be dropped if we didn't trim the end beforehand. # incomplete candles should only be dropped if we didn't trim the end beforehand.
pairdf = clean_ohlcv_dataframe(pairdf, timeframe, pairdf = clean_ohlcv_dataframe(
pair=pair, pairdf,
fill_missing=fill_missing, timeframe,
drop_incomplete=(drop_incomplete and pair=pair,
enddate == pairdf.iloc[-1]['date'])) fill_missing=fill_missing,
drop_incomplete=(drop_incomplete and enddate == pairdf.iloc[-1]["date"]),
)
self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data) self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data)
return pairdf return pairdf
def _check_empty_df( def _check_empty_df(
self, pairdf: DataFrame, pair: str, timeframe: str, candle_type: CandleType, self,
warn_no_data: bool, warn_price: bool = False) -> bool: pairdf: DataFrame,
pair: str,
timeframe: str,
candle_type: CandleType,
warn_no_data: bool,
warn_price: bool = False,
) -> bool:
""" """
Warn on empty dataframe Warn on empty dataframe
""" """
@@ -373,39 +391,55 @@ class IDataHandler(ABC):
return True return True
elif warn_price: elif warn_price:
candle_price_gap = 0 candle_price_gap = 0
if (candle_type in (CandleType.SPOT, CandleType.FUTURES) and if (
not pairdf.empty candle_type in (CandleType.SPOT, CandleType.FUTURES)
and 'close' in pairdf.columns and 'open' in pairdf.columns): and not pairdf.empty
and "close" in pairdf.columns
and "open" in pairdf.columns
):
# Detect gaps between prior close and open # Detect gaps between prior close and open
gaps = ((pairdf['open'] - pairdf['close'].shift(1)) / pairdf['close'].shift(1)) gaps = (pairdf["open"] - pairdf["close"].shift(1)) / pairdf["close"].shift(1)
gaps = gaps.dropna() gaps = gaps.dropna()
if len(gaps): if len(gaps):
candle_price_gap = max(abs(gaps)) candle_price_gap = max(abs(gaps))
if candle_price_gap > 0.1: if candle_price_gap > 0.1:
logger.info(f"Price jump in {pair}, {timeframe}, {candle_type} between two candles " logger.info(
f"of {candle_price_gap:.2%} detected.") f"Price jump in {pair}, {timeframe}, {candle_type} between two candles "
f"of {candle_price_gap:.2%} detected."
)
return False return False
def _validate_pairdata(self, pair, pairdata: DataFrame, timeframe: str, def _validate_pairdata(
candle_type: CandleType, timerange: TimeRange): self,
pair,
pairdata: DataFrame,
timeframe: str,
candle_type: CandleType,
timerange: TimeRange,
):
""" """
Validates pairdata for missing data at start end end and logs warnings. Validates pairdata for missing data at start end end and logs warnings.
:param pairdata: Dataframe to validate :param pairdata: Dataframe to validate
:param timerange: Timerange specified for start and end dates :param timerange: Timerange specified for start and end dates
""" """
if timerange.starttype == 'date': if timerange.starttype == "date":
if pairdata.iloc[0]['date'] > timerange.startdt: if pairdata.iloc[0]["date"] > timerange.startdt:
logger.warning(f"{pair}, {candle_type}, {timeframe}, " logger.warning(
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}") f"{pair}, {candle_type}, {timeframe}, "
if timerange.stoptype == 'date': f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}"
if pairdata.iloc[-1]['date'] < timerange.stopdt: )
logger.warning(f"{pair}, {candle_type}, {timeframe}, " if timerange.stoptype == "date":
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}") if pairdata.iloc[-1]["date"] < timerange.stopdt:
logger.warning(
f"{pair}, {candle_type}, {timeframe}, "
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}"
)
def rename_futures_data( def rename_futures_data(
self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType): self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType
):
""" """
Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT) Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT)
Only used for binance to support the binance futures naming unification. Only used for binance to support the binance futures naming unification.
@@ -431,18 +465,19 @@ class IDataHandler(ABC):
if funding_rate_combs: if funding_rate_combs:
logger.warning( logger.warning(
f'Migrating {len(funding_rate_combs)} funding fees to correct timeframe.') f"Migrating {len(funding_rate_combs)} funding fees to correct timeframe."
)
for pair, timeframe, candletype in funding_rate_combs: for pair, timeframe, candletype in funding_rate_combs:
old_name = self._pair_data_filename(self._datadir, pair, timeframe, candletype) old_name = self._pair_data_filename(self._datadir, pair, timeframe, candletype)
new_name = self._pair_data_filename(self._datadir, pair, ff_timeframe, candletype) new_name = self._pair_data_filename(self._datadir, pair, ff_timeframe, candletype)
if not Path(old_name).exists(): if not Path(old_name).exists():
logger.warning(f'{old_name} does not exist, skipping.') logger.warning(f"{old_name} does not exist, skipping.")
continue continue
if Path(new_name).exists(): if Path(new_name).exists():
logger.warning(f'{new_name} already exists, Removing.') logger.warning(f"{new_name} already exists, Removing.")
Path(new_name).unlink() Path(new_name).unlink()
Path(old_name).rename(new_name) Path(old_name).rename(new_name)
@@ -457,27 +492,33 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
:return: Datahandler class :return: Datahandler class
""" """
if datatype == 'json': if datatype == "json":
from .jsondatahandler import JsonDataHandler from .jsondatahandler import JsonDataHandler
return JsonDataHandler return JsonDataHandler
elif datatype == 'jsongz': elif datatype == "jsongz":
from .jsondatahandler import JsonGzDataHandler from .jsondatahandler import JsonGzDataHandler
return JsonGzDataHandler return JsonGzDataHandler
elif datatype == 'hdf5': elif datatype == "hdf5":
from .hdf5datahandler import HDF5DataHandler from .hdf5datahandler import HDF5DataHandler
return HDF5DataHandler return HDF5DataHandler
elif datatype == 'feather': elif datatype == "feather":
from .featherdatahandler import FeatherDataHandler from .featherdatahandler import FeatherDataHandler
return FeatherDataHandler return FeatherDataHandler
elif datatype == 'parquet': elif datatype == "parquet":
from .parquetdatahandler import ParquetDataHandler from .parquetdatahandler import ParquetDataHandler
return ParquetDataHandler return ParquetDataHandler
else: else:
raise ValueError(f"No datahandler for datatype {datatype} available.") raise ValueError(f"No datahandler for datatype {datatype} available.")
def get_datahandler(datadir: Path, data_format: Optional[str] = None, def get_datahandler(
data_handler: Optional[IDataHandler] = None) -> IDataHandler: datadir: Path, data_format: Optional[str] = None, data_handler: Optional[IDataHandler] = None
) -> IDataHandler:
""" """
:param datadir: Folder to save data :param datadir: Folder to save data
:param data_format: dataformat to use :param data_format: dataformat to use
@@ -485,6 +526,6 @@ def get_datahandler(datadir: Path, data_format: Optional[str] = None,
""" """
if not data_handler: if not data_handler:
HandlerClass = get_datahandlerclass(data_format or 'feather') HandlerClass = get_datahandlerclass(data_format or "feather")
data_handler = HandlerClass(datadir) data_handler = HandlerClass(datadir)
return data_handler return data_handler

View File

@@ -17,12 +17,12 @@ logger = logging.getLogger(__name__)
class JsonDataHandler(IDataHandler): class JsonDataHandler(IDataHandler):
_use_zip = False _use_zip = False
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
def ohlcv_store( def ohlcv_store(
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None: self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
) -> None:
""" """
Store data in json format "values". Store data in json format "values".
format looks as follows: format looks as follows:
@@ -37,16 +37,16 @@ class JsonDataHandler(IDataHandler):
self.create_dir_if_needed(filename) self.create_dir_if_needed(filename)
_data = data.copy() _data = data.copy()
# Convert date to int # Convert date to int
_data['date'] = _data['date'].astype(np.int64) // 1000 // 1000 _data["date"] = _data["date"].astype(np.int64) // 1000 // 1000
# Reset index, select only appropriate columns and save as json # Reset index, select only appropriate columns and save as json
_data.reset_index(drop=True).loc[:, self._columns].to_json( _data.reset_index(drop=True).loc[:, self._columns].to_json(
filename, orient="values", filename, orient="values", compression="gzip" if self._use_zip else None
compression='gzip' if self._use_zip else None) )
def _ohlcv_load(self, pair: str, timeframe: str, def _ohlcv_load(
timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
) -> DataFrame: ) -> DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
Implements the loading and conversion to a Pandas dataframe. Implements the loading and conversion to a Pandas dataframe.
@@ -59,31 +59,34 @@ class JsonDataHandler(IDataHandler):
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame :return: DataFrame with ohlcv data, or empty DataFrame
""" """
filename = self._pair_data_filename( filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
self._datadir, pair, timeframe, candle_type=candle_type)
if not filename.exists(): if not filename.exists():
# Fallback mode for 1M files # Fallback mode for 1M files
filename = self._pair_data_filename( filename = self._pair_data_filename(
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True) self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True
)
if not filename.exists(): if not filename.exists():
return DataFrame(columns=self._columns) return DataFrame(columns=self._columns)
try: try:
pairdata = read_json(filename, orient='values') pairdata = read_json(filename, orient="values")
pairdata.columns = self._columns pairdata.columns = self._columns
except ValueError: except ValueError:
logger.error(f"Could not load data for {pair}.") logger.error(f"Could not load data for {pair}.")
return DataFrame(columns=self._columns) return DataFrame(columns=self._columns)
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', pairdata = pairdata.astype(
'low': 'float', 'close': 'float', 'volume': 'float'}) dtype={
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) "open": "float",
"high": "float",
"low": "float",
"close": "float",
"volume": "float",
}
)
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
return pairdata return pairdata
def ohlcv_append( def ohlcv_append(
self, self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
pair: str,
timeframe: str,
data: DataFrame,
candle_type: CandleType
) -> None: ) -> None:
""" """
Append data to existing data structures Append data to existing data structures
@@ -145,5 +148,4 @@ class JsonDataHandler(IDataHandler):
class JsonGzDataHandler(JsonDataHandler): class JsonGzDataHandler(JsonDataHandler):
_use_zip = True _use_zip = True

View File

@@ -14,11 +14,11 @@ logger = logging.getLogger(__name__)
class ParquetDataHandler(IDataHandler): class ParquetDataHandler(IDataHandler):
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
def ohlcv_store( def ohlcv_store(
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None: self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
) -> None:
""" """
Store data in json format "values". Store data in json format "values".
format looks as follows: format looks as follows:
@@ -34,9 +34,9 @@ class ParquetDataHandler(IDataHandler):
data.reset_index(drop=True).loc[:, self._columns].to_parquet(filename) data.reset_index(drop=True).loc[:, self._columns].to_parquet(filename)
def _ohlcv_load(self, pair: str, timeframe: str, def _ohlcv_load(
timerange: Optional[TimeRange], candle_type: CandleType self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
) -> DataFrame: ) -> DataFrame:
""" """
Internal method used to load data for one pair from disk. Internal method used to load data for one pair from disk.
Implements the loading and conversion to a Pandas dataframe. Implements the loading and conversion to a Pandas dataframe.
@@ -49,28 +49,31 @@ class ParquetDataHandler(IDataHandler):
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame :return: DataFrame with ohlcv data, or empty DataFrame
""" """
filename = self._pair_data_filename( filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
self._datadir, pair, timeframe, candle_type=candle_type)
if not filename.exists(): if not filename.exists():
# Fallback mode for 1M files # Fallback mode for 1M files
filename = self._pair_data_filename( filename = self._pair_data_filename(
self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True) self._datadir, pair, timeframe, candle_type=candle_type, no_timeframe_modify=True
)
if not filename.exists(): if not filename.exists():
return DataFrame(columns=self._columns) return DataFrame(columns=self._columns)
pairdata = read_parquet(filename) pairdata = read_parquet(filename)
pairdata.columns = self._columns pairdata.columns = self._columns
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', pairdata = pairdata.astype(
'low': 'float', 'close': 'float', 'volume': 'float'}) dtype={
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) "open": "float",
"high": "float",
"low": "float",
"close": "float",
"volume": "float",
}
)
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
return pairdata return pairdata
def ohlcv_append( def ohlcv_append(
self, self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
pair: str,
timeframe: str,
data: DataFrame,
candle_type: CandleType
) -> None: ) -> None:
""" """
Append data to existing data structures Append data to existing data structures

View File

@@ -7,11 +7,20 @@ from typing import Dict, List, Optional, Tuple
from pandas import DataFrame, concat from pandas import DataFrame, concat
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS, from freqtrade.constants import (
DL_DATA_TIMEFRAMES, DOCS_LINK, Config) DATETIME_PRINT_FORMAT,
from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_ohlcv, DEFAULT_DATAFRAME_COLUMNS,
ohlcv_to_dataframe, trades_df_remove_duplicates, DL_DATA_TIMEFRAMES,
trades_list_to_df) DOCS_LINK,
Config,
)
from freqtrade.data.converter import (
clean_ohlcv_dataframe,
convert_trades_to_ohlcv,
ohlcv_to_dataframe,
trades_df_remove_duplicates,
trades_list_to_df,
)
from freqtrade.data.history.datahandlers import IDataHandler, get_datahandler from freqtrade.data.history.datahandlers import IDataHandler, get_datahandler
from freqtrade.enums import CandleType, TradingMode from freqtrade.enums import CandleType, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@@ -25,17 +34,19 @@ from freqtrade.util.migrations import migrate_data
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def load_pair_history(pair: str, def load_pair_history(
timeframe: str, pair: str,
datadir: Path, *, timeframe: str,
timerange: Optional[TimeRange] = None, datadir: Path,
fill_up_missing: bool = True, *,
drop_incomplete: bool = False, timerange: Optional[TimeRange] = None,
startup_candles: int = 0, fill_up_missing: bool = True,
data_format: Optional[str] = None, drop_incomplete: bool = False,
data_handler: Optional[IDataHandler] = None, startup_candles: int = 0,
candle_type: CandleType = CandleType.SPOT data_format: Optional[str] = None,
) -> DataFrame: data_handler: Optional[IDataHandler] = None,
candle_type: CandleType = CandleType.SPOT,
) -> DataFrame:
""" """
Load cached ohlcv history for the given pair. Load cached ohlcv history for the given pair.
@@ -54,27 +65,30 @@ def load_pair_history(pair: str,
""" """
data_handler = get_datahandler(datadir, data_format, data_handler) data_handler = get_datahandler(datadir, data_format, data_handler)
return data_handler.ohlcv_load(pair=pair, return data_handler.ohlcv_load(
timeframe=timeframe, pair=pair,
timerange=timerange, timeframe=timeframe,
fill_missing=fill_up_missing, timerange=timerange,
drop_incomplete=drop_incomplete, fill_missing=fill_up_missing,
startup_candles=startup_candles, drop_incomplete=drop_incomplete,
candle_type=candle_type, startup_candles=startup_candles,
) candle_type=candle_type,
)
def load_data(datadir: Path, def load_data(
timeframe: str, datadir: Path,
pairs: List[str], *, timeframe: str,
timerange: Optional[TimeRange] = None, pairs: List[str],
fill_up_missing: bool = True, *,
startup_candles: int = 0, timerange: Optional[TimeRange] = None,
fail_without_data: bool = False, fill_up_missing: bool = True,
data_format: str = 'feather', startup_candles: int = 0,
candle_type: CandleType = CandleType.SPOT, fail_without_data: bool = False,
user_futures_funding_rate: Optional[int] = None, data_format: str = "feather",
) -> Dict[str, DataFrame]: candle_type: CandleType = CandleType.SPOT,
user_futures_funding_rate: Optional[int] = None,
) -> Dict[str, DataFrame]:
""" """
Load ohlcv history data for a list of pairs. Load ohlcv history data for a list of pairs.
@@ -91,18 +105,21 @@ def load_data(datadir: Path,
""" """
result: Dict[str, DataFrame] = {} result: Dict[str, DataFrame] = {}
if startup_candles > 0 and timerange: if startup_candles > 0 and timerange:
logger.info(f'Using indicator startup period: {startup_candles} ...') logger.info(f"Using indicator startup period: {startup_candles} ...")
data_handler = get_datahandler(datadir, data_format) data_handler = get_datahandler(datadir, data_format)
for pair in pairs: for pair in pairs:
hist = load_pair_history(pair=pair, timeframe=timeframe, hist = load_pair_history(
datadir=datadir, timerange=timerange, pair=pair,
fill_up_missing=fill_up_missing, timeframe=timeframe,
startup_candles=startup_candles, datadir=datadir,
data_handler=data_handler, timerange=timerange,
candle_type=candle_type, fill_up_missing=fill_up_missing,
) startup_candles=startup_candles,
data_handler=data_handler,
candle_type=candle_type,
)
if not hist.empty: if not hist.empty:
result[pair] = hist result[pair] = hist
else: else:
@@ -116,14 +133,16 @@ def load_data(datadir: Path,
return result return result
def refresh_data(*, datadir: Path, def refresh_data(
timeframe: str, *,
pairs: List[str], datadir: Path,
exchange: Exchange, timeframe: str,
data_format: Optional[str] = None, pairs: List[str],
timerange: Optional[TimeRange] = None, exchange: Exchange,
candle_type: CandleType, data_format: Optional[str] = None,
) -> None: timerange: Optional[TimeRange] = None,
candle_type: CandleType,
) -> None:
""" """
Refresh ohlcv history data for a list of pairs. Refresh ohlcv history data for a list of pairs.
@@ -137,11 +156,17 @@ def refresh_data(*, datadir: Path,
""" """
data_handler = get_datahandler(datadir, data_format) data_handler = get_datahandler(datadir, data_format)
for idx, pair in enumerate(pairs): for idx, pair in enumerate(pairs):
process = f'{idx}/{len(pairs)}' process = f"{idx}/{len(pairs)}"
_download_pair_history(pair=pair, process=process, _download_pair_history(
timeframe=timeframe, datadir=datadir, pair=pair,
timerange=timerange, exchange=exchange, data_handler=data_handler, process=process,
candle_type=candle_type) timeframe=timeframe,
datadir=datadir,
timerange=timerange,
exchange=exchange,
data_handler=data_handler,
candle_type=candle_type,
)
def _load_cached_data_for_updating( def _load_cached_data_for_updating(
@@ -163,42 +188,49 @@ def _load_cached_data_for_updating(
start = None start = None
end = None end = None
if timerange: if timerange:
if timerange.starttype == 'date': if timerange.starttype == "date":
start = timerange.startdt start = timerange.startdt
if timerange.stoptype == 'date': if timerange.stoptype == "date":
end = timerange.stopdt end = timerange.stopdt
# Intentionally don't pass timerange in - since we need to load the full dataset. # Intentionally don't pass timerange in - since we need to load the full dataset.
data = data_handler.ohlcv_load(pair, timeframe=timeframe, data = data_handler.ohlcv_load(
timerange=None, fill_missing=False, pair,
drop_incomplete=True, warn_no_data=False, timeframe=timeframe,
candle_type=candle_type) timerange=None,
fill_missing=False,
drop_incomplete=True,
warn_no_data=False,
candle_type=candle_type,
)
if not data.empty: if not data.empty:
if not prepend and start and start < data.iloc[0]['date']: if not prepend and start and start < data.iloc[0]["date"]:
# Earlier data than existing data requested, redownload all # Earlier data than existing data requested, redownload all
data = DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS) data = DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
else: else:
if prepend: if prepend:
end = data.iloc[0]['date'] end = data.iloc[0]["date"]
else: else:
start = data.iloc[-1]['date'] start = data.iloc[-1]["date"]
start_ms = int(start.timestamp() * 1000) if start else None start_ms = int(start.timestamp() * 1000) if start else None
end_ms = int(end.timestamp() * 1000) if end else None end_ms = int(end.timestamp() * 1000) if end else None
return data, start_ms, end_ms return data, start_ms, end_ms
def _download_pair_history(pair: str, *, def _download_pair_history(
datadir: Path, pair: str,
exchange: Exchange, *,
timeframe: str = '5m', datadir: Path,
process: str = '', exchange: Exchange,
new_pairs_days: int = 30, timeframe: str = "5m",
data_handler: Optional[IDataHandler] = None, process: str = "",
timerange: Optional[TimeRange] = None, new_pairs_days: int = 30,
candle_type: CandleType, data_handler: Optional[IDataHandler] = None,
erase: bool = False, timerange: Optional[TimeRange] = None,
prepend: bool = False, candle_type: CandleType,
) -> bool: erase: bool = False,
prepend: bool = False,
) -> bool:
""" """
Download latest candles from the exchange for the pair and timeframe passed in parameters Download latest candles from the exchange for the pair and timeframe passed in parameters
The data is downloaded starting from the last correct data that The data is downloaded starting from the last correct data that
@@ -217,54 +249,71 @@ def _download_pair_history(pair: str, *,
try: try:
if erase: if erase:
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type): if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
logger.info(f'Deleting existing data for pair {pair}, {timeframe}, {candle_type}.') logger.info(f"Deleting existing data for pair {pair}, {timeframe}, {candle_type}.")
data, since_ms, until_ms = _load_cached_data_for_updating( data, since_ms, until_ms = _load_cached_data_for_updating(
pair, timeframe, timerange, pair,
timeframe,
timerange,
data_handler=data_handler, data_handler=data_handler,
candle_type=candle_type, candle_type=candle_type,
prepend=prepend) prepend=prepend,
)
logger.info(f'({process}) - Download history data for "{pair}", {timeframe}, ' logger.info(
f'{candle_type} and store in {datadir}. ' f'({process}) - Download history data for "{pair}", {timeframe}, '
f'From {format_ms_time(since_ms) if since_ms else "start"} to ' f"{candle_type} and store in {datadir}. "
f'{format_ms_time(until_ms) if until_ms else "now"}' f'From {format_ms_time(since_ms) if since_ms else "start"} to '
) f'{format_ms_time(until_ms) if until_ms else "now"}'
)
logger.debug("Current Start: %s", logger.debug(
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}" "Current Start: %s",
if not data.empty else 'None') f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
logger.debug("Current End: %s", )
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}" logger.debug(
if not data.empty else 'None') "Current End: %s",
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
)
# Default since_ms to 30 days if nothing is given # Default since_ms to 30 days if nothing is given
new_data = exchange.get_historic_ohlcv(pair=pair, new_data = exchange.get_historic_ohlcv(
timeframe=timeframe, pair=pair,
since_ms=since_ms if since_ms else timeframe=timeframe,
int((datetime.now() - timedelta(days=new_pairs_days) since_ms=(
).timestamp()) * 1000, since_ms
is_new_pair=data.empty, if since_ms
candle_type=candle_type, else int((datetime.now() - timedelta(days=new_pairs_days)).timestamp()) * 1000
until_ms=until_ms if until_ms else None ),
) is_new_pair=data.empty,
candle_type=candle_type,
until_ms=until_ms if until_ms else None,
)
# TODO: Maybe move parsing to exchange class (?) # TODO: Maybe move parsing to exchange class (?)
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair, new_dataframe = ohlcv_to_dataframe(
fill_missing=False, drop_incomplete=True) new_data, timeframe, pair, fill_missing=False, drop_incomplete=True
)
if data.empty: if data.empty:
data = new_dataframe data = new_dataframe
else: else:
# Run cleaning again to ensure there were no duplicate candles # Run cleaning again to ensure there were no duplicate candles
# Especially between existing and new data. # Especially between existing and new data.
data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair, data = clean_ohlcv_dataframe(
fill_missing=False, drop_incomplete=False) concat([data, new_dataframe], axis=0),
timeframe,
pair,
fill_missing=False,
drop_incomplete=False,
)
logger.debug("New Start: %s", logger.debug(
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}" "New Start: %s",
if not data.empty else 'None') f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
logger.debug("New End: %s", )
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}" logger.debug(
if not data.empty else 'None') "New End: %s",
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}" if not data.empty else "None",
)
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type) data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
return True return True
@@ -276,13 +325,18 @@ def _download_pair_history(pair: str, *,
return False return False
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str], def refresh_backtest_ohlcv_data(
datadir: Path, trading_mode: str, exchange: Exchange,
timerange: Optional[TimeRange] = None, pairs: List[str],
new_pairs_days: int = 30, erase: bool = False, timeframes: List[str],
data_format: Optional[str] = None, datadir: Path,
prepend: bool = False, trading_mode: str,
) -> List[str]: timerange: Optional[TimeRange] = None,
new_pairs_days: int = 30,
erase: bool = False,
data_format: Optional[str] = None,
prepend: bool = False,
) -> List[str]:
""" """
Refresh stored ohlcv data for backtesting and hyperopt operations. Refresh stored ohlcv data for backtesting and hyperopt operations.
Used by freqtrade download-data subcommand. Used by freqtrade download-data subcommand.
@@ -291,63 +345,77 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
pairs_not_available = [] pairs_not_available = []
data_handler = get_datahandler(datadir, data_format) data_handler = get_datahandler(datadir, data_format)
candle_type = CandleType.get_default(trading_mode) candle_type = CandleType.get_default(trading_mode)
process = '' process = ""
for idx, pair in enumerate(pairs, start=1): for idx, pair in enumerate(pairs, start=1):
if pair not in exchange.markets: if pair not in exchange.markets:
pairs_not_available.append(pair) pairs_not_available.append(pair)
logger.info(f"Skipping pair {pair}...") logger.info(f"Skipping pair {pair}...")
continue continue
for timeframe in timeframes: for timeframe in timeframes:
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
logger.debug(f'Downloading pair {pair}, {candle_type}, interval {timeframe}.') process = f"{idx}/{len(pairs)}"
process = f'{idx}/{len(pairs)}' _download_pair_history(
_download_pair_history(pair=pair, process=process, pair=pair,
datadir=datadir, exchange=exchange, process=process,
timerange=timerange, data_handler=data_handler, datadir=datadir,
timeframe=str(timeframe), new_pairs_days=new_pairs_days, exchange=exchange,
candle_type=candle_type, timerange=timerange,
erase=erase, prepend=prepend) data_handler=data_handler,
if trading_mode == 'futures': timeframe=str(timeframe),
new_pairs_days=new_pairs_days,
candle_type=candle_type,
erase=erase,
prepend=prepend,
)
if trading_mode == "futures":
# Predefined candletype (and timeframe) depending on exchange # Predefined candletype (and timeframe) depending on exchange
# Downloads what is necessary to backtest based on futures data. # Downloads what is necessary to backtest based on futures data.
tf_mark = exchange.get_option('mark_ohlcv_timeframe') tf_mark = exchange.get_option("mark_ohlcv_timeframe")
tf_funding_rate = exchange.get_option('funding_fee_timeframe') tf_funding_rate = exchange.get_option("funding_fee_timeframe")
fr_candle_type = CandleType.from_string(exchange.get_option('mark_ohlcv_price')) fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
# All exchanges need FundingRate for futures trading. # All exchanges need FundingRate for futures trading.
# The timeframe is aligned to the mark-price timeframe. # The timeframe is aligned to the mark-price timeframe.
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark)) combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
for candle_type_f, tf in combs: for candle_type_f, tf in combs:
logger.debug(f'Downloading pair {pair}, {candle_type_f}, interval {tf}.') logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.")
_download_pair_history(pair=pair, process=process, _download_pair_history(
datadir=datadir, exchange=exchange, pair=pair,
timerange=timerange, data_handler=data_handler, process=process,
timeframe=str(tf), new_pairs_days=new_pairs_days, datadir=datadir,
candle_type=candle_type_f, exchange=exchange,
erase=erase, prepend=prepend) timerange=timerange,
data_handler=data_handler,
timeframe=str(tf),
new_pairs_days=new_pairs_days,
candle_type=candle_type_f,
erase=erase,
prepend=prepend,
)
return pairs_not_available return pairs_not_available
def _download_trades_history(exchange: Exchange, def _download_trades_history(
pair: str, *, exchange: Exchange,
new_pairs_days: int = 30, pair: str,
timerange: Optional[TimeRange] = None, *,
data_handler: IDataHandler, new_pairs_days: int = 30,
trading_mode: TradingMode, timerange: Optional[TimeRange] = None,
) -> bool: data_handler: IDataHandler,
trading_mode: TradingMode,
) -> bool:
""" """
Download trade history from the exchange. Download trade history from the exchange.
Appends to previously downloaded trades data. Appends to previously downloaded trades data.
""" """
try: try:
until = None until = None
since = 0 since = 0
if timerange: if timerange:
if timerange.starttype == 'date': if timerange.starttype == "date":
since = timerange.startts * 1000 since = timerange.startts * 1000
if timerange.stoptype == 'date': if timerange.stoptype == "date":
until = timerange.stopts * 1000 until = timerange.stopts * 1000
trades = data_handler.trades_load(pair, trading_mode) trades = data_handler.trades_load(pair, trading_mode)
@@ -356,60 +424,76 @@ def _download_trades_history(exchange: Exchange,
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
# DEFAULT_TRADES_COLUMNS: 1 -> id # DEFAULT_TRADES_COLUMNS: 1 -> id
if not trades.empty and since > 0 and since < trades.iloc[0]['timestamp']: if not trades.empty and since > 0 and since < trades.iloc[0]["timestamp"]:
# since is before the first trade # since is before the first trade
logger.info(f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than " logger.info(
f"available data. Redownloading trades for {pair}...") f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than "
f"available data. Redownloading trades for {pair}..."
)
trades = trades_list_to_df([]) trades = trades_list_to_df([])
from_id = trades.iloc[-1]['id'] if not trades.empty else None from_id = trades.iloc[-1]["id"] if not trades.empty else None
if not trades.empty and since < trades.iloc[-1]['timestamp']: if not trades.empty and since < trades.iloc[-1]["timestamp"]:
# Reset since to the last available point # Reset since to the last available point
# - 5 seconds (to ensure we're getting all trades) # - 5 seconds (to ensure we're getting all trades)
since = trades.iloc[-1]['timestamp'] - (5 * 1000) since = trades.iloc[-1]["timestamp"] - (5 * 1000)
logger.info(f"Using last trade date -5s - Downloading trades for {pair} " logger.info(
f"since: {format_ms_time(since)}.") f"Using last trade date -5s - Downloading trades for {pair} "
f"since: {format_ms_time(since)}."
)
if not since: if not since:
since = dt_ts(dt_now() - timedelta(days=new_pairs_days)) since = dt_ts(dt_now() - timedelta(days=new_pairs_days))
logger.debug("Current Start: %s", 'None' if trades.empty else logger.debug(
f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}") "Current Start: %s",
logger.debug("Current End: %s", 'None' if trades.empty else "None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}") )
logger.debug(
"Current End: %s",
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
)
logger.info(f"Current Amount of trades: {len(trades)}") logger.info(f"Current Amount of trades: {len(trades)}")
# Default since_ms to 30 days if nothing is given # Default since_ms to 30 days if nothing is given
new_trades = exchange.get_historic_trades(pair=pair, new_trades = exchange.get_historic_trades(
since=since, pair=pair,
until=until, since=since,
from_id=from_id, until=until,
) from_id=from_id,
)
new_trades_df = trades_list_to_df(new_trades[1]) new_trades_df = trades_list_to_df(new_trades[1])
trades = concat([trades, new_trades_df], axis=0) trades = concat([trades, new_trades_df], axis=0)
# Remove duplicates to make sure we're not storing data we don't need # Remove duplicates to make sure we're not storing data we don't need
trades = trades_df_remove_duplicates(trades) trades = trades_df_remove_duplicates(trades)
data_handler.trades_store(pair, trades, trading_mode) data_handler.trades_store(pair, trades, trading_mode)
logger.debug("New Start: %s", 'None' if trades.empty else logger.debug(
f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}") "New Start: %s",
logger.debug("New End: %s", 'None' if trades.empty else "None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}") )
logger.debug(
"New End: %s",
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
)
logger.info(f"New Amount of trades: {len(trades)}") logger.info(f"New Amount of trades: {len(trades)}")
return True return True
except Exception: except Exception:
logger.exception( logger.exception(f'Failed to download historic trades for pair: "{pair}". ')
f'Failed to download historic trades for pair: "{pair}". '
)
return False return False
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, def refresh_backtest_trades_data(
timerange: TimeRange, trading_mode: TradingMode, exchange: Exchange,
new_pairs_days: int = 30, pairs: List[str],
erase: bool = False, data_format: str = 'feather', datadir: Path,
) -> List[str]: timerange: TimeRange,
trading_mode: TradingMode,
new_pairs_days: int = 30,
erase: bool = False,
data_format: str = "feather",
) -> List[str]:
""" """
Refresh stored trades data for backtesting and hyperopt operations. Refresh stored trades data for backtesting and hyperopt operations.
Used by freqtrade download-data subcommand. Used by freqtrade download-data subcommand.
@@ -425,15 +509,17 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
if erase: if erase:
if data_handler.trades_purge(pair, trading_mode): if data_handler.trades_purge(pair, trading_mode):
logger.info(f'Deleting existing data for pair {pair}.') logger.info(f"Deleting existing data for pair {pair}.")
logger.info(f'Downloading trades for pair {pair}.') logger.info(f"Downloading trades for pair {pair}.")
_download_trades_history(exchange=exchange, _download_trades_history(
pair=pair, exchange=exchange,
new_pairs_days=new_pairs_days, pair=pair,
timerange=timerange, new_pairs_days=new_pairs_days,
data_handler=data_handler, timerange=timerange,
trading_mode=trading_mode) data_handler=data_handler,
trading_mode=trading_mode,
)
return pairs_not_available return pairs_not_available
@@ -445,15 +531,18 @@ def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]:
:return: tuple containing min_date, max_date :return: tuple containing min_date, max_date
""" """
timeranges = [ timeranges = [
(frame['date'].min().to_pydatetime(), frame['date'].max().to_pydatetime()) (frame["date"].min().to_pydatetime(), frame["date"].max().to_pydatetime())
for frame in data.values() for frame in data.values()
] ]
return (min(timeranges, key=operator.itemgetter(0))[0], return (
max(timeranges, key=operator.itemgetter(1))[1]) min(timeranges, key=operator.itemgetter(0))[0],
max(timeranges, key=operator.itemgetter(1))[1],
)
def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, def validate_backtest_data(
max_date: datetime, timeframe_min: int) -> bool: data: DataFrame, pair: str, min_date: datetime, max_date: datetime, timeframe_min: int
) -> bool:
""" """
Validates preprocessed backtesting data for missing values and shows warnings about it that. Validates preprocessed backtesting data for missing values and shows warnings about it that.
@@ -469,89 +558,119 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
dflen = len(data) dflen = len(data)
if dflen < expected_frames: if dflen < expected_frames:
found_missing = True found_missing = True
logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values", logger.warning(
pair, expected_frames, dflen, expected_frames - dflen) "%s has missing frames: expected %s, got %s, that's %s missing values",
pair,
expected_frames,
dflen,
expected_frames - dflen,
)
return found_missing return found_missing
def download_data_main(config: Config) -> None: def download_data_main(config: Config) -> None:
timerange = TimeRange() timerange = TimeRange()
if 'days' in config: if "days" in config:
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d") time_since = (datetime.now() - timedelta(days=config["days"])).strftime("%Y%m%d")
timerange = TimeRange.parse_timerange(f'{time_since}-') timerange = TimeRange.parse_timerange(f"{time_since}-")
if 'timerange' in config: if "timerange" in config:
timerange = timerange.parse_timerange(config['timerange']) timerange = timerange.parse_timerange(config["timerange"])
# Remove stake-currency to skip checks which are not relevant for datadownload # Remove stake-currency to skip checks which are not relevant for datadownload
config['stake_currency'] = '' config["stake_currency"] = ""
pairs_not_available: List[str] = [] pairs_not_available: List[str] = []
# Init exchange # Init exchange
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
exchange = ExchangeResolver.load_exchange(config, validate=False) exchange = ExchangeResolver.load_exchange(config, validate=False)
available_pairs = [ available_pairs = [
p for p in exchange.get_markets( p
tradable_only=True, active_only=not config.get('include_inactive') for p in exchange.get_markets(
).keys() tradable_only=True, active_only=not config.get("include_inactive")
).keys()
] ]
expanded_pairs = dynamic_expand_pairlist(config, available_pairs) expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
if 'timeframes' not in config: if "timeframes" not in config:
config['timeframes'] = DL_DATA_TIMEFRAMES config["timeframes"] = DL_DATA_TIMEFRAMES
# Manual validations of relevant settings # Manual validations of relevant settings
if not config['exchange'].get('skip_pair_validation', False): if not config["exchange"].get("skip_pair_validation", False):
exchange.validate_pairs(expanded_pairs) exchange.validate_pairs(expanded_pairs)
logger.info(f"About to download pairs: {expanded_pairs}, " logger.info(
f"intervals: {config['timeframes']} to {config['datadir']}") f"About to download pairs: {expanded_pairs}, "
f"intervals: {config['timeframes']} to {config['datadir']}"
)
if len(expanded_pairs) == 0: if len(expanded_pairs) == 0:
logger.warning( logger.warning(
"No pairs available for download. " "No pairs available for download. "
"Please make sure you're using the correct Pair naming for your selected trade mode. \n" "Please make sure you're using the correct Pair naming for your selected trade mode. \n"
f"More info: {DOCS_LINK}/bot-basics/#pair-naming") f"More info: {DOCS_LINK}/bot-basics/#pair-naming"
)
for timeframe in config['timeframes']: for timeframe in config["timeframes"]:
exchange.validate_timeframes(timeframe) exchange.validate_timeframes(timeframe)
# Start downloading # Start downloading
try: try:
if config.get('download_trades'): if config.get("download_trades"):
pairs_not_available = refresh_backtest_trades_data( if not exchange.get_option("trades_has_history", True):
exchange, pairs=expanded_pairs, datadir=config['datadir'], raise OperationalException(
timerange=timerange, new_pairs_days=config['new_pairs_days'], f"Trade history not available for {exchange.name}. "
erase=bool(config.get('erase')), data_format=config['dataformat_trades'], "You cannot use --dl-trades for this exchange."
trading_mode=config.get('trading_mode', TradingMode.SPOT),
) )
pairs_not_available = refresh_backtest_trades_data(
# Convert downloaded trade data to different timeframes exchange,
convert_trades_to_ohlcv( pairs=expanded_pairs,
pairs=expanded_pairs, timeframes=config['timeframes'], datadir=config["datadir"],
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), timerange=timerange,
data_format_ohlcv=config['dataformat_ohlcv'], new_pairs_days=config["new_pairs_days"],
data_format_trades=config['dataformat_trades'], erase=bool(config.get("erase")),
candle_type=config.get('candle_type_def', CandleType.SPOT), data_format=config["dataformat_trades"],
trading_mode=config.get("trading_mode", TradingMode.SPOT),
) )
if config.get("convert_trades") or not exchange.get_option("ohlcv_has_history", True):
# Convert downloaded trade data to different timeframes
# Only auto-convert for exchanges without historic klines
convert_trades_to_ohlcv(
pairs=expanded_pairs,
timeframes=config["timeframes"],
datadir=config["datadir"],
timerange=timerange,
erase=bool(config.get("erase")),
data_format_ohlcv=config["dataformat_ohlcv"],
data_format_trades=config["dataformat_trades"],
candle_type=config.get("candle_type_def", CandleType.SPOT),
)
else: else:
if not exchange.get_option('ohlcv_has_history', True): if not exchange.get_option("ohlcv_has_history", True):
raise OperationalException( raise OperationalException(
f"Historic klines not available for {exchange.name}. " f"Historic klines not available for {exchange.name}. "
"Please use `--dl-trades` instead for this exchange " "Please use `--dl-trades` instead for this exchange "
"(will unfortunately take a long time)." "(will unfortunately take a long time)."
) )
migrate_data(config, exchange) migrate_data(config, exchange)
pairs_not_available = refresh_backtest_ohlcv_data( pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=expanded_pairs, timeframes=config['timeframes'], exchange,
datadir=config['datadir'], timerange=timerange, pairs=expanded_pairs,
new_pairs_days=config['new_pairs_days'], timeframes=config["timeframes"],
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'], datadir=config["datadir"],
trading_mode=config.get('trading_mode', 'spot'), timerange=timerange,
prepend=config.get('prepend_data', False) new_pairs_days=config["new_pairs_days"],
erase=bool(config.get("erase")),
data_format=config["dataformat_ohlcv"],
trading_mode=config.get("trading_mode", "spot"),
prepend=config.get("prepend_data", False),
) )
finally: finally:
if pairs_not_available: if pairs_not_available:
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available " logger.info(
f"on exchange {exchange.name}.") f"Pairs [{','.join(pairs_not_available)}] not available "
f"on exchange {exchange.name}."
)

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