Compare commits

...

319 Commits

Author SHA1 Message Date
Matthias 28399aaab2 Merge pull request #11814 from freqtrade/new_release
New release 2025.5
2025-05-31 17:37:56 +02:00
Matthias 560085ab92 chore: bump version to 2025.5 2025-05-31 16:34:27 +02:00
Matthias 17c315bf4a Merge branch 'stable' into new_release 2025-05-31 16:34:04 +02:00
Matthias e8145e0057 Merge pull request #11805 from viotemp1/optuna_addons
fix hyperopt repeated parameters between batches
2025-05-31 16:26:32 +02:00
Matthias ae9073885f chore: Update log wording, only log "duplicate parameters" once 2025-05-31 16:05:39 +02:00
Matthias 61886942c5 chore: fix log message missing space 2025-05-31 09:04:17 +02:00
viotemp1 12d31c4acb keep INITIAL_POINTS only in hyperopt_optimizer.py 2025-05-31 08:21:44 +03:00
viotemp1 14cfdb13c5 add back INITIAL_POINTS (default 30) for Samplers which support this 2025-05-30 18:17:15 +03:00
Matthias e58ab7852a docs: add operatorId to bitvavo docs
closes #11748
2025-05-30 07:32:36 +02:00
viotemp1 dfae7ca2ec fix duplicate params in same batch also 2025-05-29 15:41:47 +03:00
vio 17cd0452ec Merge branch 'freqtrade:develop' into optuna_addons 2025-05-29 14:22:17 +03:00
Matthias 9fe361f6fc Merge pull request #11808 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-05-29 06:44:07 +02:00
Freqtrade Bot da7f35db8d chore: update pre-commit hooks 2025-05-29 03:21:16 +00:00
Matthias 4f216eb861 Merge pull request #11790 from freqtrade/docs/compare_exit_callbacks
docs: add "exit logic comparisons" table
2025-05-28 15:22:15 +02:00
viotemp1 53383f3184 add up to 5 retries for ask in case of duplicate params 2025-05-28 09:35:20 +02:00
viotemp1 b51c937e87 fix hyperopt repeated parameters between batches 2025-05-27 13:38:03 +02:00
Matthias f3d7f67671 Merge pull request #11803 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-05-27 06:31:54 +02:00
Freqtrade Bot 87001d0806 chore: update pre-commit hooks 2025-05-27 03:20:40 +00:00
Matthias ffce0dfb2e Merge pull request #11800 from freqtrade/dependabot/github_actions/develop/astral-sh/setup-uv-6.1.0
chore(deps): bump astral-sh/setup-uv from 6.0.1 to 6.1.0
2025-05-26 08:21:17 +02:00
Matthias c1d08019d9 Merge pull request #11798 from freqtrade/dependabot/pip/develop/ruff-0.11.11
chore(deps-dev): bump ruff from 0.11.10 to 0.11.11
2025-05-26 08:18:26 +02:00
Matthias bfa95ad976 Merge pull request #11799 from freqtrade/dependabot/pip/develop/joblib-1.5.1
chore(deps): bump joblib from 1.5.0 to 1.5.1
2025-05-26 08:14:39 +02:00
Matthias 4a4b39f898 Merge pull request #11795 from freqtrade/dependabot/pip/develop/pydantic-2.11.5
chore(deps): bump pydantic from 2.11.4 to 2.11.5
2025-05-26 08:12:58 +02:00
Matthias 215499a3f8 Merge pull request #11793 from freqtrade/dependabot/pip/develop/plotly-6.1.1
chore(deps): bump plotly from 6.0.1 to 6.1.1
2025-05-26 08:08:58 +02:00
Matthias 8c11974644 Merge pull request #11791 from freqtrade/dependabot/pip/develop/types-af298ab694
chore(deps-dev): bump types-cachetools from 5.5.0.20240820 to 6.0.0.20250525 in the types group
2025-05-26 08:08:33 +02:00
Matthias 7dcbd8108e Merge pull request #11797 from freqtrade/dependabot/pip/develop/cryptography-45.0.3
chore(deps): bump cryptography from 45.0.2 to 45.0.3
2025-05-26 07:58:27 +02:00
Matthias e00b74c0e0 fix: capture ws edge-case on reconnect 2025-05-26 07:06:56 +02:00
Matthias 5ef439dd0f Merge pull request #11794 from freqtrade/dependabot/pip/develop/cachetools-6.0.0
chore(deps): bump cachetools from 5.5.2 to 6.0.0
2025-05-26 07:00:18 +02:00
dependabot[bot] 538c7b3c97 chore(deps): bump joblib from 1.5.0 to 1.5.1
Bumps [joblib](https://github.com/joblib/joblib) from 1.5.0 to 1.5.1.
- [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.5.0...1.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 04:55:08 +00:00
Matthias 81456b2340 Merge pull request #11792 from freqtrade/dependabot/pip/develop/xgboost-3.0.2
chore(deps): bump xgboost from 3.0.1 to 3.0.2
2025-05-26 06:52:38 +02:00
Matthias dfec3f16ab chore: Bump pre-commit types-cachetools 2025-05-26 06:34:46 +02:00
dependabot[bot] e67499e6cc chore(deps): bump astral-sh/setup-uv from 6.0.1 to 6.1.0
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/6b9c6063abd6010835644d4c2e1bef4cf5cd0fca...f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 03:45:35 +00:00
dependabot[bot] a1b5e7899f chore(deps-dev): bump ruff from 0.11.10 to 0.11.11
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.10 to 0.11.11.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.10...0.11.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 03:13:45 +00:00
dependabot[bot] a88a149999 chore(deps): bump cryptography from 45.0.2 to 45.0.3
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.2 to 45.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.2...45.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 03:13:38 +00:00
dependabot[bot] c646abc05d chore(deps): bump pydantic from 2.11.4 to 2.11.5
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.4 to 2.11.5.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.4...v2.11.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 03:13:27 +00:00
dependabot[bot] f2a1561d31 chore(deps): bump cachetools from 5.5.2 to 6.0.0
Bumps [cachetools](https://github.com/tkem/cachetools) from 5.5.2 to 6.0.0.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v5.5.2...v6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 03:13:20 +00:00
dependabot[bot] d7a3aeaa1b chore(deps): bump plotly from 6.0.1 to 6.1.1
Bumps [plotly](https://github.com/plotly/plotly.py) from 6.0.1 to 6.1.1.
- [Release notes](https://github.com/plotly/plotly.py/releases)
- [Changelog](https://github.com/plotly/plotly.py/blob/main/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.py/compare/v6.0.1...v6.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 03:13:18 +00:00
dependabot[bot] 4b9a63de19 chore(deps): bump xgboost from 3.0.1 to 3.0.2
Bumps [xgboost](https://github.com/dmlc/xgboost) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/dmlc/xgboost/releases)
- [Changelog](https://github.com/dmlc/xgboost/blob/master/NEWS.md)
- [Commits](https://github.com/dmlc/xgboost/compare/v3.0.1...v3.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 03:13:10 +00:00
dependabot[bot] 07985c2cae chore(deps-dev): bump types-cachetools in the types group
Bumps the types group with 1 update: [types-cachetools](https://github.com/typeshed-internal/stub_uploader).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 03:12:22 +00:00
Robert Davey 0c13414f14 Slight rewording for clarity 2025-05-25 12:47:19 +01:00
Robert Davey 1021c7a7af Minor typo 2025-05-25 12:41:31 +01:00
Robert Davey 5306fb3709 Add clarification to the exit table 2025-05-25 12:40:11 +01:00
Matthias 1afddf9db4 docs: no dots on exit comparison 2025-05-25 10:01:39 +02:00
Matthias 929538dd5f docs: refactor exit logic comparisons to includes file
this might allow us to reuse this in other parts of the docs.
2025-05-25 09:44:52 +02:00
Matthias f432e65a14 docs: add "exit logic comparisons" table 2025-05-25 09:42:37 +02:00
Matthias dfc1193c52 Merge pull request #11656 from mrpabloyeah/add-custom-roi-strategy-callback
Add custom_roi() strategy callback
2025-05-25 09:13:32 +02:00
Matthias 25077a1008 feat: write empty "custom_roi" with advanced strategy template 2025-05-25 08:56:39 +02:00
Matthias b3b3bf6c1d feat: allow break-even (0.0) as valid return from custom_roi 2025-05-25 08:54:11 +02:00
Matthias a7ebbd89c0 feat: add strategy-ResultValidator to backtesting
helps issues such as #11781
2025-05-24 16:46:29 +02:00
Matthias ee25635c79 test: update tests for new StrategyValidator 2025-05-24 16:35:12 +02:00
Matthias b8e19ae78a refactor: move dataframe assertion to it's own class 2025-05-24 16:10:15 +02:00
Matthias 937bd892fd fix: workaround for ccxt bybit stop_order bug 2025-05-24 11:20:47 +02:00
Matthias adce6e4f68 fix: don't try to place stoploss orders with blocking assets 2025-05-24 11:20:47 +02:00
Robert Caulk 0b1ba0458d Merge pull request #11490 from freqtrade/fix/continual_learning_pytorch
Fix continual learning pytorch
2025-05-24 11:12:05 +02:00
Robert Caulk 7668b95900 Merge pull request #11588 from skyoo2003/develop
fix: remove spaces from rename_dict/rename_dict_old in BaseReinforcementLearningModel
2025-05-24 10:47:31 +02:00
Robert Caulk 47465f198c Merge pull request #11772 from freqtrade/dependabot/pip/develop/xgboost-3.0.1
chore(deps): bump xgboost from 2.1.4 to 3.0.1
2025-05-24 10:35:26 +02:00
Matthias f3d03dd26a feat: ensure uniqueness among fetched orders
closes #11786
2025-05-23 07:39:35 +02:00
Matthias ecdd84efd8 fix: "until" shouldn't be in the future.
part of #11786
2025-05-23 07:26:39 +02:00
Matthias 0ad6a6a951 docs: fix broken links 2025-05-22 20:25:59 +02:00
Matthias dc97f709d7 docs: exclude "import only" links 2025-05-22 20:23:11 +02:00
Matthias 34ccf141d1 test: enhance now_is_time_to_refresh test 2025-05-22 20:03:43 +02:00
Matthias f0b16c277c docs: add "sold the bots capital" FAQ entry 2025-05-22 19:22:59 +02:00
Matthias 68061444d8 chore: remove unused imports 2025-05-22 19:11:04 +02:00
Matthias 93356286d8 fix: Activate fetch-orders pagination for binance
potential fix for #11786
2025-05-22 19:10:53 +02:00
Matthias 84e816fc5b refactor: fetch_orders pagination to base class 2025-05-22 19:07:10 +02:00
Matthias 12a482a6ba Merge pull request #11785 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-05-22 06:25:35 +02:00
Freqtrade Bot a26a40d00f chore: update pre-commit hooks 2025-05-22 03:20:06 +00:00
Matthias 64c6b4718d Merge pull request #11758 from mrpabloyeah/add-max-trade-duration-to-backtest-results
Add min/max trade duration to backtest results
2025-05-21 20:45:24 +02:00
Matthias d1ebdf5df2 chore: remove polyfill.io from mkdocs 2025-05-21 19:36:15 +02:00
Matthias 496c4947bd chore: Fix lint error 2025-05-21 19:34:30 +02:00
Matthias 82f98746bd test: update test for new behavior 2025-05-21 19:27:23 +02:00
hippocritical 65418c7c00 changed the text a bit 2025-05-20 21:32:21 +02:00
hippocritical bbf6bade7c Fixed a bug where the pairlist was just .*/USDT (with a length of 1.)
The bug happened since it just checked the length of the list itself, not what it represents. in this case .*/USDT could be any amount of pairs

when the user sets a max_open_trades of let's say 3 then the pairs only have 3 trade slots of whatever amount of pairs it really has and thereby creating a bottleneck.

This just sets the max_open_trades to -1 without even checking it, letting freqtrade itself handle the amount of trades allowed at a given time.
2025-05-20 21:30:41 +02:00
Matthias b6cde05f0d chore: remove unused import 2025-05-20 19:39:07 +02:00
Matthias 0a3a1d7eca fix: remove unnecessary kraken workaround
fixes "list-pairs" command for kraken.
2025-05-20 19:22:05 +02:00
Matthias 947cbdd858 test: add format_duration test 2025-05-20 18:15:54 +02:00
Matthias 8c999cd60c chore: treat webhook.url as secret 2025-05-20 07:05:08 +02:00
Matthias ad5d2a36bc feat: Further ENV recommendations to config schema 2025-05-20 07:05:08 +02:00
Matthias 1666746692 feat: recommend setting secrets via environment variable 2025-05-20 07:05:08 +02:00
Matthias 3a74fc85b4 chore: update schema.json 2025-05-20 07:05:08 +02:00
Matthias 92a1c304a8 feat: add additional supported keys to config schema 2025-05-20 07:05:08 +02:00
Matthias d8c24663fc Merge pull request #11736 from viotemp1/optuna_addons
add early stopping for hyperopt
2025-05-20 06:45:15 +02:00
Matthias 195c15c8dc Merge pull request #11778 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-05-20 06:27:41 +02:00
Freqtrade Bot 44c06f6d20 chore: update pre-commit hooks 2025-05-20 03:20:51 +00:00
Matthias c268f20d18 Merge pull request #11776 from freqtrade/dependabot/pip/develop/sqlalchemy-2.0.41
chore(deps): bump sqlalchemy from 2.0.40 to 2.0.41
2025-05-19 16:26:48 +02:00
Matthias 83c92f3ac1 chore: Bump sqlalchemy in pre-commit 2025-05-19 14:36:11 +02:00
dependabot[bot] b4a3eccfa3 chore(deps): bump sqlalchemy from 2.0.40 to 2.0.41
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.40 to 2.0.41.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 11:59:40 +00:00
Matthias 837d006e11 Merge pull request #11774 from freqtrade/dependabot/pip/develop/ccxt-4.4.82
chore(deps): bump ccxt from 4.4.80 to 4.4.82
2025-05-19 13:58:26 +02:00
dependabot[bot] c1f1639807 chore(deps): bump ccxt from 4.4.80 to 4.4.82
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.80 to 4.4.82.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.4.80...v4.4.82)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 06:29:16 +00:00
Matthias 8f71e45666 Merge pull request #11775 from freqtrade/dependabot/pip/develop/cryptography-45.0.2
chore(deps): bump cryptography from 44.0.3 to 45.0.2
2025-05-19 08:28:06 +02:00
Matthias 17fa2c9933 Merge pull request #11768 from freqtrade/dependabot/pip/develop/types-462239796a
chore(deps-dev): bump the types group with 2 updates
2025-05-19 08:10:35 +02:00
dependabot[bot] 22fdf157ef chore(deps): bump cryptography from 44.0.3 to 45.0.2
Bumps [cryptography](https://github.com/pyca/cryptography) from 44.0.3 to 45.0.2.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/44.0.3...45.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 05:10:41 +00:00
Matthias 6c8b39d32a Merge pull request #11770 from freqtrade/dependabot/pip/develop/python-telegram-bot-22.1
chore(deps): bump python-telegram-bot from 22.0 to 22.1
2025-05-19 07:09:30 +02:00
Matthias 5a2b3d9d87 chore: show the actually used value 2025-05-19 07:04:11 +02:00
Matthias 8641796d04 test: Add test for early-stop config adjustment 2025-05-19 07:03:41 +02:00
Matthias c5c5cfb4d5 Merge pull request #11771 from freqtrade/dependabot/pip/develop/ruff-0.11.10
chore(deps-dev): bump ruff from 0.11.9 to 0.11.10
2025-05-19 06:53:50 +02:00
Matthias c74033d7f1 chore: bump types for pre-commit 2025-05-19 06:49:45 +02:00
Matthias d8c6873ee9 Merge pull request #11769 from freqtrade/dependabot/pip/develop/mkdocs-90ecda04c5
chore(deps): bump mkdocs-material from 9.6.13 to 9.6.14 in the mkdocs group
2025-05-19 06:34:43 +02:00
Matthias b0cc2a2a1d chore: slightly improve comment wording 2025-05-19 06:32:09 +02:00
Matthias 226f9b7857 Merge pull request #11766 from AchmadFathoni/develop
docs: trades space not included in default spaces
2025-05-19 06:31:11 +02:00
dependabot[bot] 1abf72855e chore(deps): bump xgboost from 2.1.4 to 3.0.1
Bumps [xgboost](https://github.com/dmlc/xgboost) from 2.1.4 to 3.0.1.
- [Release notes](https://github.com/dmlc/xgboost/releases)
- [Changelog](https://github.com/dmlc/xgboost/blob/master/NEWS.md)
- [Commits](https://github.com/dmlc/xgboost/compare/v2.1.4...v3.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 03:10:25 +00:00
dependabot[bot] b044410e42 chore(deps-dev): bump ruff from 0.11.9 to 0.11.10
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.9 to 0.11.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/0.11.9...0.11.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 03:10:15 +00:00
dependabot[bot] b64052850f chore(deps): bump python-telegram-bot from 22.0 to 22.1
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 22.0 to 22.1.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v22.0...v22.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 03:10:08 +00:00
dependabot[bot] 8868b10042 chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 03:10:04 +00:00
dependabot[bot] b18190891d chore(deps-dev): bump the types group with 2 updates
Bumps the types group with 2 updates: [types-requests](https://github.com/typeshed-internal/stub_uploader) and [types-python-dateutil](https://github.com/typeshed-internal/stub_uploader).


Updates `types-requests` from 2.32.0.20250328 to 2.32.0.20250515
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 03:09:28 +00:00
Achmad Fathoni 5b001eec3e docs: trades space not included in default spaces 2025-05-18 21:26:57 +07:00
mrpabloyeah 882ca44816 Fix test_optimize_reports 2025-05-18 14:19:10 +02:00
mrpabloyeah 60157606fe Format only winner/loser durations 2025-05-18 13:39:50 +02:00
mrpabloyeah cbb0294cd2 Format duration to make results more readable 2025-05-18 13:02:47 +02:00
Matthias 1b4802ab73 chore: Improved wording in issue templates 2025-05-18 08:38:27 +02:00
Matthias 6b7f210ef3 chore: Improve bug report template about AI
AI generated strategies and configs cause a lot of noise.
we should be clear that people shall read the documentation first
(this will also allow us to point people at this within the issue).
2025-05-18 08:35:59 +02:00
Matthias 571b154345 Merge pull request #11763 from emmanuel-ferdman/develop
fix: display pair information in message
2025-05-17 19:37:09 +02:00
Emmanuel Ferdman 9a2b5a1219 Display pair information in message
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-05-17 07:51:03 -07:00
Matthias fe9de41d58 docs: fix Note boxes not rendering correctly 2025-05-17 16:16:27 +02:00
Matthias daff50e79e test: improve admotion test by skipping empty lines 2025-05-17 16:16:19 +02:00
Matthias 09b687d66f test: add test checking proper admotion logic 2025-05-17 16:12:19 +02:00
Matthias 665ff9f14e test: extend test_docs for collapsed info boxes 2025-05-17 15:36:32 +02:00
viotemp1 bd1a12ceaa fix formatting 2025-05-17 16:27:33 +03:00
viotemp1 d33e931a0d early stop - replace values lower than 20 with 20 and display a warning. 2025-05-17 16:23:25 +03:00
mrpabloyeah fbe38c362c Fix typo 2025-05-17 15:10:04 +02:00
mrpabloyeah 718efc828a Also add min trade duration and display the info horizontally 2025-05-17 14:22:25 +02:00
mrpabloyeah 6d3ed84807 Add max trade duration to backtest results 2025-05-17 02:50:10 +02:00
viotemp1 28e5efc902 fix docs 2025-05-17 01:15:10 +03:00
Matthias 510c47bd78 docs: update doc wording in custom_exit callback 2025-05-16 06:39:24 +02:00
Matthias 2e0065216f Merge pull request #11755 from Silur/develop
fix binance futures candle type hardcoding for trades data
2025-05-15 19:52:58 +02:00
silur ce024c74e1 fix binance futures candle type hardcoding 2025-05-15 21:31:01 +04:00
mrpabloyeah 90e2935abf Add trade_duration parameter to custom_roi 2025-05-15 11:53:48 +02:00
mrpabloyeah 12cd654bce Merge branch 'freqtrade:develop' into add-custom-roi-strategy-callback 2025-05-15 10:14:53 +02:00
Matthias c3ea793abb Merge branch 'develop' into add-custom-roi-strategy-callback 2025-05-15 07:22:25 +02:00
Matthias c901f6d1dd test: add test for custom_roi behavior 2025-05-15 07:22:18 +02:00
Matthias 193cfb634c Merge pull request #11754 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-05-15 06:28:13 +02:00
Freqtrade Bot a2b7620b64 chore: update pre-commit hooks 2025-05-15 03:19:37 +00:00
Matthias 9d91a4a298 fix: default max_open_trades to inf instead of -1
Without this, the auto-conversion doesn't backpopulate to the config

closes #11752
2025-05-14 20:14:10 +02:00
Matthias 3b6dbaccf8 test: add failing test for #11752 2025-05-14 20:12:33 +02:00
Matthias 3fe9e6b82b fix: don't use exchange.precision_mode_price
closes #11751
2025-05-14 06:59:51 +02:00
Matthias b6614045aa Merge pull request #11738 from snussik/patch-1
Update strategy-callbacks.md
2025-05-13 20:03:57 +02:00
Matthias cbf27f0bdb docs: Simplify doc examples 2025-05-13 19:38:06 +02:00
Matthias deafe39f76 Merge pull request #11750 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-05-13 07:03:13 +02:00
Freqtrade Bot 1f63eca1a8 chore: update pre-commit hooks 2025-05-13 03:20:47 +00:00
Matthias 2c0db908b5 Merge pull request #11747 from nisuhw/patch-1
Update installation.md for older Raspberry Pi 2
2025-05-12 13:14:58 +02:00
nisuhw ca9403f981 Update installation.md for older Raspberry Pi 2
I got the following error on Pi 2 (using Debian Bookworm armhf arch)
```
...
            running bdist_wheel
            running build
            running build_py
            creating build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/lock.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/backend_ctypes.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/_imp_emulation.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/model.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/ffiplatform.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/setuptools_ext.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/error.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/vengine_gen.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/api.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/__init__.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/recompiler.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/cffi_opcode.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/pkgconfig.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/verifier.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/cparser.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/_shimmed_dist_utils.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/commontypes.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/vengine_cpy.py -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/_cffi_include.h -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/parse_c_type.h -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/_embedding.h -> build/lib.linux-armv7l-cpython-311/cffi
            copying src/cffi/_cffi_errors.h -> build/lib.linux-armv7l-cpython-311/cffi
            running build_ext
            building '_cffi_backend' extension
            creating build/temp.linux-armv7l-cpython-311/src/c
            arm-linux-gnueabihf-gcc -Wsign-compare -DNDEBUG -g -fwrapv -O2 -Wall -g -fstack-protector-strong -Wformat -Werror=format-security -g -fwrapv -O2 -fPIC -DFFI_BUILDING=1 -DUSE__THREAD -DHAVE_SYNC_SYNCHRONIZE -I/usr/include/ffi -I/usr/include/libffi -I/freqtrade/.venv/include -I/usr/include/python3.11 -c src/c/_cffi_backend.c -o build/temp.linux-armv7l-cpython-311/src/c/_cffi_backend.o
            src/c/_cffi_backend.c:15:10: fatal error: ffi.h: No such file or directory
               15 | #include <ffi.h>
                  |          ^~~~~~~
            compilation terminated.
            error: command '/usr/bin/arm-linux-gnueabihf-gcc' failed with exit code 1
            [end of output]
      
        note: This error originates from a subprocess, and is likely not a problem with pip.
        ERROR: Failed building wheel for cffi
      Failed to build cffi
      ERROR: Failed to build installable wheels for some pyproject.toml based projects (cffi)
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error

× pip subprocess to install build dependencies did not run successfully.
│ exit code: 1
╰─> See above for output.

note: This error originates from a subprocess, and is likely not a problem with pip.
Failed installing dependencies
```

It can be Easily solved by installing libffi-dev before hand
2025-05-12 16:01:25 +07:00
Matthias a8d96087c1 Merge pull request #11743 from freqtrade/dependabot/pip/develop/ccxt-4.4.80
chore(deps): bump ccxt from 4.4.78 to 4.4.80
2025-05-12 08:50:16 +02:00
Matthias 1649505121 Merge pull request #11741 from freqtrade/dependabot/pip/develop/optuna-4.3.0
chore(deps): bump optuna from 4.2.1 to 4.3.0
2025-05-12 08:03:05 +02:00
Matthias 41a063e2ac Merge pull request #11745 from freqtrade/dependabot/pip/develop/datasieve-0.1.9
chore(deps): bump datasieve from 0.1.7 to 0.1.9
2025-05-12 07:45:57 +02:00
dependabot[bot] ba019ad143 chore(deps): bump optuna from 4.2.1 to 4.3.0
Bumps [optuna](https://github.com/optuna/optuna) from 4.2.1 to 4.3.0.
- [Release notes](https://github.com/optuna/optuna/releases)
- [Commits](https://github.com/optuna/optuna/compare/v4.2.1...v4.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 05:35:28 +00:00
Matthias 6f3071f6ce Merge pull request #11742 from freqtrade/dependabot/pip/develop/scipy-1.15.3
chore(deps): bump scipy from 1.15.2 to 1.15.3
2025-05-12 07:34:16 +02:00
Matthias 5cdb2dd601 Merge pull request #11744 from freqtrade/dependabot/pip/develop/ruff-0.11.9
chore(deps-dev): bump ruff from 0.11.8 to 0.11.9
2025-05-12 07:21:22 +02:00
Matthias 4db41f0225 Merge pull request #11740 from freqtrade/dependabot/pip/develop/mkdocs-b2e01260d9
chore(deps): bump mkdocs-material from 9.6.12 to 9.6.13 in the mkdocs group
2025-05-12 06:46:32 +02:00
Matthias cc8581164d Merge pull request #11739 from freqtrade/dependabot/pip/develop/pytest-5fb0fe6743
chore(deps-dev): bump pytest-timeout from 2.3.1 to 2.4.0 in the pytest group
2025-05-12 06:26:25 +02:00
dependabot[bot] 7d50353697 chore(deps): bump datasieve from 0.1.7 to 0.1.9
Bumps [datasieve](https://github.com/emergentmethods/datasieve) from 0.1.7 to 0.1.9.
- [Release notes](https://github.com/emergentmethods/datasieve/releases)
- [Commits](https://github.com/emergentmethods/datasieve/compare/v0.1.7...v0.1.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 03:43:51 +00:00
dependabot[bot] d742f8b88f chore(deps-dev): bump ruff from 0.11.8 to 0.11.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.8 to 0.11.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.8...0.11.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 03:43:39 +00:00
dependabot[bot] 22bf036a66 chore(deps): bump ccxt from 4.4.78 to 4.4.80
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.78 to 4.4.80.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.4.78...v4.4.80)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 03:43:28 +00:00
dependabot[bot] c45351beff chore(deps): bump scipy from 1.15.2 to 1.15.3
Bumps [scipy](https://github.com/scipy/scipy) from 1.15.2 to 1.15.3.
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](https://github.com/scipy/scipy/compare/v1.15.2...v1.15.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 03:43:14 +00:00
dependabot[bot] 6d1d0751e8 chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 03:42:57 +00:00
dependabot[bot] 15012ec58c chore(deps-dev): bump pytest-timeout in the pytest group
Bumps the pytest group with 1 update: [pytest-timeout](https://github.com/pytest-dev/pytest-timeout).


Updates `pytest-timeout` from 2.3.1 to 2.4.0
- [Commits](https://github.com/pytest-dev/pytest-timeout/compare/2.3.1...2.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 03:42:46 +00:00
snussik fed91ff5a1 Update strategy-callbacks.md
Taking leverage into account https://github.com/freqtrade/freqtrade-strategies/issues/322
2025-05-11 17:58:10 +03:00
viotemp1 5b92af6a90 fix some spelling errors 2025-05-10 19:45:16 +03:00
viotemp1 442a1ba50d add early stopping for hyperopt 2025-05-10 19:36:48 +03:00
Matthias c3f6aa17ee Merge pull request #11711 from freqtrade/feat/bt_historic_precision
Improve price precision logic and add significant digits calculation
2025-05-10 16:03:38 +02:00
Matthias 8dc278f1a0 refactor: simplify ws exchange handling 2025-05-10 13:30:26 +02:00
Matthias 5f907d4219 chore: move candletype check to can_use_websocket. 2025-05-10 11:43:52 +02:00
Matthias c39c5f254f Extract "can use websocket" method 2025-05-10 11:31:30 +02:00
Matthias 0dfc4ed696 refactor: extract websocket builder logic to it's own function 2025-05-10 11:24:54 +02:00
Matthias 950a0df8b1 docs: improved documentation of order object 2025-05-10 08:38:45 +02:00
Matthias 2a0dd4cf55 Merge pull request #11558 from viotemp1/optuna
switch hyperopt from scikit-optimize to  Optuna
2025-05-09 09:53:13 +02:00
Matthias 575c381e65 chore: fix mypy error 2025-05-09 09:36:08 +02:00
Matthias 87061bcce8 docs: adopt autosampler example as advanced hyperopt approach 2025-05-09 06:42:37 +02:00
Matthias a6d3995013 feat: Improved typing, exception if neither step nor decimals is set 2025-05-09 06:34:59 +02:00
Matthias d0d40f4fce test: Improve skdecimal test 2025-05-09 06:34:33 +02:00
Matthias ce7d81325d chore: improved docstring for skdecimal 2025-05-09 06:32:43 +02:00
Matthias 7cfecab7e2 Merge branch 'develop' into optuna 2025-05-08 19:25:33 +02:00
Matthias 8f8da51808 feat: round hyperopt results to 13 digits (this removes floating point errors) 2025-05-08 19:24:31 +02:00
Matthias f94fd7d5fc chore: minor formatting 2025-05-08 07:07:22 +02:00
Matthias befc41ae54 Merge pull request #11717 from freqtrade/dependabot/pip/develop/certifi-2025.4.26
chore(deps): bump certifi from 2025.01.31 to 2025.4.26
2025-05-08 07:03:09 +02:00
dependabot[bot] 02092926ab chore(deps): bump certifi from 2025.01.31 to 2025.4.26
Bumps [certifi](https://github.com/certifi/python-certifi) from 2025.01.31 to 2025.4.26.
- [Commits](https://github.com/certifi/python-certifi/compare/2025.01.31...2025.04.26)

---
updated-dependencies:
- dependency-name: certifi
  dependency-version: 2025.4.26
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-08 04:44:37 +00:00
Matthias a49b4ab93a Merge pull request #11731 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-05-08 06:23:36 +02:00
Freqtrade Bot bd7549bc8b chore: update pre-commit hooks 2025-05-08 03:19:45 +00:00
viotemp1 47e1d209db round to decimals low and high 2025-05-08 05:49:34 +03:00
viotemp1 5d2f5ec12f change SKDecimal low/high to be rounded by decimals 2025-05-07 21:57:56 +03:00
viotemp1 2d2dc7f14a Merge remote-tracking branch 'refs/remotes/origin/optuna' into optuna 2025-05-07 17:25:14 +03:00
viotemp1 43bd2a060c fix optuna userwarning the range is not divisible by step 2025-05-07 17:20:17 +03:00
Matthias b518d66aa2 chore: improve docstring for get_Trades_proxy. 2025-05-06 19:37:06 +02:00
Matthias 2ba59cebe3 Merge pull request #11724 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2025-05-06 06:33:06 +02:00
Freqtrade Bot f165c85e69 chore: update pre-commit hooks 2025-05-06 03:18:59 +00:00
Matthias fca454bba3 Merge pull request #11708 from hippocritical/develop
lookahead-analysis docs changed to better explain the output table
2025-05-05 19:52:45 +02:00
Matthias 3dc1adfad4 docs: slight formatting tweaks 2025-05-05 19:35:04 +02:00
Robert Davey 38754e0598 Small tweaks to lookahead docs 2025-05-05 11:50:29 +01:00
Matthias 8fb4446d82 Merge pull request #11716 from freqtrade/dependabot/pip/develop/cryptography-44.0.3
chore(deps): bump cryptography from 44.0.2 to 44.0.3
2025-05-05 09:37:47 +02:00
Matthias 3725ac145b Merge pull request #11720 from freqtrade/dependabot/pip/develop/humanize-4.12.3
chore(deps): bump humanize from 4.12.2 to 4.12.3
2025-05-05 08:08:13 +02:00
Matthias 7df0d06005 Merge pull request #11719 from freqtrade/dependabot/pip/develop/orjson-3.10.18
chore(deps): bump orjson from 3.10.16 to 3.10.18
2025-05-05 08:00:50 +02:00
Matthias 4e855b638b Merge pull request #11721 from freqtrade/dependabot/github_actions/develop/astral-sh/setup-uv-6.0.1
chore(deps): bump astral-sh/setup-uv from 6.0.0 to 6.0.1
2025-05-05 07:32:06 +02:00
Matthias 9783aba09c Merge pull request #11718 from freqtrade/dependabot/pip/develop/pydantic-2.11.4
chore(deps): bump pydantic from 2.11.3 to 2.11.4
2025-05-05 07:20:59 +02:00
Matthias 1936b20309 Merge pull request #11715 from freqtrade/dependabot/pip/develop/ruff-0.11.8
chore(deps-dev): bump ruff from 0.11.7 to 0.11.8
2025-05-05 06:58:02 +02:00
dependabot[bot] 69de9687d0 chore(deps): bump cryptography from 44.0.2 to 44.0.3
Bumps [cryptography](https://github.com/pyca/cryptography) from 44.0.2 to 44.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/44.0.2...44.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 04:49:45 +00:00
Matthias bdeeca7d43 Merge pull request #11714 from freqtrade/dependabot/pip/develop/ccxt-4.4.78
chore(deps): bump ccxt from 4.4.77 to 4.4.78
2025-05-05 06:48:36 +02:00
Matthias a7ba3cad85 Merge pull request #11713 from freqtrade/dependabot/pip/develop/joblib-1.5.0
chore(deps): bump joblib from 1.4.2 to 1.5.0
2025-05-05 06:30:45 +02:00
dependabot[bot] ef5fcad8cf chore(deps): bump astral-sh/setup-uv from 6.0.0 to 6.0.1
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/c7f87aa956e4c323abf06d5dec078e358f6b4d04...6b9c6063abd6010835644d4c2e1bef4cf5cd0fca)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 03:57:55 +00:00
dependabot[bot] d8de6c8f7b chore(deps): bump humanize from 4.12.2 to 4.12.3
Bumps [humanize](https://github.com/python-humanize/humanize) from 4.12.2 to 4.12.3.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/4.12.2...4.12.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 03:26:59 +00:00
dependabot[bot] c3239da266 chore(deps): bump orjson from 3.10.16 to 3.10.18
Bumps [orjson](https://github.com/ijl/orjson) from 3.10.16 to 3.10.18.
- [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.16...3.10.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 03:26:54 +00:00
dependabot[bot] 1edf948e49 chore(deps): bump pydantic from 2.11.3 to 2.11.4
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.3 to 2.11.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.11.3...v2.11.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 03:26:49 +00:00
dependabot[bot] 224c4a717b chore(deps-dev): bump ruff from 0.11.7 to 0.11.8
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.7 to 0.11.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.7...0.11.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 03:26:29 +00:00
dependabot[bot] 6202436bfd chore(deps): bump ccxt from 4.4.77 to 4.4.78
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.77 to 4.4.78.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/v4.4.77...v4.4.78)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 03:26:19 +00:00
dependabot[bot] 122144857e chore(deps): bump joblib from 1.4.2 to 1.5.0
Bumps [joblib](https://github.com/joblib/joblib) from 1.4.2 to 1.5.0.
- [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.2...1.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 03:26:13 +00:00
Matthias 92bcf672fb docs: document consumer's initial_candle_limit 2025-05-04 19:53:09 +02:00
Matthias 6f4ffa0758 chore: minor cleanup 2025-05-04 19:32:09 +02:00
Matthias 82186c8a7f docs: fix typo in docs causing note-box to not render 2025-05-04 19:26:26 +02:00
hippocritical 50596e1a6a fixed typo 2025-05-04 17:53:23 +02:00
hippocritical 9edcbbb9a8 Changed formatting to be more readable in source
Added
- How to find and remove bias? How can I salvage the strategy?
- Examples of lookahead-bias
2025-05-04 16:59:24 +02:00
Matthias a59805d739 docs: small formatting 2025-05-04 14:05:23 +02:00
Robert Davey b96c06ffdc Update lookahead-analysis.md
Englishified.
2025-05-04 12:34:48 +01:00
Matthias 8abe358c1e docs: improved rendering 2025-05-04 11:57:03 +02:00
Matthias 2142b2aea3 chore: fix type-check 2025-05-04 11:49:03 +02:00
Matthias 53ba3ced06 test: add test for backtest "get price precision" logic 2025-05-04 11:47:42 +02:00
hippocritical a0f336c31f changed lookahead-analysis.md to better explain the output of the command 2025-05-03 22:12:25 +02:00
Matthias 081fa2209a refactor: correct naming of new function 2025-05-03 20:45:52 +02:00
Matthias ca67d3fb2c chore: improve price_precision logic resiliancy 2025-05-03 20:44:14 +02:00
Matthias 5a6b43da46 feat: use hsitoric price precision for improved accuracy
closes #11203
2025-05-03 20:37:57 +02:00
Matthias 7d3fa41911 test: add tests for get_significant_digits_over_time 2025-05-03 20:30:25 +02:00
Matthias b8f3f5e8d2 fix: use month-start for the resample 2025-05-03 20:28:10 +02:00
Matthias 4849d5413f feat: add function to count "significant digits over time". 2025-05-03 20:14:37 +02:00
Matthias 8b8bf6f97d refactor: automatically load detailed bt data 2025-05-03 20:01:13 +02:00
viotemp1 9eea958e17 remove unwrap 2025-05-03 16:55:41 +03:00
Matthias f8a151c2a5 feat: Add funding_fee to backtest results
closes #11699
2025-05-03 14:14:32 +02:00
Matthias 1013c32316 fix: duplicate generate_optimizer to have a non-delayed alternative 2025-05-03 09:19:50 +02:00
viotemp1 d9ed7e1fb2 remove backtest from assign_params 2025-05-03 09:49:34 +03:00
Matthias 689da4c479 Merge pull request #11158 from freqtrade/feat/plot_annotations
add support for plot_annotations
2025-05-03 08:25:57 +02:00
Matthias 87d954a322 refactor: fix variable typo 2025-05-03 08:03:16 +02:00
Matthias 74f601b352 docs: Improved plot annotations sample 2025-05-03 08:02:09 +02:00
Matthias a3359b62d4 feat: validate annotation-type before returning
this avoids breaking the whole chart due to annotations.
2025-05-02 20:58:13 +02:00
Matthias 064708a354 feat: validate annotations and only return valid ones to avoid breaking the whole chart 2025-05-02 20:45:42 +02:00
Matthias 066a46d7aa Switch annotationType to Pydantic 2025-05-02 20:41:43 +02:00
Matthias 333660e628 feat: improved doc-string for plot-annotations 2025-05-02 20:41:30 +02:00
Matthias 81f80a6b22 docs: add visual documentation for plot annotations 2025-05-02 20:28:32 +02:00
viotemp1 dd613ac86c fix formatting 2025-05-02 21:07:34 +03:00
viotemp1 73c28890d7 move @delayed and @wrap_non_picklable_objects in hyperopt_optimizer.py
one test with analyze_per_epoch is failing
2025-05-02 21:03:51 +03:00
Matthias 61dd94ceb0 test: add tests for annotations feature 2025-05-02 19:59:23 +02:00
Matthias 1fec5a873e Merge branch 'develop' into feat/plot_annotations 2025-05-02 19:48:50 +02:00
Matthias 9c4abcc927 refactor: improve variable naming 2025-05-02 13:17:16 +02:00
Matthias b74e38ef63 refactor: improve variable naming 2025-05-02 13:17:03 +02:00
Matthias 86f640627f Merge pull request #11692 from freqtrade/reenable_htx
chore(ci): reenable htx online tests
2025-05-01 16:46:29 +02:00
Matthias e27568a9d8 chore: pin certifi until upstream issue is fixed
problematic point: https://github.com/certifi/python-certifi/issues/349
2025-05-01 16:22:47 +02:00
Matthias 149133cc44 refactor: remove _get_params_dict method 2025-05-01 12:03:04 +02:00
Matthias 1b2d5a357f test: change level of test mock 2025-05-01 11:57:17 +02:00
Matthias ecc6371733 test: reduce amount of mocking 2025-05-01 10:19:20 +02:00
Matthias 0c66180cf3 chore: add optuna to hyperopt optional dependencies 2025-05-01 10:15:35 +02:00
Matthias 5954c64cc3 chore: use ubuntu 24.04 for online tests 2025-05-01 08:55:35 +02:00
Matthias d0b7a52202 Merge pull request #11703 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2025-05-01 08:10:03 +02:00
Freqtrade Bot c18085d173 chore: update pre-commit hooks 2025-05-01 03:22:32 +00:00
Matthias 67f88ea97a Merge pull request #11681 from JamesLinxun/develop
skip trade-related columns
2025-04-30 17:59:54 +02:00
Matthias 14353afa78 chore: bump develop version to 2025.5-dev 2025-04-30 07:02:57 +02:00
James Lin a77618cc6b fix 2025-04-29 12:09:13 -04:00
JamesLinxun cac2c363fa Merge branch 'freqtrade:develop' into develop 2025-04-29 10:18:13 -04:00
viotemp1 ced1ce340a fix some formatting issues 2025-04-29 17:09:16 +03:00
James Lin 1d99b7d0f9 move ORDERFLOW_ADDED_COLUMNS to constant 2025-04-29 09:18:07 -04:00
viotemp1 5c47a75f63 move back delayed and wrap_non_picklable_objects from hyperopt_optimizer to hyperopt.
There are tests failing when using delayed and wrap_non_picklable_objects as decorator.
until I'll find a solution to run generate_optimizer standalone for analyze_per_epoch=True
2025-04-29 08:16:44 +03:00
Matthias 1ef2eeb456 chore(ci): reenable htx online tests through proxy 2025-04-29 06:58:59 +02:00
viotemp1 c32f8e972a move delayed and wrap_non_picklable_objects from hyperopt to hyperopt_optimizer 2025-04-28 18:50:09 +03:00
viotemp1 04492e75b2 remove # Suppress optuna ExperimentalWarning from skopt
with warnings.catch_warnings():
    from optuna.exceptions import ExperimentalWarning
    warnings.filterwarnings("ignore", category=FutureWarning)
this should be when importing sampler
2025-04-27 22:02:39 +03:00
viotemp1 8d0ca7f5c1 remove backtesting from generate_optimizer 2025-04-27 21:26:06 +03:00
James Lin 0a4f82bc22 update columns from ORDERFLOW_ADDED_COLUMNS 2025-04-26 10:24:53 -04:00
James Lin 5ee7f8037d fix 2025-04-26 10:20:09 -04:00
James Lin 12728c0fcc fix 2025-04-26 10:18:39 -04:00
James Lin 362f0895f4 skip trade-related columns 2025-04-25 16:13:33 -04:00
Matthias fb64ac942b chore: raise freqtrade exception so upstream handling is in place 2025-04-23 20:18:58 +02:00
Matthias 3fc40f45b3 chore: simplify diff in hyperopt-tinterface
Use aliases where possible.
2025-04-23 20:15:24 +02:00
Matthias ca5ccc8799 chore: cleanup some code 2025-04-23 20:07:40 +02:00
Matthias 057cc2538e chore: use optuna distribution aliases in parameters 2025-04-23 20:02:58 +02:00
Matthias ed22789a1a chore: cleanup unused import 2025-04-23 19:53:07 +02:00
Matthias 2abf22e37b chore: simplify usage of data_pickle_file 2025-04-23 19:50:17 +02:00
Matthias 9b08b51ad8 chore: cleanup dead code 2025-04-23 19:50:17 +02:00
Matthias 83cdf76636 chore: simplify import/exports 2025-04-23 19:50:17 +02:00
Matthias f545113840 chore: improve EstimatorType type 2025-04-23 19:50:17 +02:00
Matthias f86bc71c43 chore: cleanup some test code 2025-04-23 19:19:55 +02:00
Matthias 8af9875d45 docs: remove scikit-optimize references from docs 2025-04-23 19:17:43 +02:00
Matthias 8a9b31eccd chore: remove scikit-optimize dependency 2025-04-23 19:16:17 +02:00
mrpabloyeah b4a5f66f29 Return the trade duration along with the profit threshold returned by custom_roi 2025-04-20 13:39:35 +02:00
mrpabloyeah e105ea660b Improve documentation 2025-04-20 13:16:27 +02:00
mrpabloyeah 1149393789 Fix LocalTrade compatibility correctly 2025-04-20 01:33:34 +02:00
mrpabloyeah d4655660f4 Fix LocalTrade compatibility in custom_roi and min_roi_reached_entry 2025-04-19 16:36:13 +02:00
mrpabloyeah e656063771 Add custom_roi strategy callback 2025-04-19 15:42:44 +02:00
viotemp1 20fca07d8f fixed mypy errors
freqtrade/optimize/space/optunaspaces.py:39: error: Argument 1 to "__init__" of "IntDistribution" has incompatible type "int | float"; expected "int"  [arg-type]
	freqtrade/optimize/space/optunaspaces.py:39: error: Argument 2 to "__init__" of "IntDistribution" has incompatible type "int | float"; expected "int"  [arg-type]
remove all references for ExtraTreesRegressor and skopt.space
2025-04-13 12:39:44 +03:00
viotemp1 8ee40ade45 update docs/advanced-hyperopt.md 2025-04-13 11:27:33 +03:00
viotemp1 30ead79e11 fixed freqtrade/optimize/space/__init__.py:1:66: RUF100 [*] Unused noqa directive (unused: F401) 2025-04-13 11:22:07 +03:00
viotemp1 5c859d929b add optunaspaces.py 2025-04-13 11:06:39 +03:00
viotemp1 c89058788e remove decimal module 2025-04-13 09:49:33 +03:00
viotemp1 35c3868c56 change for SKDecimal and othercomments 2025-04-13 08:58:42 +03:00
Matthias 85689ebc1c test: update skdecimal test to use optuna 2025-04-12 12:37:17 +02:00
Matthias 4fcc9dd587 feat: use floatDistribution for SKDecimal 2025-04-12 12:10:48 +02:00
Matthias 05f19d574a chore: remove commented skopt usages 2025-04-12 10:13:42 +02:00
Matthias 1a24559729 chore: cleanup old usages 2025-04-12 10:08:41 +02:00
Matthias 7a51c9d540 types: slightly improved typing 2025-04-12 10:05:11 +02:00
Matthias 1d22377cad chore: remove some skopt usages 2025-04-12 09:58:44 +02:00
viotemp1 90aaaa50fc fix increasing memory usage. 2025-04-02 18:45:49 +03:00
Sung-Kyu Yoo bffb3120c1 fix: remove space in rename_dict/rename_dict_old 2025-04-01 22:18:26 +09:00
viotemp1 3fcf6559ab change from skopt.space.Real to optuna.distributions.FloatDistribution 2025-03-31 13:48:12 +03:00
viotemp1 85f4a8daea fix formatting 2025-03-31 00:10:52 +03:00
viotemp1 2595479e43 change CategoricalParameter and IntParameter in parameters.py to use optuna.distributions CategoricalDistribution and IntDistribution instead of skopt 2025-03-30 21:13:15 +03:00
viotemp1 b603771242 Merge branch 'freqtrade:develop' into optuna 2025-03-30 16:19:16 +03:00
viotemp1 9c1183bc59 fix formatting requirements-hyperopt.txt 2025-03-28 08:12:37 +02:00
viotemp1 6b78b1c882 fix formatting 2025-03-26 21:30:18 +02:00
viotemp1 ee3d46c8fa change default optimizer to NSGAIIISampler - best results so far 2025-03-26 21:25:56 +02:00
viotemp1 59e52bb601 fix type-errors by declaring the type of o_dimensions in convert_dimensions_to_optuna_space 2025-03-26 16:55:43 +02:00
viotemp1 553dbccec7 simplify get_optimizer 2025-03-26 16:42:09 +02:00
viotemp1 2e06eb0e7b update docs for optuna sampler 2025-03-26 08:46:05 +02:00
viotemp1 31c4d35932 update docs for optuna sampler 2025-03-26 08:44:46 +02:00
viotemp1 eb03382b2d remove SKOPT_MODEL_QUEUE_SIZE comments 2025-03-26 08:36:55 +02:00
viotemp1 fcd0c1d606 formatting fix 2025-03-25 19:16:08 +02:00
viotemp1 276ef6cdde Merge branch 'freqtrade:develop' into optuna 2025-03-25 15:08:03 +02:00
viotemp1 c5088e6b66 fix formatting 2025-03-25 15:07:09 +02:00
viotemp1 62f05964b4 change optimizer to optuna 2025-03-25 14:06:35 +02:00
Matthias b6a65ce125 fix: pickle error due to Lock object
tb_logger get's closed after training.
So we need to re-assign the new one, otherwise pickling fails.

closes #10034
2025-03-11 19:53:41 +01:00
Matthias 315421037f Merge branch 'develop' into feat/plot_annotations 2025-02-10 07:13:55 +01:00
Matthias 4ff200d315 docs: clarify docs about plot annotation callback 2024-12-28 16:38:40 +01:00
Matthias 5700f789e2 chore: improve type-safety 2024-12-28 15:49:41 +01:00
Matthias 689767e3ca chore: rename response model attribute 2024-12-28 15:46:38 +01:00
Matthias 66efc9a469 docs: add documentation for plot_annotations 2024-12-28 15:43:45 +01:00
Matthias 4c3e477667 chore: rename Annotation type 2024-12-28 15:41:10 +01:00
Matthias 8cdae5f56e feat: add plot_annotations interface 2024-12-28 15:26:37 +01:00
Matthias 347295ecaf feat: Expand pair_candles with plot_annotations call 2024-12-28 15:18:02 +01:00
Matthias 7272204d58 feat: add MarkArea type 2024-12-28 15:17:16 +01:00
101 changed files with 41089 additions and 31291 deletions
+5 -1
View File
@@ -1,6 +1,6 @@
---
name: Bug report
about: Create a report to help us improve
about: Create a report to help us improve. Do not use this for strategy assistance.
title: ''
labels: "Triage Needed"
assignees: ''
@@ -12,6 +12,10 @@ Have you searched for similar issues before posting it?
If you have discovered a bug in the bot, please [search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
If it hasn't been reported, please create a new issue.
Has your strategy or configuration been generated by an AI model, and is now not working?
This is almost certainly NOT a bug in Freqtrade, but a problem with the code your AI model generated.
Please consult the documentation. We'll close such issues and point to the documentation.
Please do not use the bug report template to request new features.
-->
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: Feature request
about: Suggest an idea for this project
about: Suggest a new feature or idea for this project
title: ''
labels: ''
assignees: ''
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: Question
about: Ask a question you could not find an answer in the docs
about: Ask a question you could not find an answer in the docs. Use this template if you've got problems with your strategy.
title: ''
labels: "Question"
assignees: ''
+5 -5
View File
@@ -38,7 +38,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
with:
activate-environment: true
enable-cache: true
@@ -171,7 +171,7 @@ jobs:
check-latest: true
- name: Install uv
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
with:
activate-environment: true
enable-cache: true
@@ -298,7 +298,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
with:
activate-environment: true
enable-cache: true
@@ -434,7 +434,7 @@ jobs:
build-linux-online:
# Run pytest with "live" checks
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@@ -446,7 +446,7 @@ jobs:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
with:
activate-environment: true
enable-cache: true
+6 -6
View File
@@ -26,12 +26,12 @@ repos:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==5.5.0.20240820
- types-cachetools==6.0.0.20250525
- types-filelock==3.2.7
- types-requests==2.32.0.20250328
- types-requests==2.32.0.20250515
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20241206
- SQLAlchemy==2.0.40
- types-python-dateutil==2.9.0.20250516
- SQLAlchemy==2.0.41
# stages: [push]
- repo: https://github.com/pycqa/isort
@@ -43,7 +43,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.11.7'
rev: 'v0.11.11'
hooks:
- id: ruff
- id: ruff-format
@@ -82,6 +82,6 @@ repos:
# Ensure github actions remain safe
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.6.0
rev: v1.8.0
hooks:
- id: zizmor
+26 -11
View File
@@ -610,11 +610,11 @@
"type": "string"
},
"chat_id": {
"description": "Telegram chat or group ID",
"description": "Telegram chat or group ID. Recommended to be set via environment variable FREQTRADE__TELEGRAM__CHAT_ID",
"type": "string"
},
"topic_id": {
"description": "Telegram topic ID - only applicable for group chats",
"description": "Telegram topic ID - only applicable for group chats. Recommended to be set via environment variable FREQTRADE__TELEGRAM__TOPIC_ID",
"type": "string"
},
"authorized_users": {
@@ -773,9 +773,11 @@
"type": "object",
"properties": {
"enabled": {
"description": "Enable webhook notifications.",
"type": "boolean"
},
"url": {
"description": "Webhook URL. Recommended to be set via environment variable FREQTRADE__WEBHOOK__URL",
"type": "string"
},
"format": {
@@ -853,6 +855,7 @@
"type": "boolean"
},
"webhook_url": {
"description": "Discord webhook URL. Recommended to be set via environment variable FREQTRADE__DISCORD__WEBHOOK_URL",
"type": "string"
},
"exit_fill": {
@@ -1168,28 +1171,35 @@
"description": "Name of the exchange.",
"type": "string"
},
"enable_ws": {
"description": "Enable WebSocket connections to the exchange.",
"type": "boolean",
"default": true
},
"key": {
"description": "API key for the exchange.",
"description": "API key for the exchange. Recommended to be set via environment variable FREQTRADE__EXCHANGE__KEY",
"type": "string",
"default": ""
},
"secret": {
"description": "API secret for the exchange.",
"description": "API secret for the exchange. Recommended to be set via environment variable FREQTRADE__EXCHANGE__SECRET",
"type": "string",
"default": ""
},
"password": {
"description": "Password for the exchange, if required.",
"description": "Password for the exchange, if required. Recommended to be set via environment variable FREQTRADE__EXCHANGE__PASSWORD",
"type": "string",
"default": ""
},
"uid": {
"description": "User ID for the exchange, if required.",
"description": "User ID for the exchange, if required. Recommended to be set via environment variable FREQTRADE__EXCHANGE__UID",
"type": "string"
},
"account_id": {
"description": "Account ID for the exchange, if required. Recommended to be set via environment variable FREQTRADE__EXCHANGE__ACCOUNT_ID",
"type": "string"
},
"wallet_address": {
"description": "Wallet address for the exchange, if required. Usually used by DEX exchanges. Recommended to be set via environment variable FREQTRADE__EXCHANGE__WALLET_ADDRESS",
"type": "string"
},
"private_key": {
"description": "Private key for the exchange, if required. Usually used by DEX exchanges. Recommended to be set via environment variable FREQTRADE__EXCHANGE__PRIVATE_KEY",
"type": "string"
},
"pair_whitelist": {
@@ -1213,6 +1223,11 @@
"type": "boolean",
"default": false
},
"enable_ws": {
"description": "Enable WebSocket connections to the exchange.",
"type": "boolean",
"default": true
},
"unknown_fee_rate": {
"description": "Fee rate for unknown markets.",
"type": "number"
+35 -38
View File
@@ -161,56 +161,53 @@ class MyAwesomeStrategy(IStrategy):
### Overriding Base estimator
You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
You can define your own optuna sampler for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
```python
class MyAwesomeStrategy(IStrategy):
class HyperOpt:
def generate_estimator(dimensions: List['Dimension'], **kwargs):
return "RF"
return "NSGAIIISampler"
```
Possible values are either one of "GP", "RF", "ET", "GBRT" (Details can be found in the [scikit-optimize documentation](https://scikit-optimize.github.io/)), or "an instance of a class that inherits from `RegressorMixin` (from sklearn) and where the `predict` method has an optional `return_std` argument, which returns `std(Y | x)` along with `E[Y | x]`".
Possible values are either one of "NSGAIISampler", "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIIISampler", "QMCSampler" (Details can be found in the [optuna-samplers documentation](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html)), or "an instance of a class that inherits from `optuna.samplers.BaseSampler`".
Some research will be necessary to find additional Regressors.
Example for `ExtraTreesRegressor` ("ET") with additional parameters:
```python
class MyAwesomeStrategy(IStrategy):
class HyperOpt:
def generate_estimator(dimensions: List['Dimension'], **kwargs):
from skopt.learning import ExtraTreesRegressor
# Corresponds to "ET" - but allows additional parameters.
return ExtraTreesRegressor(n_estimators=100)
```
The `dimensions` parameter is the list of `skopt.space.Dimension` objects corresponding to the parameters to be optimized. It can be used to create isotropic kernels for the `skopt.learning.GaussianProcessRegressor` estimator. Here's an example:
```python
class MyAwesomeStrategy(IStrategy):
class HyperOpt:
def generate_estimator(dimensions: List['Dimension'], **kwargs):
from skopt.utils import cook_estimator
from skopt.learning.gaussian_process.kernels import (Matern, ConstantKernel)
kernel_bounds = (0.0001, 10000)
kernel = (
ConstantKernel(1.0, kernel_bounds) *
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=2.5)
)
kernel += (
ConstantKernel(1.0, kernel_bounds) *
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=1.5)
)
return cook_estimator("GP", space=dimensions, kernel=kernel, n_restarts_optimizer=2)
```
Some research will be necessary to find additional Samplers (from optunahub) for example.
!!! Note
While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used.
If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters.
If you're unsure about this, best use one of the Defaults (`"NSGAIIISampler"` has proven to be the most versatile) without further parameters.
??? Example "Using `AutoSampler` from Optunahub"
[AutoSampler docs](https://hub.optuna.org/samplers/auto_sampler/)
Install the necessary dependencies
``` bash
pip install optunahub cmaes torch scipy
```
Implement `generate_estimator()` in your strategy
``` python
# ...
from freqtrade.strategy.interface import IStrategy
from typing import List
import optunahub
# ...
class my_strategy(IStrategy):
class HyperOpt:
def generate_estimator(dimensions: List["Dimension"], **kwargs):
if "random_state" in kwargs.keys():
return optunahub.load_module("samplers/auto_sampler").AutoSampler(seed=kwargs["random_state"])
else:
return optunahub.load_module("samplers/auto_sampler").AutoSampler()
```
Obviously the same approach will work for all other Samplers optuna supports.
## Space options
+8 -11
View File
@@ -315,7 +315,6 @@ $RepeatedMsgReduction on
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
So, the following are the examples of possible addresses:
* `"address": "/dev/log"` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
@@ -323,20 +322,18 @@ So, the following are the examples of possible addresses:
* `"address": "localhost:514"` -- log to local syslog using UDP socket, if it listens on port 514.
* `"address": "<ip>:514"` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
??? Info "Deprecated - configure syslog via command line"
`--logfile syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
`--logfile syslog:<syslog_address>` -- send log messages to `syslog` service using the `<syslog_address>` as the syslog address.
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
The syslog address can be either a Unix domain socket (socket filename) or a UDP socket specification, consisting of IP address and UDP port, separated by the `:` character.
So, the following are the examples of possible usages:
So, the following are the examples of possible usages:
* `--logfile syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
* `--logfile syslog` -- same as above, the shortcut for `/dev/log`.
* `--logfile syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
* `--logfile syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
* `--logfile syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
* `--logfile syslog:/dev/log` -- log to syslog (rsyslog) using the `/dev/log` socket, suitable for most systems.
* `--logfile syslog` -- same as above, the shortcut for `/dev/log`.
* `--logfile syslog:/var/run/syslog` -- log to syslog (rsyslog) using the `/var/run/syslog` socket. Use this on MacOS.
* `--logfile syslog:localhost:514` -- log to local syslog using UDP socket, if it listens on port 514.
* `--logfile syslog:<ip>:514` -- log to remote syslog at IP address and port 514. This may be used on Windows for remote logging to an external syslog server.
### Logging to journald
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+3
View File
@@ -16,6 +16,7 @@ usage: freqtrade hyperopt [-h] [-v] [--no-color] [--logfile FILE] [-V]
[--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME] [--disable-param-export]
[--ignore-missing-spaces] [--analyze-per-epoch]
[--early-stop INT]
options:
-h, --help show this help message and exit
@@ -87,6 +88,8 @@ options:
Suppress errors for any requested Hyperopt spaces that
do not contain any parameters.
--analyze-per-epoch Run populate_indicators once per epoch.
--early-stop INT Early stop hyperopt if no improvement after (default:
0) epochs.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
+2 -2
View File
@@ -180,7 +180,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to exit a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
| `stoploss` | **Required.** Value as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md#trailing-stop-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Boolean
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-different-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
@@ -672,7 +672,7 @@ Should you experience problems you suspect are caused by websockets, you can dis
}
```
Should you be required to use a proxy, please refer to the [proxy section](#using-proxy-with-freqtrade) for more information.
Should you be required to use a proxy, please refer to the [proxy section](#using-a-proxy-with-freqtrade) for more information.
!!! Info "Rollout"
We're implementing this out slowly, ensuring stability of your bots.
+19
View File
@@ -367,6 +367,25 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
## Bitvavo
If your account is required to use an operatorId, you can set it in the configuration file as follows:
``` json
"exchange": {
"name": "bitvavo",
"key": "",
"secret": "",
"ccxt_config": {
"options": {
"operatorId": "123567"
}
},
}
```
Bitvavo expects the `operatorId` to be an integer.
## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
+9 -4
View File
@@ -102,6 +102,14 @@ You can use "current" market data by using the [dataprovider](strategy-customiza
You can use the `/stopentry` command in Telegram to prevent future trade entry, followed by `/forceexit all` (sell all open trades).
### I sold the bot's capital and now there's errors in the log
Freqtrade assumes that the trades it opens are managed only though the bot.
If you happen to (accidentally) sell the bot's capital, freqtrade will try to recover by trying to re-find on-exchange orders.
This is a best-effort approach, and will not work in all cases, especially when using order types that are not supported by freqtrade (OCO, iceberg, etc.), or when working with older trades (where the exchange no longer provides full order information).
The exact limits will vary between exchanges - with the details usually being documented in the exchange's API documentation.
### I want to run multiple bots on the same machine
Please look at the [advanced setup documentation Page](advanced-setup.md#running-multiple-instances-of-freqtrade).
@@ -219,10 +227,7 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us
First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations.
The GPU improvements would only apply to pandas-native calculations - or ones written by yourself.
For hyperopt, freqtrade is using scikit-optimize, which is built on top of scikit-learn.
Their statement about GPU support is [pretty clear](https://scikit-learn.org/stable/faq.html#will-you-add-gpu-support).
GPU's also are only good at crunching numbers (floating point operations).
GPU's are only good at crunching numbers (floating point operations).
For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting).
As such, GPU's are not too well suited for most parts of hyperopt.
+2 -2
View File
@@ -181,7 +181,7 @@ You can ask for each of the defined features to be included also for informative
In total, the number of features the user of the presented example strategy has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
$= 3 * 3 * 3 * 2 * 2 = 108$.
!!! note "Learn more about creative feature engineering"
!!! note "Learn more about creative feature engineering"
Check out our [medium article](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665) geared toward helping users learn how to creatively engineer features.
### Gain finer control over `feature_engineering_*` functions with `metadata`
@@ -310,7 +310,7 @@ class MyCoolTransform(BaseTransform):
If you have created your own custom `IFreqaiModel` with a custom `train()`/`predict()` function, *and* you still rely on `data_cleaning_train/predict()`, then you will need to migrate to the new pipeline. If your model does *not* rely on `data_cleaning_train/predict()`, then you do not need to worry about this migration.
More details about the migration can be found [here](strategy_migration.md#freqai---new-data-pipeline).
More details about the migration can be found [here](strategy_migration.md#freqai-new-data-pipeline).
## Outlier detection
+1 -1
View File
@@ -79,7 +79,7 @@ pip install -r requirements-freqai.txt
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:stable_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. If you would like to use PyTorch or Reinforcement learning, you should use the torch or RL tags, `image: freqtradeorg/freqtrade:stable_freqaitorch`, `image: freqtradeorg/freqtrade:stable_freqairl`.
!!! note "docker-compose-freqai.yml"
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
### FreqAI position in open-source machine learning landscape
+5 -3
View File
@@ -1,10 +1,10 @@
# Hyperopt
This page explains how to tune your strategy by finding the optimal
parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `scikit-optimize` package to accomplish this.
parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `optuna` package to accomplish this.
The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time.
In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions).
In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses one of optuna's sampler algorithms (currently NSGAIIISampler) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions).
Hyperopt requires historic data to be available, just as backtesting does (hyperopt runs backtesting many times with different parameters).
To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation.
@@ -490,6 +490,8 @@ freqtrade hyperopt --config config.json --hyperopt-loss <hyperoptlossname> --str
```
The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs.
The `--early-stop` option will set after how many epochs with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Any value greater than 0 and lower than 20 it will be replaced by 20. Early stop is by default disabled (`--early-stop=0`)
Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results.
The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below.
@@ -532,7 +534,7 @@ Legal values are:
* `trailing`: search for the best trailing stop values
* `trades`: search for the best max open trades values
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
* `default`: `all` except `trailing` and `protection`
* `default`: `all` except `trailing`, `trades` and `protection`
* space-separated list of any of the above values for example `--spaces roi stoploss`
The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy.
@@ -0,0 +1,18 @@
## Exit logic comparisons
Freqtrade allows your strategy to implement different exit logic using signal-based or callback-based functions.
This section aims to compare each different function, helping you to choose the one that best fits your needs.
* **`populate_exit_trend()`** - Vectorized signal-based exit logic using indicators in the main dataframe
**Use** to define exit signals based on indicators or other data that can be calculated in a vectorized manner.
🚫 **Don't use** to customize exit conditions for each individual trade, or if trade data is necessary to make an exit decision.
* **`custom_exit()`** - Custom exit logic that will fully exit a trade immediately, called for every open trade at every bot loop iteration until a trade is closed.
**Use** to specify exit conditions for each individual trade (including any additional adjusted orders using `adjust_trade_position()`), or if trade data is necessary to make an exit decision, e.g. using profit data to exit.
🚫 **Don't use** when you want to exit using vectorised indicator-based data (use a `populate_exit_trend()` signal instead), or as a proxy for `custom_stoploss()`, and be aware that rate-based exits in backtesting can be inaccurate.
* **`custom_stoploss()`** - Custom trailing stoploss, called for every open trade every iteration until a trade is closed. The value returned here is also used for [stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade).
**Use** to customize the stoploss logic to set a dynamic stoploss based on trade data or other conditions.
🚫 **Don't use** to exit a trade immediately based on a specific condition. Use `custom_exit()` for that purpose.
* **`custom_roi()`** - Custom ROI, called for every open trade every iteration until a trade is closed.
**Use** to specify a minimum ROI threshold ("take-profit") to exit a trade at this ROI level at some point within the trade duration, based on profit or other conditions.
🚫 **Don't use** to exit a trade immediately based on a specific condition. Use `custom_exit()`.
🚫 **Don't use** for static ROI. Use `minimal_roi`.
+1 -1
View File
@@ -87,7 +87,7 @@ OS Specific steps are listed first, the common section below is necessary for al
```bash
sudo apt-get install python3-venv libatlas-base-dev cmake curl
sudo apt-get install python3-venv libatlas-base-dev cmake curl libffi-dev
# Use piwheels.org to speed up installation
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
+70 -37
View File
@@ -1,23 +1,21 @@
# Lookahead analysis
This page explains how to validate your strategy in terms of look ahead bias.
This page explains how to validate your strategy in terms of lookahead bias.
Checking look ahead bias is the bane of any strategy since it is sometimes very easy to introduce backtest bias -
but very hard to detect.
Lookahead bias is the bane of any strategy since it is sometimes very easy to introduce this bias, but can be very hard to detect.
Backtesting initializes all timestamps at once and calculates all indicators in the beginning.
This means that if your indicators or entry/exit signals could look into future candles and falsify your backtest.
Backtesting initializes all timestamps (loads the whole dataframe into memory) and calculates all indicators at once.
This means that if your indicators or entry/exit signals look into future candles, this will falsify your backtest.
Lookahead-analysis requires historic data to be available.
The `lookahead-analysis` command requires historic data to be available.
To learn how to get data for the pairs and exchange you're interested in,
head over to the [Data Downloading](data-download.md) section of the documentation.
`lookahead-analysis` also supports freqai strategies.
This command is built upon backtesting since it internally chains backtests and pokes at the strategy to provoke it to show look ahead bias.
This is done by not looking at the strategy itself - but at the results it returned.
The results are things like changed indicator-values and moved entries/exits compared to the full backtest.
This command internally chains backtests and pokes at the strategy to provoke it to show lookahead bias.
This is done by not looking at the strategy code itself, but at changed indicator values and moved entries/exits compared to the full backtest.
You can use commands of [Backtesting](backtesting.md).
It also supports the lookahead-analysis of freqai strategies.
`lookahead-analysis` can use the typical options of [Backtesting](backtesting.md), but forces the following options:
- `--cache` is forced to "none".
- `--max-open-trades` is forced to be at least equal to the number of pairs.
@@ -25,48 +23,83 @@ It also supports the lookahead-analysis of freqai strategies.
- `--stake-amount` is forced to be a static 10000 (10k).
- `--enable-protections` is forced to be off.
Those are set to avoid users accidentally generating false positives.
These are set to avoid users accidentally generating false positives.
## Lookahead-analysis command reference
--8<-- "commands/lookahead-analysis.md"
!!! Note ""
The above Output was reduced to options `lookahead-analysis` adds on top of regular backtesting commands.
### Summary
Checks a given strategy for look ahead bias via lookahead-analysis
Look ahead bias means that the backtest uses data from future candles thereby not making it viable beyond backtesting
and producing false hopes for the one backtesting.
!!! Note
The above output was reduced to options that `lookahead-analysis` adds on top of regular backtesting commands.
### Introduction
Many strategies - without the programmer knowing - have fallen prey to look ahead bias.
Many strategies, without the programmer knowing, have fallen prey to lookahead bias.
This typically makes the strategy backtest look profitable, sometimes to extremes, but this is not realistic as the strategy is "cheating" by looking at data it would not have in dry or live modes.
Any backtest will populate the full dataframe including all time stamps at the beginning.
If the programmer is not careful or oblivious how things work internally
(which sometimes can be really hard to find out) then it will just look into the future making the strategy amazing
but not realistic.
The reason why strategies can "cheat" is because the freqtrade backtesting process populates the full dataframe including all candle timestamps at the outset.
If the programmer is not careful or oblivious how things work internally
(which sometimes can be really hard to find out) then the strategy will look into the future.
This command is made to try to verify the validity in the form of the aforementioned look ahead bias.
This command is made to try to verify the validity in the form of the aforementioned lookahead bias.
### How does the command work?
It will start with a backtest of all pairs to generate a baseline for indicators and entries/exits.
After the backtest ran, it will look if the `minimum-trade-amount` is met
and if not cancel the lookahead-analysis for this strategy.
After this initial backtest runs, it will look if the `minimum-trade-amount` is met and if not cancel the lookahead-analysis for this strategy.
If this happens, use a wider timerange to get more trades for the analysis, or use a timerange where more trades occur.
After setting the baseline it will then do additional runs for every entry and exit separately.
When a verification-backtest is done, it will compare the indicators as the signal (either entry or exit) and report the bias.
After all signals have been verified or falsified a result-table will be generated for the user to see.
After setting the baseline it will then do additional backtest runs for every entry and exit separately.
When these verification backtests complete, it will compare the indicators at the signal candles (both entry or exit)
and report the bias.
After all signals have been verified or falsified a result table will be generated for the user to see.
### How to find and remove bias? How can I salvage a biased strategy?
If you found a biased strategy online and want to have the same results, just without bias,
then you will be out of luck most of the time.
Usually the bias in the strategy is THE driving factor for "too good to be true" profits.
Removing conditions or indicators that push the profits up from bias will usually make the strategy significantly worse.
You might be able to salvage it partially if the biased indicators or conditions are not the core of the strategy, or there
are other entry and exit signals that are not biased.
### Examples of lookahead-bias
- `shift(-10)` looks 10 candles into the future.
- Using `iloc[]` in populate_* functions to access a specific row in the dataframe.
- For-loops are prone to introduce lookahead bias if you don't tightly control which numbers are looped through.
- Aggregation functions like `.mean()`, `.min()` and `.max()`, without a rolling window,
will calculate the value over the **whole** dataframe, so the signal candle will "see" a value including future candles.
A non-biased example would be to look back candles using `rolling()` instead:
e.g. `dataframe['volume_mean_12'] = dataframe['volume'].rolling(12).mean()`
- `ta.MACD(dataframe, 12, 26, 1)` will introduce bias with a signalperiod of 1.
### What do the columns in the results table mean?
- `filename`: name of the checked strategy file
- `strategy`: checked strategy class name
- `has_bias`: result of the lookahead-analysis. `No` would be good, `Yes` would be bad.
- `total_signals`: number of checked signals (default is 20)
- `biased_entry_signals`: found bias in that many entries
- `biased_exit_signals`: found bias in that many exits
- `biased_indicators`: shows you the indicators themselves that are defined in populate_indicators
You might get false positives in the `biased_exit_signals` if you have biased entry signals paired with those exits.
However, a biased entry will usually result in a biased exit too,
even if the exit itself does not produce the bias -
especially if your entry and exit conditions use the same biased indicator.
**Address the bias in the entries first, then address the exits.**
### Caveats
- `lookahead-analysis` can only verify / falsify the trades it calculated and verified.
If the strategy has many different signals / signal types, it's up to you to select appropriate parameters to ensure that all signals have triggered at least once. Not triggered signals will not have been verified.
This could lead to a false-negative (the strategy will then be reported as non-biased).
- `lookahead-analysis` has access to everything that backtesting has too.
Please don't provoke any configs like enabling position stacking.
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` amount and neither leftover money in your wallet.
- In the results table, the `biased_indicators` column will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased. These are not biased and can safely be ignored.
If the strategy has many different signals / signal types, it's up to you to select appropriate parameters to ensure that all signals have triggered at least once. Signals that are not triggered will not have been verified.
This would lead to a false-negative, i.e. the strategy will be reported as non-biased.
- `lookahead-analysis` has access to the same backtesting options and this can introduce problems.
Please don't use any options like enabling position stacking as this will distort the number of checked signals.
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
and that you have enough capital in the backtest wallet configuration.
- In the results table, the `biased_indicators` column
will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased.
**These are not biased and can safely be ignored.**
+1
View File
@@ -50,6 +50,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
| `ping_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `false`.*<br> **Datatype:** Boolean.
| `initial_candle_limit` | Initial candles to expect from the Producer.<br>*Defaults to `1500`.*<br> **Datatype:** Integer - Number of candles.
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
+1 -1
View File
@@ -1,6 +1,6 @@
markdown==3.8
mkdocs==1.6.1
mkdocs-material==9.6.12
mkdocs-material==9.6.14
mdx_truly_sane_lists==1.3
pymdown-extensions==10.15
jinja2==3.1.6
+261 -10
View File
@@ -12,6 +12,7 @@ Currently available callbacks:
* [`custom_stake_amount()`](#stake-size-management)
* [`custom_exit()`](#custom-exit-signal)
* [`custom_stoploss()`](#custom-stoploss)
* [`custom_roi()`](#custom-roi)
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
* [`check_entry_timeout()` and `check_exit_timeout()`](#custom-order-timeout-rules)
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
@@ -26,6 +27,9 @@ Currently available callbacks:
--8<-- "includes/strategy-imports.md"
--8<-- "includes/strategy-exit-comparisons.md"
## Bot start
A simple callback which is called once when the strategy is loaded.
@@ -121,7 +125,7 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
Allows to define custom exit signals, indicating that specified position should be sold. This is very useful when we need to customize exit conditions for each individual trade, or if you need trade data to make an exit decision.
Allows to define custom exit signals, indicating that specified position should be closed (full exit). This is very useful when we need to customize exit conditions for each individual trade, or if you need trade data to make an exit decision.
For example you could implement a 1:2 risk-reward ROI with `custom_exit()`.
@@ -178,6 +182,8 @@ Returning `None` will be interpreted as "no desire to change", and is the only s
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)).
If you're on futures markets, please take note of the [stoploss and leverage](stoploss.md#stoploss-and-leverage) section, as the stoploss value returned from `custom_stoploss` is the risk for this trade - not the relative price movement.
!!! Note "Use of dates"
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
@@ -233,7 +239,7 @@ class AwesomeStrategy(IStrategy):
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the current_rate
"""
return -0.04
return -0.04 * trade.leverage
```
#### Time based trailing stop
@@ -255,9 +261,9 @@ class AwesomeStrategy(IStrategy):
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
if current_time - timedelta(minutes=120) > trade.open_date_utc:
return -0.05
return -0.05 * trade.leverage
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
return -0.10
return -0.10 * trade.leverage
return None
```
@@ -284,9 +290,9 @@ class AwesomeStrategy(IStrategy):
return stoploss_from_open(0.10, current_profit, is_short=trade.is_short, leverage=trade.leverage)
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
if current_time - timedelta(minutes=120) > trade.open_date_utc:
return -0.05
return -0.05 * trade.leverage
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
return -0.10
return -0.10 * trade.leverage
return None
```
@@ -309,10 +315,10 @@ class AwesomeStrategy(IStrategy):
**kwargs) -> float | None:
if pair in ("ETH/BTC", "XRP/BTC"):
return -0.10
return -0.10 * trade.leverage
elif pair in ("LTC/BTC"):
return -0.05
return -0.15
return -0.05 * trade.leverage
return -0.15 * trade.leverage
```
#### Trailing stoploss with positive offset
@@ -341,7 +347,7 @@ class AwesomeStrategy(IStrategy):
desired_stoploss = current_profit / 2
# Use a minimum of 2.5% and a maximum of 5%
return max(min(desired_stoploss, 0.05), 0.025)
return max(min(desired_stoploss, 0.05), 0.025) * trade.leverage
```
#### Stepped stoploss
@@ -497,6 +503,135 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
---
## Custom ROI
Called for open trade every iteration (roughly every 5 seconds) until a trade is closed.
The usage of the custom ROI method must be enabled by setting `use_custom_roi=True` on the strategy object.
This method allows you to define a custom minimum ROI threshold for exiting a trade, expressed as a ratio (e.g., `0.05` for 5% profit). If both `minimal_roi` and `custom_roi` are defined, the lower of the two thresholds will trigger an exit. For example, if `minimal_roi` is set to `{"0": 0.10}` (10% at 0 minutes) and `custom_roi` returns `0.05`, the trade will exit when the profit reaches 5%. Also, if `custom_roi` returns `0.10` and `minimal_roi` is set to `{"0": 0.05}` (5% at 0 minutes), the trade will be closed when the profit reaches 5%.
The method must return a float representing the new ROI threshold as a ratio, or `None` to fall back to the `minimal_roi` logic. Returning `NaN` or `inf` values is considered invalid and will be treated as `None`, causing the bot to use the `minimal_roi` configuration.
### Custom ROI examples
The following examples illustrate how to use the `custom_roi` function to implement different ROI logics.
#### Custom ROI per side
Use different ROI thresholds depending on the `side`. In this example, 5% for long entries and 2% for short entries.
```python
# Default imports
class AwesomeStrategy(IStrategy):
use_custom_roi = True
# ... populate_* methods
def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
entry_tag: str | None, side: str, **kwargs) -> float | None:
"""
Custom ROI logic, returns a new minimum ROI threshold (as a ratio, e.g., 0.05 for +5%).
Only called when use_custom_roi is set to True.
If used at the same time as minimal_roi, an exit will be triggered when the lower
threshold is reached. Example: If minimal_roi = {"0": 0.01} and custom_roi returns 0.05,
an exit will be triggered if profit reaches 5%.
:param pair: Pair that's currently analyzed.
:param trade: trade object.
:param current_time: datetime object, containing the current datetime.
:param trade_duration: Current trade duration in minutes.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the current trade.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New ROI value as a ratio, or None to fall back to minimal_roi logic.
"""
return 0.05 if side == "long" else 0.02
```
#### Custom ROI per pair
Use different ROI thresholds depending on the `pair`.
```python
# Default imports
class AwesomeStrategy(IStrategy):
use_custom_roi = True
# ... populate_* methods
def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
entry_tag: str | None, side: str, **kwargs) -> float | None:
stake = trade.stake_currency
roi_map = {
f"BTC/{stake}": 0.02, # 2% for BTC
f"ETH/{stake}": 0.03, # 3% for ETH
f"XRP/{stake}": 0.04, # 4% for XRP
}
return roi_map.get(pair, 0.01) # 1% for any other pair
```
#### Custom ROI per entry tag
Use different ROI thresholds depending on the `entry_tag` provided with the buy signal.
```python
# Default imports
class AwesomeStrategy(IStrategy):
use_custom_roi = True
# ... populate_* methods
def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
entry_tag: str | None, side: str, **kwargs) -> float | None:
roi_by_tag = {
"breakout": 0.08, # 8% if tag is "breakout"
"rsi_overbought": 0.05, # 5% if tag is "rsi_overbought"
"mean_reversion": 0.03, # 3% if tag is "mean_reversion"
}
return roi_by_tag.get(entry_tag, 0.01) # 1% if tag is unknown
```
#### Custom ROI based on ATR
ROI value may be derived from indicators stored in dataframe. This example uses the ATR ratio as ROI.
``` python
# Default imports
# <...>
import talib.abstract as ta
class AwesomeStrategy(IStrategy):
use_custom_roi = True
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# <...>
dataframe["atr"] = ta.ATR(dataframe, timeperiod=10)
def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
entry_tag: str | None, side: str, **kwargs) -> float | None:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
atr_ratio = last_candle["atr"] / last_candle["close"]
return atr_ratio # Returns the ATR value as ratio
```
---
## Custom order price rules
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
@@ -1107,3 +1242,119 @@ class AwesomeStrategy(IStrategy):
return None
```
## Plot annotations callback
The plot annotations callback is called whenever freqUI requests data to display a chart.
This callback has no meaning in the trade cycle context and is only used for charting purposes.
The strategy can then return a list of `AnnotationType` objects to be displayed on the chart.
Depending on the content returned - the chart can display horizontal areas, vertical areas, or boxes.
The full object looks like this:
``` json
{
"type": "area", // Type of the annotation, currently only "area" is supported
"start": "2024-01-01 15:00:00", // Start date of the area
"end": "2024-01-01 16:00:00", // End date of the area
"y_start": 94000.2, // Price / y axis value
"y_end": 98000, // Price / y axis value
"color": "",
"label": "some label"
}
```
The below example will mark the chart with areas for the hours 8 and 15, with a grey color, highlighting the market open and close hours.
This is obviously a very basic example.
``` python
# Default imports
class AwesomeStrategy(IStrategy):
def plot_annotations(
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
) -> list[AnnotationType]:
"""
Retrieve area annotations for a chart.
Must be returned as array, with type, label, color, start, end, y_start, y_end.
All settings except for type are optional - though it usually makes sense to include either
"start and end" or "y_start and y_end" for either horizontal or vertical plots
(or all 4 for boxes).
:param pair: Pair that's currently analyzed
:param start_date: Start date of the chart data being requested
:param end_date: End date of the chart data being requested
:param dataframe: DataFrame with the analyzed data for the chart
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: List of AnnotationType objects
"""
annotations = []
while start_dt < end_date:
start_dt += timedelta(hours=1)
if start_dt.hour in (8, 15):
annotations.append(
{
"type": "area",
"label": "Trade open and close hours",
"start": start_dt,
"end": start_dt + timedelta(hours=1),
# Omitting y_start and y_end will result in a vertical area spanning the whole height of the main Chart
"color": "rgba(133, 133, 133, 0.4)",
}
)
return annotations
```
Entries will be validated, and won't be passed to the UI if they don't correspond to the expected schema and will log an error if they don't.
!!! Warning "Many annotations"
Using too many annotations can cause the UI to hang, especially when plotting large amounts of historic data.
Use the annotation feature with care.
### Plot annotations example
![FreqUI - plot Annotations](assets/freqUI-chart-annotations-dark.png#only-dark)
![FreqUI - plot Annotations](assets/freqUI-chart-annotations-light.png#only-light)
??? Info "Code used for the plot above"
This is an example code and should be treated as such.
``` python
# Default imports
class AwesomeStrategy(IStrategy):
def plot_annotations(
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
) -> list[AnnotationType]:
annotations = []
while start_dt < end_date:
start_dt += timedelta(hours=1)
if (start_dt.hour % 4) == 0:
mark_areas.append(
{
"type": "area",
"label": "4h",
"start": start_dt,
"end": start_dt + timedelta(hours=1),
"color": "rgba(133, 133, 133, 0.4)",
}
)
elif (start_dt.hour % 2) == 0:
price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
mark_areas.append(
{
"type": "area",
"label": "2h",
"start": start_dt,
"end": start_dt + timedelta(hours=1),
"y_end": price * 1.01,
"y_start": price * 0.99,
"color": "rgba(0, 255, 0, 0.4)",
}
)
return annotations
```
+7 -5
View File
@@ -134,15 +134,17 @@ Most properties here can be None as they are dependent on the exchange response.
|------------|-------------|-------------|
| `trade` | Trade | Trade object this order is attached to |
| `ft_pair` | string | Pair this order is for |
| `ft_is_open` | boolean | is the order filled? |
| `ft_is_open` | boolean | is the order still open? |
| `order_type` | string | Order type as defined on the exchange - usually market, limit or stoploss |
| `status` | string | Status as defined by ccxt. Usually open, closed, expired or canceled |
| `side` | string | Buy or Sell |
| `status` | string | Status as defined by [ccxt's order structure](https://docs.ccxt.com/#/README?id=order-structure). Usually open, closed, expired, canceled or rejected |
| `side` | string | buy or sell |
| `price` | float | Price the order was placed at |
| `average` | float | Average price the order filled at |
| `amount` | float | Amount in base currency |
| `filled` | float | Filled amount (in base currency) |
| `remaining` | float | Remaining amount |
| `filled` | float | Filled amount (in base currency) (use `safe_filled` instead) |
| `safe_filled` | float | Filled amount (in base currency) - guaranteed to not be None |
| `remaining` | float | Remaining amount (use `safe_remaining` instead) |
| `safe_remaining` | float | Remaining amount - either taken from the exchange or calculated. |
| `cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*) |
| `stake_amount` | float | Stake amount used for this order. *Added in 2023.7.* |
| `stake_amount_filled` | float | Filled Stake amount used for this order. *Added in 2024.11.* |
+1 -1
View File
@@ -45,4 +45,4 @@ Please refer to the corresponding installation sections (common problems linked
Common problems and their solutions:
* [ta-lib update on windows](windows_installation.md#2-install-ta-lib)
* [ta-lib update on windows](windows_installation.md#install-ta-lib)
+1 -1
View File
@@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2025.4"
__version__ = "2025.5"
if "dev" in __version__:
from pathlib import Path
+1
View File
@@ -78,6 +78,7 @@ ARGS_HYPEROPT = [
"disableparamexport",
"hyperopt_ignore_missing_space",
"analyze_per_epoch",
"early_stop",
]
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE, "stoploss_range"]
+7
View File
@@ -262,6 +262,13 @@ AVAILABLE_CLI_OPTIONS = {
metavar="INT",
default=constants.HYPEROPT_EPOCH,
),
"early_stop": Arg(
"--early-stop",
help="Early stop hyperopt if no improvement after (default: %(default)d) epochs.",
type=check_int_positive,
metavar="INT",
default=0, # 0 to disable by default
),
"spaces": Arg(
"--spaces",
help="Specify which parameters to hyperopt. Space-separated list.",
+64 -14
View File
@@ -26,6 +26,8 @@ __MESSAGE_TYPE_DICT: dict[str, dict[str, str]] = {x: {"type": "object"} for x in
__IN_STRATEGY = "\nUsually specified in the strategy and missing in the configuration."
__VIA_ENV = "Recommended to be set via environment variable"
CONF_SCHEMA = {
"type": "object",
"properties": {
@@ -468,11 +470,16 @@ CONF_SCHEMA = {
},
"token": {"description": "Telegram bot token.", "type": "string"},
"chat_id": {
"description": "Telegram chat or group ID",
"description": (
f"Telegram chat or group ID. {__VIA_ENV} FREQTRADE__TELEGRAM__CHAT_ID"
),
"type": "string",
},
"topic_id": {
"description": "Telegram topic ID - only applicable for group chats",
"description": (
"Telegram topic ID - only applicable for group chats. "
f"{__VIA_ENV} FREQTRADE__TELEGRAM__TOPIC_ID"
),
"type": "string",
},
"authorized_users": {
@@ -574,8 +581,11 @@ CONF_SCHEMA = {
"description": "Webhook settings.",
"type": "object",
"properties": {
"enabled": {"type": "boolean"},
"url": {"type": "string"},
"enabled": {"description": "Enable webhook notifications.", "type": "boolean"},
"url": {
"description": f"Webhook URL. {__VIA_ENV} FREQTRADE__WEBHOOK__URL",
"type": "string",
},
"format": {"type": "string", "enum": WEBHOOK_FORMAT_OPTIONS, "default": "form"},
"retries": {"type": "integer", "minimum": 0},
"retry_delay": {"type": "number", "minimum": 0},
@@ -587,7 +597,12 @@ CONF_SCHEMA = {
"type": "object",
"properties": {
"enabled": {"type": "boolean"},
"webhook_url": {"type": "string"},
"webhook_url": {
"description": (
f"Discord webhook URL. {__VIA_ENV} FREQTRADE__DISCORD__WEBHOOK_URL"
),
"type": "string",
},
"exit_fill": {
"type": "array",
"items": {"type": "object"},
@@ -806,27 +821,57 @@ CONF_SCHEMA = {
"type": "object",
"properties": {
"name": {"description": "Name of the exchange.", "type": "string"},
"enable_ws": {
"description": "Enable WebSocket connections to the exchange.",
"type": "boolean",
"default": True,
},
"key": {
"description": "API key for the exchange.",
"description": (
f"API key for the exchange. {__VIA_ENV} FREQTRADE__EXCHANGE__KEY"
),
"type": "string",
"default": "",
},
"secret": {
"description": "API secret for the exchange.",
"description": (
f"API secret for the exchange. {__VIA_ENV} FREQTRADE__EXCHANGE__SECRET"
),
"type": "string",
"default": "",
},
"password": {
"description": "Password for the exchange, if required.",
"description": (
"Password for the exchange, if required. "
f"{__VIA_ENV} FREQTRADE__EXCHANGE__PASSWORD"
),
"type": "string",
"default": "",
},
"uid": {"description": "User ID for the exchange, if required.", "type": "string"},
"uid": {
"description": (
"User ID for the exchange, if required. "
f"{__VIA_ENV} FREQTRADE__EXCHANGE__UID"
),
"type": "string",
},
"account_id": {
"description": (
"Account ID for the exchange, if required. "
f"{__VIA_ENV} FREQTRADE__EXCHANGE__ACCOUNT_ID"
),
"type": "string",
},
"wallet_address": {
"description": (
"Wallet address for the exchange, if required. "
"Usually used by DEX exchanges. "
f"{__VIA_ENV} FREQTRADE__EXCHANGE__WALLET_ADDRESS"
),
"type": "string",
},
"private_key": {
"description": (
"Private key for the exchange, if required. Usually used by DEX exchanges. "
f"{__VIA_ENV} FREQTRADE__EXCHANGE__PRIVATE_KEY"
),
"type": "string",
},
"pair_whitelist": {
"description": "List of whitelisted trading pairs.",
"type": "array",
@@ -847,6 +892,11 @@ CONF_SCHEMA = {
"type": "boolean",
"default": False,
},
"enable_ws": {
"description": "Enable WebSocket connections to the exchange.",
"type": "boolean",
"default": True,
},
"unknown_fee_rate": {
"description": "Fee rate for unknown markets.",
"type": "number",
@@ -29,6 +29,7 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
"telegram.chat_id",
"discord.webhook_url",
"api_server.password",
"webhook.url",
]
config = deepcopy(config)
for key in keys_to_remove:
+1 -1
View File
@@ -104,7 +104,7 @@ def _validate_unlimited_amount(conf: dict[str, Any]) -> None:
"""
if (
not conf.get("edge", {}).get("enabled")
and conf.get("max_open_trades") == float("inf")
and (conf.get("max_open_trades") == float("inf") or conf.get("max_open_trades") == -1)
and conf.get("stake_amount") == UNLIMITED_STAKE_AMOUNT
):
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
+13
View File
@@ -334,6 +334,19 @@ class Configuration:
("print_all", "Parameter --print-all detected ..."),
]
self._args_to_config_loop(config, configurations)
es_epochs = self.args.get("early_stop", 0)
if es_epochs > 0:
if es_epochs < 20:
logger.warning(
f"Early stop epochs {es_epochs} lower than 20. It will be replaced with 20."
)
config.update({"early_stop": 20})
else:
config.update({"early_stop": self.args["early_stop"]})
logger.info(
f"Parameter --early-stop detected ... Will early stop hyperopt if no improvement "
f"after {config.get('early_stop')} epochs ..."
)
configurations = [
("print_json", "Parameter --print-json detected ..."),
+13
View File
@@ -71,6 +71,19 @@ DEFAULT_DATAFRAME_COLUMNS = ["date", "open", "high", "low", "close", "volume"]
# it has wide consequences for stored trades files
DEFAULT_TRADES_COLUMNS = ["timestamp", "id", "type", "side", "price", "amount", "cost"]
DEFAULT_ORDERFLOW_COLUMNS = ["level", "bid", "ask", "delta"]
ORDERFLOW_ADDED_COLUMNS = [
"trades",
"orderflow",
"imbalances",
"stacked_imbalances_bid",
"stacked_imbalances_ask",
"max_delta",
"min_delta",
"bid",
"ask",
"delta",
"total_trades",
]
TRADES_DTYPES = {
"timestamp": "int64",
"id": "str",
+1
View File
@@ -25,6 +25,7 @@ from .bt_fileutils import (
trade_list_to_dataframe,
update_backtest_metadata,
)
from .historic_precision import get_tick_size_over_time
from .trade_parallelism import (
analyze_trade_parallelism,
evaluate_result_multi,
@@ -52,6 +52,7 @@ BT_DATA_COLUMNS = [
"open_timestamp",
"close_timestamp",
"orders",
"funding_fees",
]
@@ -356,6 +357,8 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
df["max_stake_amount"] = df["stake_amount"]
if "orders" not in df.columns:
df["orders"] = None
if "funding_fees" not in df.columns:
df["funding_fees"] = 0.0
return df
@@ -0,0 +1,27 @@
from pandas import DataFrame, Series
def get_tick_size_over_time(candles: DataFrame) -> Series:
"""
Calculate the number of significant digits for candles over time.
It's using the Monthly maximum of the number of significant digits for each month.
:param candles: DataFrame with OHLCV data
:return: Series with the average number of significant digits for each month
"""
# count the number of significant digits for the open and close prices
for col in ["open", "high", "low", "close"]:
candles[f"{col}_count"] = (
candles[col].round(14).astype(str).str.extract(r"\.(\d*[1-9])")[0].str.len()
)
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
axis=1
)
candles1 = candles.set_index("date", drop=True)
# Group by month and calculate the average number of significant digits
monthly_count_avg1 = candles1["max_count"].resample("MS").max()
# monthly_open_count_avg
# convert monthly_open_count_avg from 5.0 to 0.00001, 4.0 to 0.0001, ...
monthly_open_count_avg = 1 / 10**monthly_count_avg1
return monthly_open_count_avg
+1 -15
View File
@@ -9,26 +9,12 @@ from datetime import datetime
import numpy as np
import pandas as pd
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, Config
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, ORDERFLOW_ADDED_COLUMNS, Config
from freqtrade.exceptions import DependencyException
logger = logging.getLogger(__name__)
ORDERFLOW_ADDED_COLUMNS = [
"trades",
"orderflow",
"imbalances",
"stacked_imbalances_bid",
"stacked_imbalances_ask",
"max_delta",
"min_delta",
"bid",
"ask",
"delta",
"total_trades",
]
def _init_dataframe_with_trades_columns(dataframe: pd.DataFrame):
"""
+5 -1
View File
@@ -32,19 +32,23 @@ class Binance(Exchange):
"stop_price_param": "stopPrice",
"stop_price_prop": "stopPrice",
"stoploss_order_types": {"limit": "stop_loss_limit"},
"stoploss_blocks_assets": True, # By default stoploss orders block assets
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
"trades_has_history": True,
"fetch_orders_limit_minutes": None,
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ws_enabled": True,
}
_ft_has_futures: FtHas = {
"funding_fee_candle_limit": 1000,
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
"order_time_in_force": ["GTC", "FOK", "IOC"],
"tickers_have_price": False,
"floor_leverage": True,
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
"stop_price_type_field": "workingType",
"order_props_in_contracts": ["amount", "cost", "filled", "remaining"],
"stop_price_type_value_mapping": {
@@ -400,7 +404,7 @@ class Binance(Exchange):
since = max(since, listing_date)
_, res = await download_archive_trades(
CandleType.SPOT,
CandleType.FUTURES if self.trading_mode == "futures" else CandleType.SPOT,
pair,
since_ms=since,
until_ms=until,
File diff suppressed because it is too large Load Diff
+17 -20
View File
@@ -12,7 +12,6 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
from freqtrade.util.datetime_helpers import dt_now, dt_ts
logger = logging.getLogger(__name__)
@@ -35,6 +34,7 @@ class Bybit(Exchange):
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
"ws_enabled": True,
"trades_has_history": False, # Endpoint doesn't support pagination
"fetch_orders_limit_minutes": 7 * 1440, # 7 days
"exchange_has_overrides": {
# Bybit spot does not support fetch_order
# Unless the account is unified.
@@ -49,6 +49,7 @@ class Bybit(Exchange):
"funding_fee_candle_limit": 200,
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"},
"stoploss_blocks_assets": False,
# bybit response parsing fails to populate stopLossPrice
"stop_price_prop": "stopPrice",
"stop_price_type_field": "triggerBy",
@@ -139,6 +140,21 @@ class Bybit(Exchange):
params["position_idx"] = 0
return params
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict:
params = super()._get_stop_params(
side=side,
ordertype=ordertype,
stop_price=stop_price,
)
# work around ccxt bug introduced in https://github.com/ccxt/ccxt/pull/25887
# Where create_order ain't returning an ID any longer.
params.update(
{
"method": "privatePostV5OrderCreate",
}
)
return params
def _order_needs_price(self, side: BuySell, ordertype: str) -> bool:
# Bybit requires price for market orders - but only for classic accounts,
# and only in spot mode
@@ -234,25 +250,6 @@ class Bybit(Exchange):
logger.warning(f"Could not update funding fees for {pair}.")
return 0.0
def fetch_orders(
self, pair: str, since: datetime, params: dict | None = None
) -> list[CcxtOrder]:
"""
Fetch all orders for a pair "since"
:param pair: Pair for the query
:param since: Starting time for the query
"""
# On bybit, the distance between since and "until" can't exceed 7 days.
# we therefore need to split the query into multiple queries.
orders = []
while since < dt_now():
until = since + timedelta(days=7, minutes=-1)
orders += super().fetch_orders(pair, since, params={"until": dt_ts(until)})
since = until
return orders
def fetch_order(self, order_id: str, pair: str, params: dict | None = None) -> CcxtOrder:
if self.exchange_has("fetchOrder"):
# Set acknowledged to True to avoid ccxt exception
+75 -29
View File
@@ -131,6 +131,7 @@ class Exchange:
"stop_price_param": "stopLossPrice", # Used for stoploss_on_exchange request
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
"stoploss_order_types": {},
"stoploss_blocks_assets": True, # By default stoploss orders block assets
"order_time_in_force": ["GTC"],
"ohlcv_params": {},
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
@@ -155,6 +156,7 @@ class Exchange:
"ccxt_futures_name": "swap",
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
"order_props_in_contracts": ["amount", "filled", "remaining"],
"fetch_orders_limit_minutes": None, # "fetch_orders" is not time-limited by default
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
"marketOrderRequiresPrice": False,
"exchange_has_overrides": {}, # Dictionary overriding ccxt's "has".
@@ -267,11 +269,11 @@ class Exchange:
exchange_conf.get("ccxt_async_config", {}), ccxt_async_config
)
self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config)
self._has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws_enabled"]
_has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws_enabled"]
if (
self._config["runmode"] in TRADE_MODES
and exchange_conf.get("enable_ws", True)
and self._has_watch_ohlcv
and _has_watch_ohlcv
):
self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config)
self._exchange_ws = ExchangeWS(self._config, self._ws_async)
@@ -1742,7 +1744,7 @@ class Exchange:
return orders
@retrier(retries=0)
def fetch_orders(
def _fetch_orders(
self, pair: str, since: datetime, params: dict | None = None
) -> list[CcxtOrder]:
"""
@@ -1781,6 +1783,24 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
def fetch_orders(
self, pair: str, since: datetime, params: dict | None = None
) -> list[CcxtOrder]:
if self._config["dry_run"]:
return []
if (limit := self._ft_has.get("fetch_orders_limit_minutes")) is not None:
orders = []
while since < dt_now():
orders += self._fetch_orders(pair, since)
# Since with 1 minute overlap
since = since + timedelta(minutes=limit - 1)
# Ensure each order is unique based on order id
orders = list({order["id"]: order for order in orders}.values())
return orders
else:
return self._fetch_orders(pair, since, params=params)
@retrier
def fetch_trading_fees(self) -> dict[str, Any]:
"""
@@ -2414,6 +2434,53 @@ class Exchange:
data = sorted(data, key=lambda x: x[0])
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
def _try_build_from_websocket(
self, pair: str, timeframe: str, candle_type: CandleType
) -> Coroutine[Any, Any, OHLCVResponse] | None:
"""
Try to build a coroutine to get data from websocket.
"""
if self._can_use_websocket(self._exchange_ws, pair, timeframe, candle_type):
candle_ts = dt_ts(timeframe_to_prev_date(timeframe))
prev_candle_ts = dt_ts(date_minus_candles(timeframe, 1))
candles = self._exchange_ws.ohlcvs(pair, timeframe)
half_candle = int(candle_ts - (candle_ts - prev_candle_ts) * 0.5)
last_refresh_time = int(
self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0)
)
if (
candles
and (
(len(candles) > 1 and candles[-1][0] >= prev_candle_ts)
# Edgecase on reconnect, where 1 candle is available but it's the current one
or (len(candles) == 1 and candles[-1][0] < candle_ts)
)
and last_refresh_time >= half_candle
):
# Usable result, candle contains the previous candle.
# Also, we check if the last refresh time is no more than half the candle ago.
logger.debug(f"reuse watch result for {pair}, {timeframe}, {last_refresh_time}")
return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_ts)
logger.info(
f"Couldn't reuse watch for {pair}, {timeframe}, falling back to REST api. "
f"{candle_ts < last_refresh_time}, {candle_ts}, {last_refresh_time}, "
f"{format_ms_time(candle_ts)}, {format_ms_time(last_refresh_time)} "
)
return None
def _can_use_websocket(
self, exchange_ws: ExchangeWS | None, pair: str, timeframe: str, candle_type: CandleType
) -> TypeGuard[ExchangeWS]:
"""
Check if we can use websocket for this pair.
Acts as typeguard for exchangeWs
"""
if exchange_ws and candle_type in (CandleType.SPOT, CandleType.FUTURES):
return True
return False
def _build_coroutine(
self,
pair: str,
@@ -2423,8 +2490,8 @@ class Exchange:
cache: bool,
) -> Coroutine[Any, Any, OHLCVResponse]:
not_all_data = cache and self.required_candle_call_count > 1
if cache and candle_type in (CandleType.SPOT, CandleType.FUTURES):
if self._has_watch_ohlcv and self._exchange_ws:
if cache:
if self._can_use_websocket(self._exchange_ws, pair, timeframe, candle_type):
# Subscribe to websocket
self._exchange_ws.schedule_ohlcv(pair, timeframe, candle_type)
@@ -2432,30 +2499,9 @@ class Exchange:
candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
min_ts = dt_ts(date_minus_candles(timeframe, candle_limit - 5))
if self._exchange_ws:
candle_ts = dt_ts(timeframe_to_prev_date(timeframe))
prev_candle_ts = dt_ts(date_minus_candles(timeframe, 1))
candles = self._exchange_ws.ohlcvs(pair, timeframe)
half_candle = int(candle_ts - (candle_ts - prev_candle_ts) * 0.5)
last_refresh_time = int(
self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0)
)
if (
candles
and candles[-1][0] >= prev_candle_ts
and last_refresh_time >= half_candle
):
# Usable result, candle contains the previous candle.
# Also, we check if the last refresh time is no more than half the candle ago.
logger.debug(f"reuse watch result for {pair}, {timeframe}, {last_refresh_time}")
return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_ts)
logger.info(
f"Couldn't reuse watch for {pair}, {timeframe}, falling back to REST api. "
f"{candle_ts < last_refresh_time}, {candle_ts}, {last_refresh_time}, "
f"{format_ms_time(candle_ts)}, {format_ms_time(last_refresh_time)} "
)
if ws_resp := self._try_build_from_websocket(pair, timeframe, candle_type):
# We have a usable websocket response
return ws_resp
# Check if 1 call can get us updated candles without hole in the data.
if min_ts < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0):
+3
View File
@@ -15,6 +15,7 @@ class FtHas(TypedDict, total=False):
stop_price_type_field: str
stop_price_type_value_mapping: dict
stoploss_order_types: dict[str, str]
stoploss_blocks_assets: bool
# ohlcv
ohlcv_params: dict
ohlcv_candle_limit: int
@@ -38,6 +39,8 @@ class FtHas(TypedDict, total=False):
l2_limit_range: list[int] | None
l2_limit_range_required: bool
l2_limit_upper: int | None
# fetch_orders
fetch_orders_limit_minutes: int | None
# Futures
ccxt_futures_name: str # usually swap
mark_ohlcv_price: str
+1
View File
@@ -46,6 +46,7 @@ class Gate(Exchange):
"funding_fee_candle_limit": 90,
"stop_price_type_field": "price_type",
"l2_limit_upper": 300,
"stoploss_blocks_assets": False,
"stop_price_type_value_mapping": {
PriceType.LAST: 0,
PriceType.MARK: 1,
+1
View File
@@ -32,6 +32,7 @@ class Hyperliquid(Exchange):
_ft_has_futures: FtHas = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit"},
"stoploss_blocks_assets": False,
"stop_price_prop": "stopPrice",
"funding_fee_timeframe": "1h",
"funding_fee_candle_limit": 500,
+1 -13
View File
@@ -12,7 +12,7 @@ from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import CcxtBalances, FtHas, Tickers
from freqtrade.exchange.exchange_types import CcxtBalances, FtHas
logger = logging.getLogger(__name__)
@@ -49,18 +49,6 @@ class Kraken(Exchange):
return parent_check and market.get("darkpool", False) is False
def get_tickers(
self,
symbols: list[str] | None = None,
*,
cached: bool = False,
market_type: TradingMode | None = None,
) -> Tickers:
# Only fetch tickers for current stake currency
# Otherwise the request for kraken becomes too large.
symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]]))
return super().get_tickers(symbols=symbols, cached=cached, market_type=market_type)
def consolidate_balances(self, balances: CcxtBalances) -> CcxtBalances:
"""
Consolidate balances for the same currency.
+1
View File
@@ -44,6 +44,7 @@ class Okx(Exchange):
PriceType.MARK: "index",
PriceType.INDEX: "mark",
},
"stoploss_blocks_assets": False,
"ws_enabled": True,
}
@@ -318,13 +318,13 @@ class BaseReinforcementLearningModel(IFreqaiModel):
rename_dict = {
"%-raw_open": "open",
"%-raw_low": "low",
"%-raw_high": " high",
"%-raw_high": "high",
"%-raw_close": "close",
}
rename_dict_old = {
f"%-{pair}raw_open_{tf}": "open",
f"%-{pair}raw_low_{tf}": "low",
f"%-{pair}raw_high_{tf}": " high",
f"%-{pair}raw_high_{tf}": "high",
f"%-{pair}raw_close_{tf}": "close",
}
+6 -1
View File
@@ -16,7 +16,7 @@ from pandas import DataFrame
from sklearn.model_selection import train_test_split
from freqtrade.configuration import TimeRange
from freqtrade.constants import DOCS_LINK, Config
from freqtrade.constants import DOCS_LINK, ORDERFLOW_ADDED_COLUMNS, Config
from freqtrade.data.converter import reduce_dataframe_footprint
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_seconds
@@ -709,6 +709,11 @@ class FreqaiDataKitchen:
skip_columns = [
(f"{s}_{suffix}") for s in ["date", "open", "high", "low", "close", "volume"]
]
for s in ORDERFLOW_ADDED_COLUMNS:
if s in dataframe.columns and f"{s}_{suffix}" in dataframe.columns:
skip_columns.append(f"{s}_{suffix}")
dataframe = dataframe.drop(columns=skip_columns)
return dataframe
+2
View File
@@ -763,6 +763,8 @@ class IFreqaiModel(ABC):
init_model = None
else:
init_model = self.dd.model_dictionary[pair]
# Set "fresh" tb_logger - the one in model_dictionary has the writer closed.
init_model.tb_logger = self.tb_logger
return init_model
+7 -3
View File
@@ -467,7 +467,7 @@ class FreqtradeBot(LoggingMixin):
order = trade.select_order("stoploss", False)
if order:
logger.info(
f"Updating {trade.exit_side}-fee on trade {trade}"
f"Updating {trade.exit_side}-fee on trade {trade} "
f"for order {order.order_id}."
)
self.update_trade_state(
@@ -485,7 +485,7 @@ class FreqtradeBot(LoggingMixin):
open_order = trade.select_order(trade.entry_side, True)
if order and open_order is None:
logger.info(
f"Updating {trade.entry_side}-fee on trade {trade}"
f"Updating {trade.entry_side}-fee on trade {trade} "
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id, send_msg=False)
@@ -1476,7 +1476,11 @@ class FreqtradeBot(LoggingMixin):
self.handle_protections(trade.pair, trade.trade_direction)
return True
if not trade.has_open_position or not trade.is_open:
if (
not trade.has_open_position
or not trade.is_open
or (trade.has_open_orders and self.exchange.get_option("stoploss_blocks_assets", True))
):
# The trade can be closed already (sell-order fill confirmation came in this iteration)
return False
+1
View File
@@ -7,4 +7,5 @@ from freqtrade.ft_types.backtest_result_type import (
BacktestResultType,
get_BacktestResultType_default,
)
from freqtrade.ft_types.plot_annotation_type import AnnotationType
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType
@@ -0,0 +1,18 @@
from datetime import datetime
from typing import Literal
from pydantic import TypeAdapter
from typing_extensions import Required, TypedDict
class AnnotationType(TypedDict, total=False):
type: Required[Literal["area"]]
start: str | datetime
end: str | datetime
y_start: float
y_end: float
color: str
label: str
AnnotationTypeTA = TypeAdapter(AnnotationType)
+1 -2
View File
@@ -129,7 +129,6 @@ class LookaheadAnalysis(BaseAnalysis):
backtesting._set_strategy(backtesting.strategylist[0])
varholder.data, varholder.timerange = backtesting.load_bt_data()
backtesting.load_bt_data_detail()
varholder.timeframe = backtesting.timeframe
varholder.indicators = backtesting.strategy.advise_all_indicators(varholder.data)
@@ -236,7 +235,7 @@ class LookaheadAnalysis(BaseAnalysis):
return None
if "force_exit" in result_row["exit_reason"]:
logger.info(
"found force-exit in pair: {result_row['pair']}, "
f"found force-exit in pair: {result_row['pair']}, "
f"timerange:{result_row['open_date']}-{result_row['close_date']}, "
f"idx: {idx}, skipping this one to avoid a false-positive."
)
@@ -153,14 +153,8 @@ class LookaheadAnalysisSubFunctions:
raise OperationalException(
"Targeted trade amount can't be smaller than minimum trade amount."
)
if len(config["pairs"]) > config.get("max_open_trades", 0):
logger.info(
"Max_open_trades were less than amount of pairs "
"or defined in the strategy. "
"Set max_open_trades to amount of pairs "
"just to avoid false positives."
)
config["max_open_trades"] = len(config["pairs"])
config["max_open_trades"] = -1
logger.info("Forced max_open_trades to -1 (same amount as there are pairs)")
min_dry_run_wallet = 1000000000
if get_dry_run_wallet(config) < min_dry_run_wallet:
-1
View File
@@ -149,7 +149,6 @@ class RecursiveAnalysis(BaseAnalysis):
backtesting._set_strategy(backtesting.strategylist[0])
varholder.data, varholder.timerange = backtesting.load_bt_data()
backtesting.load_bt_data_detail()
varholder.timeframe = backtesting.timeframe
varholder.indicators = backtesting.strategy.advise_all_indicators(varholder.data)
+57 -17
View File
@@ -9,14 +9,18 @@ from collections import defaultdict
from copy import deepcopy
from datetime import datetime, timedelta
from numpy import nan
from pandas import DataFrame
from numpy import isnan, nan
from pandas import DataFrame, Series
from freqtrade import constants
from freqtrade.configuration import TimeRange, validate_config_consistency
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, IntOrInf, LongShort
from freqtrade.data import history
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
from freqtrade.data.btanalysis import (
find_existing_backtest_stats,
get_tick_size_over_time,
trade_list_to_dataframe,
)
from freqtrade.data.converter import trim_dataframe, trim_dataframes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.metrics import combined_dataframes_with_rel_mean
@@ -35,7 +39,7 @@ from freqtrade.exchange import (
price_to_precision,
timeframe_to_seconds,
)
from freqtrade.exchange.exchange import Exchange
from freqtrade.exchange.exchange import TICK_SIZE, Exchange
from freqtrade.ft_types import (
BacktestContentType,
BacktestContentTypeIcomplete,
@@ -121,6 +125,7 @@ class Backtesting:
self.order_id_counter: int = 0
config["dry_run"] = True
self.price_pair_prec: dict[str, Series] = {}
self.run_ids: dict[str, str] = {}
self.strategylist: list[IStrategy] = []
self.all_bt_content: dict[str, BacktestContentType] = {}
@@ -315,9 +320,15 @@ class Backtesting:
)
self.progress.set_new_value(1)
self._load_bt_data_detail()
self.price_pair_prec = {}
for pair in self.pairlists.whitelist:
if pair in data:
# Load price precision logic
self.price_pair_prec[pair] = get_tick_size_over_time(data[pair])
return data, self.timerange
def load_bt_data_detail(self) -> None:
def _load_bt_data_detail(self) -> None:
"""
Loads backtest detail data (smaller timeframe) if necessary.
"""
@@ -384,6 +395,22 @@ class Backtesting:
else:
self.futures_data = {}
def get_pair_precision(self, pair: str, current_time: datetime) -> tuple[float | None, int]:
"""
Get pair precision at that moment in time
:param pair: Pair to get precision for
:param current_time: Time to get precision for
:return: tuple of price precision, precision_mode_price for the pair at that given time.
"""
precision_series = self.price_pair_prec.get(pair)
if precision_series is not None:
precision = precision_series.asof(current_time)
if not isnan(precision):
# Force tick size if we define the precision
return precision, TICK_SIZE
return self.exchange.get_precision_price(pair), self.precision_mode_price
def disable_database_use(self):
disable_database_use(self.timeframe)
@@ -474,7 +501,12 @@ class Backtesting:
return data
def _get_close_rate(
self, row: tuple, trade: LocalTrade, exit_: ExitCheckTuple, trade_dur: int
self,
row: tuple,
trade: LocalTrade,
current_time: datetime,
exit_: ExitCheckTuple,
trade_dur: int,
) -> float:
"""
Get close rate for backtesting result
@@ -487,7 +519,7 @@ class Backtesting:
):
return self._get_close_rate_for_stoploss(row, trade, exit_, trade_dur)
elif exit_.exit_type == (ExitType.ROI):
return self._get_close_rate_for_roi(row, trade, exit_, trade_dur)
return self._get_close_rate_for_roi(row, trade, current_time, exit_, trade_dur)
else:
return row[OPEN_IDX]
@@ -546,12 +578,21 @@ class Backtesting:
return stoploss_value
def _get_close_rate_for_roi(
self, row: tuple, trade: LocalTrade, exit_: ExitCheckTuple, trade_dur: int
self,
row: tuple,
trade: LocalTrade,
current_time: datetime,
exit_: ExitCheckTuple,
trade_dur: int,
) -> float:
is_short = trade.is_short or False
leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
roi_entry, roi = self.strategy.min_roi_reached_entry(
trade, # type: ignore[arg-type]
trade_dur,
current_time,
)
if roi is not None and roi_entry is not None:
if roi == -1 and roi_entry % self.timeframe_min == 0:
# When force_exiting with ROI=-1, the roi time will always be equal to trade_dur.
@@ -758,7 +799,7 @@ class Backtesting:
amount_ = amount if amount is not None else trade.amount
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
try:
close_rate = self._get_close_rate(row, trade, exit_, trade_dur)
close_rate = self._get_close_rate(row, trade, current_time, exit_, trade_dur)
except ValueError:
return None
# call the custom exit price,with default value as previous close_rate
@@ -792,7 +833,7 @@ class Backtesting:
)
if rate is not None and rate != close_rate:
close_rate = price_to_precision(
rate, trade.price_precision, self.precision_mode_price
rate, trade.price_precision, trade.precision_mode_price
)
# We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately
@@ -925,6 +966,7 @@ class Backtesting:
trade: LocalTrade | None,
order_type: str,
price_precision: float | None,
precision_mode_price: int,
) -> tuple[float, float, float, float]:
if order_type == "limit":
new_rate = strategy_safe_wrapper(
@@ -940,9 +982,7 @@ class Backtesting:
# We can't place orders higher than current high (otherwise it'd be a stop limit entry)
# which freqtrade does not support in live.
if new_rate is not None and new_rate != propose_rate:
propose_rate = price_to_precision(
new_rate, price_precision, self.precision_mode_price
)
propose_rate = price_to_precision(new_rate, price_precision, precision_mode_price)
if direction == "short":
propose_rate = max(propose_rate, row[LOW_IDX])
else:
@@ -1035,7 +1075,7 @@ class Backtesting:
pos_adjust = trade is not None and requested_rate is None
stake_amount_ = stake_amount or (trade.stake_amount if trade else 0.0)
precision_price = self.exchange.get_precision_price(pair)
precision_price, precision_mode_price = self.get_pair_precision(pair, current_time)
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
pair,
@@ -1048,6 +1088,7 @@ class Backtesting:
trade,
order_type,
precision_price,
precision_mode_price,
)
# replace proposed rate if another rate was requested
@@ -1123,7 +1164,7 @@ class Backtesting:
amount_precision=precision_amount,
price_precision=precision_price,
precision_mode=self.precision_mode,
precision_mode_price=self.precision_mode_price,
precision_mode_price=precision_mode_price,
contract_size=contract_size,
orders=[],
)
@@ -1767,7 +1808,6 @@ class Backtesting:
data: dict[str, DataFrame] = {}
data, timerange = self.load_bt_data()
self.load_bt_data_detail()
logger.info("Dataload complete. Calculating indicators")
self.load_prior_backtest()
+79 -58
View File
@@ -4,6 +4,7 @@
This module contains the hyperopt logic
"""
import gc
import logging
import random
from datetime import datetime
@@ -13,14 +14,15 @@ from pathlib import Path
from typing import Any
import rapidjson
from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects
from joblib import Parallel, cpu_count
from optuna.trial import FrozenTrial, Trial, TrialState
from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
from freqtrade.enums import HyperoptState
from freqtrade.exceptions import OperationalException
from freqtrade.misc import file_dump_json, plural
from freqtrade.optimize.hyperopt.hyperopt_logger import logging_mp_handle, logging_mp_setup
from freqtrade.optimize.hyperopt.hyperopt_optimizer import HyperOptimizer
from freqtrade.optimize.hyperopt.hyperopt_optimizer import INITIAL_POINTS, HyperOptimizer
from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput
from freqtrade.optimize.hyperopt_tools import (
HyperoptStateContainer,
@@ -33,12 +35,6 @@ from freqtrade.util import get_progress_tracker
logger = logging.getLogger(__name__)
INITIAL_POINTS = 30
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
# in the skopt model queue, to optimize memory consumption
SKOPT_MODEL_QUEUE_SIZE = 10
log_queue: Any
@@ -92,7 +88,8 @@ class Hyperopt:
self.hyperopt_table_header = 0
self.print_json = self.config.get("print_json", False)
self.hyperopter = HyperOptimizer(self.config)
self.hyperopter = HyperOptimizer(self.config, self.data_pickle_file)
self.count_skipped_epochs = 0
@staticmethod
def get_lock_filename(config: Config) -> str:
@@ -158,61 +155,62 @@ class Hyperopt:
log_queue, logging.INFO if self.config["verbosity"] < 1 else logging.DEBUG
)
return self.hyperopter.generate_optimizer(*args, **kwargs)
return self.hyperopter.generate_optimizer_wrapped(*args, **kwargs)
return parallel(delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked)
return parallel(optimizer_wrapper(v) for v in asked)
def _set_random_state(self, random_state: int | None) -> int:
return random_state or random.randint(1, 2**16 - 1) # noqa: S311
def get_asked_points(self, n_points: int) -> tuple[list[list[Any]], list[bool]]:
def get_optuna_asked_points(self, n_points: int, dimensions: dict) -> list[Any]:
asked: list[list[Any]] = []
for i in range(n_points):
asked.append(self.opt.ask(dimensions))
return asked
def duplicate_optuna_asked_points(self, trial: Trial, asked_trials: list[FrozenTrial]) -> bool:
asked_trials_no_dups: list[FrozenTrial] = []
trials_to_consider = trial.study.get_trials(deepcopy=False, states=[TrialState.COMPLETE])
# Check whether we already evaluated the sampled `params`.
for t in reversed(trials_to_consider):
if trial.params == t.params:
return True
# Check whether same`params` in one batch (asked_trials). Autosampler is doing this.
for t in asked_trials:
if t.params not in asked_trials_no_dups:
asked_trials_no_dups.append(t)
if len(asked_trials_no_dups) != len(asked_trials):
return True
return False
def get_asked_points(self, n_points: int, dimensions: dict) -> tuple[list[Any], list[bool]]:
"""
Enforce points returned from `self.opt.ask` have not been already evaluated
Steps:
1. Try to get points using `self.opt.ask` first
2. Discard the points that have already been evaluated
3. Retry using `self.opt.ask` up to 3 times
4. If still some points are missing in respect to `n_points`, random sample some points
5. Repeat until at least `n_points` points in the `asked_non_tried` list
6. Return a list with length truncated at `n_points`
3. Retry using `self.opt.ask` up to `n_points` times
"""
def unique_list(a_list):
new_list = []
for item in a_list:
if item not in new_list:
new_list.append(item)
return new_list
asked_non_tried: list[FrozenTrial] = []
optuna_asked_trials = self.get_optuna_asked_points(n_points=n_points, dimensions=dimensions)
asked_non_tried += [
x
for x in optuna_asked_trials
if not self.duplicate_optuna_asked_points(x, optuna_asked_trials)
]
i = 0
asked_non_tried: list[list[Any]] = []
is_random_non_tried: list[bool] = []
while i < 5 and len(asked_non_tried) < n_points:
if i < 3:
self.opt.cache_ = {}
asked = unique_list(self.opt.ask(n_points=n_points * 5 if i > 0 else n_points))
is_random = [False for _ in range(len(asked))]
else:
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
is_random = [True for _ in range(len(asked))]
is_random_non_tried += [
rand
for x, rand in zip(asked, is_random, strict=False)
if x not in self.opt.Xi and x not in asked_non_tried
]
asked_non_tried += [
x for x in asked if x not in self.opt.Xi and x not in asked_non_tried
]
while i < 2 * n_points and len(asked_non_tried) < n_points:
asked_new = self.get_optuna_asked_points(n_points=1, dimensions=dimensions)[0]
if not self.duplicate_optuna_asked_points(asked_new, asked_non_tried):
asked_non_tried.append(asked_new)
i += 1
if len(asked_non_tried) < n_points:
if self.count_skipped_epochs == 0:
logger.warning("Duplicate params detected. Maybe your search space is too small?")
self.count_skipped_epochs += n_points - len(asked_non_tried)
if asked_non_tried:
return (
asked_non_tried[: min(len(asked_non_tried), n_points)],
is_random_non_tried[: min(len(asked_non_tried), n_points)],
)
else:
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
return asked_non_tried, [False for _ in range(len(asked_non_tried))]
def evaluate_result(self, val: dict[str, Any], current: int, is_random: bool):
"""
@@ -258,9 +256,7 @@ class Hyperopt:
config_jobs = self.config.get("hyperopt_jobs", -1)
logger.info(f"Number of parallel jobs set as: {config_jobs}")
self.opt = self.hyperopter.get_optimizer(
config_jobs, self.random_state, INITIAL_POINTS, SKOPT_MODEL_QUEUE_SIZE
)
self.opt = self.hyperopter.get_optimizer(self.random_state)
self._setup_logging_mp_workaround()
try:
with Parallel(n_jobs=config_jobs) as parallel:
@@ -276,9 +272,11 @@ class Hyperopt:
if self.analyze_per_epoch:
# First analysis not in parallel mode when using --analyze-per-epoch.
# This allows dataprovider to load it's informative cache.
asked, is_random = self.get_asked_points(n_points=1)
f_val0 = self.hyperopter.generate_optimizer(asked[0])
self.opt.tell(asked, [f_val0["loss"]])
asked, is_random = self.get_asked_points(
n_points=1, dimensions=self.hyperopter.o_dimensions
)
f_val0 = self.hyperopter.generate_optimizer(asked[0].params)
self.opt.tell(asked[0], [f_val0["loss"]])
self.evaluate_result(f_val0, 1, is_random[0])
pbar.update(task, advance=1)
start += 1
@@ -290,9 +288,18 @@ class Hyperopt:
n_rest = (i + 1) * jobs - (self.total_epochs - start)
current_jobs = jobs - n_rest if n_rest > 0 else jobs
asked, is_random = self.get_asked_points(n_points=current_jobs)
f_val = self.run_optimizer_parallel(parallel, asked)
self.opt.tell(asked, [v["loss"] for v in f_val])
asked, is_random = self.get_asked_points(
n_points=current_jobs, dimensions=self.hyperopter.o_dimensions
)
f_val = self.run_optimizer_parallel(
parallel,
[asked1.params for asked1 in asked],
)
f_val_loss = [v["loss"] for v in f_val]
for o_ask, v in zip(asked, f_val_loss, strict=False):
self.opt.tell(o_ask, v)
for j, val in enumerate(f_val):
# Use human-friendly indexes here (starting from 1)
@@ -301,10 +308,24 @@ class Hyperopt:
self.evaluate_result(val, current, is_random[j])
pbar.update(task, advance=1)
logging_mp_handle(log_queue)
gc.collect()
if (
self.hyperopter.es_epochs > 0
and self.hyperopter.es_terminator.should_terminate(self.opt)
):
logger.info(f"Early stopping after {(i + 1) * jobs} epochs")
break
except KeyboardInterrupt:
print("User interrupted..")
if self.count_skipped_epochs > 0:
logger.info(
f"{self.count_skipped_epochs} {plural(self.count_skipped_epochs, 'epoch')} "
f"skipped due to duplicate parameters."
)
logger.info(
f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'."
+1 -1
View File
@@ -12,7 +12,7 @@ from freqtrade.exceptions import OperationalException
with suppress(ImportError):
from skopt.space import Dimension
from freqtrade.optimize.space import Dimension
from freqtrade.optimize.hyperopt.hyperopt_interface import EstimatorType, IHyperOpt
@@ -8,19 +8,18 @@ import math
from abc import ABC
from typing import TypeAlias
from sklearn.base import RegressorMixin
from skopt.space import Categorical, Dimension, Integer
from optuna.samplers import BaseSampler
from freqtrade.constants import Config
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import round_dict
from freqtrade.optimize.space import SKDecimal
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__)
EstimatorType: TypeAlias = RegressorMixin | str
EstimatorType: TypeAlias = BaseSampler | str
class IHyperOpt(ABC):
@@ -44,10 +43,11 @@ class IHyperOpt(ABC):
def generate_estimator(self, dimensions: list[Dimension], **kwargs) -> EstimatorType:
"""
Return base_estimator.
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
inheriting from RegressorMixin (from sklearn).
Can be any of "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIISampler"
"NSGAIIISampler", "QMCSampler" or an instance of a class
inheriting from BaseSampler (from optuna.samplers).
"""
return "ET"
return "NSGAIIISampler"
def generate_roi_table(self, params: dict) -> dict[int, float]:
"""
+118 -78
View File
@@ -7,10 +7,14 @@ import logging
import sys
import warnings
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from joblib import dump, load
import optuna
from joblib import delayed, dump, load, wrap_non_picklable_objects
from joblib.externals import cloudpickle
from optuna.exceptions import ExperimentalWarning
from optuna.terminator import BestValueStagnationEvaluator, Terminator
from pandas import DataFrame
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
@@ -20,7 +24,7 @@ from freqtrade.data.metrics import calculate_market_change
from freqtrade.enums import HyperoptState
from freqtrade.exceptions import OperationalException
from freqtrade.ft_types import BacktestContentType
from freqtrade.misc import deep_merge_dicts
from freqtrade.misc import deep_merge_dicts, round_dict
from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOptLoss to allow unpickling classes from these modules
@@ -28,21 +32,32 @@ from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools
from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.optimize.space import (
DimensionProtocol,
SKDecimal,
ft_CategoricalDistribution,
ft_FloatDistribution,
ft_IntDistribution,
)
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
# Suppress scikit-learn FutureWarnings from skopt
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=FutureWarning)
from skopt import Optimizer
from skopt.space import Dimension
logger = logging.getLogger(__name__)
INITIAL_POINTS = 30
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
optuna_samplers_dict = {
"TPESampler": optuna.samplers.TPESampler,
"GPSampler": optuna.samplers.GPSampler,
"CmaEsSampler": optuna.samplers.CmaEsSampler,
"NSGAIISampler": optuna.samplers.NSGAIISampler,
"NSGAIIISampler": optuna.samplers.NSGAIIISampler,
"QMCSampler": optuna.samplers.QMCSampler,
}
class HyperOptimizer:
"""
@@ -50,15 +65,16 @@ class HyperOptimizer:
This class is sent to the hyperopt worker processes.
"""
def __init__(self, config: Config) -> None:
self.buy_space: list[Dimension] = []
self.sell_space: list[Dimension] = []
self.protection_space: list[Dimension] = []
self.roi_space: list[Dimension] = []
self.stoploss_space: list[Dimension] = []
self.trailing_space: list[Dimension] = []
self.max_open_trades_space: list[Dimension] = []
self.dimensions: list[Dimension] = []
def __init__(self, config: Config, data_pickle_file: Path) -> None:
self.buy_space: list[DimensionProtocol] = []
self.sell_space: list[DimensionProtocol] = []
self.protection_space: list[DimensionProtocol] = []
self.roi_space: list[DimensionProtocol] = []
self.stoploss_space: list[DimensionProtocol] = []
self.trailing_space: list[DimensionProtocol] = []
self.max_open_trades_space: list[DimensionProtocol] = []
self.dimensions: list[DimensionProtocol] = []
self.o_dimensions: dict = {}
self.config = config
self.min_date: datetime
@@ -86,12 +102,14 @@ class HyperOptimizer:
)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
self.data_pickle_file = (
self.config["user_data_dir"] / "hyperopt_results" / "hyperopt_tickerdata.pkl"
)
self.data_pickle_file = data_pickle_file
self.market_change = 0.0
self.es_epochs = config.get("early_stop", 0)
if self.es_epochs > 0 and self.es_epochs < 0.2 * config.get("epochs", 0):
logger.warning(f"Early stop epochs {self.es_epochs} lower than 20% of total epochs")
if HyperoptTools.has_space(self.config, "sell"):
# Make sure use_exit_signal is enabled
self.config["use_exit_signal"] = True
@@ -127,18 +145,6 @@ class HyperOptimizer:
cloudpickle.register_pickle_by_value(mod)
self.hyperopt_pickle_magic(modules.__bases__)
def _get_params_dict(
self, dimensions: list[Dimension], raw_params: list[Any]
) -> dict[str, Any]:
# Ensure the number of dimensions match
# the number of parameters in the list.
if len(raw_params) != len(dimensions):
raise ValueError("Mismatch in number of search-space dimensions.")
# Return a dict where the keys are the names of the dimensions
# and the values are taken from the list of parameters.
return {d.name: v for d, v in zip(dimensions, raw_params, strict=False)}
def _get_params_details(self, params: dict) -> dict:
"""
Return the params for each space
@@ -146,27 +152,36 @@ class HyperOptimizer:
result: dict = {}
if HyperoptTools.has_space(self.config, "buy"):
result["buy"] = {p.name: params.get(p.name) for p in self.buy_space}
result["buy"] = round_dict({p.name: params.get(p.name) for p in self.buy_space}, 13)
if HyperoptTools.has_space(self.config, "sell"):
result["sell"] = {p.name: params.get(p.name) for p in self.sell_space}
result["sell"] = round_dict({p.name: params.get(p.name) for p in self.sell_space}, 13)
if HyperoptTools.has_space(self.config, "protection"):
result["protection"] = {p.name: params.get(p.name) for p in self.protection_space}
result["protection"] = round_dict(
{p.name: params.get(p.name) for p in self.protection_space}, 13
)
if HyperoptTools.has_space(self.config, "roi"):
result["roi"] = {
str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items()
}
result["roi"] = round_dict(
{str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items()}, 13
)
if HyperoptTools.has_space(self.config, "stoploss"):
result["stoploss"] = {p.name: params.get(p.name) for p in self.stoploss_space}
result["stoploss"] = round_dict(
{p.name: params.get(p.name) for p in self.stoploss_space}, 13
)
if HyperoptTools.has_space(self.config, "trailing"):
result["trailing"] = self.custom_hyperopt.generate_trailing_params(params)
result["trailing"] = round_dict(
self.custom_hyperopt.generate_trailing_params(params), 13
)
if HyperoptTools.has_space(self.config, "trades"):
result["max_open_trades"] = {
"max_open_trades": (
self.backtesting.strategy.max_open_trades
if self.backtesting.strategy.max_open_trades != float("inf")
else -1
)
}
result["max_open_trades"] = round_dict(
{
"max_open_trades": (
self.backtesting.strategy.max_open_trades
if self.backtesting.strategy.max_open_trades != float("inf")
else -1
)
},
13,
)
return result
@@ -246,7 +261,12 @@ class HyperOptimizer:
# noinspection PyProtectedMember
attr.value = params_dict[attr_name]
def generate_optimizer(self, raw_params: list[Any]) -> dict[str, Any]:
@delayed
@wrap_non_picklable_objects
def generate_optimizer_wrapped(self, params_dict: dict[str, Any]) -> dict[str, Any]:
return self.generate_optimizer(params_dict)
def generate_optimizer(self, params_dict: dict[str, Any]) -> dict[str, Any]:
"""
Used Optimize function.
Called once per epoch to optimize whatever is configured.
@@ -254,7 +274,6 @@ class HyperOptimizer:
"""
HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE)
backtest_start_time = datetime.now(timezone.utc)
params_dict = self._get_params_dict(self.dimensions, raw_params)
# Apply parameters
if HyperoptTools.has_space(self.config, "buy"):
@@ -304,9 +323,9 @@ class HyperOptimizer:
with self.data_pickle_file.open("rb") as f:
processed = load(f, mmap_mode="r")
if self.analyze_per_epoch:
# Data is not yet analyzed, rerun populate_indicators.
processed = self.advise_and_trim(processed)
if self.analyze_per_epoch:
# Data is not yet analyzed, rerun populate_indicators.
processed = self.advise_and_trim(processed)
bt_results = self.backtesting.backtest(
processed=processed, start_date=self.min_date, end_date=self.max_date
@@ -318,10 +337,10 @@ class HyperOptimizer:
"backtest_end_time": int(backtest_end_time.timestamp()),
}
)
return self._get_results_dict(
result = self._get_results_dict(
bt_results, self.min_date, self.max_date, params_dict, processed=processed
)
return result
def _get_results_dict(
self,
@@ -378,33 +397,55 @@ class HyperOptimizer:
"total_profit": total_profit,
}
def convert_dimensions_to_optuna_space(self, s_dimensions: list[DimensionProtocol]) -> dict:
o_dimensions: dict[str, optuna.distributions.BaseDistribution] = {}
for original_dim in s_dimensions:
if isinstance(
original_dim,
ft_CategoricalDistribution | ft_IntDistribution | ft_FloatDistribution | SKDecimal,
):
o_dimensions[original_dim.name] = original_dim
else:
raise OperationalException(
f"Unknown search space {original_dim.name} - {original_dim} / \
{type(original_dim)}"
)
return o_dimensions
def get_optimizer(
self,
cpu_count: int,
random_state: int,
initial_points: int,
model_queue_size: int,
) -> Optimizer:
dimensions = self.dimensions
estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions)
acq_optimizer = "sampling"
if isinstance(estimator, str):
if estimator not in ("GP", "RF", "ET", "GBRT"):
raise OperationalException(f"Estimator {estimator} not supported.")
else:
acq_optimizer = "auto"
logger.info(f"Using estimator {estimator}.")
return Optimizer(
dimensions,
base_estimator=estimator,
acq_optimizer=acq_optimizer,
n_initial_points=initial_points,
acq_optimizer_kwargs={"n_jobs": cpu_count},
random_state=random_state,
model_queue_size=model_queue_size,
):
o_sampler = self.custom_hyperopt.generate_estimator(
dimensions=self.dimensions, random_state=random_state
)
self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions)
if isinstance(o_sampler, str):
if o_sampler not in optuna_samplers_dict.keys():
raise OperationalException(f"Optuna Sampler {o_sampler} not supported.")
with warnings.catch_warnings():
warnings.filterwarnings(action="ignore", category=ExperimentalWarning)
if o_sampler in ["NSGAIIISampler", "NSGAIISampler"]:
sampler = optuna_samplers_dict[o_sampler](
seed=random_state, population_size=INITIAL_POINTS
)
elif o_sampler in ["GPSampler", "TPESampler", "CmaEsSampler"]:
sampler = optuna_samplers_dict[o_sampler](
seed=random_state, n_startup_trials=INITIAL_POINTS
)
else:
sampler = optuna_samplers_dict[o_sampler](seed=random_state)
else:
sampler = o_sampler
if self.es_epochs > 0:
with warnings.catch_warnings():
warnings.filterwarnings(action="ignore", category=ExperimentalWarning)
self.es_terminator = Terminator(BestValueStagnationEvaluator(self.es_epochs))
logger.info(f"Using optuna sampler {o_sampler}.")
return optuna.create_study(sampler=sampler, direction="minimize")
def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]:
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
@@ -423,7 +464,6 @@ class HyperOptimizer:
def prepare_hyperopt_data(self) -> None:
HyperoptStateContainer.set_state(HyperoptState.DATALOAD)
data, self.timerange = self.backtesting.load_bt_data()
self.backtesting.load_bt_data_detail()
logger.info("Dataload complete. Calculating indicators")
if not self.analyze_per_epoch:
+1 -1
View File
@@ -107,7 +107,7 @@ class HyperoptTools:
"""
Tell if the space value is contained in the configuration
"""
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
# The following spaces are not included in the 'default' set of spaces
if space in ("trailing", "protection", "trades"):
return any(s in config["spaces"] for s in [space, "all"])
else:
@@ -370,8 +370,18 @@ def text_table_add_metrics(strat_results: dict) -> None:
f"{strat_results['winning_days']} / "
f"{strat_results['draw_days']} / {strat_results['losing_days']}",
),
("Avg. Duration Winners", f"{strat_results['winner_holding_avg']}"),
("Avg. Duration Loser", f"{strat_results['loser_holding_avg']}"),
(
"Min/Max/Avg. Duration Winners",
f"{strat_results.get('winner_holding_min', 'N/A')} / "
f"{strat_results.get('winner_holding_max', 'N/A')} / "
f"{strat_results.get('winner_holding_avg', 'N/A')}",
),
(
"Min/Max/Avg. Duration Losers",
f"{strat_results.get('loser_holding_min', 'N/A')} / "
f"{strat_results.get('loser_holding_max', 'N/A')} / "
f"{strat_results.get('loser_holding_avg', 'N/A')}",
),
(
"Max Consecutive Wins / Loss",
(
@@ -23,7 +23,7 @@ from freqtrade.ft_types import (
BacktestResultType,
get_BacktestResultType_default,
)
from freqtrade.util import decimals_per_coin, fmt_coin, get_dry_run_wallet
from freqtrade.util import decimals_per_coin, fmt_coin, format_duration, get_dry_run_wallet
logger = logging.getLogger(__name__)
@@ -336,22 +336,44 @@ def generate_trading_stats(results: DataFrame) -> dict[str, Any]:
}
winning_trades = results.loc[results["profit_ratio"] > 0]
winning_duration = winning_trades["trade_duration"]
draw_trades = results.loc[results["profit_ratio"] == 0]
losing_trades = results.loc[results["profit_ratio"] < 0]
losing_duration = losing_trades["trade_duration"]
holding_avg = (
timedelta(minutes=round(results["trade_duration"].mean()))
if not results.empty
else timedelta()
)
winner_holding_min = (
timedelta(minutes=round(winning_duration[winning_duration > 0].min()))
if not winning_duration.empty
else timedelta()
)
winner_holding_max = (
timedelta(minutes=round(winning_duration.max()))
if not winning_duration.empty
else timedelta()
)
winner_holding_avg = (
timedelta(minutes=round(winning_trades["trade_duration"].mean()))
if not winning_trades.empty
timedelta(minutes=round(winning_duration.mean()))
if not winning_duration.empty
else timedelta()
)
loser_holding_min = (
timedelta(minutes=round(losing_duration[losing_duration > 0].min()))
if not losing_duration.empty
else timedelta()
)
loser_holding_max = (
timedelta(minutes=round(losing_duration.max()))
if not losing_duration.empty
else timedelta()
)
loser_holding_avg = (
timedelta(minutes=round(losing_trades["trade_duration"].mean()))
if not losing_trades.empty
timedelta(minutes=round(losing_duration.mean()))
if not losing_duration.empty
else timedelta()
)
winstreak, loss_streak = calc_streak(results)
@@ -363,9 +385,17 @@ def generate_trading_stats(results: DataFrame) -> dict[str, Any]:
"winrate": len(winning_trades) / len(results) if len(results) else 0.0,
"holding_avg": holding_avg,
"holding_avg_s": holding_avg.total_seconds(),
"winner_holding_avg": winner_holding_avg,
"winner_holding_min": format_duration(winner_holding_min),
"winner_holding_min_s": winner_holding_min.total_seconds(),
"winner_holding_max": format_duration(winner_holding_max),
"winner_holding_max_s": winner_holding_max.total_seconds(),
"winner_holding_avg": format_duration(winner_holding_avg),
"winner_holding_avg_s": winner_holding_avg.total_seconds(),
"loser_holding_avg": loser_holding_avg,
"loser_holding_min": format_duration(loser_holding_min),
"loser_holding_min_s": loser_holding_min.total_seconds(),
"loser_holding_max": format_duration(loser_holding_max),
"loser_holding_max_s": loser_holding_max.total_seconds(),
"loser_holding_avg": format_duration(loser_holding_avg),
"loser_holding_avg_s": loser_holding_avg.total_seconds(),
"max_consecutive_wins": winstreak,
"max_consecutive_losses": loss_streak,
+15 -2
View File
@@ -1,3 +1,16 @@
from skopt.space import Categorical, Dimension, Integer, Real # noqa: F401
from .decimalspace import SKDecimal
from .optunaspaces import (
DimensionProtocol,
ft_CategoricalDistribution,
ft_FloatDistribution,
ft_IntDistribution,
)
from .decimalspace import SKDecimal # noqa: F401
# Alias for the distribution classes
Dimension = DimensionProtocol
Categorical = ft_CategoricalDistribution
Integer = ft_IntDistribution
Real = ft_FloatDistribution
__all__ = ["Categorical", "Dimension", "Integer", "Real", "SKDecimal"]
+27 -39
View File
@@ -1,47 +1,35 @@
import numpy as np
from skopt.space import Integer
from optuna.distributions import FloatDistribution
class SKDecimal(Integer):
class SKDecimal(FloatDistribution):
def __init__(
self,
low,
high,
decimals=3,
prior="uniform",
base=10,
transform=None,
low: float,
high: float,
*,
step: float | None = None,
decimals: int | None = None,
name=None,
dtype=np.int64,
):
self.decimals = decimals
"""
FloatDistribution with a fixed step size.
Only one of step or decimals can be set.
:param low: lower bound
:param high: upper bound
:param step: step size (e.g. 0.001)
:param decimals: number of decimal places to round to (e.g. 3)
:param name: name of the distribution
"""
if decimals is not None and step is not None:
raise ValueError("You can only set one of decimals or step")
if decimals is None and step is None:
raise ValueError("You must set one of decimals or step")
# Convert decimals to step
self.step = step or (1 / 10**decimals if decimals else 1)
self.name = name
self.pow_dot_one = pow(0.1, self.decimals)
self.pow_ten = pow(10, self.decimals)
_low = int(low * self.pow_ten)
_high = int(high * self.pow_ten)
# trunc to precision to avoid points out of space
self.low_orig = round(_low * self.pow_dot_one, self.decimals)
self.high_orig = round(_high * self.pow_dot_one, self.decimals)
super().__init__(_low, _high, prior, base, transform, name, dtype)
def __repr__(self):
return (
f"Decimal(low={self.low_orig}, high={self.high_orig}, decimals={self.decimals}, "
f"prior='{self.prior}', transform='{self.transform_}')"
super().__init__(
low=round(low, decimals) if decimals else low,
high=round(high, decimals) if decimals else high,
step=self.step,
)
def __contains__(self, point):
if isinstance(point, list):
point = np.array(point)
return self.low_orig <= point <= self.high_orig
def transform(self, Xt):
return super().transform([int(v * self.pow_ten) for v in Xt])
def inverse_transform(self, Xt):
res = super().inverse_transform(Xt)
# equivalent to [round(x * pow(0.1, self.decimals), self.decimals) for x in res]
return [int(v) / self.pow_ten for v in res]
+59
View File
@@ -0,0 +1,59 @@
from collections.abc import Sequence
from typing import Any, Protocol
from optuna.distributions import CategoricalDistribution, FloatDistribution, IntDistribution
class DimensionProtocol(Protocol):
name: str
class ft_CategoricalDistribution(CategoricalDistribution):
def __init__(
self,
categories: Sequence[Any],
name: str,
**kwargs,
):
self.name = name
self.categories = categories
# if len(categories) <= 1:
# raise Exception(f"need at least 2 categories for {name}")
return super().__init__(categories)
def __repr__(self):
return f"CategoricalDistribution({self.categories})"
class ft_IntDistribution(IntDistribution):
def __init__(
self,
low: int | float,
high: int | float,
name: str,
**kwargs,
):
self.name = name
self.low = int(low)
self.high = int(high)
return super().__init__(self.low, self.high, **kwargs)
def __repr__(self):
return f"IntDistribution(low={self.low}, high={self.high})"
class ft_FloatDistribution(FloatDistribution):
def __init__(
self,
low: float,
high: float,
name: str,
**kwargs,
):
self.name = name
self.low = low
self.high = high
return super().__init__(low, high, **kwargs)
def __repr__(self):
return f"FloatDistribution(low={self.low}, high={self.high}, step={self.step})"
+14 -10
View File
@@ -1027,12 +1027,12 @@ class LocalTrade:
Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees
"""
open_trade = FtPrecise(amount) * FtPrecise(open_rate)
fees = open_trade * FtPrecise(self.fee_open)
open_value = FtPrecise(amount) * FtPrecise(open_rate)
fees = open_value * FtPrecise(self.fee_open)
if self.is_short:
return float(open_trade - fees)
return float(open_value - fees)
else:
return float(open_trade + fees)
return float(open_value + fees)
def recalc_open_trade_value(self) -> None:
"""
@@ -1062,13 +1062,13 @@ class LocalTrade:
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float | None) -> FtPrecise:
close_trade = amount * FtPrecise(rate)
fees = close_trade * FtPrecise(fee or 0.0)
close_value = amount * FtPrecise(rate)
fees = close_value * FtPrecise(fee or 0.0)
if self.is_short:
return close_trade + fees
return close_value + fees
else:
return close_trade - fees
return close_value - fees
def calc_close_trade_value(self, rate: float, amount: float | None = None) -> float:
"""
@@ -1802,11 +1802,15 @@ class Trade(ModelBase, LocalTrade):
close_date: datetime | None = None,
) -> list["LocalTrade"]:
"""
Helper function to query Trades.j
Helper function to query Trades.
Returns a List of trades, filtered on the parameters given.
In live mode, converts the filter to a database query and returns all rows
In Backtest mode, uses filters on Trade.bt_trades to get the result.
:param pair: Filter by pair
:param is_open: Filter by open/closed status
:param open_date: Filter by open_date (filters via trade.open_date > input)
:param close_date: Filter by close_date (filters via trade.close_date > input)
and will implicitly only return closed trades.
:return: unsorted List[Trade]
"""
if Trade.use_db:
+1 -1
View File
@@ -79,7 +79,7 @@ class StrategyResolver(IResolver):
("ignore_buying_expired_candle_after", 0),
("position_adjustment_enable", False),
("max_entry_position_adjustment", -1),
("max_open_trades", -1),
("max_open_trades", float("inf")),
]
for attribute, default in attributes:
StrategyResolver._override_attribute_helper(strategy, config, attribute, default)
-1
View File
@@ -62,7 +62,6 @@ def __run_backtest_bg(btconfig: Config):
from freqtrade.optimize.backtesting import Backtesting
ApiBG.bt["bt"] = Backtesting(btconfig)
ApiBG.bt["bt"].load_bt_data_detail()
else:
ApiBG.bt["bt"].config = btconfig
ApiBG.bt["bt"].init_backtest()
+2 -1
View File
@@ -5,7 +5,7 @@ from pydantic import AwareDatetime, BaseModel, RootModel, SerializeAsAny, model_
from freqtrade.constants import DL_DATA_TIMEFRAMES, IntOrInf
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
from freqtrade.ft_types import ValidExchangesType
from freqtrade.ft_types import AnnotationType, ValidExchangesType
from freqtrade.rpc.api_server.webserver_bgwork import ProgressTask
@@ -539,6 +539,7 @@ class PairHistory(BaseModel):
columns: list[str]
all_columns: list[str] = []
data: SerializeAsAny[list[Any]]
annotations: list[AnnotationType] | None = None
length: int
buy_signals: int
sell_signals: int
+16 -1
View File
@@ -32,6 +32,7 @@ from freqtrade.enums import (
from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_msecs
from freqtrade.exchange.exchange_utils import price_to_precision
from freqtrade.ft_types import AnnotationType
from freqtrade.loggers import bufferHandler
from freqtrade.persistence import CustomDataWrapper, KeyValueStore, PairLocks, Trade
from freqtrade.persistence.models import PairLock
@@ -1356,6 +1357,7 @@ class RPC:
dataframe: DataFrame,
last_analyzed: datetime,
selected_cols: list[str] | None,
annotations: list[AnnotationType],
) -> dict[str, Any]:
has_content = len(dataframe) != 0
dataframe_columns = list(dataframe.columns)
@@ -1411,6 +1413,7 @@ class RPC:
"data_start_ts": 0,
"data_stop": "",
"data_stop_ts": 0,
"annotations": annotations,
}
if has_content:
res.update(
@@ -1429,8 +1432,16 @@ class RPC:
"""Analyzed dataframe in Dict form"""
_data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
annotations = self._freqtrade.strategy.ft_plot_annotations(pair=pair, dataframe=_data)
return RPC._convert_dataframe_to_dict(
self._freqtrade.config["strategy"], pair, timeframe, _data, last_analyzed, selected_cols
self._freqtrade.config["strategy"],
pair,
timeframe,
_data,
last_analyzed,
selected_cols,
annotations,
)
def __rpc_analysed_dataframe_raw(
@@ -1531,6 +1542,7 @@ class RPC:
)
data = _data[pair]
annotations = []
if config.get("strategy"):
strategy.dp = DataProvider(config, exchange=exchange, pairlists=None)
strategy.ft_bot_start()
@@ -1539,6 +1551,8 @@ class RPC:
df_analyzed = trim_dataframe(
df_analyzed, timerange_parsed, startup_candles=startup_candles
)
annotations = strategy.ft_plot_annotations(pair=pair, dataframe=df_analyzed)
else:
df_analyzed = data
@@ -1549,6 +1563,7 @@ class RPC:
df_analyzed.copy(),
dt_now(),
selected_cols,
annotations,
)
def _rpc_plot_config(self) -> dict[str, Any]:
+2
View File
@@ -6,6 +6,7 @@ from freqtrade.exchange import (
timeframe_to_prev_date,
timeframe_to_seconds,
)
from freqtrade.ft_types import AnnotationType
from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.strategy.informative_decorator import informative
from freqtrade.strategy.interface import IStrategy
@@ -44,4 +45,5 @@ __all__ = [
"merge_informative_pair",
"stoploss_from_absolute",
"stoploss_from_open",
"AnnotationType",
]
+130 -37
View File
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta, timezone
from math import isinf, isnan
from pandas import DataFrame
from pydantic import ValidationError
from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, Config, IntOrInf, ListPairsWithTimeframes
from freqtrade.data.converter import populate_dataframe_with_trades
@@ -27,6 +28,7 @@ from freqtrade.enums import (
)
from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
from freqtrade.ft_types import AnnotationType
from freqtrade.misc import remove_entry_exit_signals
from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.strategy.hyper import HyperStrategyMixin
@@ -36,6 +38,7 @@ from freqtrade.strategy.informative_decorator import (
_create_and_merge_informative_pair,
_format_pair_name,
)
from freqtrade.strategy.strategy_validation import StrategyResultValidator
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import dt_now
from freqtrade.wallets import Wallets
@@ -65,6 +68,7 @@ class IStrategy(ABC, HyperStrategyMixin):
_ft_params_from_file: dict
# associated minimal roi
minimal_roi: dict = {}
use_custom_roi: bool = False
# associated stoploss
stoploss: float
@@ -464,6 +468,35 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return self.stoploss
def custom_roi(
self,
pair: str,
trade: Trade,
current_time: datetime,
trade_duration: int,
entry_tag: str | None,
side: str,
**kwargs,
) -> float | None:
"""
Custom ROI logic, returns a new minimum ROI threshold (as a ratio, e.g., 0.05 for +5%).
Only called when use_custom_roi is set to True.
If used at the same time as minimal_roi, an exit will be triggered when the lower
threshold is reached. Example: If minimal_roi = {"0": 0.01} and custom_roi returns 0.05,
an exit will be triggered if profit reaches 5%.
:param pair: Pair that's currently analyzed.
:param trade: trade object.
:param current_time: datetime object, containing the current datetime.
:param trade_duration: Current trade duration in minutes.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the current trade.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New ROI value as a ratio, or None to fall back to minimal_roi logic.
"""
return None
def custom_entry_price(
self,
pair: str,
@@ -834,6 +867,24 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return None
def plot_annotations(
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
) -> list[AnnotationType]:
"""
Retrieve area annotations for a chart.
Must be returned as array, with type, label, color, start, end, y_start, y_end.
All settings except for type are optional - though it usually makes sense to include either
"start and end" or "y_start and y_end" for either horizontal or vertical plots
(or all 4 for boxes).
:param pair: Pair that's currently analyzed
:param start_date: Start date of the chart data being requested
:param end_date: End date of the chart data being requested
:param dataframe: DataFrame with the analyzed data for the chart
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: List of AnnotationType objects
"""
return []
def populate_any_indicators(
self,
pair: str,
@@ -1193,13 +1244,15 @@ class IStrategy(ABC, HyperStrategyMixin):
return
try:
df_len, df_close, df_date = self.preserve_df(dataframe)
validator = StrategyResultValidator(
dataframe, warn_only=not self.disable_dataframe_checks
)
dataframe = strategy_safe_wrapper(self._analyze_ticker_internal, message="")(
dataframe, {"pair": pair}
)
self.assert_df(dataframe, df_len, df_close, df_date)
validator.assert_df(dataframe)
except StrategyError as error:
logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
return
@@ -1216,31 +1269,6 @@ class IStrategy(ABC, HyperStrategyMixin):
for pair in pairs:
self.analyze_pair(pair)
@staticmethod
def preserve_df(dataframe: DataFrame) -> tuple[int, float, datetime]:
"""keep some data for dataframes"""
return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1]
def assert_df(self, dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
"""
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
"""
message_template = "Dataframe returned from strategy has mismatching {}."
message = ""
if dataframe is None:
message = "No dataframe returned (return statement missing?)."
elif df_len != len(dataframe):
message = message_template.format("length")
elif df_close != dataframe["close"].iloc[-1]:
message = message_template.format("last close price")
elif df_date != dataframe["date"].iloc[-1]:
message = message_template.format("last date")
if message:
if self.disable_dataframe_checks:
logger.warning(message)
else:
raise StrategyError(message)
def get_latest_candle(
self,
pair: str,
@@ -1618,18 +1646,49 @@ class IStrategy(ABC, HyperStrategyMixin):
return ExitCheckTuple(exit_type=ExitType.NONE)
def min_roi_reached_entry(self, trade_dur: int) -> tuple[int | None, float | None]:
def min_roi_reached_entry(
self,
trade: Trade,
trade_dur: int,
current_time: datetime,
) -> tuple[int | None, float | None]:
"""
Based on trade duration defines the ROI entry that may have been reached.
:param trade_dur: trade duration in minutes
:return: minimal ROI entry value or None if none proper ROI entry was found.
"""
# Get custom ROI if use_custom_roi is set to True
custom_roi = None
if self.use_custom_roi:
custom_roi = strategy_safe_wrapper(
self.custom_roi, default_retval=None, supress_error=True
)(
pair=trade.pair,
trade=trade,
current_time=current_time,
trade_duration=trade_dur,
entry_tag=trade.enter_tag,
side=trade.trade_direction,
)
if custom_roi is None or isnan(custom_roi) or isinf(custom_roi):
custom_roi = None
logger.debug(f"Custom ROI function did not return a valid ROI for {trade.pair}")
# Get highest entry in ROI dict where key <= trade-duration
roi_list = [x for x in self.minimal_roi.keys() if x <= trade_dur]
if not roi_list:
return None, None
roi_entry = max(roi_list)
return roi_entry, self.minimal_roi[roi_entry]
if roi_list:
roi_entry = max(roi_list)
min_roi = self.minimal_roi[roi_entry]
else:
roi_entry = None
min_roi = None
# The lowest available value is used to trigger an exit.
if custom_roi is not None and (min_roi is None or custom_roi < min_roi):
return trade_dur, custom_roi
else:
return roi_entry, min_roi
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
"""
@@ -1640,7 +1699,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
# Check if time matches and current rate is above threshold
trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)
_, roi = self.min_roi_reached_entry(trade_dur)
_, roi = self.min_roi_reached_entry(trade, trade_dur, current_time)
if roi is None:
return False
else:
@@ -1681,10 +1740,14 @@ class IStrategy(ABC, HyperStrategyMixin):
Has positive effects on memory usage for whatever reason - also when
using only one strategy.
"""
return {
pair: self.advise_indicators(pair_data.copy(), {"pair": pair}).copy()
for pair, pair_data in data.items()
}
res = {}
for pair, pair_data in data.items():
validator = StrategyResultValidator(
pair_data, warn_only=not self.disable_dataframe_checks
)
res[pair] = self.advise_indicators(pair_data.copy(), {"pair": pair}).copy()
validator.assert_df(res[pair])
return res
def ft_advise_signals(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
@@ -1780,3 +1843,33 @@ class IStrategy(ABC, HyperStrategyMixin):
if "exit_long" not in df.columns:
df = df.rename({"sell": "exit_long"}, axis="columns")
return df
def ft_plot_annotations(self, pair: str, dataframe: DataFrame) -> list[AnnotationType]:
"""
Internal wrapper around plot_dataframe
"""
if len(dataframe) > 0:
annotations = strategy_safe_wrapper(self.plot_annotations)(
pair=pair,
dataframe=dataframe,
start_date=dataframe.iloc[0]["date"].to_pydatetime(),
end_date=dataframe.iloc[-1]["date"].to_pydatetime(),
)
from freqtrade.ft_types.plot_annotation_type import AnnotationTypeTA
annotations_new: list[AnnotationType] = []
for annotation in annotations:
if isinstance(annotation, dict):
# Convert to AnnotationType
try:
AnnotationTypeTA.validate_python(annotation)
annotations_new.append(annotation)
except ValidationError as e:
logger.error(f"Invalid annotation data: {annotation}. Error: {e}")
else:
# Already an AnnotationType
annotations_new.append(annotation)
return annotations_new
return []
+21 -16
View File
@@ -14,9 +14,12 @@ from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer
with suppress(ImportError):
from skopt.space import Categorical, Integer, Real
from freqtrade.optimize.space import SKDecimal
from freqtrade.optimize.space import (
Categorical,
Integer,
Real,
SKDecimal,
)
from freqtrade.exceptions import OperationalException
@@ -51,7 +54,8 @@ class BaseParameter(ABC):
name is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical).
:param kwargs: Extra parameters to optuna.distributions.
(IntDistribution|FloatDistribution|CategoricalDistribution).
"""
if "name" in kwargs:
raise OperationalException(
@@ -109,7 +113,7 @@ class NumericParameter(BaseParameter):
parameter fieldname is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.*.
:param kwargs: Extra parameters to optuna.distributions.*.
"""
if high is not None and isinstance(low, Sequence):
raise OperationalException(f"{self.__class__.__name__} space invalid.")
@@ -151,7 +155,7 @@ class IntParameter(NumericParameter):
parameter fieldname is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.Integer.
:param kwargs: Extra parameters to optuna.distributions.IntDistribution.
"""
super().__init__(
@@ -160,7 +164,7 @@ class IntParameter(NumericParameter):
def get_space(self, name: str) -> "Integer":
"""
Create skopt optimization space.
Create optuna distribution space.
:param name: A name of parameter field.
"""
return Integer(low=self.low, high=self.high, name=name, **self._space_params)
@@ -174,7 +178,7 @@ class IntParameter(NumericParameter):
calculating 100ds of indicators.
"""
if self.can_optimize():
# Scikit-optimize ranges are "inclusive", while python's "range" is exclusive
# optuna distributions ranges are "inclusive", while python's "range" is exclusive
return range(self.low, self.high + 1)
else:
return range(self.value, self.value + 1)
@@ -205,7 +209,7 @@ class RealParameter(NumericParameter):
parameter fieldname is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.Real.
:param kwargs: Extra parameters to optuna.distributions.FloatDistribution.
"""
super().__init__(
low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs
@@ -213,7 +217,7 @@ class RealParameter(NumericParameter):
def get_space(self, name: str) -> "Real":
"""
Create skopt optimization space.
Create optimization space.
:param name: A name of parameter field.
"""
return Real(low=self.low, high=self.high, name=name, **self._space_params)
@@ -246,7 +250,7 @@ class DecimalParameter(NumericParameter):
parameter fieldname is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.Integer.
:param kwargs: Extra parameters to optuna's NumericParameter.
"""
self._decimals = decimals
default = round(default, self._decimals)
@@ -257,7 +261,7 @@ class DecimalParameter(NumericParameter):
def get_space(self, name: str) -> "SKDecimal":
"""
Create skopt optimization space.
Create optimization space.
:param name: A name of parameter field.
"""
return SKDecimal(
@@ -305,7 +309,8 @@ class CategoricalParameter(BaseParameter):
name is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.Categorical.
:param kwargs: Compatibility. Optuna's CategoricalDistribution does not
accept extra parameters
"""
if len(categories) < 2:
raise OperationalException(
@@ -316,10 +321,10 @@ class CategoricalParameter(BaseParameter):
def get_space(self, name: str) -> "Categorical":
"""
Create skopt optimization space.
Create optuna distribution space.
:param name: A name of parameter field.
"""
return Categorical(self.opt_range, name=name, **self._space_params)
return Categorical(self.opt_range, name=name)
@property
def range(self):
@@ -355,7 +360,7 @@ class BooleanParameter(CategoricalParameter):
name is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.Categorical.
:param kwargs: Extra parameters to optuna.distributions.CategoricalDistribution.
"""
categories = [True, False]
+42
View File
@@ -0,0 +1,42 @@
import logging
from datetime import datetime
from pandas import DataFrame
from freqtrade.exceptions import StrategyError
logger = logging.getLogger(__name__)
class StrategyResultValidator:
def __init__(self, dataframe: DataFrame, warn_only: bool = False):
self._warn_only = warn_only
self._length: int = len(dataframe)
self._close: float = dataframe["close"].iloc[-1]
self._date: datetime = dataframe["date"].iloc[-1]
def assert_df(self, dataframe: DataFrame):
"""
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
Raises a StrategyError if the dataframe does not match the expected values.
If warn_only is set, it will log a warning instead of raising an error.
:param dataframe: DataFrame to validate
:raises StrategyError: If the dataframe does not match the expected values.
:logs Warning: If warn_only is set and the dataframe does not match the expected values.
"""
message_template = "Dataframe returned from strategy has mismatching {}."
message = ""
if dataframe is None:
message = "No dataframe returned (return statement missing?)."
elif self._length != len(dataframe):
message = message_template.format("length")
elif self._close != dataframe["close"].iloc[-1]:
message = message_template.format("last close price")
elif self._date != dataframe["date"].iloc[-1]:
message = message_template.format("last date")
if message:
if self._warn_only:
logger.warning(message)
else:
raise StrategyError(message)
+1
View File
@@ -28,6 +28,7 @@ from freqtrade.strategy import (
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
AnnotationType,
)
# --------------------------------
@@ -135,6 +135,37 @@ def custom_stake_amount(
"""
return proposed_stake
use_custom_roi = True
def custom_roi(
self,
pair: str,
trade: Trade,
current_time: datetime,
trade_duration: int,
entry_tag: str | None,
side: str,
**kwargs,
) -> float | None:
"""
Custom ROI logic, returns a new minimum ROI threshold (as a ratio, e.g., 0.05 for +5%).
Only called when use_custom_roi is set to True.
If used at the same time as minimal_roi, an exit will be triggered when the lower
threshold is reached. Example: If minimal_roi = {"0": 0.01} and custom_roi returns 0.05,
an exit will be triggered if profit reaches 5%.
:param pair: Pair that's currently analyzed.
:param trade: trade object.
:param current_time: datetime object, containing the current datetime.
:param trade_duration: Current trade duration in minutes.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the current trade.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New ROI value as a ratio, or None to fall back to minimal_roi logic.
"""
return None
use_custom_stoploss = True
def custom_stoploss(
@@ -399,3 +430,21 @@ def order_filled(
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def plot_annotations(
self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
) -> list[AnnotationType]:
"""
Retrieve area annotations for a chart.
Must be returned as array, with type, label, color, start, end, y_start, y_end.
All settings except for type are optional - though it usually makes sense to include either
"start and end" or "y_start and y_end" for either horizontal or vertical plots
(or all 4 for boxes).
:param pair: Pair that's currently analyzed
:param start_date: Start date of the chart data being requested
:param end_date: End date of the chart data being requested
:param dataframe: DataFrame with the analyzed data for the chart
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: List of AnnotationType objects
"""
return []
+8 -1
View File
@@ -13,7 +13,13 @@ from freqtrade.util.datetime_helpers import (
shorten_date,
)
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
from freqtrade.util.formatters import decimals_per_coin, fmt_coin, fmt_coin2, round_value
from freqtrade.util.formatters import (
decimals_per_coin,
fmt_coin,
fmt_coin2,
format_duration,
round_value,
)
from freqtrade.util.ft_precise import FtPrecise
from freqtrade.util.measure_time import MeasureTime
from freqtrade.util.periodic_cache import PeriodicCache
@@ -44,6 +50,7 @@ __all__ = [
"shorten_date",
"decimals_per_coin",
"round_value",
"format_duration",
"fmt_coin",
"fmt_coin2",
"MeasureTime",
+14
View File
@@ -1,3 +1,5 @@
from datetime import timedelta
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
@@ -66,3 +68,15 @@ def fmt_coin2(
val = f"{val} {coin}"
return val
def format_duration(td: timedelta) -> str:
"""
Format a timedelta object to "XXd HH:MM" format
:param td: Timedelta object to format
:return: Formatted time string
"""
d = td.days
h, r = divmod(td.seconds, 3600)
m, s = divmod(r, 60)
return f"{d}d {h:02d}:{m:02d}"
+1 -1
View File
@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2025.4"
__version__ = "2025.5"
if "dev" in __version__:
from pathlib import Path
+3 -1
View File
@@ -86,8 +86,10 @@ extra_css:
- "stylesheets/ft.extra.css"
extra_javascript:
- javascripts/config.js
- https://polyfill.io/v3/polyfill.min.js?features=es6
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
exclude_docs: |
/commands/*
/includes/*
markdown_extensions:
- attr_list
- admonition
+2 -1
View File
@@ -79,7 +79,8 @@ plot = ["plotly>=4.0"]
hyperopt = [
"scipy",
"scikit-learn",
"ft-scikit-optimize>=0.9.2",
"optuna > 4.0.0",
"cmaes",
"filelock",
]
freqai = [
+5 -5
View File
@@ -7,7 +7,7 @@
-r docs/requirements-docs.txt
coveralls==4.0.1
ruff==0.11.7
ruff==0.11.11
mypy==1.15.0
pre-commit==4.2.0
pytest==8.3.5
@@ -15,7 +15,7 @@ pytest-asyncio==0.26.0
pytest-cov==6.1.1
pytest-mock==3.14.0
pytest-random-order==1.1.1
pytest-timeout==2.3.1
pytest-timeout==2.4.0
pytest-xdist==3.6.1
isort==6.0.1
# For datetime mocking
@@ -25,8 +25,8 @@ time-machine==2.16.0
nbconvert==7.16.6
# mypy types
types-cachetools==5.5.0.20240820
types-cachetools==6.0.0.20250525
types-filelock==3.2.7
types-requests==2.32.0.20250328
types-requests==2.32.0.20250515
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20241206
types-python-dateutil==2.9.0.20250516
+3 -3
View File
@@ -4,9 +4,9 @@
# Required for freqai
scikit-learn==1.6.1
joblib==1.4.2
joblib==1.5.1
catboost==1.2.8; 'arm' not in platform_machine
lightgbm==4.6.0
xgboost==2.1.4
xgboost==3.0.2
tensorboard==2.19.0
datasieve==0.1.7
datasieve==0.1.9
+3 -2
View File
@@ -2,7 +2,8 @@
-r requirements.txt
# Required for hyperopt
scipy==1.15.2
scipy==1.15.3
scikit-learn==1.6.1
ft-scikit-optimize==0.9.2
filelock==3.18.0
optuna==4.3.0
cmaes==0.11.1
+1 -1
View File
@@ -1,4 +1,4 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==6.0.1
plotly==6.1.1
+10 -9
View File
@@ -4,24 +4,25 @@ bottleneck==1.4.2
numexpr==2.10.2
pandas-ta==0.3.14b
ccxt==4.4.77
cryptography==44.0.2
ccxt==4.4.82
cryptography==45.0.3
aiohttp==3.9.5
SQLAlchemy==2.0.40
python-telegram-bot==22.0
SQLAlchemy==2.0.41
python-telegram-bot==22.1
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.12.2
cachetools==5.5.2
humanize==4.12.3
cachetools==6.0.0
requests==2.32.3
urllib3==2.4.0
certifi==2025.4.26
jsonschema==4.23.0
TA-Lib==0.4.38
technical==1.5.0
tabulate==0.9.0
pycoingecko==3.2.0
jinja2==3.1.6
joblib==1.4.2
joblib==1.5.1
rich==14.0.0
pyarrow==20.0.0; platform_machine != 'armv7l'
@@ -31,14 +32,14 @@ py_find_1st==1.1.7
# Load ticker files 30% faster
python-rapidjson==1.20
# Properly format api responses
orjson==3.10.16
orjson==3.10.18
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.115.12
pydantic==2.11.3
pydantic==2.11.5
uvicorn==0.34.2
pyjwt==2.10.1
aiofiles==24.1.0
+92
View File
@@ -0,0 +1,92 @@
# pragma pylint: disable=missing-docstring, C0103
from datetime import timezone
import pandas as pd
from numpy import nan
from pandas import DataFrame, Timestamp
from freqtrade.data.btanalysis.historic_precision import get_tick_size_over_time
def test_get_tick_size_over_time():
"""
Test the get_tick_size_over_time function with predefined data
"""
# Create test dataframe with different levels of precision
data = {
"date": [
Timestamp("2020-01-01 00:00:00", tz=timezone.utc),
Timestamp("2020-01-02 00:00:00", tz=timezone.utc),
Timestamp("2020-01-03 00:00:00", tz=timezone.utc),
Timestamp("2020-01-15 00:00:00", tz=timezone.utc),
Timestamp("2020-01-16 00:00:00", tz=timezone.utc),
Timestamp("2020-01-31 00:00:00", tz=timezone.utc),
Timestamp("2020-02-01 00:00:00", tz=timezone.utc),
Timestamp("2020-02-15 00:00:00", tz=timezone.utc),
Timestamp("2020-03-15 00:00:00", tz=timezone.utc),
],
"open": [1.23456, 1.234, 1.23, 1.2, 1.23456, 1.234, 2.3456, 2.34, 2.34],
"high": [1.23457, 1.235, 1.24, 1.3, 1.23456, 1.235, 2.3457, 2.34, 2.34],
"low": [1.23455, 1.233, 1.22, 1.1, 1.23456, 1.233, 2.3455, 2.34, 2.34],
"close": [1.23456, 1.234, 1.23, 1.2, 1.23456, 1.234, 2.3456, 2.34, 2.34],
"volume": [100, 200, 300, 400, 500, 600, 700, 800, 900],
}
candles = DataFrame(data)
# Calculate significant digits
result = get_tick_size_over_time(candles)
# Check that the result is a pandas Series
assert isinstance(result, pd.Series)
# Check that we have three months of data (Jan, Feb and March 2020 )
assert len(result) == 3
# Before
assert result.asof("2019-01-01 00:00:00+00:00") is nan
# January should have 5 significant digits (based on 1.23456789 being the most precise value)
# which should be converted to 0.00001
assert result.asof("2020-01-01 00:00:00+00:00") == 0.00001
assert result.asof("2020-01-01 00:00:00+00:00") == 0.00001
assert result.asof("2020-02-25 00:00:00+00:00") == 0.0001
assert result.asof("2020-03-25 00:00:00+00:00") == 0.01
assert result.asof("2020-04-01 00:00:00+00:00") == 0.01
# Value far past the last date should be the last value
assert result.asof("2025-04-01 00:00:00+00:00") == 0.01
assert result.iloc[0] == 0.00001
def test_get_tick_size_over_time_real_data(testdatadir):
"""
Test the get_tick_size_over_time function with real data from the testdatadir
"""
from freqtrade.data.history import load_pair_history
# Load some test data from the testdata directory
pair = "UNITTEST/BTC"
timeframe = "1m"
candles = load_pair_history(
datadir=testdatadir,
pair=pair,
timeframe=timeframe,
)
# Make sure we have test data
assert not candles.empty, "No test data found, cannot run test"
# Calculate significant digits
result = get_tick_size_over_time(candles)
assert isinstance(result, pd.Series)
# Verify that all values are between 0 and 1 (valid precision values)
assert all(result > 0)
assert all(result < 1)
assert all(result <= 0.0001)
assert all(result >= 0.00000001)
+23 -6
View File
@@ -96,10 +96,27 @@ def test_bybit_fetch_orders(default_conf, mocker, limit_order):
limit_order["sell"],
]
)
api_mock.fetch_open_orders = MagicMock(return_value=[limit_order["buy"]])
api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order["buy"]])
api_mock.fetch_open_orders = MagicMock(
side_effect=[
[{**limit_order["buy"], "id": 1}],
[{**limit_order["buy"], "id": 2}],
[{**limit_order["buy"], "id": 3}],
]
)
api_mock.fetch_closed_orders = MagicMock(
side_effect=[
[{**limit_order["buy"], "id": 5}],
[{**limit_order["buy"], "id": 6}],
[{**limit_order["buy"], "id": 7}],
]
)
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
def exchange_has(value):
if value == "fetchOrders":
return False
return True
mocker.patch(f"{EXMS}.exchange_has", side_effect=exchange_has)
start_time = datetime.now(timezone.utc) - timedelta(days=20)
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bybit")
@@ -111,9 +128,9 @@ def test_bybit_fetch_orders(default_conf, mocker, limit_order):
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="bybit")
res = exchange.fetch_orders("mocked", start_time)
# Bybit will call the endpoint 3 times, as it has a limit of 7 days per call
assert api_mock.fetch_orders.call_count == 3
assert api_mock.fetch_open_orders.call_count == 0
assert api_mock.fetch_closed_orders.call_count == 0
assert api_mock.fetch_orders.call_count == 0
assert api_mock.fetch_open_orders.call_count == 3
assert api_mock.fetch_closed_orders.call_count == 3
assert len(res) == 2 * 3
+25 -5
View File
@@ -1726,12 +1726,17 @@ def test_fetch_positions(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
api_mock = MagicMock()
api_mock.fetch_orders = MagicMock(
return_value=[
limit_order["buy"],
limit_order["sell"],
call_count = 1
def return_value(*args, **kwargs):
nonlocal call_count
call_count += 2
return [
{**limit_order["buy"], "id": call_count},
{**limit_order["sell"], "id": call_count + 1},
]
)
api_mock.fetch_orders = MagicMock(side_effect=return_value)
api_mock.fetch_open_orders = MagicMock(return_value=[limit_order["buy"]])
api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order["buy"]])
@@ -2136,6 +2141,21 @@ def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_mach
time_machine.move_to(start_dt + timedelta(minutes=5, seconds=1), tick=False)
assert exchange._now_is_time_to_refresh(pair, "5m", candle_type) is True
# Test with 1d data
start_day_dt = datetime(2023, 12, 1, 0, 0, 0, tzinfo=timezone.utc)
last_closed_candle_1d = dt_ts(start_day_dt - timedelta(days=1))
exchange._pairs_last_refresh_time[(pair, "1d", candle_type)] = last_closed_candle_1d
time_machine.move_to(start_day_dt - timedelta(seconds=5), tick=False)
assert exchange._now_is_time_to_refresh(pair, "1d", candle_type) is False
time_machine.move_to(start_day_dt + timedelta(hours=20, seconds=5), tick=False)
assert exchange._now_is_time_to_refresh(pair, "1d", candle_type) is False
# Next candle closed - now we refresh.
time_machine.move_to(start_day_dt + timedelta(days=1, seconds=0), tick=False)
assert exchange._now_is_time_to_refresh(pair, "1d", candle_type) is True
@pytest.mark.parametrize("candle_type", ["mark", ""])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
+8 -9
View File
@@ -408,14 +408,13 @@ EXCHANGES = {
"candle_count": 200,
"orderbook_max_entries": 50,
},
# TODO: verify why htx is not working in CI.
# "htx": {
# "pair": "ETH/BTC",
# "stake_currency": "BTC",
# "hasQuoteVolume": True,
# "timeframe": "1h",
# "candle_count": 1000,
# },
"htx": {
"pair": "ETH/BTC",
"stake_currency": "BTC",
"hasQuoteVolume": True,
"timeframe": "1h",
"candle_count": 1000,
},
"bitvavo": {
"pair": "BTC/EUR",
"stake_currency": "EUR",
@@ -600,7 +599,7 @@ def exchange_ws(request, exchange_conf, exchange_mode, class_mocker):
else:
pytest.skip("Exchange does not support futures.")
if not exchange._has_watch_ohlcv:
if not exchange._exchange_ws:
pytest.skip("Exchange does not support watch_ohlcv.")
yield exchange, name, pair
exchange.close()
+27 -2
View File
@@ -22,6 +22,7 @@ from freqtrade.data.history import get_timerange
from freqtrade.enums import CandleType, ExitType, RunMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_next_date, timeframe_to_prev_date
from freqtrade.exchange.exchange_utils import DECIMAL_PLACES, TICK_SIZE
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename, get_strategy_run_id
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import LocalTrade, Trade
@@ -348,6 +349,29 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
assert processed["UNITTEST/BTC"].equals(processed2["UNITTEST/BTC"])
def test_get_pair_precision_bt(default_conf, mocker) -> None:
patch_exchange(mocker)
default_conf["timeframe"] = "30m"
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
pair = "UNITTEST/BTC"
backtesting.pairlists._whitelist = [pair]
ex_mock = mocker.patch(f"{EXMS}.get_precision_price", return_value=1e-5)
data, timerange = backtesting.load_bt_data()
assert data
assert backtesting.get_pair_precision(pair, dt_utc(2018, 1, 1)) == (1e-8, TICK_SIZE)
assert ex_mock.call_count == 0
assert backtesting.get_pair_precision(pair, dt_utc(2017, 12, 15)) == (1e-8, TICK_SIZE)
assert ex_mock.call_count == 0
# Fallback to exchange logic
assert backtesting.get_pair_precision(pair, dt_utc(2017, 1, 15)) == (1e-5, DECIMAL_PLACES)
assert ex_mock.call_count == 1
assert backtesting.get_pair_precision("ETH/BTC", dt_utc(2017, 1, 15)) == (1e-5, DECIMAL_PLACES)
assert ex_mock.call_count == 2
def test_backtest_abort(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
@@ -828,6 +852,7 @@ def test_backtest_one(default_conf, mocker, testdatadir) -> None:
},
],
],
"funding_fees": [0.0, 0.0],
}
)
pd.testing.assert_frame_equal(results, expected)
@@ -991,7 +1016,7 @@ def test_backtest_one_detail_futures(
timerange=timerange,
candle_type=CandleType.FUTURES,
)
backtesting.load_bt_data_detail()
backtesting._load_bt_data_detail()
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
@@ -1119,7 +1144,7 @@ def test_backtest_one_detail_futures_funding_fees(
timerange=timerange,
candle_type=CandleType.FUTURES,
)
backtesting.load_bt_data_detail()
backtesting._load_bt_data_detail()
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
@@ -80,6 +80,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
"is_short": [False, False],
"open_timestamp": [1517251200000, 1517283000000],
"close_timestamp": [1517263200000, 1517285400000],
"funding_fees": [0.0, 0.0],
}
)
results_no = results.drop(columns=["orders"])
+83 -33
View File
@@ -1,13 +1,12 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
from datetime import datetime, timedelta
from functools import wraps
from functools import partial, wraps
from pathlib import Path
from unittest.mock import ANY, MagicMock, PropertyMock
import pandas as pd
import pytest
from filelock import Timeout
from skopt.space import Integer
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
from freqtrade.data.history import load_data
@@ -17,7 +16,7 @@ from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.optimize.space import SKDecimal
from freqtrade.optimize.space import SKDecimal, ft_IntDistribution
from freqtrade.strategy import IntParameter
from freqtrade.util import dt_utc
from tests.conftest import (
@@ -171,6 +170,48 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None
setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
def test_setup_hyperopt_early_stop_setup(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf)
args = [
"hyperopt",
"--config",
"config.json",
"--strategy",
"HyperoptableStrategy",
"--early-stop",
"1",
]
conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
assert isinstance(conf, dict)
assert conf["early_stop"] == 20
msg = (
r"Parameter --early-stop detected ... "
r"Will early stop hyperopt if no improvement after (20|25) epochs ..."
)
msg_adjust = r"Early stop epochs .* lower than 20. It will be replaced with 20."
assert log_has_re(msg_adjust, caplog)
assert log_has_re(msg, caplog)
caplog.clear()
args = [
"hyperopt",
"--config",
"config.json",
"--strategy",
CURRENT_TEST_STRATEGY,
"--early-stop",
"25",
]
conf1 = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
assert isinstance(conf1, dict)
assert conf1["early_stop"] == 25
assert not log_has_re(msg_adjust, caplog)
assert log_has_re(msg, caplog)
def test_start_not_installed(mocker, default_conf, import_fails) -> None:
start_mock = MagicMock()
patched_configuration_load_config_file(mocker, default_conf)
@@ -578,7 +619,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
"buy_plusdi": 0.02,
"buy_rsi": 35,
},
"roi": {"0": 0.12000000000000001, "20.0": 0.02, "50.0": 0.01, "110.0": 0},
"roi": {"0": 0.12, "20.0": 0.02, "50.0": 0.01, "110.0": 0},
"protection": {
"protection_cooldown_lookback": 20,
"protection_enabled": True,
@@ -606,9 +647,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10)
hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13)
hyperopt.hyperopter.init_spaces()
generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(
list(optimizer_param.values())
)
generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param)
assert generate_optimizer_value == response_expected
@@ -1088,8 +1127,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None
assert opt.backtesting.strategy.max_open_trades != 1
opt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: "ET1"
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
opt.get_optimizer(2, 42, 2, 2)
with pytest.raises(OperationalException, match="Optuna Sampler ET1 not supported."):
opt.get_optimizer(42)
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@@ -1186,19 +1225,27 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmp_path, fe
def test_SKDecimal():
space = SKDecimal(1, 2, decimals=2)
assert 1.5 in space
assert 2.5 not in space
assert space.low == 100
assert space.high == 200
assert space._contains(1.5)
assert not space._contains(2.5)
assert space.low == 1
assert space.high == 2
assert space.inverse_transform([200]) == [2.0]
assert space.inverse_transform([100]) == [1.0]
assert space.inverse_transform([150, 160]) == [1.5, 1.6]
assert space._contains(1.51)
assert space._contains(1.01)
# Falls out of the space with 2 decimals
assert not space._contains(1.511)
assert not space._contains(1.111222)
assert space.transform([1.5]) == [150]
assert space.transform([2.0]) == [200]
assert space.transform([1.0]) == [100]
assert space.transform([1.5, 1.6]) == [150, 160]
with pytest.raises(ValueError):
SKDecimal(1, 2, step=5, decimals=0.2)
with pytest.raises(ValueError):
SKDecimal(1, 2, step=None, decimals=None)
s = SKDecimal(1, 2, step=0.1, decimals=None)
assert s.step == 0.1
assert s._contains(1.1)
assert not s._contains(1.11)
def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path, fee) -> None:
@@ -1217,10 +1264,6 @@ def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path,
}
)
hyperopt = Hyperopt(hyperopt_conf)
mocker.patch(
"freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict",
return_value={"max_open_trades": -1},
)
assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto)
@@ -1228,7 +1271,7 @@ def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path,
hyperopt.start()
assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 1
assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 3
def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> None:
@@ -1246,9 +1289,15 @@ def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> N
}
)
hyperopt = Hyperopt(hyperopt_conf)
def optuna_mock(hyperopt, *args, **kwargs):
a = hyperopt.get_optuna_asked_points(*args, **kwargs)
a[0]._cached_frozen_trial.params["max_open_trades"] = -1
return a, [True]
mocker.patch(
"freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict",
return_value={"max_open_trades": -1},
"freqtrade.optimize.hyperopt.Hyperopt.get_asked_points",
side_effect=partial(optuna_mock, hyperopt),
)
assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto)
@@ -1266,8 +1315,8 @@ def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> N
hyperopt = Hyperopt(hyperopt_conf)
mocker.patch(
"freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict",
return_value={"max_open_trades": -1},
"freqtrade.optimize.hyperopt.Hyperopt.get_asked_points",
side_effect=partial(optuna_mock, hyperopt),
)
assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto)
@@ -1304,7 +1353,7 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No
assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto)
hyperopt.hyperopter.custom_hyperopt.max_open_trades_space = lambda: [
Integer(1, 10, name="max_open_trades")
ft_IntDistribution(1, 10, "max_open_trades")
]
first_time_evaluated = False
@@ -1313,9 +1362,10 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal first_time_evaluated
stake_amount = func(*args, **kwargs)
if first_time_evaluated is False:
assert stake_amount == 1
assert stake_amount == 2
first_time_evaluated = True
return stake_amount
@@ -1329,5 +1379,5 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No
hyperopt.start()
assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 8
assert hyperopt.config["max_open_trades"] == 8
assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 4
assert hyperopt.config["max_open_trades"] == 4
+1 -1
View File
@@ -490,4 +490,4 @@ def test_config_overrides(lookahead_conf):
lookahead_conf = LookaheadAnalysisSubFunctions.calculate_config_overrides(lookahead_conf)
assert lookahead_conf["dry_run_wallet"] == 1000000000
assert lookahead_conf["max_open_trades"] == 3
assert lookahead_conf["max_open_trades"] == -1
+3 -3
View File
@@ -40,7 +40,7 @@ from freqtrade.optimize.optimize_reports.optimize_reports import (
generate_tag_metrics,
)
from freqtrade.resolvers.strategy_resolver import StrategyResolver
from freqtrade.util import dt_ts
from freqtrade.util import dt_ts, format_duration
from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc
from tests.conftest import CURRENT_TEST_STRATEGY, log_has_re
from tests.data.test_history import _clean_test_file
@@ -482,8 +482,8 @@ def test_generate_trading_stats(testdatadir):
bt_data = load_backtest_data(filename)
res = generate_trading_stats(bt_data)
assert isinstance(res, dict)
assert res["winner_holding_avg"] == timedelta(seconds=1440)
assert res["loser_holding_avg"] == timedelta(days=1, seconds=21420)
assert res["winner_holding_avg"] == format_duration(timedelta(seconds=1440))
assert res["loser_holding_avg"] == format_duration(timedelta(days=1, seconds=21420))
assert "wins" in res
assert "losses" in res
assert "draws" in res
+17
View File
@@ -1864,7 +1864,21 @@ def test_api_pair_candles(botclient, ohlcv_history):
ohlcv_history["exit_short"] = 0
ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history, CandleType.SPOT)
fake_plot_annotations = [
{
"type": "area",
"start": "2024-01-01 15:00:00",
"end": "2024-01-01 16:00:00",
"y_start": 94000.2,
"y_end": 98000,
"color": "",
"label": "some label",
}
]
plot_annotations_mock = MagicMock(return_value=fake_plot_annotations)
ftbot.strategy.plot_annotations = plot_annotations_mock
for call in ("get", "post"):
plot_annotations_mock.reset_mock()
if call == "get":
rc = client_get(
client,
@@ -1894,6 +1908,8 @@ def test_api_pair_candles(botclient, ohlcv_history):
assert resp["data_start_ts"] == 1511686200000
assert resp["data_stop"] == "2017-11-26 09:00:00+00:00"
assert resp["data_stop_ts"] == 1511686800000
assert resp["annotations"] == fake_plot_annotations
assert plot_annotations_mock.call_count == 1
assert isinstance(resp["columns"], list)
base_cols = {
"date",
@@ -2235,6 +2251,7 @@ def test_api_pair_history(botclient, tmp_path, mocker):
assert result["data_start_ts"] == 1515628800000
assert result["data_stop"] == "2018-01-12 00:00:00+00:00"
assert result["data_stop_ts"] == 1515715200000
assert result["annotations"] == []
lfm.reset_mock()
# No data found
+85 -48
View File
@@ -6,7 +6,7 @@ from pathlib import Path
from unittest.mock import MagicMock
import pytest
from pandas import DataFrame
from pandas import DataFrame, concat
from freqtrade.configuration import TimeRange
from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH
@@ -27,6 +27,7 @@ from freqtrade.strategy.parameters import (
IntParameter,
RealParameter,
)
from freqtrade.strategy.strategy_validation import StrategyResultValidator
from freqtrade.util import dt_now
from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re
@@ -124,7 +125,7 @@ def test_returns_latest_signal(ohlcv_history):
def test_analyze_pair_empty(mocker, caplog, ohlcv_history):
mocker.patch.object(_STRATEGY.dp, "ohlcv", return_value=ohlcv_history)
mocker.patch.object(_STRATEGY, "_analyze_ticker_internal", return_value=DataFrame([]))
mocker.patch.object(_STRATEGY, "assert_df")
mocker.patch("freqtrade.strategy.interface.StrategyResultValidator.assert_df")
_STRATEGY.analyze_pair("ETH/BTC")
@@ -174,7 +175,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
mocked_history.loc[1, "enter_long"] = 1
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY, "assert_df")
mocker.patch("freqtrade.strategy.interface.StrategyResultValidator.assert_df")
assert (None, None) == _STRATEGY.get_latest_candle(
"xyz", default_conf["timeframe"], mocked_history
@@ -194,7 +195,9 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
mocked_history.loc[1, "enter_long"] = 1
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY, "assert_df")
mocker.patch(
"freqtrade.strategy.interface.StrategyResultValidator.assert_df",
)
assert (SignalDirection.LONG, None) == _STRATEGY.get_entry_signal(
"xyz", default_conf["timeframe"], mocked_history
@@ -237,7 +240,10 @@ def test_assert_df_raise(mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY.dp, "ohlcv", return_value=ohlcv_history)
mocker.patch.object(_STRATEGY.dp, "get_analyzed_dataframe", return_value=(mocked_history, 0))
mocker.patch.object(_STRATEGY, "assert_df", side_effect=StrategyError("Dataframe returned..."))
mocker.patch(
"freqtrade.strategy.interface.StrategyResultValidator.assert_df",
side_effect=StrategyError("Dataframe returned..."),
)
_STRATEGY.analyze_pair("xyz")
assert log_has(
"Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...", caplog
@@ -248,59 +254,39 @@ def test_assert_df(ohlcv_history, caplog):
df_len = len(ohlcv_history) - 1
ohlcv_history.loc[:, "enter_long"] = 0
ohlcv_history.loc[:, "exit_long"] = 0
validator = StrategyResultValidator(ohlcv_history, warn_only=False)
# Ensure it's running when passed correctly
_STRATEGY.assert_df(
ohlcv_history,
len(ohlcv_history),
ohlcv_history.loc[df_len, "close"],
ohlcv_history.loc[df_len, "date"],
)
validator.assert_df(ohlcv_history)
with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."):
_STRATEGY.assert_df(
ohlcv_history,
len(ohlcv_history) + 1,
ohlcv_history.loc[df_len, "close"],
ohlcv_history.loc[df_len, "date"],
)
validator.assert_df(concat([ohlcv_history, ohlcv_history]))
with pytest.raises(
StrategyError, match=r"Dataframe returned from strategy.*last close price\."
):
_STRATEGY.assert_df(
ohlcv_history,
len(ohlcv_history),
ohlcv_history.loc[df_len, "close"] + 0.01,
ohlcv_history.loc[df_len, "date"],
)
df = ohlcv_history.copy()
df.loc[df_len, "close"] += 0.01
validator.assert_df(df)
with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last date\."):
_STRATEGY.assert_df(
ohlcv_history,
len(ohlcv_history),
ohlcv_history.loc[df_len, "close"],
ohlcv_history.loc[0, "date"],
)
df = ohlcv_history.copy()
df.loc[df_len, "date"] = ohlcv_history.loc[0, "date"] - timedelta(days=1)
validator.assert_df(df)
with pytest.raises(
StrategyError, match=r"No dataframe returned \(return statement missing\?\)."
):
_STRATEGY.assert_df(
None,
len(ohlcv_history),
ohlcv_history.loc[df_len, "close"],
ohlcv_history.loc[0, "date"],
)
validator.assert_df(None)
validator = StrategyResultValidator(ohlcv_history, warn_only=True)
_STRATEGY.disable_dataframe_checks = True
caplog.clear()
_STRATEGY.assert_df(
ohlcv_history,
len(ohlcv_history),
ohlcv_history.loc[2, "close"],
ohlcv_history.loc[0, "date"],
)
df = ohlcv_history.copy()
df.loc[df_len, "date"] = ohlcv_history.loc[0, "date"] - timedelta(days=1)
validator.assert_df(df)
assert log_has_re(r"Dataframe returned from strategy.*last date\.", caplog)
# reset to avoid problems in other tests due to test leakage
_STRATEGY.disable_dataframe_checks = False
def test_advise_all_indicators(default_conf, testdatadir) -> None:
@@ -425,6 +411,57 @@ def test_min_roi_reached3(default_conf, fee) -> None:
assert strategy.min_roi_reached(trade, 0.31, dt_now() - timedelta(minutes=2))
def test_min_roi_reached_custom_roi(default_conf, fee) -> None:
strategy = StrategyResolver.load_strategy(default_conf)
# Move traditional ROI out of the way
strategy.minimal_roi = {0: 2000}
strategy.use_custom_roi = True
def custom_roi(*args, trade: Trade, current_time: datetime, **kwargs):
trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)
# Profit is reduced after 30 minutes.
if trade.pair == "XRP/BTC":
return 0.2
if trade_dur > 30:
return 0.05
return 0.1
strategy.custom_roi = MagicMock(side_effect=custom_roi)
trade = Trade(
pair="ETH/BTC",
stake_amount=0.001,
amount=5,
open_date=dt_now() - timedelta(hours=1),
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange="binance",
open_rate=1,
)
assert not strategy.min_roi_reached(trade, 0.02, dt_now() - timedelta(minutes=56))
assert strategy.custom_roi.call_count == 1
assert strategy.min_roi_reached(trade, 0.12, dt_now() - timedelta(minutes=56))
# after 30 minutes, the profit is reduced to 5%
assert strategy.min_roi_reached(trade, 0.12, dt_now() - timedelta(minutes=29))
assert strategy.min_roi_reached(trade, 0.06, dt_now() - timedelta(minutes=29))
assert strategy.min_roi_reached(trade, 0.051, dt_now() - timedelta(minutes=29))
# Comparison to exactly 5% should not trigger
assert not strategy.min_roi_reached(trade, 0.05, dt_now() - timedelta(minutes=29))
# XRP/BTC has a custom roi of 20%.
trade.pair = "XRP/BTC"
assert not strategy.min_roi_reached(trade, 0.12, dt_now() - timedelta(minutes=56))
assert not strategy.min_roi_reached(trade, 0.12, dt_now() - timedelta(minutes=1))
# XRP/BTC is not time related
assert strategy.min_roi_reached(trade, 0.201, dt_now() - timedelta(minutes=1))
assert strategy.min_roi_reached(trade, 0.201, dt_now() - timedelta(minutes=56))
assert strategy.custom_roi.call_count == 10
@pytest.mark.parametrize(
"profit,adjusted,expected,liq,trailing,custom,profit2,adjusted2,expected2,custom_stop",
[
@@ -895,7 +932,7 @@ def test_is_informative_pairs_callback(default_conf):
def test_hyperopt_parameters():
HyperoptStateContainer.set_state(HyperoptState.INDICATORS)
from skopt.space import Categorical, Integer, Real
from optuna.distributions import CategoricalDistribution, FloatDistribution, IntDistribution
with pytest.raises(OperationalException, match=r"Name is determined.*"):
IntParameter(low=0, high=5, default=1, name="hello")
@@ -926,7 +963,7 @@ def test_hyperopt_parameters():
intpar = IntParameter(low=0, high=5, default=1, space="buy")
assert intpar.value == 1
assert isinstance(intpar.get_space(""), Integer)
assert isinstance(intpar.get_space(""), IntDistribution)
assert isinstance(intpar.range, range)
assert len(list(intpar.range)) == 1
# Range contains ONLY the default / value.
@@ -938,7 +975,7 @@ def test_hyperopt_parameters():
fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space="buy")
assert fltpar.value == 1
assert isinstance(fltpar.get_space(""), Real)
assert isinstance(fltpar.get_space(""), FloatDistribution)
fltpar = DecimalParameter(low=0.0, high=0.5, default=0.14, decimals=1, space="buy")
assert fltpar.value == 0.1
@@ -955,7 +992,7 @@ def test_hyperopt_parameters():
["buy_rsi", "buy_macd", "buy_none"], default="buy_macd", space="buy"
)
assert catpar.value == "buy_macd"
assert isinstance(catpar.get_space(""), Categorical)
assert isinstance(catpar.get_space(""), CategoricalDistribution)
assert isinstance(catpar.range, list)
assert len(list(catpar.range)) == 1
# Range contains ONLY the default / value.
@@ -966,7 +1003,7 @@ def test_hyperopt_parameters():
boolpar = BooleanParameter(default=True, space="buy")
assert boolpar.value is True
assert isinstance(boolpar.get_space(""), Categorical)
assert isinstance(boolpar.get_space(""), CategoricalDistribution)
assert isinstance(boolpar.range, list)
assert len(list(boolpar.range)) == 1
+11
View File
@@ -385,6 +385,17 @@ def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf):
assert strategy.max_open_trades == float("inf")
assert default_conf["max_open_trades"] == float("inf")
# test if the default value is set to infinity (V2 doesn't set max_open_trades explicitly)
del default_conf["max_open_trades"]
default_conf.update(
{
"strategy": "StrategyTestV2",
}
)
strategy2 = StrategyResolver.load_strategy(default_conf)
assert strategy2.max_open_trades == float("inf")
assert default_conf["max_open_trades"] == float("inf")
def test_strategy_max_open_trades_infinity_from_config(caplog, default_conf, mocker):
caplog.set_level(logging.INFO)
+44 -2
View File
@@ -2,9 +2,51 @@
# Test Documentation boxes -
# !!! <TYPE>: is not allowed!
# !!! <TYPE> "title" - Title needs to be quoted!
grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*
# Same for ???
grep -Er '^(!{3}|\?{3})\s\S+:|^(!{3}|\?{3})\s\S+\s[^"]' docs/*
format_issues=$?
if [ $? -ne 0 ]; then
failed=0
# Check for the presence of the "!!!" or "???" markers in the files
# the consecutive non-empty line must begin with 4 spaces
while read -r file; do
awk -v fname="$file" '
/^(!!!|\?\?\?)/ {
current_line_number = NR
current_line = $0
found_next_content = 0
while (getline nextLine > 0) {
# Skip empty lines
if (nextLine ~ /^$/) {
continue
}
found_next_content = 1
if (nextLine !~ /^ /) {
print "File:", fname
print "Error in Line ", current_line_number, "Expected next non-empty line to start with 4 spaces"
print ">>", current_line
print "------"
exit 1
}
break
}
if (!found_next_content) {
print "File:", fname
print "Error in Line ", current_line_number, "Found marker but no content following it"
print ">>", current_line
print "------"
exit 1
}
}
' "$file"
if [ $? -ne 0 ]; then
failed=1
fi
done < <(find . -type f -name "*.md")
if [ $format_issues -eq 1 ] && [ $failed -eq 0 ]; then
echo "Docs test success."
exit 0
fi

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