Compare commits

..

331 Commits

Author SHA1 Message Date
Matthias 466e834680 Merge pull request #11160 from freqtrade/new_release
New release 2024.12
2024-12-30 06:58:30 +01:00
Matthias 9e0b9962ce chore: bump version to 2024.12 2024-12-29 15:28:46 +01:00
Matthias 4d2863baf5 Merge branch 'stable' into new_release 2024-12-29 15:28:30 +01:00
Matthias 0032f9a960 chore: bump ccxt to 4.4.43
this adds support for myokx, the OKX EAA subsidiary.
2024-12-28 12:49:55 +01:00
Matthias 5ca31508fb chore: remove "former huobi" terminology 2024-12-28 12:46:57 +01:00
Matthias a58fde367c chore: remove "former OKEX" terminology 2024-12-28 12:46:35 +01:00
Matthias 3f41910eb4 docs: add myokx special case documentation 2024-12-28 12:10:25 +01:00
Matthias 9a201566f2 chore: fix typo in telegram help message 2024-12-28 12:05:55 +01:00
Matthias a180cb95dc chore: Improve list-exchanges output 2024-12-28 12:05:02 +01:00
Matthias fbb757fa2f chore: may myokx to okx - it's just an alias for european users 2024-12-28 12:03:37 +01:00
Matthias 6ee38adbb5 fix: round open_rate to tradable precisionThis prevents odd display issues with 16 decimals in case of multiple entries. 2024-12-27 19:18:10 +01:00
Matthias 6f3dca44a3 fix: apply hyperliquid market rate fix to fetch_orders 2024-12-27 18:16:37 +01:00
Matthias a4ae5b74a4 chore: add log_response after average modification 2024-12-27 18:04:29 +01:00
Matthias fc970a1af4 chore: Remove pin of matplotlib 2024-12-27 13:39:13 +01:00
Matthias b5748fee6e docs: add FAQ about resetting a bot's database 2024-12-26 13:54:21 +01:00
Matthias b092dfa2c9 Merge pull request #11149 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-12-26 12:10:19 +01:00
xmatthias 2ad1fbd771 chore: update pre-commit hooks 2024-12-26 03:06:05 +00:00
Matthias 1ca594bbcb chore: show "total" profit if necessary 2024-12-25 18:04:17 +01:00
Matthias e66611c965 chore: fix 2 minor potential bugs 2024-12-25 17:53:12 +01:00
Matthias c5a95dc682 refactor: reuse logic from get-status for get status-table 2024-12-25 17:40:02 +01:00
Matthias d7ccbfef20 test: improve test accuracy 2024-12-25 17:37:21 +01:00
Matthias 79f35bb553 chore: add docstring to rpc_status_table 2024-12-25 16:30:52 +01:00
Matthias 9e30e5c34c Merge pull request #11144 from freqtrade/deprecate/hdf5
Deprecate hdf5
2024-12-25 08:09:57 +01:00
Matthias 4963a9668f chore: fix typo 2024-12-24 15:35:34 +01:00
Matthias e59bd71c17 Merge pull request #11143 from freqtrade/feat/telegram_group_topics
Support Telegram group topics, add /tg_info command
2024-12-24 15:29:33 +01:00
Matthias 5750d05891 feat: deprecate hdf5 as data storage 2024-12-24 15:19:22 +01:00
Matthias 1810a911d4 chore: update chatID to be identical throughout docs. 2024-12-24 14:42:39 +01:00
Matthias 51ae0f0377 chore: check for update.message being present 2024-12-24 14:05:17 +01:00
Matthias 215b648edf Merge pull request #11141 from andrasmining/develop
Adding drawdown information for hyperopt-list --export-csv command & small fix stake currency source
2024-12-24 13:55:23 +01:00
Matthias d73f01701e chore: reduce verbosity for topic_id wrong check 2024-12-24 13:52:51 +01:00
Matthias 010b282679 docs: document /tg_info command 2024-12-24 13:46:29 +01:00
Matthias 6ef8291b2e test: add test for tg_info, update chat_id to be not 0 2024-12-24 13:38:34 +01:00
Andras Kovacs b820810afe hyperopt-list --export-csv command stake currency fix
Take stake currency from hyperopt result file instead of config
2024-12-24 13:33:58 +01:00
Matthias cef21860eb feat: add _tg_info endpoint for easier information gathering 2024-12-24 13:27:15 +01:00
Andras Kovacs 0c08656965 Merge branch 'develop' of https://github.com/andrasmining/freqtrade into develop 2024-12-24 12:54:21 +01:00
Andras Kovacs 59b1460e30 Adding drawdown information for hyperopt-list --export-csv command 2024-12-24 12:52:35 +01:00
Matthias b75ac1243a docs: document group-topic-id 2024-12-24 12:17:15 +01:00
Matthias 93af2b7b1d Merge pull request #11138 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-12-24 08:18:17 +01:00
xmatthias 7092edfe4c chore: update pre-commit hooks 2024-12-24 03:06:46 +00:00
Matthias 0663335011 chore: update config_schema with topic_id attribute 2024-12-23 20:12:21 +01:00
Matthias 6cc0c2be91 feat: add support for "topic_id" in telegram notifications 2024-12-23 20:11:24 +01:00
Matthias 4a256ed7ca chore: failed messages shouldn't return the wrapper but None 2024-12-23 19:57:44 +01:00
Matthias 2a526a815e fix: don't over-state stake-amount on first entry
closes #11135
2024-12-23 17:53:44 +01:00
Matthias 0a81658881 test: add test to confirm behavior in #11135 2024-12-23 17:53:44 +01:00
Matthias 5ac95a0507 test: close mock order for funding-fee test 2024-12-23 13:47:33 +01:00
Matthias 77e36ed313 Merge pull request #11125 from freqtrade/dependabot/pip/develop/technical-1.5.0
chore(deps): bump technical from 1.4.4 to 1.5.0
2024-12-23 11:16:12 +01:00
Matthias 52933e977b Merge pull request #11128 from freqtrade/dependabot/pip/develop/mypy-1.14.0
chore(deps-dev): bump mypy from 1.13.0 to 1.14.0
2024-12-23 09:06:20 +01:00
dependabot[bot] cf4c29a57f chore(deps): bump technical from 1.4.4 to 1.5.0
Bumps [technical](https://github.com/freqtrade/technical) from 1.4.4 to 1.5.0.
- [Release notes](https://github.com/freqtrade/technical/releases)
- [Commits](https://github.com/freqtrade/technical/compare/1.4.4...1.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 07:43:41 +00:00
Matthias 90cd381efa Merge pull request #11129 from freqtrade/dependabot/pip/develop/jinja2-3.1.5
chore(deps): bump jinja2 from 3.1.4 to 3.1.5
2024-12-23 08:42:28 +01:00
dependabot[bot] 9405f415d0 chore(deps-dev): bump mypy from 1.13.0 to 1.14.0
Bumps [mypy](https://github.com/python/mypy) from 1.13.0 to 1.14.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.13.0...v1.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 07:10:13 +00:00
Matthias 3dcc130903 Merge pull request #11131 from freqtrade/dependabot/pip/develop/ccxt-4.4.42
chore(deps): bump ccxt from 4.4.40 to 4.4.42
2024-12-23 08:09:13 +01:00
dependabot[bot] 9d022180e7 chore(deps): bump jinja2 from 3.1.4 to 3.1.5
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.4...3.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 07:04:05 +00:00
Matthias ba42d8d8c6 Merge pull request #11133 from freqtrade/dependabot/pip/develop/ruff-0.8.4
chore(deps-dev): bump ruff from 0.8.3 to 0.8.4
2024-12-23 08:03:01 +01:00
Matthias 117887ff3a Merge pull request #11122 from freqtrade/dependabot/github_actions/develop/astral-sh/setup-uv-5
chore(deps): bump astral-sh/setup-uv from 4 to 5
2024-12-23 08:01:42 +01:00
Matthias b62fa8190b Merge pull request #11123 from freqtrade/dependabot/pip/develop/mkdocs-c52ef7e6a7
chore(deps): bump mkdocs-material from 9.5.48 to 9.5.49 in the mkdocs group
2024-12-23 08:01:33 +01:00
Matthias eadd4a7bb2 Merge pull request #11127 from freqtrade/dependabot/pip/develop/psutil-6.1.1
chore(deps): bump psutil from 6.1.0 to 6.1.1
2024-12-23 08:01:19 +01:00
Matthias a93a1b8b48 Merge pull request #11130 from freqtrade/dependabot/pip/develop/urllib3-2.3.0
chore(deps): bump urllib3 from 2.2.3 to 2.3.0
2024-12-23 08:01:04 +01:00
Matthias 12cbe70371 Merge pull request #11126 from freqtrade/dependabot/pip/develop/pydantic-2.10.4
chore(deps): bump pydantic from 2.10.3 to 2.10.4
2024-12-23 08:00:44 +01:00
dependabot[bot] ed3367244c chore(deps-dev): bump ruff from 0.8.3 to 0.8.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.3 to 0.8.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.8.3...0.8.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 03:47:30 +00:00
dependabot[bot] b8b69eecb8 chore(deps): bump ccxt from 4.4.40 to 4.4.42
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.40 to 4.4.42.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.4.40...4.4.42)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 03:47:08 +00:00
dependabot[bot] 65acd5d827 chore(deps): bump urllib3 from 2.2.3 to 2.3.0
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.3 to 2.3.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.2.3...2.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 03:47:00 +00:00
dependabot[bot] 4d6732cd19 chore(deps): bump psutil from 6.1.0 to 6.1.1
Bumps [psutil](https://github.com/giampaolo/psutil) from 6.1.0 to 6.1.1.
- [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/release-6.1.0...release-6.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 03:46:39 +00:00
dependabot[bot] dc5942115d chore(deps): bump pydantic from 2.10.3 to 2.10.4
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.3 to 2.10.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.10.3...v2.10.4)

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

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


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 03:46:01 +00:00
dependabot[bot] 07682c396e chore(deps): bump astral-sh/setup-uv from 4 to 5
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 4 to 5.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 03:31:33 +00:00
Matthias 503305cbdb fix: properly pass timerange to data-download 2024-12-22 16:02:49 +01:00
Matthias 32f592444a Merge pull request #11119 from AchmadFathoni/fix_notebook
Fix KeyError: 'max_drawdown' in jupyter notebook
2024-12-21 19:52:03 +01:00
Achmad Fathoni 04813e452c Change unavailable "max_drawdown" key 2024-12-22 00:28:21 +07:00
Achmad Fathoni b50d9f5c59 Fix KeyError: 'max_drawdown' in jupyter notebook 2024-12-22 00:14:07 +07:00
Matthias 40cb70fca1 fix: one-side pairlocks not showing in UI 2024-12-21 14:30:47 +01:00
Matthias 0b0bbd4c42 chore: improve comment wording 2024-12-20 18:18:44 +01:00
Matthias 4704f2b08e chore: fix blosc version selectors 2024-12-20 06:38:15 +01:00
Matthias f9bd11f6fb chore: blosc2 only supports python 3.11+ 2024-12-20 06:29:42 +01:00
Matthias 0d73e961a4 chore: pin blosc2 for raspberry builds 2024-12-20 06:26:45 +01:00
Matthias e1f759e70e Merge pull request #11112 from froggleston/frog-docs-api-2
Add params to REST API table description, fix first column width
2024-12-19 21:06:03 +01:00
Robert Davey 66380566c8 Update docs/stylesheets/ft.extra.css
Co-authored-by: Matthias <xmatthias@outlook.com>
2024-12-19 19:35:28 +00:00
Matthias 8cb69e4d5a test: Update test for new liquidation price precision rounding 2024-12-19 19:37:55 +01:00
Matthias 8ee2c849b4 fix: round liquidation price to precision in a different spot
closes #11110
2024-12-19 19:37:35 +01:00
Matthias 564d874869 chore: improve typing 2024-12-19 19:25:00 +01:00
froggleston d38fcd2db4 Fix missing EOL 2024-12-19 16:24:07 +00:00
froggleston 06e20b38b5 Add params to REST API table description, fix first column width 2024-12-19 16:18:30 +00:00
Matthias c8b7449b25 Merge pull request #11111 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-12-19 08:10:34 +01:00
Matthias 6edfebdfed Merge pull request #11102 from froggleston/frog-docs-api-1
Add more detail to the REST API endpoint docs
2024-12-19 08:10:01 +01:00
Matthias c4b39a4fb1 docs: add suggestion to prefix all urls with base url. 2024-12-19 06:50:36 +01:00
xmatthias cdcca3da1c chore: update pre-commit hooks 2024-12-19 03:12:44 +00:00
Matthias 44d1f169c3 chore: type rpc._freqtradebot for improved DX 2024-12-17 20:39:04 +01:00
Matthias d805ee6f3d chore: improve type safety 2024-12-17 20:38:46 +01:00
Matthias ed2427852b test: adapt starting capital tests for tradable_balance_ratio 2024-12-17 20:32:11 +01:00
Matthias 5af894047e fix: starting balance should include tradable_balance_ratio 2024-12-17 20:08:40 +01:00
Matthias fced0c4952 Merge pull request #11105 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-12-17 06:30:46 +01:00
xmatthias 609893f7ec chore: update pre-commit hooks 2024-12-17 03:15:40 +00:00
Matthias 3f795a8918 chore: improve logic for live wallet update 2024-12-16 20:57:44 +01:00
Matthias a5907db5d8 chore: update pip-install command for windows ci 2024-12-16 19:02:21 +01:00
froggleston ad7b70a354 Add more detail to the REST API endpoint docs 2024-12-16 15:15:57 +00:00
Matthias d992772492 Merge pull request #11097 from freqtrade/dependabot/pip/develop/scikit-learn-1.6.0
chore(deps): bump scikit-learn from 1.5.2 to 1.6.0
2024-12-16 06:33:25 +01:00
Matthias 7ba879c441 Merge pull request #11096 from freqtrade/dependabot/pip/develop/matplotlib-3.10.0
chore(deps): bump matplotlib from 3.9.3 to 3.10.0
2024-12-16 06:32:08 +01:00
Matthias 3fa2238e9c Merge pull request #11095 from freqtrade/dependabot/pip/develop/pytest-c02b329de1
chore(deps-dev): bump pytest-asyncio from 0.24.0 to 0.25.0 in the pytest group
2024-12-16 06:31:41 +01:00
Matthias 274136dacb Merge pull request #11094 from freqtrade/dependabot/github_actions/develop/pypa/gh-action-pypi-publish-1.12.3
chore(deps): bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3
2024-12-16 06:31:12 +01:00
Matthias d5d65cfd56 Merge pull request #11098 from freqtrade/dependabot/pip/develop/uvicorn-0.34.0
chore(deps): bump uvicorn from 0.32.1 to 0.34.0
2024-12-16 06:30:42 +01:00
Matthias 17c11d09fc Merge pull request #11099 from freqtrade/dependabot/pip/develop/janus-2.0.0
chore(deps): bump janus from 1.1.0 to 2.0.0
2024-12-16 06:30:16 +01:00
Matthias 6bf42173a1 Merge pull request #11100 from freqtrade/dependabot/pip/develop/ruff-0.8.3
chore(deps-dev): bump ruff from 0.8.2 to 0.8.3
2024-12-16 06:29:22 +01:00
dependabot[bot] c8649a77d2 chore(deps-dev): bump ruff from 0.8.2 to 0.8.3
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.2 to 0.8.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.8.2...0.8.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 03:26:36 +00:00
dependabot[bot] d780d56b9a chore(deps): bump janus from 1.1.0 to 2.0.0
Bumps [janus](https://github.com/aio-libs/janus) from 1.1.0 to 2.0.0.
- [Release notes](https://github.com/aio-libs/janus/releases)
- [Changelog](https://github.com/aio-libs/janus/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/janus/compare/v1.1.0...v2.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 03:26:25 +00:00
dependabot[bot] 2587b549a0 chore(deps): bump uvicorn from 0.32.1 to 0.34.0
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.32.1 to 0.34.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.32.1...0.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 03:26:10 +00:00
dependabot[bot] dd805c4c25 chore(deps): bump scikit-learn from 1.5.2 to 1.6.0
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.5.2 to 1.6.0.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.5.2...1.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 03:26:01 +00:00
dependabot[bot] 5cd1892dee chore(deps): bump matplotlib from 3.9.3 to 3.10.0
Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.9.3 to 3.10.0.
- [Release notes](https://github.com/matplotlib/matplotlib/releases)
- [Commits](https://github.com/matplotlib/matplotlib/compare/v3.9.3...v3.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 03:25:53 +00:00
dependabot[bot] f575e56e46 chore(deps-dev): bump pytest-asyncio in the pytest group
Bumps the pytest group with 1 update: [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio).


Updates `pytest-asyncio` from 0.24.0 to 0.25.0
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.24.0...v0.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 03:25:42 +00:00
dependabot[bot] af7974adda chore(deps): bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.2 to 1.12.3.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.2...v1.12.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 03:25:38 +00:00
Matthias 685811ef63 feat: update backtest_api with new storage interface 2024-12-15 15:10:06 +01:00
Matthias 12ae36e99e test: use tmp_path instead of testdatadir for writing 2024-12-15 15:02:35 +01:00
Matthias abfe683763 test: fix tests data pollution 2024-12-15 15:02:16 +01:00
Matthias a1ab8de593 test: Update tests for new bt storage method 2024-12-15 13:20:25 +01:00
Matthias b04c5aec88 refactor: move backtest "storage" logic to storage file 2024-12-15 13:18:25 +01:00
Matthias b862927c8c chore: don't exclude tests from pyright 2024-12-15 13:07:39 +01:00
Matthias 5744e16a07 refactor: use 1 structure to save analysis signals 2024-12-14 19:50:59 +01:00
Matthias 11f0a59031 docs: slightly improve position adjust doc wording 2024-12-14 18:20:47 +01:00
Matthias 8dfbf342fd Merge pull request #11086 from freqtrade/fix-docs
add warning about loose logic
2024-12-14 14:13:11 +01:00
Matthias 5f947a86c7 docs: improve ordering in adjust trade position callback docs 2024-12-14 13:47:14 +01:00
Matthias 0f18e78bb4 Merge pull request #11084 from freqtrade/improve/orderflow_times
Improve orderflow execution times
2024-12-14 13:43:55 +01:00
Matthias d0c5d614f7 docs: add note about incompatibility between orderflow and freqAI 2024-12-14 13:08:13 +01:00
Matthias e2e1c610d8 Merge pull request #11087 from freqtrade/bump/ccxt
Bump ccxt to 4.4.40
2024-12-14 12:52:04 +01:00
Matthias 1e74aa3087 chore: bump ccxt to 4.4.40 2024-12-14 08:04:29 +01:00
Stefano Ariestasia f54a141918 fix typo 2024-12-14 14:41:32 +09:00
Stefano Ariestasia b54d8c53a7 add warning about loose logic 2024-12-14 14:37:16 +09:00
Matthias 016cabb7b2 chore: simplify imports 2024-12-13 14:59:35 +01:00
Matthias 9129266f54 feat: greatly improve performance of orderflow by not using apply 2024-12-13 14:28:26 +01:00
Matthias e8376d5b79 feat: add timeframe_to_DateOffset 2024-12-13 14:27:54 +01:00
Matthias cc7b1a5642 chore: clean up install_windows call 2024-12-13 14:24:27 +01:00
Matthias f11fe14cdd chore: remove system mode, set path globally 2024-12-13 14:24:27 +01:00
Matthias 219e6b420e chore: Try using uv venv creation 2024-12-13 14:24:27 +01:00
Matthias 3a6ef8c34b chore: use link mode for UV installs 2024-12-13 14:24:27 +01:00
Matthias 4f318a5981 chore: uv - different caching 2024-12-13 14:24:27 +01:00
Matthias f978a5c754 chore: uv - different caching 2024-12-13 14:24:27 +01:00
Matthias f378e10752 chore: link mode copy 2024-12-13 14:24:27 +01:00
Matthias 30909d13ba chore: uv: glob 2024-12-13 14:24:27 +01:00
Matthias 8cd03c9624 chore: uv - enable cache 2024-12-13 14:24:27 +01:00
Matthias 8f617d01d4 chore: further uv tests 2024-12-13 14:24:27 +01:00
Matthias 569ac2bc5b chore: move uv work 2024-12-13 14:24:27 +01:00
Matthias c1be014499 chore: package name lowercase ?? 2024-12-13 14:24:27 +01:00
Matthias 7d1ac0248b chore: only binary ... 2024-12-13 14:24:27 +01:00
Matthias aa8e504ef7 chore: remove non-existing UV argument 2024-12-13 14:24:27 +01:00
Matthias 6963141eb8 chore: fix aliasing 2024-12-13 14:24:27 +01:00
Matthias 3295b1c8de chore: try using UV for windows runs 2024-12-13 14:24:27 +01:00
Matthias 0159d25e48 chore: add missing dev-deps to pyproject.toml 2024-12-13 14:24:27 +01:00
Matthias abbfe04642 Merge pull request #11080 from stash86/docs-dca
add more explanation regarding reducing position partially
2024-12-12 20:11:39 +01:00
Matthias 8cf3c7b826 chore: use FtPrecise for stake_amount_filled calculation
closes #11073
2024-12-12 20:08:35 +01:00
Matthias 0fb927e709 Merge pull request #11081 from freqtrade/frog-docs-state-1
Add initial_state option to FAQ
2024-12-12 16:43:08 +01:00
Robert Davey c00b248d1a Add initial_state option to FAQ 2024-12-12 13:17:52 +00:00
Stefano d2b4d1e183 add more explanation regarding reducing position partially 2024-12-12 11:45:40 +00:00
Matthias 8fb9140c21 Merge pull request #11046 from freqtrade/fix/orderflow_cache
Fix orderflow cache
2024-12-12 06:49:58 +01:00
Matthias cb9d828ba0 Merge pull request #11079 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-12-12 06:28:12 +01:00
xmatthias 467c0dfffa chore: update pre-commit hooks 2024-12-12 03:16:05 +00:00
Matthias d887a4aa4a Merge branch 'develop' into fix/orderflow_cache 2024-12-11 19:28:06 +01:00
Matthias 34835752f8 chore: improve typing in percentChange Pairlist 2024-12-11 07:25:07 +01:00
Matthias b9aa78b987 fix: crash in PercentChange pairlist
Could happen if pairs were removed due to empty Ticker when the pairlist is not a generator.

closes #11074
2024-12-11 07:12:50 +01:00
Matthias d8fa782f13 test: Add test with faulty behavior
part of #11074
2024-12-11 07:07:47 +01:00
Matthias f3d7b249be test: Improve wallet tests 2024-12-10 21:05:05 +01:00
Matthias 097836d193 feat: improve logic for liquidation price calc 2024-12-10 21:05:04 +01:00
Matthias 7552ad5edb Merge pull request #11070 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-12-10 06:31:30 +01:00
xmatthias e9cf0a71d8 chore: update pre-commit hooks 2024-12-10 05:05:39 +00:00
Matthias cf731d7296 Merge pull request #11057 from freqtrade/dependabot/pip/develop/types-e0d1a7aad6
chore(deps-dev): bump the types group with 2 updates
2024-12-09 07:20:55 +01:00
Matthias 946e6fd4c2 Merge pull request #11066 from freqtrade/dependabot/pip/develop/pydantic-2.10.3
chore(deps): bump pydantic from 2.10.2 to 2.10.3
2024-12-09 07:10:14 +01:00
Matthias fe9d1a053e chore: bump pre-commit dependencies 2024-12-09 06:40:49 +01:00
Matthias 6b1ac499ba tests: more tests for cross wallet 2024-12-09 06:39:54 +01:00
Matthias 0fc0b2a1be test: add test for cross balance logic 2024-12-09 06:39:54 +01:00
Matthias 6887ed4bf7 feat: add cross margin balance logic 2024-12-09 06:39:54 +01:00
Matthias 8cb82df055 chore: simplify dry_wallets call 2024-12-09 06:39:54 +01:00
dependabot[bot] 6788214671 chore(deps): bump pydantic from 2.10.2 to 2.10.3
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.2 to 2.10.3.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.10.2...v2.10.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 05:32:50 +00:00
Matthias 4c2488ff65 Merge pull request #11065 from freqtrade/dependabot/pip/develop/fastapi-0.115.6
chore(deps): bump fastapi from 0.115.5 to 0.115.6
2024-12-09 06:31:58 +01:00
Matthias 05b101b612 Merge pull request #11064 from freqtrade/dependabot/pip/develop/ccxt-4.4.37
chore(deps): bump ccxt from 4.4.35 to 4.4.37
2024-12-09 06:29:55 +01:00
Matthias 65ab47dc63 Merge pull request #11063 from freqtrade/dependabot/pip/develop/python-telegram-bot-21.9
chore(deps): bump python-telegram-bot from 21.8 to 21.9
2024-12-09 06:29:21 +01:00
Matthias 23579df8da Merge pull request #11058 from freqtrade/dependabot/pip/develop/mkdocs-3ba6cc2ae5
chore(deps): bump mkdocs-material from 9.5.47 to 9.5.48 in the mkdocs group
2024-12-09 06:29:05 +01:00
Matthias 2b91be6237 Merge pull request #11061 from freqtrade/dependabot/pip/develop/ruff-0.8.2
chore(deps-dev): bump ruff from 0.8.1 to 0.8.2
2024-12-09 06:26:55 +01:00
dependabot[bot] f1fabb07bf chore(deps): bump fastapi from 0.115.5 to 0.115.6
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.115.5 to 0.115.6.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.5...0.115.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 03:47:54 +00:00
dependabot[bot] 8aea015452 chore(deps): bump ccxt from 4.4.35 to 4.4.37
Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.4.35 to 4.4.37.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/4.4.35...4.4.37)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 03:47:48 +00:00
dependabot[bot] 83471228e7 chore(deps): bump python-telegram-bot from 21.8 to 21.9
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 21.8 to 21.9.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v21.8...v21.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 03:47:37 +00:00
dependabot[bot] e97a647b54 chore(deps-dev): bump ruff from 0.8.1 to 0.8.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.1 to 0.8.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.8.1...0.8.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 03:47:13 +00:00
dependabot[bot] f5dceb0b09 chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 03:46:15 +00:00
dependabot[bot] d4fbdeee94 chore(deps-dev): bump the types group with 2 updates
Bumps the types group with 2 updates: [types-tabulate](https://github.com/python/typeshed) and [types-python-dateutil](https://github.com/python/typeshed).


Updates `types-tabulate` from 0.9.0.20240106 to 0.9.0.20241207
- [Commits](https://github.com/python/typeshed/commits)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 03:45:29 +00:00
Matthias f529cfe8cf chore: fix wrong return typing 2024-12-08 16:58:31 +01:00
Matthias 4d36aaff39 feat: convert get_valid_pair_combination to generator 2024-12-08 16:54:17 +01:00
Matthias f07aec457a chore: remove unnecessary imports 2024-12-08 16:50:10 +01:00
Matthias d898c80e65 test: Add test for get_conversion_rate 2024-12-08 16:40:56 +01:00
Matthias 2ff0abc6e7 refactor: conversation rate retrieval to exchange class
for better reusability across the bot.
2024-12-08 15:44:46 +01:00
Matthias 0276e65f39 test: update rpc test for new behavior 2024-12-08 15:29:23 +01:00
Matthias 4cd8e6b444 chore: simplify rpc_balance method by relying on exchange cache 2024-12-08 15:22:40 +01:00
Matthias 58357a0746 chore: calculate total_stake only once 2024-12-08 14:41:26 +01:00
Matthias d0223e6f48 chore: simplify dry-run wallet update 2024-12-08 14:41:26 +01:00
Matthias 142ea68dce chore: fix oddity of price_to_precision 2024-12-08 14:41:26 +01:00
Matthias 654d2ab63d fix: Round stoploss_dist to price_precision 2024-12-08 14:41:26 +01:00
Matthias a30f281478 Merge pull request #11023 from freqtrade/dependabot/pip/develop/xgboost-2.1.3
chore(deps): bump xgboost from 2.0.3 to 2.1.3
2024-12-08 13:46:58 +01:00
Matthias ae1baf5789 test: mock price_to_precision for liquidation calculations 2024-12-08 13:35:46 +01:00
Matthias 3c88bdc60c fix: round liquidation price to price precision 2024-12-08 13:35:46 +01:00
Matthias 6d698e584b chore: return 0 total balances
total may be 0, while still having "free" balance (cross futures scenarios)
2024-12-08 13:35:46 +01:00
Matthias 193b989342 chore: Be more precise with binance account limitation 2024-12-08 13:35:46 +01:00
Matthias 8dc322d7f2 chore: don't allow negative stake amounts 2024-12-08 13:35:46 +01:00
Matthias eee5d710e7 chore: patch torch all the time -
"list"tests do load the modules as well - so they
need the same patch.
2024-12-08 10:06:23 +01:00
Matthias 489db37bbd Merge pull request #10979 from amalysh/develop
allow json in environment variables
2024-12-08 08:55:21 +01:00
Matthias 10b5d5e56b docs: add "list parsing" logic to documentation 2024-12-08 08:35:13 +01:00
Matthias 934bcf253e test: add tests for list parsing 2024-12-08 08:34:54 +01:00
Matthias 0683ba3a54 feat: limit environment-variable json parsing to lists 2024-12-08 08:30:45 +01:00
Matthias a85f200450 Merge pull request #11000 from freqtrade/feat/multi_wallet
Dry-run multi-wallet support
2024-12-07 17:49:32 +01:00
Matthias 98e0a5f101 chore: remove unused arguments in loss functions 2024-12-07 15:52:14 +01:00
Matthias fb9e11b7b5 chore: improve type safety 2024-12-07 13:21:18 +01:00
Matthias e77ca024d7 chore: improve naming, don't duplicate column list 2024-12-06 20:16:46 +01:00
Matthias 63f8217cd5 chore: ensure exif-data is removed from images 2024-12-06 20:11:46 +01:00
Matthias 9f5cb3a07e chore: add exif-stripper to ensure exif data is removed 2024-12-06 20:11:09 +01:00
Matthias e09d1b42f2 chore: eliminate duplicate trades data grouping 2024-12-06 20:09:43 +01:00
Matthias 267d9333a1 chore: remove pointless, very noisy log message. 2024-12-06 20:09:07 +01:00
Matthias ab39ac29e8 fix: ensure data type is maintained when data comes from cache. 2024-12-06 20:05:33 +01:00
Matthias 6c25feabf2 tests: assert type of orderflow object 2024-12-06 20:05:07 +01:00
Matthias 171157c100 chore: further simplify orderflow code 2024-12-06 07:03:24 +01:00
Matthias 3137d7cf2c chore: remove unnecessary config alias 2024-12-06 06:43:01 +01:00
Matthias 6ab528748e tests: Update orderflow tests 2024-12-06 06:40:15 +01:00
Matthias 621dfc136e fix: Improved caching
closes #11008
2024-12-06 06:38:49 +01:00
Matthias 9d07f5dc2e chore: reduce orderflow code duplication 2024-12-05 19:37:36 +01:00
Matthias 48dd86bd9b chore: use .at for assignments 2024-12-05 19:33:49 +01:00
Matthias 4879582896 feat: use dataframe.at directly to avoid intermediate series 2024-12-05 19:32:40 +01:00
Matthias 6584f86bce test: add Spy to improve test 2024-12-05 19:19:17 +01:00
Matthias 82d517fcbb test: improve orderflow test 2024-12-05 07:12:35 +01:00
Matthias ff371c43e0 tests: add test to ensure caching works
part of #11008
2024-12-05 07:11:58 +01:00
Matthias 0bf0e1808c chore: add todo for future cleanup 2024-12-05 06:58:25 +01:00
Matthias 6be5947f69 chore: Move local Import out of the loop 2024-12-05 06:37:19 +01:00
Matthias b1d7d7304d Merge pull request #11041 from freqtrade/update/binance-leverage-tiers
Update Binance Leverage Tiers
2024-12-05 06:29:25 +01:00
xmatthias 1d0702e143 chore: update pre-commit hooks 2024-12-05 03:16:51 +00:00
Matthias 298ce335b9 chore: eliminate duplicate trades data grouping 2024-12-04 19:56:26 +01:00
Matthias e04f630f41 chore: further improve typing in orderflow 2024-12-04 07:10:53 +01:00
Matthias 7dfee16e25 chore: improved logic ordering in orderflow method 2024-12-04 07:09:09 +01:00
Matthias d6c21e2af6 chore: orderflow: better typing 2024-12-03 20:13:25 +01:00
Matthias ebae0a7248 chore: improve typing of new functionality 2024-12-03 19:44:26 +01:00
Matthias fe834f00a2 fix: import causing long startup time 2024-12-03 19:37:14 +01:00
Robert Caulk 3855d44bf5 Merge pull request #11005 from freqtrade/fix/freqai_bug
freqAI bug causing failures on 2nd backtest
2024-12-03 19:18:43 +01:00
Matthias ad8f621287 tests: Improve hyperopt loss tests 2024-12-03 19:17:01 +01:00
Matthias 18305a5bf6 docs: update hyperopt docs to include new argument 2024-12-03 19:16:48 +01:00
Matthias b0b73bf166 feat: add starting_balance as argument to hyperopt_loss_function 2024-12-03 19:15:57 +01:00
Matthias 7a8971b9b6 feat: use get_dry_run_wallet helper 2024-12-03 07:27:45 +01:00
Matthias c082e5f6a6 feat: add dry_run_wallet helper 2024-12-03 07:26:47 +01:00
Matthias ef0703f209 fix: Set timeframe for api calls
closes #11009
2024-12-03 06:53:39 +01:00
Matthias 196c122e0a Merge pull request #11031 from freqtrade/update/pre-commit-hooks
Update pre-commit hooks
2024-12-03 06:30:30 +01:00
xmatthias 57d5a55ca0 chore: update pre-commit hooks 2024-12-03 03:17:35 +00:00
Matthias 94e09f9fa0 Merge pull request #11028 from xzmeng/fix-none
fix: check if days is None before conversion
2024-12-02 20:11:00 +01:00
Matthias 2f4bd6a0c9 chore: split pypi and pypi test jobs to enable proper attestation
Related: https://github.com/pypa/gh-action-pypi-publish/issues/283
2024-12-02 20:03:53 +01:00
Matthias 58d3555102 Merge pull request #11027 from xzmeng/fix-cond
fix: invert API download condition from `and` to `or`
2024-12-02 18:22:58 +01:00
Matthias b032c329e3 Merge pull request #11017 from freqtrade/dependabot/pip/develop/pyarrow-18.1.0
chore(deps): bump pyarrow from 18.0.0 to 18.1.0
2024-12-02 18:19:43 +01:00
Meng Xiangzhuo 73f00d2bc3 fix: check if days is None before conversion 2024-12-03 00:31:34 +08:00
Meng Xiangzhuo cf896bbb52 chore: add the missing space in log 2024-12-03 00:04:56 +08:00
Meng Xiangzhuo ba162e35c4 fix: invert logic and to or 2024-12-02 23:56:59 +08:00
Matthias 00e9178d5f chore: bump pyarrow wheel 2024-12-02 16:04:23 +01:00
Matthias 62b0995f6a Merge pull request #11020 from freqtrade/dependabot/pip/develop/ruff-0.8.1
chore(deps-dev): bump ruff from 0.8.0 to 0.8.1
2024-12-02 07:38:15 +01:00
Matthias 4107e5cc9a Merge pull request #11021 from freqtrade/dependabot/pip/develop/cryptography-44.0.0
chore(deps): bump cryptography from 43.0.3 to 44.0.0
2024-12-02 07:26:15 +01:00
Matthias c841146968 chore: Remove non-suppored callback from XGBRFRegressor
According to the XGBoost docs, this probably never worked
as it was never supported - but is now raising an error for user clarity.
2024-12-02 07:12:30 +01:00
dependabot[bot] 6c9e6cc198 chore(deps): bump xgboost from 2.0.3 to 2.1.3
Bumps [xgboost](https://github.com/dmlc/xgboost) from 2.0.3 to 2.1.3.
- [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.0.3...v2.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 06:00:19 +00:00
dependabot[bot] 980bfddd8a chore(deps-dev): bump ruff from 0.8.0 to 0.8.1
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.0 to 0.8.1.
- [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.8.0...0.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 05:37:46 +00:00
dependabot[bot] 06327e8f04 chore(deps): bump cryptography from 42.0.8 to 44.0.0
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.8 to 44.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.8...44.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 05:37:30 +00:00
Matthias 2290b44f82 Merge pull request #11018 from freqtrade/dependabot/pip/develop/matplotlib-3.9.3
chore(deps): bump matplotlib from 3.9.2 to 3.9.3
2024-12-02 06:37:00 +01:00
Matthias c0cec2b9de Merge pull request #11015 from freqtrade/dependabot/pip/develop/python-telegram-bot-21.8
chore(deps): bump python-telegram-bot from 21.7 to 21.8
2024-12-02 06:36:46 +01:00
Matthias e376889f3d Merge pull request #11013 from freqtrade/dependabot/pip/develop/pytest-e05860fc06
chore(deps-dev): bump pytest from 8.3.3 to 8.3.4 in the pytest group
2024-12-02 06:36:33 +01:00
Matthias 8584cacb6e Merge pull request #11016 from freqtrade/dependabot/pip/develop/pydantic-2.10.2
chore(deps): bump pydantic from 2.10.1 to 2.10.2
2024-12-02 06:34:33 +01:00
Matthias 3ff19babf5 Merge pull request #11019 from freqtrade/dependabot/pip/develop/pyjwt-2.10.1
chore(deps): bump pyjwt from 2.10.0 to 2.10.1
2024-12-02 06:34:23 +01:00
Matthias 8b5aa08c57 Merge pull request #11014 from freqtrade/dependabot/pip/develop/mkdocs-16f9823690
chore(deps): bump mkdocs-material from 9.5.45 to 9.5.47 in the mkdocs group
2024-12-02 06:33:38 +01:00
dependabot[bot] cffcb2b985 chore(deps): bump pyjwt from 2.10.0 to 2.10.1
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.10.0 to 2.10.1.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.10.0...2.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:19:54 +00:00
dependabot[bot] 1da8b68906 chore(deps): bump matplotlib from 3.9.2 to 3.9.3
Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.9.2 to 3.9.3.
- [Release notes](https://github.com/matplotlib/matplotlib/releases)
- [Commits](https://github.com/matplotlib/matplotlib/compare/v3.9.2...v3.9.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:19:49 +00:00
dependabot[bot] d53f4fb81b chore(deps): bump pyarrow from 18.0.0 to 18.1.0
Bumps [pyarrow](https://github.com/apache/arrow) from 18.0.0 to 18.1.0.
- [Release notes](https://github.com/apache/arrow/releases)
- [Commits](https://github.com/apache/arrow/compare/apache-arrow-18.0.0...apache-arrow-18.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:19:42 +00:00
dependabot[bot] 02fcd3b489 chore(deps): bump pydantic from 2.10.1 to 2.10.2
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.1 to 2.10.2.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.10.1...v2.10.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:19:25 +00:00
dependabot[bot] 7b70a7a68a chore(deps): bump python-telegram-bot from 21.7 to 21.8
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 21.7 to 21.8.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v21.7...v21.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:19:08 +00:00
dependabot[bot] 5060725ef5 chore(deps): bump mkdocs-material in the mkdocs group
Bumps the mkdocs group with 1 update: [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:19:00 +00:00
dependabot[bot] 6bf3218665 chore(deps-dev): bump pytest from 8.3.3 to 8.3.4 in the pytest group
Bumps the pytest group with 1 update: [pytest](https://github.com/pytest-dev/pytest).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-02 03:18:44 +00:00
Matthias 29f86afaf4 Merge pull request #10858 from xzmeng/binance-public-data
Improve binance historic OHLCV download speed
2024-12-01 19:41:40 +01:00
Matthias ac5a3b9217 Merge pull request #11010 from freqtrade/bump/ta-lib
Bump ta lib to 0.4.34
2024-12-01 19:39:40 +01:00
Matthias d7d73364e9 chore: version bump 2024.12-dev 2024-12-01 19:38:37 +01:00
Matthias 1510c5517b chore: update ta-lib wheels for 0.4.34 2024-12-01 15:28:48 +01:00
Matthias f6e2fcbf15 chore: bump ta-lib to 0.4.34 2024-12-01 15:28:37 +01:00
Matthias 162c79029f chore: slightly reorganize test file layout 2024-11-29 07:12:54 +01:00
Matthias 5f363f5c9d chore: properly remove ccxt reference 2024-11-29 07:00:42 +01:00
Matthias bd5877ad49 chore: add space in log message 2024-11-29 06:55:33 +01:00
Matthias 71cf7aedb5 chore: remove unnecessary test-file
that test is flaky for new pairs - and wouldn't run in production anyway.
2024-11-29 06:48:39 +01:00
Matthias fa1e0af19f chore: remove direct exchange connection 2024-11-29 06:46:55 +01:00
Matthias a6199680cd fix: freqAI bug causing failures on 2nd backtest 2024-11-28 19:53:41 +01:00
Matthias d1710826e6 chore: improved naming 2024-11-28 07:27:29 +01:00
Matthias 675b996f57 chore: improved naming in test case 2024-11-28 07:20:32 +01:00
Matthias 21777a5804 chore: do more pinpointed imports 2024-11-28 07:09:57 +01:00
Matthias b1ca00b037 chore: don't shadow pandas builtin methods 2024-11-28 07:04:28 +01:00
Matthias e2a09f272a chore: Improve naming, remove unnecessary method 2024-11-28 06:55:15 +01:00
Matthias 0e77c89d79 chore: simplify code structure by moving conditional to parent method 2024-11-28 06:43:51 +01:00
Matthias 69c1de7e4a chore: move fallback param to config directly 2024-11-27 18:20:44 +01:00
Matthias 09308e568d fix: increase code reliability by not relying on stake-currency to be in the dict 2024-11-27 07:24:11 +01:00
Matthias 671821aeb3 docs: Add documentation for dry-run-wallet as dict 2024-11-27 06:48:07 +01:00
Matthias 7369331e2d tests: add test for multi-pair dry-run wallets 2024-11-27 06:41:54 +01:00
Matthias e6e193f252 test: assert tickers is called a 2nd time if necessary 2024-11-27 06:41:54 +01:00
Matthias 2bb111605c feat: update rpc_balance to fetch spot tickers
Happens if there is no market for the given pair
2024-11-27 06:41:54 +01:00
Matthias 37aba6f7d5 feat: Allow fetch_tickers from different marketsegment 2024-11-27 06:41:54 +01:00
Matthias 3fc259bb9b feat: add non-trading balance to wallet 2024-11-27 06:41:54 +01:00
Matthias 15c1a8ee0b chore: reduce dict lookups, reuse attribute 2024-11-27 06:41:54 +01:00
Matthias b4b6de4e0d test: update test ... 2024-11-27 06:41:54 +01:00
Matthias 5b0be7e1a9 feat: support dict like dry_run_wallet 2024-11-27 06:41:54 +01:00
Matthias 92e64927b0 feat: allow object as dry-run balance 2024-11-27 06:41:54 +01:00
Alexander Malysh c3032feaf7 * allow json in env variables 2024-11-24 14:23:04 +01:00
Meng Xiangzhuo 0f53dc1b7b chore: improve log level to warning when falling back to API 2024-11-15 12:08:50 +08:00
xzmeng 19f96d60e3 refactor: streamline error handling by raising instead of returning 2024-11-14 09:47:23 +08:00
xzmeng bfdbf0248c refactor: rename asset_type to asset_type_url_segment 2024-11-14 07:51:58 +08:00
xzmeng 660863392b refactor: rename fetch_ohlcv to download_archive_ohlcv 2024-11-14 07:29:37 +08:00
xzmeng 8baa0f7310 chore: add user friendly warnings 2024-11-14 06:06:15 +08:00
xzmeng 6b18c4f24c feat: cancel uncompleted tasks before return 2024-11-14 05:24:31 +08:00
xzmeng 40f70a1cc0 chore: warning when fall back to rest API 2024-11-14 04:36:24 +08:00
Meng Xiangzhuo c869dfea3f chore: add more docstrings 2024-11-13 17:32:25 +08:00
Meng Xiangzhuo 39b4263b8b chore: improve comments 2024-11-13 17:14:49 +08:00
Meng Xiangzhuo cf7016b36d chore: remove unused code 2024-11-13 16:09:02 +08:00
Meng Xiangzhuo 1893ac7ed6 tests: patch thirty party libs from imported location 2024-11-13 13:08:29 +08:00
Meng Xiangzhuo a748d105ed chore: remove performance compare script 2024-11-13 12:45:46 +08:00
Meng Xiangzhuo fc307bcf5b tests: fix 2024-11-13 12:44:27 +08:00
Meng Xiangzhuo 03033a0684 refactor: use exchange.markets to avoid loading ccxt markets 2024-11-13 12:36:18 +08:00
Meng Xiangzhuo 7ca39a5c38 Merge branch 'develop' into binance-public-data 2024-11-13 12:14:50 +08:00
Meng Xiangzhuo d7555e1f29 feat: support proxy from environment variables 2024-11-13 12:03:41 +08:00
Meng Xiangzhuo 37726fba58 refactor: use CCXT for pair to symbol conversion 2024-11-13 11:55:10 +08:00
Meng Xiangzhuo c4cf582c9d refacotr: default to stop on 404 2024-11-13 11:18:32 +08:00
Meng Xiangzhuo acc53065e5 refactor: strip fast download logic into a method 2024-11-13 10:16:06 +08:00
Meng Xiangzhuo cf0f232635 refactor: move download klines count message 2024-11-02 06:00:37 +08:00
Meng Xiangzhuo e2ee7f7b2f feat: fall back to rest API by catching all exceptions 2024-11-02 05:25:05 +08:00
Meng Xiangzhuo 76187d31cf feat: more binance fast download timeframes 2024-11-02 04:12:57 +08:00
Meng Xiangzhuo c3bbedbc56 refactor: create coroutines on demand
to avoid "coroutine was never awaited" warnings
2024-11-02 04:10:46 +08:00
Meng Xiangzhuo 3c76af9dab tests: fix windows 2024-10-31 04:18:37 +08:00
Meng Xiangzhuo ad12a9eb99 tests: setup windows asyncio loop 2024-10-31 03:29:21 +08:00
Meng Xiangzhuo 45bf046645 tests: add performance comparison 2024-10-31 01:51:54 +08:00
Meng Xiangzhuo 2ceda2987c feat: add binance 1s ohlcv fast download 2024-10-30 23:51:25 +08:00
Meng Xiangzhuo 1aa863a92f tests: fix 2024-10-30 22:11:37 +08:00
Meng Xiangzhuo a417698fcd tests: fix test and improve coverage 2024-10-30 11:30:12 +08:00
Meng Xiangzhuo e49b5b03db feat: stop on 404 to prevent missing data 2024-10-30 07:59:06 +08:00
Meng Xiangzhuo b5f503dbbb tests: binance compare data.binance.vision to rest API 2024-10-30 07:30:03 +08:00
Meng Xiangzhuo 04d3633545 feat: implement fetch data from data.binance.vision 2024-10-30 07:29:52 +08:00
Meng Xiangzhuo 4e585c5c34 feat: implement Binance.get_historic_ohlcv detail 2024-10-30 07:29:38 +08:00
Meng Xiangzhuo 867aae868d refactor: move is_new_pair logic to Binance.get_historic_ohlcv 2024-10-30 07:29:19 +08:00
111 changed files with 8757 additions and 2784 deletions
+44 -14
View File
@@ -276,14 +276,23 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Pip cache (Windows)
uses: actions/cache@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
path: ~\AppData\Local\pip\Cache
key: pip-${{ matrix.os }}-${{ matrix.python-version }}
enable-cache: true
cache-dependency-glob: "requirements**.txt"
cache-suffix: "${{ matrix.python-version }}"
prune-cache: false
- name: Installation
run: |
uv venv
.venv\Scripts\activate
# persist the venv path for future steps
"$(pwd)/.venv/Scripts" >> $env:GITHUB_PATH
function uvpipFunction { uv pip $args }
Set-Alias -name pip -value uvpipFunction
./build_helpers/install_windows.ps1
- name: Tests
@@ -517,13 +526,40 @@ jobs:
ft_client/dist
retention-days: 10
deploy-pypi:
name: "Deploy to PyPI"
deploy-test-pypi:
name: "Publish Python 🐍 distribution 📦 to TestPyPI"
needs: [ build ]
runs-on: ubuntu-22.04
if: (github.event_name == 'release')
environment:
name: release
name: testpypi
url: https://test.pypi.org/p/freqtrade
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Download artifact 📦
uses: actions/download-artifact@v4
with:
pattern: freqtrade*-build
path: dist
merge-multiple: true
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.12.3
with:
repository-url: https://test.pypi.org/legacy/
deploy-pypi:
name: "Publish Python 🐍 distribution 📦 to PyPI"
needs: [ build ]
runs-on: ubuntu-22.04
if: (github.event_name == 'release')
environment:
name: pypi
url: https://pypi.org/p/freqtrade
permissions:
id-token: write
@@ -538,14 +574,8 @@ jobs:
path: dist
merge-multiple: true
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.12.2
with:
repository-url: https://test.pypi.org/legacy/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.2
uses: pypa/gh-action-pypi-publish@v1.12.3
deploy-docker:
+9 -4
View File
@@ -9,7 +9,7 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.13.0"
rev: "v1.14.0"
hooks:
- id: mypy
exclude: build_helpers
@@ -17,8 +17,8 @@ repos:
- types-cachetools==5.5.0.20240820
- types-filelock==3.2.7
- types-requests==2.32.0.20241016
- types-tabulate==0.9.0.20240106
- types-python-dateutil==2.9.0.20241003
- types-tabulate==0.9.0.20241207
- types-python-dateutil==2.9.0.20241206
- SQLAlchemy==2.0.36
# stages: [push]
@@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.8.0'
rev: 'v0.8.4'
hooks:
- id: ruff
- id: ruff-format
@@ -56,6 +56,11 @@ repos:
.*\.md
)$
- repo: https://github.com/stefmolin/exif-stripper
rev: 0.6.1
hooks:
- id: strip-exif
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
+3 -2
View File
@@ -32,10 +32,11 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [BingX](https://bingx.com/invite/0EM9RX)
- [X] [Bybit](https://bybit.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi)
- [X] [HTX](https://www.htx.com/)
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [Kraken](https://kraken.com/)
- [X] [OKX](https://okx.com/) (Former OKEX)
- [X] [OKX](https://okx.com/)
- [X] [MyOKX](https://okx.com/) (OKX EEA)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Supported Futures Exchanges (experimental)
+4 -5
View File
@@ -1,11 +1,10 @@
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
python -m pip install --upgrade pip wheel
python -m pip install --upgrade pip
python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
pip install --find-links=build_helpers\ --prefer-binary TA-Lib
pip install -U wheel "numpy<2"
pip install --only-binary ta-lib --find-links=build_helpers\ ta-lib
pip install -r requirements-dev.txt
pip install -e .
+16 -3
View File
@@ -102,8 +102,17 @@
},
"dry_run_wallet": {
"description": "Initial wallet balance for dry run mode.",
"type": "number",
"default": 1000
"type": [
"number",
"object"
],
"default": 1000,
"patternProperties": {
"^[a-zA-Z0-9]+$": {
"type": "number"
}
},
"additionalProperties": false
},
"cancel_open_orders_on_exit": {
"description": "Cancel open orders when exiting.",
@@ -592,7 +601,11 @@
"type": "string"
},
"chat_id": {
"description": "Telegram chat ID",
"description": "Telegram chat or group ID",
"type": "string"
},
"topic_id": {
"description": "Telegram topic ID - only applicable for group chats",
"type": "string"
},
"allow_custom_messages": {
+2
View File
@@ -39,6 +39,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
config: Config,
processed: dict[str, DataFrame],
backtest_stats: dict[str, Any],
starting_balance: float,
**kwargs,
) -> float:
"""
@@ -70,6 +71,7 @@ Currently, the arguments are:
* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting.
* `backtest_stats`: Backtesting statistics using the same format as the backtesting file "strategy" substructure. Available fields can be seen in `generate_strategy_stats()` in `optimize_reports.py`.
* `starting_balance`: Starting balance used for backtesting.
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
+1
View File
@@ -4,6 +4,7 @@ This guide walks you through utilizing public trade data for advanced orderflow
!!! Warning "Experimental Feature"
The orderflow feature is currently in beta and may be subject to changes in future releases. Please report any issues or feedback on the [Freqtrade GitHub repository](https://github.com/freqtrade/freqtrade/issues).
It's also currently not been tested with freqAI - and combining these two features is considered out of scope at this point.
!!! Warning "Performance"
Orderflow requires raw trades data. This data is rather large, and can cause a slow initial startup, when freqtrade needs to download the trades data for the last X candles. Additionally, enabling this feature will cause increased memory usage. Please ensure to have sufficient resources available.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+29 -3
View File
@@ -39,13 +39,19 @@ Please note that Environment variables will overwrite corresponding settings in
Common example:
```
``` bash
FREQTRADE__TELEGRAM__CHAT_ID=<telegramchatid>
FREQTRADE__TELEGRAM__TOKEN=<telegramToken>
FREQTRADE__EXCHANGE__KEY=<yourExchangeKey>
FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
```
Json lists are parsed as json - so you can use the following to set a list of pairs:
``` bash
export FREQTRADE__EXCHANGE__PAIR_WHITELIST='["BTC/USDT", "ETH/USDT"]'
```
!!! Note
Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable.
@@ -54,7 +60,7 @@ FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
??? Warning "Loading sequence"
Environment variables are loaded after the initial configuration. As such, you cannot provide the path to the configuration through environment variables. Please use `--config path/to/config.json` for that.
This also applies to user_dir to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location.
This also applies to `user_dir` to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location.
### Multiple configuration files
@@ -168,7 +174,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). Usually missing in configuration, and specified in the strategy. [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode. [More information below](#dry-run-wallet)<br>*Defaults to `1000`.* <br> **Datatype:** Float or Dict
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `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
@@ -225,6 +231,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.only_from_ccxt` | Prevent data-download from data.binance.vision. Leaving this as false can greatly speed up downloads, but may be problematic if the site is not available.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| | **Plugins**
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
@@ -323,6 +330,25 @@ To limit this calculation in case of large stoploss values, the calculated minim
!!! Warning
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected.
#### Dry-run wallet
When running in dry-run mode, the bot will use a simulated wallet to execute trades. The starting balance of this wallet is defined by `dry_run_wallet` (defaults to 1000).
For more complex scenarios, you can also assign a dictionary to `dry_run_wallet` to define the starting balance for each currency.
```json
"dry_run_wallet": {
"BTC": 0.01,
"ETH": 2,
"USDT": 1000
}
```
Command line options (`--dry-run-wallet`) can be used to override the configuration value, but only for the float value, not for the dictionary. If you'd like to use the dictionary, please adjust the configuration file.
!!! Note
Balances not in stake-currency will not be used for trading, but are shown as part of the wallet balance.
On Cross-margin exchanges, the wallet balance may be used to calculate the available collateral for trading.
#### Tradable balance
By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade.
+1 -1
View File
@@ -162,7 +162,7 @@ Freqtrade currently supports the following data-formats:
* `feather` - a dataformat based on Apache Arrow
* `json` - plain "text" json files
* `jsongz` - a gzip-zipped version of json files
* `hdf5` - a high performance datastore
* `hdf5` - a high performance datastore (deprecated)
* `parquet` - columnar datastore (OHLCV only)
By default, both OHLCV data and trades data are stored in the `feather` format.
+7 -1
View File
@@ -81,4 +81,10 @@ version 2023.3 saw the removal of `populate_any_indicators` in favor of split me
## Removal of `protections` from configuration
Setting protections from the configuration via `"protections": [],` has been removed in 2024.10, after having raised deprecation warnings for over 3 years.
Setting protections from the configuration via `"protections": [],` has been removed in 2024.10, after having raised deprecation warnings for over 3 years.
## hdf5 data storage
Using hdf5 as data storage has been deprecated in 2024.12 and will be removed in 2025.1. We recommend switching to the feather data format.
Please use the [`convert-data` subcommand](data-download.md#sub-command-convert-data) to convert your existing data to one of the supported formats.
+5 -2
View File
@@ -217,12 +217,12 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force)
For Kucoin, it is suggested to add `"KCS/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `KCS` on the account or unless you're willing to disable using `KCS` for fees.
Kucoin accounts may use `KCS` for fees, and if a trade happens to be on `KCS`, further trades may consume this position and make the initial `KCS` trade unsellable as the expected amount is not there anymore.
## HTX (formerly Huobi)
## HTX
!!! Tip "Stoploss on Exchange"
HTX supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
## OKX (former OKEX)
## OKX
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
@@ -236,6 +236,9 @@ OKX requires a passphrase for each api key, you will therefore need to add this
}
```
If you've registered with OKX on the host my.okx.com (OKX EAA)- you will need to use `"myokx"` as the exchange name.
Using the wrong exchange will result in the error "OKX Error 50119: API key doesn't exist" - as the 2 are separate entities.
!!! Warning
OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
+8
View File
@@ -40,6 +40,10 @@ This could be caused by the following reasons:
* The installation did not complete successfully.
* Please check the [Installation documentation](installation.md).
### The bot starts, but in STOPPED mode
Make sure you set the `initial_state` config option to `"running"` in your config.json
### I have waited 5 minutes, why hasn't the bot made any trades yet?
* Depending on the buy strategy, the amount of whitelisted coins, the
@@ -129,6 +133,10 @@ This message is a warning that the candles had a price jump of > 30%.
This might be a sign that the pair stopped trading, and some token exchange took place (e.g. COCOS in 2021 - where price jumped from 0.0000154 to 0.01621).
This message is often accompanied by ["Missing data fillup"](#im-getting-missing-data-fillup-messages-in-the-log) - as trading on such pairs is often stopped for some time.
### I want to reset the bot's database
To reset the bot's database, you can either delete the database (by default `tradesv3.sqlite` or `tradesv3.dryrun.sqlite`), or use a different database url via `--db-url` (e.g. `sqlite:///mynewdatabase.sqlite`).
### I'm getting "Outdated history for pair xxx" in the log
The bot is trying to tell you that it got an outdated last candle (not the last complete candle).
+3 -2
View File
@@ -44,10 +44,11 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Bitmart](https://bitmart.com/)
- [X] [Bybit](https://bybit.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [HTX](https://www.htx.com/) (Former Huobi)
- [X] [HTX](https://www.htx.com/)
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
- [X] [Kraken](https://kraken.com/)
- [X] [OKX](https://okx.com/) (Former OKEX)
- [X] [OKX](https://okx.com/)
- [X] [MyOKX](https://okx.com/) (OKX EEA)
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Supported Futures Exchanges (experimental)
+2 -2
View File
@@ -1,7 +1,7 @@
markdown==3.7
mkdocs==1.6.1
mkdocs-material==9.5.45
mkdocs-material==9.5.49
mdx_truly_sane_lists==1.3
pymdown-extensions==10.12
jinja2==3.1.4
jinja2==3.1.5
mike==2.1.3
+67 -53
View File
@@ -88,8 +88,9 @@ Make sure that the following 2 lines are available in your docker-compose file:
### Consuming the API
You can consume the API by using `freqtrade-client` (also available as `scripts/rest_client.py`).
This command can be installed independent of the bot by using `pip install freqtrade-client`.
We advise consuming the API by using the supported `freqtrade-client` package (also available as `scripts/rest_client.py`).
This command can be installed independent of any running freqtrade bot by using `pip install freqtrade-client`.
This module is designed to be lightweight, and only depends on the `requests` and `python-rapidjson` modules, skipping all heavy dependencies freqtrade otherwise needs.
@@ -144,57 +145,6 @@ This method will work for all arguments - check the "show" command for a list of
For a full list of available commands, please refer to the list below.
### Available endpoints
| Command | Description |
|----------|-------------|
| `ping` | Simple command testing the API Readiness - requires no authentication.
| `start` | Starts the trader.
| `stop` | Stops the trader.
| `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `reload_config` | Reloads the configuration file.
| `trades` | List last trades. Limited to 500 trades per call.
| `trade/<tradeid>` | Get specific trade.
| `trades/<tradeid>` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `trades/<tradeid>/open-order` | DELETE - Cancel open order for this trade.
| `trades/<tradeid>/reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.
| `show_config` | Shows part of the current configuration with relevant settings to operation.
| `logs` | Shows last log messages.
| `status` | Lists all open trades.
| `count` | Displays number of trades used and available.
| `entries [pair]` | Shows profit statistics for each enter tags for given pair (or all pairs if pair isn't given). Pair is optional.
| `exits [pair]` | Shows profit statistics for each exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
| `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
| `locks` | Displays currently locked pairs.
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `locks add <pair>, <until>, [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe).
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forceexit <trade_id> [order_type] [amount]` | Instantly exits the given trade (ignoring `minimum_roi`), using the given order type ("market" or "limit", uses your config setting if not specified), and the chosen amount (full sell if not specified).
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`force_entry_enable` must be set to True)
| `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`force_entry_enable` must be set to True)
| `performance` | Show performance of each finished trade grouped by pair.
| `balance` | Show account balance per currency.
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
| `weekly <n>` | Shows profit or loss per week, over the last n days (n defaults to 4).
| `monthly <n>` | Shows profit or loss per month, over the last n days (n defaults to 3).
| `stats` | Display a summary of profit / loss reasons as well as average holding times.
| `whitelist` | Show the current whitelist.
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
| `edge` | Show validated pairs by Edge if it is enabled.
| `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
| `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
| `plot_config` | Get plot config from the strategy (or nothing if not configured). **Alpha**
| `strategies` | List strategies in strategy directory. **Alpha**
| `strategy <strategy>` | Get specific Strategy content. **Alpha**
| `available_pairs` | List available backtest data. **Alpha**
| `version` | Show version.
| `sysinfo` | Show information about the system load.
| `health` | Show bot health (last bot loop).
!!! Warning "Alpha status"
Endpoints labeled with *Alpha status* above may change at any time without notice.
Possible commands can be listed from the rest-client script using the `help` command.
``` bash
@@ -266,6 +216,14 @@ forceexit
health
Provides a quick health check of the running bot.
lock_add
Manually lock a specific pair
:param pair: Pair to lock
:param until: Lock until this date (format "2024-03-30 16:00:00Z")
:param side: Side to lock (long, short, *)
:param reason: Reason for the lock
locks
Return current locks
@@ -353,6 +311,62 @@ whitelist
```
### Available endpoints
If you wish to call the REST API manually via another route, e.g. directly via `curl`, the table below shows the relevant URL endpoints and parameters.
All endpoints in the below table need to be prefixed with the base URL of the API, e.g. `http://127.0.0.1:8080/api/v1/` - so the command becomes `http://127.0.0.1:8080/api/v1/<command>`.
| Endpoint | Method | Description / Parameters |
|-----------|--------|--------------------------|
| `/ping` | GET | Simple command testing the API Readiness - requires no authentication.
| `/start` | POST | Starts the trader.
| `/stop` | POST | Stops the trader.
| `/stopbuy` | POST | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_config` | POST | Reloads the configuration file.
| `/trades` | GET | List last trades. Limited to 500 trades per call.
| `/trade/<tradeid>` | GET | Get specific trade.<br/>*Params:*<br/>- `tradeid` (`int`)
| `/trades/<tradeid>` | DELETE | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.<br/>*Params:*<br/>- `tradeid` (`int`)
| `/trades/<tradeid>/open-order` | DELETE | Cancel open order for this trade.<br/>*Params:*<br/>- `tradeid` (`int`)
| `/trades/<tradeid>/reload` | POST | Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.<br/>*Params:*<br/>- `tradeid` (`int`)
| `/show_config` | GET | Shows part of the current configuration with relevant settings to operation.
| `/logs` | GET | Shows last log messages.
| `/status` | GET | Lists all open trades.
| `/count` | GET | Displays number of trades used and available.
| `/entries` | GET | Shows profit statistics for each enter tags for given pair (or all pairs if pair isn't given). Pair is optional.<br/>*Params:*<br/>- `pair` (`str`)
| `/exits` | GET | Shows profit statistics for each exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.<br/>*Params:*<br/>- `pair` (`str`)
| `/mix_tags` | GET | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.<br/>*Params:*<br/>- `pair` (`str`)
| `/locks` | GET | Displays currently locked pairs.
| `/locks` | POST | Locks a pair until "until". (Until will be rounded up to the nearest timeframe). Side is optional and is either `long` or `short` (default is `long`). Reason is optional.<br/>*Params:*<br/>- `<pair>` (`str`)<br/>- `<until>` (`datetime`)<br/>- `[side]` (`str`)<br/>- `[reason]` (`str`)
| `/locks/<lockid>` | DELETE | Deletes (disables) the lock by id.<br/>*Params:*<br/>- `lockid` (`int`)
| `/profit` | GET | Display a summary of your profit/loss from close trades and some stats about your performance.
| `/forceexit` | POST | Instantly exits the given trade (ignoring `minimum_roi`), using the given order type ("market" or "limit", uses your config setting if not specified), and the chosen amount (full sell if not specified). If `all` is supplied as the `tradeid`, then all currently open trades will be forced to exit.<br/>*Params:*<br/>- `<tradeid>` (`int` or `str`)<br/>- `<ordertype>` (`str`)<br/>- `[amount]` (`float`)
| `/forceenter` | POST | Instantly enters the given pair. Side is optional and is either `long` or `short` (default is `long`). Rate is optional. (`force_entry_enable` must be set to True)<br/>*Params:*<br/>- `<pair>` (`str`)<br/>- `<side>` (`str`)<br/>- `[rate]` (`float`)
| `/performance` | GET | Show performance of each finished trade grouped by pair.
| `/balance` | GET | Show account balance per currency.
| `/daily` | GET | Shows profit or loss per day, over the last n days (n defaults to 7).<br/>*Params:*<br/>- `<n>` (`int`)
| `/weekly` | GET | Shows profit or loss per week, over the last n days (n defaults to 4).<br/>*Params:*<br/>- `<n>` (`int`)
| `/monthly` | GET | Shows profit or loss per month, over the last n days (n defaults to 3).<br/>*Params:*<br/>- `<n>` (`int`)
| `/stats` | GET | Display a summary of profit / loss reasons as well as average holding times.
| `/whitelist` | GET | Show the current whitelist.
| `/blacklist` | GET | Show the current blacklist.
| `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `pair` (`str`)
| `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`)
| `/edge` | GET | Show validated pairs by Edge if it is enabled.
| `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
| `/pair_candles` | POST | Returns dataframe for a pair / timeframe combination while the bot is running, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
| `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
| `/pair_history` | POST | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
| `/plot_config` | GET | Get plot config from the strategy (or nothing if not configured). **Alpha**
| `/strategies` | GET | List strategies in strategy directory. **Alpha**
| `/strategy/<strategy>` | GET | Get specific Strategy content by strategy class name. **Alpha**<br/>*Params:*<br/>- `<strategy>` (`str`)
| `/available_pairs` | GET | List available backtest data. **Alpha**
| `/version` | GET | Show version.
| `/sysinfo` | GET | Show information about the system load.
| `/health` | GET | Show bot health (last bot loop).
!!! Warning "Alpha status"
Endpoints labeled with *Alpha status* above may change at any time without notice.
### Message WebSocket
The API Server includes a websocket endpoint for subscribing to RPC messages from the freqtrade Bot.
+2 -2
View File
@@ -30,8 +30,8 @@ The Order-type will be ignored if only one mode is available.
|----------|-------------|
| Binance | limit |
| Binance Futures | market, limit |
| Bingx | market, limit |
| HTX (former Huobi) | limit |
| Bingx | market, limit |
| HTX | limit |
| kraken | market, limit |
| Gate | limit |
| Okx | limit |
+19 -8
View File
@@ -767,6 +767,15 @@ Adjustment orders can be assigned with a tag by returning a 2 element Tuple, wit
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
!!! Danger "Loose Logic"
On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, for example your logic for extra entry is only to check RSI of last candle is below 30, then when such condition fulfilled, your bot will do extra re-entry every 5 secs until either it run out of money, it hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived.
Same thing also can happen with partial exit. So be sure to have a strict logic and/or check for the last filled order.
!!! Warning "Backtesting"
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.
### Increase position
The strategy is expected to return a positive **stake_amount** (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades).
@@ -776,16 +785,22 @@ If there are not enough funds in the wallet (the return value is above `max_stak
Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
!!! Note "About stake size"
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
Using `"unlimited"` stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order.
### Decrease position
The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit.
Returning the full owned stake at that point (`-trade.stake_amount`) results in a full exit.
Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal.
!!! Note "About stake size"
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
Using `"unlimited"` stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order.
For a partial exit, it's important to know that the formula used to calculate the amount of the coin for the partial exit order is `amount to be exited partially = negative_stake_amount * trade.amount / trade.stake_amount`, where `negative_stake_amount` is the value returned from the `adjust_trade_position` function. As seen in the formula, the formula doesn't care about current profit/loss of the position. It only cares about `trade.amount` and `trade.stake_amount` which aren't affected by the price movement at all.
For example, let's say you buy 2 SHITCOIN/USDT at open rate of 50, which means the trade's stake amount is 100 USDT. Now the price raises to 200 and you want to sell half of it. In that case, you have to return -50% of `trade.stake_amount` (0.5 * 100 USDT) which equals to -50. The bot will calculate the amount it needed to sell, which is `50 * 2 / 100` which equals 1 SHITCOIN/USDT. If you return -200 (50% of 2 * 200), the bot will ignore it since `trade.stake_amount` is only 100 USDT but you asked to sell 200 USDT which means you are asking to sell 4 SHITCOIN/USDT.
Back to the example above, since current rate is 200, the current USDT value of your trade is now 400 USDT. Let's say you want to partially sell 100 USDT to take out the initial investment and leave the profit in the trade hoping that the price keeps rising. In that case, you have to do a different approach. First, you need to calculate the exact amount you needed to sell. In this case, since you want to sell 100 USDT worth based of current rate, the exact amount you need to partially sell is `100 * 2 / 400` which equals 0.5 SHITCOIN/USDT. Since we know now the exact amount we want to sell (0.5), the value you need to return in the `adjust_trade_position` function is `-amount to be exited partially * trade.stake_amount / trade.amount`, which equals -25. The bot will sell 0.5 SHITCOIN/USDT, keeping 1.5 in trade. You will receive 100 USDT from the partial exit.
!!! Warning "Stoploss calculation"
Stoploss is still calculated from the initial opening price, not averaged price.
@@ -793,10 +808,6 @@ Returning a value more than the above (so remaining stake_amount would become ne
While `/stopentry` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades.
!!! Warning "Backtesting"
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.
!!! Warning "Performance with many position adjustments"
Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively.
Each of the orders will be attached to the trade object for the duration of the trade - hence increasing memory usage.
+1 -1
View File
@@ -152,7 +152,7 @@ print(stats["strategy"][strategy]["pairlist"])
# Get market change (average change of all pairs from start to end of the backtest period)
print(stats["strategy"][strategy]["market_change"])
# Maximum drawdown ()
print(stats["strategy"][strategy]["max_drawdown"])
print(stats["strategy"][strategy]["max_drawdown_abs"])
# Maximum drawdown start and end
print(stats["strategy"][strategy]["drawdown_start"])
print(stats["strategy"][strategy]["drawdown_end"])
+4
View File
@@ -15,3 +15,7 @@
.md-version__list {
font-weight: 500 !important;
}
#available-endpoints ~ .md-typeset__scrollwrap .md-typeset__table th:first-of-type {
width: 35% !important;
}
+23 -4
View File
@@ -45,15 +45,22 @@ Get your "Id", you will use it for the config parameter `chat_id`.
#### Use Group id
You can use bots in telegram groups by just adding them to the group. You can find the group id by first adding a [RawDataBot](https://telegram.me/rawdatabot) to your group. The Group id is shown as id in the `"chat"` section, which the RawDataBot will send to you:
To get the group ID, you can add the bot to the group, start freqtrade, and issue a `/tg_info` command.
This will return the group id to you, without having to use some random bot.
While "chat_id" is still required, it doesn't need to be set to this particular group id for this command.
The response will also contain the "topic_id" if necessary - both in a format ready to copy/paste into your configuration.
``` json
"chat":{
"id":-1001332619709
{
"enabled": true,
"token": "********",
"chat_id": "-1001332619709",
"topic_id": "122"
}
```
For the Freqtrade configuration, you can then use the full value (including `-` if it's there) as string:
For the Freqtrade configuration, you can then use the full value (including `-` ) as string:
```json
"chat_id": "-1001332619709"
@@ -62,6 +69,18 @@ For the Freqtrade configuration, you can then use the full value (including `-`
!!! Warning "Using telegram groups"
When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasant surprises.
##### Group Topic ID
To use a specific topic in a group, you can use the `topic_id` parameter in the configuration. This will allow you to use the bot in a specific topic in a group.
Without this, the bot will always respond to the general channel in the group if topics are enabled for a group chat.
```json
"chat_id": "-1001332619709",
"topic_id": "3"
```
Similar to the group-id - you can use `/tg_info` from the topic/thread to get the correct topic-id.
## Control telegram noise
Freqtrade provides means to control the verbosity of your telegram bot.
+1 -1
View File
@@ -1,6 +1,6 @@
"""Freqtrade bot"""
__version__ = "2024.11"
__version__ = "2024.12"
if "dev" in __version__:
from pathlib import Path
+2 -2
View File
@@ -17,7 +17,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[
:return: Configuration
"""
from freqtrade.configuration import setup_utils_configuration
from freqtrade.util import fmt_coin
from freqtrade.util import fmt_coin, get_dry_run_wallet
config = setup_utils_configuration(args, method)
@@ -26,7 +26,7 @@ def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[
RunMode.HYPEROPT: "hyperoptimization",
}
if method in no_unlimited_runmodes.keys():
wallet_size = config["dry_run_wallet"] * config["tradable_balance_ratio"]
wallet_size = get_dry_run_wallet(config) * config["tradable_balance_ratio"]
# tradable_balance_ratio
if (
config["stake_amount"] != constants.UNLIMITED_STAKE_AMOUNT
+8 -2
View File
@@ -85,8 +85,10 @@ CONF_SCHEMA = {
},
"dry_run_wallet": {
"description": "Initial wallet balance for dry run mode.",
"type": "number",
"type": ["number", "object"],
"default": DRY_RUN_WALLET,
"patternProperties": {r"^[a-zA-Z0-9]+$": {"type": "number"}},
"additionalProperties": False,
},
"cancel_open_orders_on_exit": {
"description": "Cancel open orders when exiting.",
@@ -458,7 +460,11 @@ CONF_SCHEMA = {
},
"token": {"description": "Telegram bot token.", "type": "string"},
"chat_id": {
"description": "Telegram chat ID",
"description": "Telegram chat or group ID",
"type": "string",
},
"topic_id": {
"description": "Telegram topic ID - only applicable for group chats",
"type": "string",
},
"allow_custom_messages": {
@@ -2,6 +2,8 @@ import logging
import os
from typing import Any
import rapidjson
from freqtrade.constants import ENV_VAR_PREFIX
from freqtrade.misc import deep_merge_dicts
@@ -20,6 +22,14 @@ def _get_var_typed(val):
return True
elif val.lower() in ("f", "false"):
return False
# try to convert from json
try:
value = rapidjson.loads(val)
# Limited to lists for now
if isinstance(value, list):
return value
except rapidjson.JSONDecodeError:
pass
# keep as string
return val
+95 -102
View File
@@ -4,20 +4,31 @@ Functions to convert orderflow data from public_trades
import logging
import time
import typing
from collections import OrderedDict
from datetime import datetime
import numpy as np
import pandas as pd
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, Config
from freqtrade.enums import RunMode
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):
"""
@@ -25,53 +36,73 @@ def _init_dataframe_with_trades_columns(dataframe: pd.DataFrame):
:param dataframe: Dataframe to populate
"""
# Initialize columns with appropriate dtypes
dataframe["trades"] = np.nan
dataframe["orderflow"] = np.nan
dataframe["imbalances"] = np.nan
dataframe["stacked_imbalances_bid"] = np.nan
dataframe["stacked_imbalances_ask"] = np.nan
dataframe["max_delta"] = np.nan
dataframe["min_delta"] = np.nan
dataframe["bid"] = np.nan
dataframe["ask"] = np.nan
dataframe["delta"] = np.nan
dataframe["total_trades"] = np.nan
for column in ORDERFLOW_ADDED_COLUMNS:
dataframe[column] = np.nan
# Ensure the 'trades' column is of object type
dataframe["trades"] = dataframe["trades"].astype(object)
dataframe["orderflow"] = dataframe["orderflow"].astype(object)
dataframe["imbalances"] = dataframe["imbalances"].astype(object)
dataframe["stacked_imbalances_bid"] = dataframe["stacked_imbalances_bid"].astype(object)
dataframe["stacked_imbalances_ask"] = dataframe["stacked_imbalances_ask"].astype(object)
# Set columns to object type
for column in (
"trades",
"orderflow",
"imbalances",
"stacked_imbalances_bid",
"stacked_imbalances_ask",
):
dataframe[column] = dataframe[column].astype(object)
def timeframe_to_DateOffset(timeframe: str) -> pd.DateOffset:
"""
Translates the timeframe interval value written in the human readable
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
of seconds for one timeframe interval.
"""
from freqtrade.exchange import timeframe_to_seconds
timeframe_seconds = timeframe_to_seconds(timeframe)
timeframe_minutes = timeframe_seconds // 60
if timeframe_minutes < 1:
return pd.DateOffset(seconds=timeframe_seconds)
elif 59 < timeframe_minutes < 1440:
return pd.DateOffset(hours=timeframe_minutes // 60)
elif 1440 <= timeframe_minutes < 10080:
return pd.DateOffset(days=timeframe_minutes // 1440)
elif 10000 < timeframe_minutes < 43200:
return pd.DateOffset(weeks=1)
elif timeframe_minutes >= 43200 and timeframe_minutes < 525600:
return pd.DateOffset(months=1)
elif timeframe == "1y":
return pd.DateOffset(years=1)
else:
return pd.DateOffset(minutes=timeframe_minutes)
def _calculate_ohlcv_candle_start_and_end(df: pd.DataFrame, timeframe: str):
from freqtrade.exchange import timeframe_to_next_date, timeframe_to_resample_freq
from freqtrade.exchange import timeframe_to_resample_freq
timeframe_frequency = timeframe_to_resample_freq(timeframe)
# calculate ohlcv candle start and end
if df is not None and not df.empty:
timeframe_frequency = timeframe_to_resample_freq(timeframe)
dofs = timeframe_to_DateOffset(timeframe)
# calculate ohlcv candle start and end
df["datetime"] = pd.to_datetime(df["date"], unit="ms")
df["candle_start"] = df["datetime"].dt.floor(timeframe_frequency)
# used in _now_is_time_to_refresh_trades
df["candle_end"] = df["candle_start"].apply(
lambda candle_start: timeframe_to_next_date(timeframe, candle_start)
)
df["candle_end"] = df["candle_start"] + dofs
df.drop(columns=["datetime"], inplace=True)
def populate_dataframe_with_trades(
cached_grouped_trades: OrderedDict[tuple[datetime, datetime], pd.DataFrame],
cached_grouped_trades: pd.DataFrame | None,
config: Config,
dataframe: pd.DataFrame,
trades: pd.DataFrame,
) -> tuple[pd.DataFrame, OrderedDict[tuple[datetime, datetime], pd.DataFrame]]:
) -> tuple[pd.DataFrame, pd.DataFrame]:
"""
Populates a dataframe with trades
:param dataframe: Dataframe to populate
:param trades: Trades to populate with
:return: Dataframe with trades populated
"""
timeframe = config["timeframe"]
config_orderflow = config["orderflow"]
@@ -94,71 +125,52 @@ def populate_dataframe_with_trades(
# group trades by candle start
trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False)
# Create Series to hold complex data
trades_series = pd.Series(index=dataframe.index, dtype=object)
orderflow_series = pd.Series(index=dataframe.index, dtype=object)
imbalances_series = pd.Series(index=dataframe.index, dtype=object)
stacked_imbalances_bid_series = pd.Series(index=dataframe.index, dtype=object)
stacked_imbalances_ask_series = pd.Series(index=dataframe.index, dtype=object)
trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False)
candle_start: datetime
for candle_start, trades_grouped_df in trades_grouped_by_candle_start:
is_between = candle_start == dataframe["date"]
if is_between.any():
from freqtrade.exchange import timeframe_to_next_date
# there can only be one row with the same date
index = dataframe.index[is_between][0]
candle_next = timeframe_to_next_date(timeframe, typing.cast(datetime, candle_start))
if candle_next not in trades_grouped_by_candle_start.groups:
logger.warning(
f"candle at {candle_start} with {len(trades_grouped_df)} trades "
f"might be unfinished, because no finished trades at {candle_next}"
)
indices = dataframe.index[is_between].tolist()
# Add trades to each candle
trades_series.loc[indices] = [
trades_grouped_df.drop(columns=["candle_start", "candle_end"]).to_dict(
orient="records"
)
]
# Use caching mechanism
if (candle_start, candle_next) in cached_grouped_trades:
cache_entry = cached_grouped_trades[
(typing.cast(datetime, candle_start), candle_next)
]
# dataframe.loc[is_between] = cache_entry # doesn't take, so we need workaround:
# Create a dictionary of the column values to be assigned
update_dict = {c: cache_entry[c].iat[0] for c in cache_entry.columns}
# Assign the values using the update_dict
dataframe.loc[is_between, update_dict.keys()] = pd.DataFrame(
[update_dict], index=dataframe.loc[is_between].index
)
if (
cached_grouped_trades is not None
and (candle_start == cached_grouped_trades["date"]).any()
):
# Check if the trades are already in the cache
cache_idx = cached_grouped_trades.index[
cached_grouped_trades["date"] == candle_start
][0]
for col in ORDERFLOW_ADDED_COLUMNS:
dataframe.at[index, col] = cached_grouped_trades.at[cache_idx, col]
continue
dataframe.at[index, "trades"] = trades_grouped_df.drop(
columns=["candle_start", "candle_end"]
).to_dict(orient="records")
# Calculate orderflow for each candle
orderflow = trades_to_volumeprofile_with_total_delta_bid_ask(
trades_grouped_df, scale=config_orderflow["scale"]
)
orderflow_series.loc[indices] = [orderflow.to_dict(orient="index")]
dataframe.at[index, "orderflow"] = orderflow.to_dict(orient="index")
# orderflow_series.loc[[index]] = [orderflow.to_dict(orient="index")]
# Calculate imbalances for each candle's orderflow
imbalances = trades_orderflow_to_imbalances(
orderflow,
imbalance_ratio=config_orderflow["imbalance_ratio"],
imbalance_volume=config_orderflow["imbalance_volume"],
)
imbalances_series.loc[indices] = [imbalances.to_dict(orient="index")]
dataframe.at[index, "imbalances"] = imbalances.to_dict(orient="index")
stacked_imbalance_range = config_orderflow["stacked_imbalance_range"]
stacked_imbalances_bid_series.loc[indices] = [
stacked_imbalance_bid(
imbalances, stacked_imbalance_range=stacked_imbalance_range
)
]
stacked_imbalances_ask_series.loc[indices] = [
stacked_imbalance_ask(
imbalances, stacked_imbalance_range=stacked_imbalance_range
)
]
dataframe.at[index, "stacked_imbalances_bid"] = stacked_imbalance_bid(
imbalances, stacked_imbalance_range=stacked_imbalance_range
)
dataframe.at[index, "stacked_imbalances_ask"] = stacked_imbalance_ask(
imbalances, stacked_imbalance_range=stacked_imbalance_range
)
bid = np.where(
trades_grouped_df["side"].str.contains("sell"), trades_grouped_df["amount"], 0
@@ -168,39 +180,20 @@ def populate_dataframe_with_trades(
trades_grouped_df["side"].str.contains("buy"), trades_grouped_df["amount"], 0
)
deltas_per_trade = ask - bid
min_delta = deltas_per_trade.cumsum().min()
max_delta = deltas_per_trade.cumsum().max()
dataframe.loc[indices, "max_delta"] = max_delta
dataframe.loc[indices, "min_delta"] = min_delta
dataframe.at[index, "max_delta"] = deltas_per_trade.cumsum().max()
dataframe.at[index, "min_delta"] = deltas_per_trade.cumsum().min()
dataframe.loc[indices, "bid"] = bid.sum()
dataframe.loc[indices, "ask"] = ask.sum()
dataframe.loc[indices, "delta"] = (
dataframe.loc[indices, "ask"] - dataframe.loc[indices, "bid"]
dataframe.at[index, "bid"] = bid.sum()
dataframe.at[index, "ask"] = ask.sum()
dataframe.at[index, "delta"] = (
dataframe.at[index, "ask"] - dataframe.at[index, "bid"]
)
dataframe.loc[indices, "total_trades"] = len(trades_grouped_df)
dataframe.at[index, "total_trades"] = len(trades_grouped_df)
# Cache the result
cached_grouped_trades[(typing.cast(datetime, candle_start), candle_next)] = (
dataframe.loc[is_between].copy()
)
# Maintain cache size
if (
config.get("runmode") in (RunMode.DRY_RUN, RunMode.LIVE)
and len(cached_grouped_trades) > config_orderflow["cache_size"]
):
cached_grouped_trades.popitem(last=False)
else:
logger.debug(f"Found NO candles for trades starting with {candle_start}")
logger.debug(f"trades.groups_keys in {time.time() - start_time} seconds")
# Merge the complex data Series back into the DataFrame
dataframe["trades"] = trades_series
dataframe["orderflow"] = orderflow_series
dataframe["imbalances"] = imbalances_series
dataframe["stacked_imbalances_bid"] = stacked_imbalances_bid_series
dataframe["stacked_imbalances_ask"] = stacked_imbalances_ask_series
# Cache the entire dataframe
cached_grouped_trades = dataframe.tail(config_orderflow["cache_size"]).copy()
except Exception as e:
logger.exception("Error populating dataframe with trades")
@@ -551,6 +551,13 @@ def get_datahandlerclass(datatype: str) -> type[IDataHandler]:
elif datatype == "hdf5":
from .hdf5datahandler import HDF5DataHandler
logger.warning(
"DEPRECATED: The hdf5 dataformat is deprecated and will be removed in the "
"next release. "
"Please use the convert-data command to convert your data to a supported format."
"We recommend using the feather format, as it is faster and is more space-efficient."
)
return HDF5DataHandler
elif datatype == "feather":
from .featherdatahandler import FeatherDataHandler
+2 -1
View File
@@ -285,6 +285,7 @@ def _download_pair_history(
candle_type=candle_type,
until_ms=until_ms if until_ms else None,
)
logger.info(f"Downloaded data for {pair} with length {len(new_dataframe)}.")
if data.empty:
data = new_dataframe
else:
@@ -603,7 +604,7 @@ def download_data(
Download data function. Used from both cli and API.
"""
timerange = TimeRange()
if "days" in config:
if "days" in config and config["days"] is not None:
time_since = (datetime.now() - timedelta(days=config["days"])).strftime("%Y%m%d")
timerange = TimeRange.parse_timerange(f"{time_since}-")
+103 -16
View File
@@ -5,13 +5,18 @@ from datetime import datetime, timezone
from pathlib import Path
import ccxt
from pandas import DataFrame
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.binance_public_data import concat_safe, download_archive_ohlcv
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_types import FtHas, OHLCVResponse, Tickers
from freqtrade.exchange.exchange_types import FtHas, Tickers
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts
logger = logging.getLogger(__name__)
@@ -52,8 +57,14 @@ class Binance(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
tickers = super().get_tickers(symbols=symbols, cached=cached)
def get_tickers(
self,
symbols: list[str] | None = None,
*,
cached: bool = False,
market_type: TradingMode | None = None,
) -> Tickers:
tickers = super().get_tickers(symbols=symbols, cached=cached, market_type=market_type)
if self.trading_mode == TradingMode.FUTURES:
# Binance's future result has no bid/ask values.
# Therefore we must fetch that from fetch_bids_asks and combine the two results.
@@ -80,7 +91,10 @@ class Binance(Exchange):
"\nHedge Mode is not supported by freqtrade. "
"Please change 'Position Mode' on your binance futures account."
)
if assets_margin.get("multiAssetsMargin") is True:
if (
assets_margin.get("multiAssetsMargin") is True
and self.margin_mode != MarginMode.CROSS
):
msg += (
"\nMulti-Asset Mode is not supported by freqtrade. "
"Please change 'Asset Mode' on your binance futures account."
@@ -97,23 +111,24 @@ class Binance(Exchange):
except ccxt.BaseError as e:
raise OperationalException(e) from e
async def _async_get_historic_ohlcv(
def get_historic_ohlcv(
self,
pair: str,
timeframe: str,
since_ms: int,
candle_type: CandleType,
is_new_pair: bool = False,
raise_: bool = False,
until_ms: int | None = None,
) -> OHLCVResponse:
) -> DataFrame:
"""
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
Does not work for other exchanges, which don't return the earliest data when called with "0"
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
if is_new_pair:
x = await self._async_get_candle_history(pair, timeframe, candle_type, 0)
x = self.loop.run_until_complete(
self._async_get_candle_history(pair, timeframe, candle_type, 0)
)
if x and x[3] and x[3][0] and x[3][0][0] > since_ms:
# Set starting date to first available candle.
since_ms = x[3][0][0]
@@ -121,17 +136,89 @@ class Binance(Exchange):
f"Candle-data for {pair} available starting with "
f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}."
)
if until_ms and since_ms >= until_ms:
logger.warning(
f"No available candle-data for {pair} before "
f"{dt_from_ts(until_ms).isoformat()}"
)
return DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
return await super()._async_get_historic_ohlcv(
pair=pair,
timeframe=timeframe,
since_ms=since_ms,
is_new_pair=is_new_pair,
raise_=raise_,
candle_type=candle_type,
until_ms=until_ms,
if (
self._config["exchange"].get("only_from_ccxt", False)
or
# only download timeframes with significant improvements,
# otherwise fall back to rest API
not (
(candle_type == CandleType.SPOT and timeframe in ["1s", "1m", "3m", "5m"])
or (
candle_type == CandleType.FUTURES
and timeframe in ["1m", "3m", "5m", "15m", "30m"]
)
)
):
return super().get_historic_ohlcv(
pair=pair,
timeframe=timeframe,
since_ms=since_ms,
candle_type=candle_type,
is_new_pair=is_new_pair,
until_ms=until_ms,
)
else:
# Download from data.binance.vision
return self.get_historic_ohlcv_fast(
pair=pair,
timeframe=timeframe,
since_ms=since_ms,
candle_type=candle_type,
is_new_pair=is_new_pair,
until_ms=until_ms,
)
def get_historic_ohlcv_fast(
self,
pair: str,
timeframe: str,
since_ms: int,
candle_type: CandleType,
is_new_pair: bool = False,
until_ms: int | None = None,
) -> DataFrame:
"""
Fastly fetch OHLCV data by leveraging https://data.binance.vision.
"""
df = self.loop.run_until_complete(
download_archive_ohlcv(
candle_type=candle_type,
pair=pair,
timeframe=timeframe,
since_ms=since_ms,
until_ms=until_ms,
markets=self.markets,
)
)
# download the remaining data from rest API
if df.empty:
rest_since_ms = since_ms
else:
rest_since_ms = dt_ts(df.iloc[-1].date) + timeframe_to_msecs(timeframe)
# make sure since <= until
if until_ms and rest_since_ms > until_ms:
rest_df = DataFrame()
else:
rest_df = super().get_historic_ohlcv(
pair=pair,
timeframe=timeframe,
since_ms=rest_since_ms,
candle_type=candle_type,
is_new_pair=is_new_pair,
until_ms=until_ms,
)
all_df = concat_safe([df, rest_df])
return all_df
def funding_fee_cutoff(self, open_date: datetime):
"""
Funding fees are only charged at full hours (usually every 4-8h).
File diff suppressed because it is too large Load Diff
+281
View File
@@ -0,0 +1,281 @@
"""
Fetch daily-archived OHLCV data from https://data.binance.vision/
"""
import asyncio
import logging
import zipfile
from datetime import date, timedelta
from io import BytesIO
from typing import Any
import aiohttp
import pandas as pd
from pandas import DataFrame
from freqtrade.enums import CandleType
from freqtrade.misc import chunks
from freqtrade.util.datetime_helpers import dt_from_ts, dt_now
logger = logging.getLogger(__name__)
class Http404(Exception):
def __init__(self, msg, date, url):
super().__init__(msg)
self.date = date
self.url = url
class BadHttpStatus(Exception):
"""Not 200/404"""
pass
async def download_archive_ohlcv(
candle_type: CandleType,
pair: str,
timeframe: str,
*,
since_ms: int,
until_ms: int | None,
markets: dict[str, Any],
stop_on_404: bool = True,
) -> DataFrame:
"""
Fetch OHLCV data from https://data.binance.vision
The function makes its best effort to download data within the time range
[`since_ms`, `until_ms`] -- including `since_ms`, but excluding `until_ms`.
If `stop_one_404` is True, this returned DataFrame is guaranteed to start from `since_ms`
with no gaps in the data.
:candle_type: Currently only spot and futures are supported
:pair: symbol name in CCXT convention
:since_ms: the start timestamp of data, including itself
:until_ms: the end timestamp of data, excluding itself
:param until_ms: `None` indicates the timestamp of the latest available data
:markets: the CCXT markets dict, when it's None, the function will load the markets data
from a new `ccxt.binance` instance
:param stop_on_404: Stop to download the following data when a 404 returned
:return: the date range is between [since_ms, until_ms), return an empty DataFrame if no data
available in the time range
"""
try:
if candle_type == CandleType.SPOT:
asset_type_url_segment = "spot"
elif candle_type == CandleType.FUTURES:
asset_type_url_segment = "futures/um"
else:
raise ValueError(f"Unsupported CandleType: {candle_type}")
symbol = markets[pair]["id"]
start = dt_from_ts(since_ms)
end = dt_from_ts(until_ms) if until_ms else dt_now()
# We use two days ago as the last available day because the daily archives are daily
# uploaded and have several hours delay
last_available_date = dt_now() - timedelta(days=2)
end = min(end, last_available_date)
if start >= end:
return DataFrame()
df = await _download_archive_ohlcv(
asset_type_url_segment, symbol, pair, timeframe, start, end, stop_on_404
)
logger.debug(
f"Downloaded data for {pair} from https://data.binance.vision with length {len(df)}."
)
except Exception as e:
logger.warning(
"An exception occurred during fast download from Binance, falling back to "
"the slower REST API, this can take more time.",
exc_info=e,
)
df = DataFrame()
if not df.empty:
# only return the data within the requested time range
return df.loc[(df["date"] >= start) & (df["date"] < end)]
else:
return df
def concat_safe(dfs) -> DataFrame:
if all(df is None for df in dfs):
return DataFrame()
else:
return pd.concat(dfs)
async def _download_archive_ohlcv(
asset_type_url_segment: str,
symbol: str,
pair: str,
timeframe: str,
start: date,
end: date,
stop_on_404: bool,
) -> DataFrame:
# daily dataframes, `None` indicates missing data in that day (when `stop_on_404` is False)
dfs: list[DataFrame | None] = []
# the current day being processing, starting at 1.
current_day = 0
connector = aiohttp.TCPConnector(limit=100)
async with aiohttp.ClientSession(connector=connector, trust_env=True) as session:
# the HTTP connections has been throttled by TCPConnector
for dates in chunks(list(date_range(start, end)), 1000):
tasks = [
asyncio.create_task(
get_daily_ohlcv(asset_type_url_segment, symbol, timeframe, date, session)
)
for date in dates
]
for task in tasks:
current_day += 1
try:
df = await task
except Http404 as e:
if stop_on_404:
logger.debug(f"Failed to download {e.url} due to 404.")
# A 404 error on the first day indicates missing data
# on https://data.binance.vision, we provide the warning and the advice.
# https://github.com/freqtrade/freqtrade/blob/acc53065e5fa7ab5197073276306dc9dc3adbfa3/tests/exchange_online/test_binance_compare_ohlcv.py#L7
if current_day == 1:
logger.warning(
f"Fast download is unavailable due to missing data: "
f"{e.url}. Falling back to the slower REST API, "
"which may take more time."
)
if pair in ["BTC/USDT:USDT", "ETH/USDT:USDT", "BCH/USDT:USDT"]:
logger.warning(
f"To avoid the delay, you can first download {pair} using "
"`--timerange <start date>-20200101`, and then download the "
"remaining data with `--timerange 20200101-<end date>`."
)
else:
logger.warning(
f"Binance fast download for {pair} stopped at {e.date} due to "
f"missing data: {e.url}, falling back to rest API for the "
"remaining data, this can take more time."
)
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
return concat_safe(dfs)
else:
dfs.append(None)
except BaseException as e:
logger.warning(f"An exception raised: : {e}")
# Directly return the existing data, do not allow the gap within the data
await cancel_and_await_tasks(tasks[tasks.index(task) + 1 :])
return concat_safe(dfs)
else:
dfs.append(df)
return concat_safe(dfs)
async def cancel_and_await_tasks(unawaited_tasks):
"""Cancel and await the tasks"""
logger.debug("Try to cancel uncompleted download tasks.")
for task in unawaited_tasks:
task.cancel()
await asyncio.gather(*unawaited_tasks, return_exceptions=True)
logger.debug("All download tasks were awaited.")
def date_range(start: date, end: date):
date = start
while date <= end:
yield date
date += timedelta(days=1)
def binance_vision_zip_name(symbol: str, timeframe: str, date: date) -> str:
return f"{symbol}-{timeframe}-{date.strftime('%Y-%m-%d')}.zip"
def binance_vision_zip_url(
asset_type_url_segment: str, symbol: str, timeframe: str, date: date
) -> str:
"""
example urls:
https://data.binance.vision/data/spot/daily/klines/BTCUSDT/1s/BTCUSDT-1s-2023-10-27.zip
https://data.binance.vision/data/futures/um/daily/klines/BTCUSDT/1h/BTCUSDT-1h-2023-10-27.zip
"""
url = (
f"https://data.binance.vision/data/{asset_type_url_segment}/daily/klines/{symbol}"
f"/{timeframe}/{binance_vision_zip_name(symbol, timeframe, date)}"
)
return url
async def get_daily_ohlcv(
asset_type_url_segment: str,
symbol: str,
timeframe: str,
date: date,
session: aiohttp.ClientSession,
retry_count: int = 3,
retry_delay: float = 0.0,
) -> DataFrame:
"""
Get daily OHLCV from https://data.binance.vision
See https://github.com/binance/binance-public-data
:asset_type_url_segment: `spot` or `futures/um`
:symbol: binance symbol name, e.g. BTCUSDT
:timeframe: e.g. 1m, 1h
:date: the returned DataFrame will cover the entire day of `date` in UTC
:session: an aiohttp.ClientSession instance
:retry_count: times to retry before returning the exceptions
:retry_delay: the time to wait before every retry
:return: A dataframe containing columns date,open,high,low,close,volume
"""
url = binance_vision_zip_url(asset_type_url_segment, symbol, timeframe, date)
logger.debug(f"download data from binance: {url}")
retry = 0
while True:
if retry > 0:
sleep_secs = retry * retry_delay
logger.debug(
f"[{retry}/{retry_count}] retry to download {url} after {sleep_secs} seconds"
)
await asyncio.sleep(sleep_secs)
try:
async with session.get(url) as resp:
if resp.status == 200:
content = await resp.read()
logger.debug(f"Successfully downloaded {url}")
with zipfile.ZipFile(BytesIO(content)) as zipf:
with zipf.open(zipf.namelist()[0]) as csvf:
# https://github.com/binance/binance-public-data/issues/283
first_byte = csvf.read(1)[0]
if chr(first_byte).isdigit():
header = None
else:
header = 0
csvf.seek(0)
df = pd.read_csv(
csvf,
usecols=[0, 1, 2, 3, 4, 5],
names=["date", "open", "high", "low", "close", "volume"],
header=header,
)
df["date"] = pd.to_datetime(df["date"], unit="ms", utc=True)
return df
elif resp.status == 404:
logger.debug(f"Failed to download {url}")
raise Http404(f"404: {url}", date, url)
else:
raise BadHttpStatus(f"{resp.status} - {resp.reason}")
except Exception as e:
retry += 1
if isinstance(e, Http404) or retry > retry_count:
logger.debug(f"Failed to get data from {url}: {e}")
raise
+1
View File
@@ -47,6 +47,7 @@ MAP_EXCHANGE_CHILDCLASS = {
"binanceje": "binance",
"binanceusdm": "binance",
"okex": "okx",
"myokx": "okx",
"gateio": "gate",
"huboi": "htx",
}
+75 -21
View File
@@ -7,7 +7,7 @@ import asyncio
import inspect
import logging
import signal
from collections.abc import Coroutine
from collections.abc import Coroutine, Generator
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from math import floor, isnan
@@ -201,7 +201,7 @@ class Exchange:
self._cache_lock = Lock()
# Cache for 10 minutes ...
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=4, ttl=60 * 10)
# Cache values for 300 to avoid frequent polling of the exchange for prices
# Caching only applies to RPC methods, so prices for open trades are still
# refreshed once every iteration.
@@ -705,14 +705,22 @@ class Exchange:
f"Available currencies are: {', '.join(quote_currencies)}"
)
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> Generator[str, None, None]:
"""
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
"""
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
yielded = False
for pair in (
f"{curr_1}/{curr_2}",
f"{curr_2}/{curr_1}",
f"{curr_1}/{curr_2}:{curr_2}",
f"{curr_2}/{curr_1}:{curr_1}",
):
if pair in self.markets and self.markets[pair].get("active"):
return pair
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
yielded = True
yield pair
if not yielded:
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
def validate_timeframes(self, timeframe: str | None) -> None:
"""
@@ -1801,24 +1809,37 @@ class Exchange:
raise OperationalException(e) from e
@retrier
def get_tickers(self, symbols: list[str] | None = None, *, cached: bool = False) -> Tickers:
def get_tickers(
self,
symbols: list[str] | None = None,
*,
cached: bool = False,
market_type: TradingMode | None = None,
) -> Tickers:
"""
:param symbols: List of symbols to fetch
:param cached: Allow cached result
:param market_type: Market type to fetch - either spot or futures.
:return: fetch_tickers result
"""
tickers: Tickers
if not self.exchange_has("fetchTickers"):
return {}
cache_key = f"fetch_tickers_{market_type}" if market_type else "fetch_tickers"
if cached:
with self._cache_lock:
tickers = self._fetch_tickers_cache.get("fetch_tickers") # type: ignore
tickers = self._fetch_tickers_cache.get(cache_key) # type: ignore
if tickers:
return tickers
try:
tickers = self._api.fetch_tickers(symbols)
# Re-map futures to swap
market_types = {
TradingMode.FUTURES: "swap",
}
params = {"type": market_types.get(market_type, market_type)} if market_type else {}
tickers = self._api.fetch_tickers(symbols, params)
with self._cache_lock:
self._fetch_tickers_cache["fetch_tickers"] = tickers
self._fetch_tickers_cache[cache_key] = tickers
return tickers
except ccxt.NotSupported as e:
raise OperationalException(
@@ -1842,7 +1863,39 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
# Pricing info
def get_conversion_rate(self, coin: str, currency: str) -> float | None:
"""
Quick and cached way to get conversion rate one currency to the other.
Can then be used as "rate * amount" to convert between currencies.
:param coin: Coin to convert
:param currency: Currency to convert to
:returns: Conversion rate from coin to currency
:raises: ExchangeErrors
"""
if coin == currency:
return 1.0
tickers = self.get_tickers(cached=True)
try:
for pair in self.get_valid_pair_combination(coin, currency):
ticker: Ticker | None = tickers.get(pair, None)
if not ticker:
tickers_other: Tickers = self.get_tickers(
cached=True,
market_type=(
TradingMode.SPOT
if self.trading_mode != TradingMode.SPOT
else TradingMode.FUTURES
),
)
ticker = tickers_other.get(pair, None)
if ticker:
rate: float | None = ticker.get("last", None)
if rate and pair.startswith(currency) and not pair.endswith(currency):
rate = 1.0 / rate
return rate
except ValueError:
return None
return None
@retrier
def fetch_ticker(self, pair: str) -> Ticker:
@@ -2198,10 +2251,13 @@ class Exchange:
# If cost is None or 0.0 -> falsy, return None
return None
try:
comb = self.get_valid_pair_combination(fee_curr, self._config["stake_currency"])
tick = self.fetch_ticker(comb)
fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask")
for comb in self.get_valid_pair_combination(
fee_curr, self._config["stake_currency"]
):
tick = self.fetch_ticker(comb)
fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask")
if tick:
break
except (ValueError, ExchangeError):
fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None)
if not fee_to_quote_rate:
@@ -2244,8 +2300,9 @@ class Exchange:
:param pair: Pair to download
:param timeframe: Timeframe to get data for
:param since_ms: Timestamp in milliseconds to get history from
:param until_ms: Timestamp in milliseconds to get history up to
:param candle_type: '', mark, index, premiumIndex, or funding_rate
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
:param until_ms: Timestamp in milliseconds to get history up to
:return: Dataframe with candle (OHLCV) data
"""
pair, _, _, data, _ = self.loop.run_until_complete(
@@ -2254,11 +2311,10 @@ class Exchange:
timeframe=timeframe,
since_ms=since_ms,
until_ms=until_ms,
is_new_pair=is_new_pair,
candle_type=candle_type,
)
)
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
logger.debug(f"Downloaded data for {pair} from ccxt with length {len(data)}.")
return ohlcv_to_dataframe(data, timeframe, pair, fill_missing=False, drop_incomplete=True)
async def _async_get_historic_ohlcv(
@@ -2267,13 +2323,11 @@ class Exchange:
timeframe: str,
since_ms: int,
candle_type: CandleType,
is_new_pair: bool = False,
raise_: bool = False,
until_ms: int | None = None,
) -> OHLCVResponse:
"""
Download historic ohlcv
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
@@ -3612,7 +3666,7 @@ class Exchange:
Wherein, "+" or "-" depends on whether the contract goes long or short:
"-" for long, and "+" for short.
okex: https://www.okx.com/support/hc/en-us/articles/
okx: https://www.okx.com/support/hc/en-us/articles/
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
:param pair: Pair to calculate liquidation price for
+10 -4
View File
@@ -23,6 +23,7 @@ from freqtrade.exchange.common import (
BAD_EXCHANGES,
EXCHANGE_HAS_OPTIONAL,
EXCHANGE_HAS_REQUIRED,
MAP_EXCHANGE_CHILDCLASS,
SUPPORTED_EXCHANGES,
)
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date
@@ -91,21 +92,24 @@ def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]:
def _build_exchange_list_entry(
exchange_name: str, exchangeClasses: dict[str, Any]
) -> ValidExchangesType:
exchange_name = exchange_name.lower()
valid, comment, ex_mod = validate_exchange(exchange_name)
mapped_exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name).lower()
is_alias = getattr(ex_mod, "alias", False)
result: ValidExchangesType = {
"name": getattr(ex_mod, "name", exchange_name),
"classname": exchange_name,
"valid": valid,
"supported": exchange_name.lower() in SUPPORTED_EXCHANGES,
"supported": mapped_exchange_name in SUPPORTED_EXCHANGES and not is_alias,
"comment": comment,
"dex": getattr(ex_mod, "dex", False),
"is_alias": getattr(ex_mod, "alias", False),
"is_alias": is_alias,
"alias_for": inspect.getmro(ex_mod.__class__)[1]().id
if getattr(ex_mod, "alias", False)
else None,
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
}
if resolved := exchangeClasses.get(exchange_name.lower()):
if resolved := exchangeClasses.get(mapped_exchange_name):
supported_modes = [{"trading_mode": "spot", "margin_mode": ""}] + [
{"trading_mode": tm.value, "margin_mode": mm.value}
for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs
@@ -308,7 +312,9 @@ def price_to_precision(
decimal_to_precision(
price,
rounding_mode=rounding_mode,
precision=price_precision,
precision=int(price_precision)
if precisionMode != TICK_SIZE
else price_precision,
counting_mode=precisionMode,
)
)
+31 -4
View File
@@ -1,6 +1,7 @@
"""Hyperliquid exchange subclass"""
import logging
from copy import deepcopy
from datetime import datetime
from freqtrade.constants import BuySell
@@ -157,9 +158,15 @@ class Hyperliquid(Exchange):
logger.warning(f"Could not update funding fees for {pair}.")
return 0.0
def fetch_order(self, order_id: str, pair: str, params: dict | None = None) -> CcxtOrder:
order = super().fetch_order(order_id, pair, params)
def _adjust_hyperliquid_order(
self,
order: dict,
) -> dict:
"""
Adjusts order response for Hyperliquid
:param order: Order response from Hyperliquid
:return: Adjusted order response
"""
if (
order["average"] is None
and order["status"] in ("canceled", "closed")
@@ -168,7 +175,9 @@ class Hyperliquid(Exchange):
# Hyperliquid does not fill the average price in the order response
# Fetch trades to calculate the average price to have the actual price
# the order was executed at
trades = self.get_trades_for_order(order_id, pair, since=dt_from_ts(order["timestamp"]))
trades = self.get_trades_for_order(
order["id"], order["symbol"], since=dt_from_ts(order["timestamp"])
)
if trades:
total_amount = sum(t["amount"] for t in trades)
@@ -177,5 +186,23 @@ class Hyperliquid(Exchange):
if total_amount
else None
)
return order
def fetch_order(self, order_id: str, pair: str, params: dict | None = None) -> CcxtOrder:
order = super().fetch_order(order_id, pair, params)
order = self._adjust_hyperliquid_order(order)
self._log_exchange_response("fetch_order2", order)
return order
def fetch_orders(
self, pair: str, since: datetime, params: dict | None = None
) -> list[CcxtOrder]:
orders = super().fetch_orders(pair, since, params)
for idx, order in enumerate(deepcopy(orders)):
order2 = self._adjust_hyperliquid_order(order)
orders[idx] = order2
self._log_exchange_response("fetch_orders2", orders)
return orders
+8 -2
View File
@@ -50,11 +50,17 @@ 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) -> Tickers:
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)
return super().get_tickers(symbols=symbols, cached=cached, market_type=market_type)
def consolidate_balances(self, balances: CcxtBalances) -> CcxtBalances:
"""
+1 -1
View File
@@ -849,7 +849,7 @@ class FreqaiDataKitchen:
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
dataframe = self.remove_special_chars_from_feature_names(dataframe)
self.get_unique_classes_from_labels(dataframe)
self.get_unique_classes_from_labels(dataframe)
if self.config.get("reduce_df_footprint", False):
dataframe = reduce_dataframe_footprint(dataframe)
@@ -5,7 +5,6 @@ from xgboost import XGBRFRegressor
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.freqai.tensorboard import TBCallback
logger = logging.getLogger(__name__)
@@ -45,7 +44,12 @@ class XGBoostRFRegressor(BaseRegressionModel):
model = XGBRFRegressor(**self.model_training_parameters)
model.set_params(callbacks=[TBCallback(dk.data_path)])
# Callbacks are not supported for XGBRFRegressor, and version 2.1.x started to throw
# the following error:
# NotImplementedError: `early_stopping_rounds` and `callbacks` are not implemented
# for random forest.
# model.set_params(callbacks=[TBCallback(dk.data_path)])
model.fit(
X=X,
y=y,
@@ -55,6 +59,6 @@ class XGBoostRFRegressor(BaseRegressionModel):
xgb_model=xgb_model,
)
# set the callbacks to empty so that we can serialize to disk later
model.set_params(callbacks=[])
# model.set_params(callbacks=[])
return model
+2 -2
View File
@@ -1197,7 +1197,7 @@ class FreqtradeBot(LoggingMixin):
trade.pair, side="entry", is_short=trade.is_short, refresh=False
)
stake_amount = trade.stake_amount
if not fill:
if not fill and trade.nr_of_successful_entries > 0:
# If we have open orders, we need to add the stake amount of the open orders
# as it's not yet included in the trade.stake_amount
stake_amount += sum(
@@ -1303,7 +1303,7 @@ class FreqtradeBot(LoggingMixin):
logger.warning(
f"Unable to handle stoploss on exchange for {trade.pair}: {exception}"
)
# Check if we can sell our current pair
# Check if we can exit our current pair
if not trade.has_open_orders and trade.is_open and self.handle_trade(trade):
trades_closed += 1
+6 -3
View File
@@ -27,10 +27,13 @@ def update_liquidation_prices(
total_wallet_stake = 0.0
if dry_run:
# Parameters only needed for cross margin
total_wallet_stake = wallets.get_total(stake_currency)
total_wallet_stake = wallets.get_collateral()
logger.info("Updating liquidation price for all open trades.")
open_trades = Trade.get_open_trades()
logger.info(
"Updating liquidation price for all open trades. "
f"Collateral {total_wallet_stake} {stake_currency}."
)
open_trades: list[Trade] = Trade.get_open_trades()
for t in open_trades:
# TODO: This should be done in a batch update
t.set_liquidation_price(
@@ -10,7 +10,7 @@ from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis
from freqtrade.resolvers import StrategyResolver
from freqtrade.util import print_rich_table
from freqtrade.util import get_dry_run_wallet, print_rich_table
logger = logging.getLogger(__name__)
@@ -163,7 +163,7 @@ class LookaheadAnalysisSubFunctions:
config["max_open_trades"] = len(config["pairs"])
min_dry_run_wallet = 1000000000
if config["dry_run_wallet"] < min_dry_run_wallet:
if get_dry_run_wallet(config) < min_dry_run_wallet:
logger.info(
"Dry run wallet was not set to 1 billion, pushing it up there "
"just to avoid false positives"
+16 -28
View File
@@ -47,8 +47,7 @@ from freqtrade.optimize.optimize_reports import (
generate_rejected_signals,
generate_trade_signal_candles,
show_backtest_results,
store_backtest_analysis_results,
store_backtest_stats,
store_backtest_results,
)
from freqtrade.persistence import (
CustomDataWrapper,
@@ -121,10 +120,12 @@ class Backtesting:
self.run_ids: dict[str, str] = {}
self.strategylist: list[IStrategy] = []
self.all_results: dict[str, dict] = {}
self.processed_dfs: dict[str, dict] = {}
self.analysis_results: dict[str, dict[str, DataFrame]] = {
"signals": {},
"rejected": {},
"exited": {},
}
self.rejected_dict: dict[str, list] = {}
self.rejected_df: dict[str, dict] = {}
self.exited_dfs: dict[str, dict] = {}
self._exchange_name = self.config["exchange"]["name"]
if not exchange:
@@ -1590,15 +1591,13 @@ class Backtesting:
self.config.get("export", "none") == "signals"
and self.dataprovider.runmode == RunMode.BACKTEST
):
self.processed_dfs[strategy_name] = generate_trade_signal_candles(
preprocessed_tmp, results, "open_date"
)
self.rejected_df[strategy_name] = generate_rejected_signals(
preprocessed_tmp, self.rejected_dict
)
self.exited_dfs[strategy_name] = generate_trade_signal_candles(
preprocessed_tmp, results, "close_date"
)
signals = generate_trade_signal_candles(preprocessed_tmp, results, "open_date")
rejected = generate_rejected_signals(preprocessed_tmp, self.rejected_dict)
exited = generate_trade_signal_candles(preprocessed_tmp, results, "close_date")
self.analysis_results["signals"][strategy_name] = signals
self.analysis_results["rejected"][strategy_name] = rejected
self.analysis_results["exited"][strategy_name] = exited
return min_date, max_date
@@ -1662,23 +1661,12 @@ class Backtesting:
dt_appendix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
if self.config.get("export", "none") in ("trades", "signals"):
combined_res = combined_dataframes_with_rel_mean(data, min_date, max_date)
store_backtest_stats(
self.config["exportfilename"],
store_backtest_results(
self.config,
self.results,
dt_appendix,
market_change_data=combined_res,
)
if (
self.config.get("export", "none") == "signals"
and self.dataprovider.runmode == RunMode.BACKTEST
):
store_backtest_analysis_results(
self.config["exportfilename"],
self.processed_dfs,
self.rejected_df,
self.exited_dfs,
dt_appendix,
analysis_results=self.analysis_results,
)
# Results may be mixed up now. Sort them so they follow --strategy-list order.
@@ -28,6 +28,7 @@ from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLo
from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools
from freqtrade.optimize.optimize_reports import generate_strategy_stats
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
from freqtrade.util.dry_run_wallet import get_dry_run_wallet
# Suppress scikit-learn FutureWarnings from skopt
@@ -363,6 +364,7 @@ class HyperOptimizer:
config=self.config,
processed=processed,
backtest_stats=strat_stats,
starting_balance=get_dry_run_wallet(self.config),
)
return {
"loss": loss,
@@ -9,7 +9,6 @@ from datetime import datetime
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_calmar
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -24,10 +23,9 @@ class CalmarHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(
results: DataFrame,
trade_count: int,
min_date: datetime,
max_date: datetime,
config: Config,
starting_balance: float,
*args,
**kwargs,
) -> float:
@@ -36,7 +34,6 @@ class CalmarHyperOptLoss(IHyperOptLoss):
Uses Calmar Ratio calculation.
"""
starting_balance = config["dry_run_wallet"]
calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance)
# print(expected_returns_mean, max_drawdown, calmar_ratio)
return -calmar_ratio
@@ -31,6 +31,7 @@ class IHyperOptLoss(ABC):
config: Config,
processed: dict[str, DataFrame],
backtest_stats: dict[str, Any],
starting_balance: float,
**kwargs,
) -> float:
"""
@@ -7,7 +7,6 @@ Hyperoptimization.
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_underwater
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -21,7 +20,9 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss):
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float:
def hyperopt_loss_function(
results: DataFrame, starting_balance: float, *args, **kwargs
) -> float:
"""
Objective function.
@@ -31,7 +32,7 @@ class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss):
total_profit = results["profit_abs"].sum()
try:
drawdown_df = calculate_underwater(
results, value_col="profit_abs", starting_balance=config["dry_run_wallet"]
results, value_col="profit_abs", starting_balance=starting_balance
)
max_drawdown = abs(min(drawdown_df["drawdown"]))
relative_drawdown = max(drawdown_df["drawdown_relative"])
@@ -33,7 +33,6 @@ TARGET_TRADE_AMOUNT variable sets the minimum number of trades required to avoid
import numpy as np
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -57,7 +56,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss):
def hyperopt_loss_function(
results: DataFrame,
trade_count: int,
config: Config,
starting_balance: float,
**kwargs,
) -> float:
total_profit = results["profit_abs"].sum()
@@ -83,7 +82,7 @@ class MultiMetricHyperOptLoss(IHyperOptLoss):
# Calculate drawdown
try:
drawdown = calculate_max_drawdown(
results, starting_balance=config["dry_run_wallet"], value_col="profit_abs"
results, starting_balance=starting_balance, value_col="profit_abs"
)
relative_account_drawdown = drawdown.relative_account_drawdown
except ValueError:
@@ -10,7 +10,6 @@ individual needs.
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -21,12 +20,14 @@ DRAWDOWN_MULT = 0.075
class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(results: DataFrame, config: Config, *args, **kwargs) -> float:
def hyperopt_loss_function(
results: DataFrame, starting_balance: float, *args, **kwargs
) -> float:
total_profit = results["profit_abs"].sum()
try:
drawdown = calculate_max_drawdown(
results, starting_balance=config["dry_run_wallet"], value_col="profit_abs"
results, starting_balance=starting_balance, value_col="profit_abs"
)
relative_account_drawdown = drawdown.relative_account_drawdown
except ValueError:
@@ -9,7 +9,6 @@ from datetime import datetime
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_sharpe
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -24,10 +23,9 @@ class SharpeHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(
results: DataFrame,
trade_count: int,
min_date: datetime,
max_date: datetime,
config: Config,
starting_balance: float,
*args,
**kwargs,
) -> float:
@@ -36,7 +34,6 @@ class SharpeHyperOptLoss(IHyperOptLoss):
Uses Sharpe Ratio calculation.
"""
starting_balance = config["dry_run_wallet"]
sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance)
# print(expected_returns_mean, up_stdev, sharp_ratio)
return -sharp_ratio
@@ -9,7 +9,6 @@ from datetime import datetime
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_sortino
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -24,10 +23,9 @@ class SortinoHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(
results: DataFrame,
trade_count: int,
min_date: datetime,
max_date: datetime,
config: Config,
starting_balance: float,
*args,
**kwargs,
) -> float:
@@ -36,7 +34,6 @@ class SortinoHyperOptLoss(IHyperOptLoss):
Uses Sortino Ratio calculation.
"""
starting_balance = config["dry_run_wallet"]
sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance)
# print(expected_returns_mean, down_stdev, sortino_ratio)
return -sortino_ratio
+8 -2
View File
@@ -374,7 +374,6 @@ class HyperoptTools:
trials = json_normalize(results, max_level=1)
trials["Best"] = ""
trials["Stake currency"] = config["stake_currency"]
base_metrics = [
"Best",
@@ -383,11 +382,13 @@ class HyperoptTools:
"results_metrics.profit_mean",
"results_metrics.profit_median",
"results_metrics.profit_total",
"Stake currency",
"results_metrics.stake_currency",
"results_metrics.profit_total_abs",
"results_metrics.holding_avg",
"results_metrics.trade_count_long",
"results_metrics.trade_count_short",
"results_metrics.max_drawdown_abs",
"results_metrics.max_drawdown_account",
"loss",
"is_initial_point",
"is_best",
@@ -409,6 +410,8 @@ class HyperoptTools:
"Avg duration",
"Trade count long",
"Trade count short",
"Max drawdown",
"Max drawdown percent",
"Objective",
"is_initial_point",
"is_best",
@@ -432,6 +435,9 @@ class HyperoptTools:
trials["Avg profit"] = trials["Avg profit"].apply(
lambda x: f"{x * perc_multi:,.2f}%" if not isna(x) else ""
)
trials["Max drawdown percent"] = trials["Max drawdown percent"].apply(
lambda x: f"{x * perc_multi:,.2f}%" if not isna(x) else ""
)
trials["Objective"] = trials["Objective"].apply(
lambda x: f"{x:,.5f}" if x != 100000 else ""
)
@@ -11,10 +11,7 @@ from freqtrade.optimize.optimize_reports.bt_output import (
text_table_strategy,
text_table_tags,
)
from freqtrade.optimize.optimize_reports.bt_storage import (
store_backtest_analysis_results,
store_backtest_stats,
)
from freqtrade.optimize.optimize_reports.bt_storage import store_backtest_results
from freqtrade.optimize.optimize_reports.optimize_reports import (
generate_all_periodic_breakdown_stats,
generate_backtest_stats,
@@ -4,6 +4,7 @@ from pathlib import Path
from pandas import DataFrame
from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.enums.runmode import RunMode
from freqtrade.ft_types import BacktestResultType
from freqtrade.misc import file_dump_joblib, file_dump_json
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
@@ -29,21 +30,27 @@ def _generate_filename(recordfilename: Path, appendix: str, suffix: str) -> Path
return filename
def store_backtest_stats(
recordfilename: Path,
def store_backtest_results(
config: dict,
stats: BacktestResultType,
dtappendix: str,
*,
market_change_data: DataFrame | None = None,
analysis_results: dict[str, dict[str, DataFrame]] | None = None,
) -> Path:
"""
Stores backtest results
:param recordfilename: Path object, which can either be a filename or a directory.
Filenames will be appended with a timestamp right before the suffix
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
Stores backtest results and analysis data
:param config: Configuration dictionary
:param stats: Dataframe containing the backtesting statistics
:param dtappendix: Datetime to use for the filename
:param market_change_data: Dataframe containing market change data
:param analysis_results: Dictionary containing analysis results
"""
# Path object, which can either be a filename or a directory.
# Filenames will be appended with a timestamp right before the suffix
# while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
recordfilename: Path = config["exportfilename"]
filename = _generate_filename(recordfilename, dtappendix, ".json")
# Store metadata separately.
@@ -65,6 +72,21 @@ def store_backtest_stats(
filename_mc, compression_level=9, compression="lz4"
)
if (
config.get("export", "none") == "signals"
and analysis_results is not None
and config.get("runmode", RunMode.OTHER) == RunMode.BACKTEST
):
_store_backtest_analysis_data(
recordfilename, analysis_results["signals"], dtappendix, "signals"
)
_store_backtest_analysis_data(
recordfilename, analysis_results["rejected"], dtappendix, "rejected"
)
_store_backtest_analysis_data(
recordfilename, analysis_results["exited"], dtappendix, "exited"
)
return filename
@@ -86,15 +108,3 @@ def _store_backtest_analysis_data(
file_dump_joblib(filename, data)
return filename
def store_backtest_analysis_results(
recordfilename: Path,
candles: dict[str, dict],
trades: dict[str, dict],
exited: dict[str, dict],
dtappendix: str,
) -> None:
_store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals")
_store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected")
_store_backtest_analysis_data(recordfilename, exited, dtappendix, "exited")
@@ -18,7 +18,7 @@ from freqtrade.data.metrics import (
calculate_sortino,
)
from freqtrade.ft_types import BacktestResultType
from freqtrade.util import decimals_per_coin, fmt_coin
from freqtrade.util import decimals_per_coin, fmt_coin, get_dry_run_wallet
logger = logging.getLogger(__name__)
@@ -69,7 +69,7 @@ def generate_rejected_signals(
def _generate_result_line(
result: DataFrame, starting_balance: int, first_column: str | list[str]
result: DataFrame, starting_balance: float, first_column: str | list[str]
) -> dict:
"""
Generate one result dict, with "first_column" as key.
@@ -111,7 +111,7 @@ def _generate_result_line(
def generate_pair_metrics(
pairlist: list[str],
stake_currency: str,
starting_balance: int,
starting_balance: float,
results: DataFrame,
skip_nan: bool = False,
) -> list[dict]:
@@ -144,7 +144,7 @@ def generate_pair_metrics(
def generate_tag_metrics(
tag_type: Literal["enter_tag", "exit_reason"] | list[Literal["enter_tag", "exit_reason"]],
starting_balance: int,
starting_balance: float,
results: DataFrame,
skip_nan: bool = False,
) -> list[dict]:
@@ -373,7 +373,7 @@ def generate_strategy_stats(
return {}
config = content["config"]
max_open_trades = min(config["max_open_trades"], len(pairlist))
start_balance = config["dry_run_wallet"]
start_balance = get_dry_run_wallet(config)
stake_currency = config["stake_currency"]
pair_results = generate_pair_metrics(
+3 -3
View File
@@ -39,7 +39,7 @@ class PairLock(ModelBase):
@staticmethod
def query_pair_locks(
pair: str | None, now: datetime, side: str = "*"
pair: str | None, now: datetime, side: str | None = None
) -> ScalarResult["PairLock"]:
"""
Get all currently active locks for this pair
@@ -53,9 +53,9 @@ class PairLock(ModelBase):
]
if pair:
filters.append(PairLock.pair == pair)
if side != "*":
if side is not None and side != "*":
filters.append(or_(PairLock.side == side, PairLock.side == "*"))
else:
elif side is not None:
filters.append(PairLock.side == "*")
return PairLock.session.scalars(select(PairLock).filter(*filters))
+3 -2
View File
@@ -67,13 +67,14 @@ class PairLocks:
@staticmethod
def get_pair_locks(
pair: str | None, now: datetime | None = None, side: str = "*"
pair: str | None, now: datetime | None = None, side: str | None = None
) -> Sequence[PairLock]:
"""
Get all currently active locks for this pair
:param pair: Pair to check for. Returns all current locks if pair is empty
:param now: Datetime object (generated via datetime.now(timezone.utc)).
defaults to datetime.now(timezone.utc)
:param side: Side get locks for, can be 'long', 'short', '*' or None
"""
if not now:
now = datetime.now(timezone.utc)
@@ -88,7 +89,7 @@ class PairLocks:
lock.lock_end_time >= now
and lock.active is True
and (pair is None or lock.pair == pair)
and (lock.side == "*" or lock.side == side)
and (side is None or lock.side == "*" or lock.side == side)
)
]
return locks
+18 -4
View File
@@ -172,12 +172,20 @@ class Order(ModelBase):
@property
def stake_amount(self) -> float:
"""Amount in stake currency used for this order"""
return self.safe_amount * self.safe_price / self.trade.leverage
return float(
FtPrecise(self.safe_amount)
* FtPrecise(self.safe_price)
/ FtPrecise(self.trade.leverage)
)
@property
def stake_amount_filled(self) -> float:
"""Filled Amount in stake currency used for this order"""
return self.safe_filled * self.safe_price / self.trade.leverage
return float(
FtPrecise(self.safe_filled)
* FtPrecise(self.safe_price)
/ FtPrecise(self.trade.leverage)
)
def __repr__(self):
return (
@@ -769,7 +777,9 @@ class LocalTrade:
"""
if liquidation_price is None:
return
self.liquidation_price = liquidation_price
self.liquidation_price = price_to_precision(
liquidation_price, self.price_precision, self.precision_mode_price
)
def set_funding_fees(self, funding_fee: float) -> None:
"""
@@ -1239,7 +1249,11 @@ class LocalTrade:
if current_amount_tr > 0.0:
# Trade is still open
# Leverage not updated, as we don't allow changing leverage through DCA at the moment.
self.open_rate = float(current_stake / current_amount)
self.open_rate = price_to_precision(
float(current_stake / current_amount),
self.price_precision,
self.precision_mode_price,
)
self.amount = current_amount_tr
self.stake_amount = float(current_stake) / (self.leverage or 1.0)
self.fee_open_cost = self.fee_open * float(self.max_stake_amount)
+2 -1
View File
@@ -28,6 +28,7 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import get_dry_run_wallet
logger = logging.getLogger(__name__)
@@ -706,7 +707,7 @@ def plot_profit(config: Config) -> None:
trades,
config["timeframe"],
config.get("stake_currency", ""),
config.get("available_capital", config["dry_run_wallet"]),
config.get("available_capital", get_dry_run_wallet(config)),
)
store_plot_file(
fig,
@@ -8,7 +8,7 @@ defined period or as coming from ticker
import logging
from datetime import timedelta
from typing import Any
from typing import TypedDict
from cachetools import TTLCache
from pandas import DataFrame
@@ -24,6 +24,11 @@ from freqtrade.util import dt_now, format_ms_time
logger = logging.getLogger(__name__)
class SymbolWithPercentage(TypedDict):
symbol: str
percentage: float | None
class PercentChangePairList(IPairList):
is_pairlist_generator = True
supports_backtesting = SupportsBacktesting.NO
@@ -191,7 +196,6 @@ class PercentChangePairList(IPairList):
for k, v in tickers.items()
if (
self._exchange.get_pair_quote_currency(k) == self._stake_currency
and (self._use_range or v.get("percentage") is not None)
and v["symbol"] in _pairlist
)
]
@@ -212,13 +216,15 @@ class PercentChangePairList(IPairList):
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: new whitelist
"""
filtered_tickers: list[dict[str, Any]] = [{"symbol": k} for k in pairlist]
filtered_tickers: list[SymbolWithPercentage] = [
{"symbol": k, "percentage": None} for k in pairlist
]
if self._use_range:
# calculating using lookback_period
self.fetch_percent_change_from_lookback_period(filtered_tickers)
filtered_tickers = self.fetch_percent_change_from_lookback_period(filtered_tickers)
else:
# Fetching 24h change by default from supported exchange tickers
self.fetch_percent_change_from_tickers(filtered_tickers, tickers)
filtered_tickers = self.fetch_percent_change_from_tickers(filtered_tickers, tickers)
if self._min_value is not None:
filtered_tickers = [v for v in filtered_tickers if v["percentage"] > self._min_value]
@@ -228,7 +234,7 @@ class PercentChangePairList(IPairList):
sorted_tickers = sorted(
filtered_tickers,
reverse=self._sort_direction == "desc",
key=lambda t: t["percentage"],
key=lambda t: t["percentage"], # type: ignore
)
# Validate whitelist to only have active market pairs
@@ -240,7 +246,7 @@ class PercentChangePairList(IPairList):
return pairs
def fetch_candles_for_lookback_period(
self, filtered_tickers: list[dict[str, str]]
self, filtered_tickers: list[SymbolWithPercentage]
) -> dict[PairWithTimeframe, DataFrame]:
since_ms = (
int(
@@ -262,7 +268,6 @@ class PercentChangePairList(IPairList):
)
* 1000
)
# todo: utc date output for starting date
self.log_once(
f"Using change range of {self._lookback_period} candles, timeframe: "
f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
@@ -277,7 +282,9 @@ class PercentChangePairList(IPairList):
candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms)
return candles
def fetch_percent_change_from_lookback_period(self, filtered_tickers: list[dict[str, Any]]):
def fetch_percent_change_from_lookback_period(
self, filtered_tickers: list[SymbolWithPercentage]
) -> list[SymbolWithPercentage]:
# get lookback period in ms, for exchange ohlcv fetch
candles = self.fetch_candles_for_lookback_period(filtered_tickers)
@@ -302,16 +309,23 @@ class PercentChangePairList(IPairList):
filtered_tickers[i]["percentage"] = pct_change
else:
filtered_tickers[i]["percentage"] = 0
return filtered_tickers
def fetch_percent_change_from_tickers(self, filtered_tickers: list[dict[str, Any]], tickers):
for i, p in enumerate(filtered_tickers):
def fetch_percent_change_from_tickers(
self, filtered_tickers: list[SymbolWithPercentage], tickers
) -> list[SymbolWithPercentage]:
valid_tickers: list[SymbolWithPercentage] = []
for p in filtered_tickers:
# Filter out assets
if not self._validate_pair(
p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None
if (
self._validate_pair(
p["symbol"], tickers[p["symbol"]] if p["symbol"] in tickers else None
)
and p["symbol"] != "UNI/USDT"
):
filtered_tickers.remove(p)
else:
filtered_tickers[i]["percentage"] = tickers[p["symbol"]]["percentage"]
p["percentage"] = tickers[p["symbol"]]["percentage"]
valid_tickers.append(p)
return valid_tickers
def _validate_pair(self, pair: str, ticker: Ticker | None) -> bool:
"""
+3 -3
View File
@@ -43,7 +43,7 @@ router = APIRouter()
def __run_backtest_bg(btconfig: Config):
from freqtrade.data.metrics import combined_dataframes_with_rel_mean
from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_stats
from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_results
from freqtrade.resolvers import StrategyResolver
asyncio.set_event_loop(asyncio.new_event_loop())
@@ -101,8 +101,8 @@ def __run_backtest_bg(btconfig: Config):
if btconfig.get("export", "none") == "trades":
combined_res = combined_dataframes_with_rel_mean(ApiBG.bt["data"], min_date, max_date)
fn = store_backtest_stats(
btconfig["exportfilename"],
fn = store_backtest_results(
btconfig,
ApiBG.bt["bt"].results,
datetime.now().strftime("%Y-%m-%d_%H-%M-%S"),
market_change_data=combined_res,
@@ -57,7 +57,7 @@ def pairlists_evaluate(
config_loc = deepcopy(config)
config_loc["stake_currency"] = ""
config_loc["pairs"] = payload.pairs
config_loc["timeframe"] = payload.timeframes
config_loc["timerange"] = payload.timerange
config_loc["days"] = payload.days
config_loc["timeframes"] = payload.timeframes
config_loc["erase"] = payload.erase
+4 -2
View File
@@ -6,7 +6,7 @@ from fastapi.exceptions import HTTPException
from freqtrade import __version__
from freqtrade.data.history import get_datahandler
from freqtrade.enums import CandleType, TradingMode
from freqtrade.enums import CandleType, State, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (
@@ -217,7 +217,7 @@ def edge(rpc: RPC = Depends(get_rpc)):
@router.get("/show_config", response_model=ShowConfig, tags=["info"])
def show_config(rpc: RPC | None = Depends(get_rpc_optional), config=Depends(get_config)):
state = ""
state: State | str = ""
strategy_version = None
if rpc:
state = rpc._freqtrade.state
@@ -357,6 +357,7 @@ def pair_history(
config = deepcopy(config)
config.update(
{
"timeframe": timeframe,
"strategy": strategy,
"timerange": timerange,
"freqaimodel": freqaimodel if freqaimodel else config.get("freqaimodel"),
@@ -377,6 +378,7 @@ def pair_history_filtered(
config = deepcopy(config)
config.update(
{
"timeframe": payload.timeframe,
"strategy": payload.strategy,
"timerange": payload.timerange,
"freqaimodel": (
+109 -94
View File
@@ -7,7 +7,7 @@ from abc import abstractmethod
from collections.abc import Generator, Sequence
from datetime import date, datetime, timedelta, timezone
from math import isnan
from typing import Any, cast
from typing import TYPE_CHECKING, Any
import psutil
from dateutil.relativedelta import relativedelta
@@ -32,14 +32,21 @@ from freqtrade.enums import (
)
from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.exchange.exchange_types import Ticker, Tickers
from freqtrade.exchange.exchange_utils import price_to_precision
from freqtrade.loggers import bufferHandler
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.rpc.rpc_types import RPCSendMsg
from freqtrade.util import decimals_per_coin, dt_now, dt_ts_def, format_date, shorten_date
from freqtrade.util import (
decimals_per_coin,
dt_from_ts,
dt_now,
dt_ts_def,
format_date,
shorten_date,
)
from freqtrade.util.datetime_helpers import dt_humanize_delta
from freqtrade.wallets import PositionWallet, Wallet
@@ -98,6 +105,10 @@ class RPC:
# Bind _fiat_converter if needed
_fiat_converter: CryptoToFiatConverter | None = None
if TYPE_CHECKING:
from freqtrade.freqtradebot import FreqtradeBot
_freqtrade: FreqtradeBot
def __init__(self, freqtrade) -> None:
"""
@@ -201,7 +212,7 @@ class RPC:
# calculate profit and send message to user
if trade.is_open:
try:
current_rate = self._freqtrade.exchange.get_rate(
current_rate: float = self._freqtrade.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=False
)
except (ExchangeError, PricingError):
@@ -219,7 +230,7 @@ class RPC:
else:
# Closed trade ...
current_rate = trade.close_rate
current_rate = trade.close_rate or 0.0
current_profit = trade.close_profit or 0.0
current_profit_abs = trade.close_profit_abs or 0.0
@@ -243,7 +254,11 @@ class RPC:
stoploss_entry_dist_ratio = stop_entry.profit_ratio
# calculate distance to stoploss
stoploss_current_dist = trade.stop_loss - current_rate
stoploss_current_dist = price_to_precision(
trade.stop_loss - current_rate,
trade.price_precision,
trade.precision_mode_price,
)
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
trade_dict = trade.to_json()
@@ -264,6 +279,7 @@ class RPC:
stoploss_entry_dist=stoploss_entry_dist,
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
open_orders=oo_details,
nr_of_successful_entries=trade.nr_of_successful_entries,
)
)
results.append(trade_dict)
@@ -271,83 +287,82 @@ class RPC:
def _rpc_status_table(
self, stake_currency: str, fiat_display_currency: str
) -> tuple[list, list, float]:
trades: list[Trade] = Trade.get_open_trades()
) -> tuple[list, list, float, float]:
"""
:return: list of trades, list of columns, sum of fiat profit
"""
nonspot = self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT
if not trades:
if not Trade.get_open_trades():
raise RPCException("no active trade")
else:
trades_list = []
fiat_profit_sum = nan
for trade in trades:
# calculate profit and send message to user
try:
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side="exit", is_short=trade.is_short, refresh=False
)
except (PricingError, ExchangeError):
current_rate = nan
trade_profit = nan
profit_str = f"{nan:.2%}"
else:
if trade.nr_of_successful_entries > 0:
profit = trade.calculate_profit(current_rate)
trade_profit = profit.profit_abs
profit_str = f"{profit.profit_ratio:.2%}"
else:
trade_profit = 0.0
profit_str = f"{0.0:.2f}"
leverage = f"{trade.leverage:.3g}"
direction_str = (
(f"S {leverage}x" if trade.is_short else f"L {leverage}x") if nonspot else ""
trades_list = []
fiat_profit_sum = nan
fiat_total_profit_sum = nan
for trade in self._rpc_trade_status():
# Format profit as a string with the right sign
profit = f"{trade['profit_ratio']:.2%}"
fiat_profit = trade.get("profit_fiat", None)
if fiat_profit is None or isnan(fiat_profit):
fiat_profit = trade.get("profit_abs", 0.0)
if not isnan(fiat_profit):
profit += f" ({fiat_profit:.2f})"
fiat_profit_sum = (
fiat_profit if isnan(fiat_profit_sum) else fiat_profit_sum + fiat_profit
)
total_profit = trade.get("total_profit_fiat", None)
if total_profit is None or isnan(total_profit):
total_profit = trade.get("total_profit_abs", 0.0)
if not isnan(total_profit):
fiat_total_profit_sum = (
total_profit
if isnan(fiat_total_profit_sum)
else fiat_total_profit_sum + total_profit
)
if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount(
trade_profit, stake_currency, fiat_display_currency
)
if not isnan(fiat_profit):
profit_str += f" ({fiat_profit:.2f})"
fiat_profit_sum = (
fiat_profit if isnan(fiat_profit_sum) else fiat_profit_sum + fiat_profit
)
else:
profit_str += f" ({trade_profit:.2f})"
fiat_profit_sum = (
trade_profit if isnan(fiat_profit_sum) else fiat_profit_sum + trade_profit
)
active_attempt_side_symbols = [
"*" if (oo and oo.ft_order_side == trade.entry_side) else "**"
for oo in trade.open_orders
]
# Format the active order side symbols
active_order_side = ""
orders = trade.get("orders", [])
if orders:
active_order_side = ".".join(
"*" if (o.get("is_open") and o.get("ft_is_entry")) else "**"
for o in orders
if o.get("is_open")
)
# example: '*.**.**' trying to enter, exit and exit with 3 different orders
active_attempt_side_symbols_str = ".".join(active_attempt_side_symbols)
# Direction string for non-spot
direction_str = ""
if nonspot:
leverage = trade.get("leverage", 1.0)
direction_str = f"{'S' if trade.get('is_short') else 'L'} {leverage:.3g}x"
detail_trade = [
f"{trade.id} {direction_str}",
trade.pair + active_attempt_side_symbols_str,
shorten_date(dt_humanize_delta(trade.open_date_utc)),
profit_str,
]
detail_trade = [
f"{trade['trade_id']} {direction_str}",
f"{trade['pair']}{active_order_side}",
shorten_date(dt_humanize_delta(dt_from_ts(trade["open_timestamp"]))),
profit,
]
if self._config.get("position_adjustment_enable", False):
max_entry_str = ""
if self._config.get("max_entry_position_adjustment", -1) > 0:
max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
filled_entries = trade.nr_of_successful_entries
detail_trade.append(f"{filled_entries}{max_entry_str}")
trades_list.append(detail_trade)
profitcol = "Profit"
if self._fiat_converter:
profitcol += " (" + fiat_display_currency + ")"
else:
profitcol += " (" + stake_currency + ")"
columns = ["ID L/S" if nonspot else "ID", "Pair", "Since", profitcol]
# Add number of entries if position adjustment is enabled
if self._config.get("position_adjustment_enable", False):
columns.append("# Entries")
return trades_list, columns, fiat_profit_sum
max_entry_str = ""
if self._config.get("max_entry_position_adjustment", -1) > 0:
max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
filled_entries = trade.get("nr_of_successful_entries", 0)
detail_trade.append(f"{filled_entries}{max_entry_str}")
trades_list.append(detail_trade)
columns = [
"ID L/S" if nonspot else "ID",
"Pair",
"Since",
f"Profit ({fiat_display_currency if self._fiat_converter else stake_currency})",
]
if self._config.get("position_adjustment_enable", False):
columns.append("# Entries")
return trades_list, columns, fiat_profit_sum, fiat_total_profit_sum
def _rpc_timeunit_profit(
self,
@@ -572,8 +587,8 @@ class RPC:
# Doing the sum is not right - overall profit needs to be based on initial capital
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
starting_balance = self._freqtrade.wallets.get_starting_balance()
profit_closed_ratio_fromstart = 0
profit_all_ratio_fromstart = 0
profit_closed_ratio_fromstart = 0.0
profit_all_ratio_fromstart = 0.0
if starting_balance:
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
@@ -670,7 +685,7 @@ class RPC:
}
def __balance_get_est_stake(
self, coin: str, stake_currency: str, amount: float, balance: Wallet, tickers: Tickers
self, coin: str, stake_currency: str, amount: float, balance: Wallet
) -> tuple[float, float]:
est_stake = 0.0
est_bot_stake = 0.0
@@ -681,14 +696,18 @@ class RPC:
est_stake = balance.free
est_bot_stake = amount
else:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
rate: float | None = cast(Ticker, tickers.get(pair, {})).get("last", None)
if rate:
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
rate = 1.0 / rate
est_stake = rate * balance.total
est_bot_stake = rate * amount
try:
rate: float | None = self._freqtrade.exchange.get_conversion_rate(
coin, stake_currency
)
if rate:
est_stake = rate * balance.total
est_bot_stake = rate * amount
return est_stake, est_bot_stake
except (ExchangeError, PricingError) as e:
logger.warning(f"Error {e} getting rate for {coin}")
pass
return est_stake, est_bot_stake
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> dict:
@@ -696,10 +715,6 @@ class RPC:
currencies: list[dict] = []
total = 0.0
total_bot = 0.0
try:
tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True)
except ExchangeError:
raise RPCException("Error getting current tickers.")
open_trades: list[Trade] = Trade.get_open_trades()
open_assets: dict[str, Trade] = {t.safe_base_currency: t for t in open_trades}
@@ -715,7 +730,7 @@ class RPC:
coin: str
balance: Wallet
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
if not balance.total:
if not balance.total and not balance.free:
continue
trade = open_assets.get(coin, None)
@@ -726,7 +741,7 @@ class RPC:
try:
est_stake, est_stake_bot = self.__balance_get_est_stake(
coin, stake_currency, trade_amount, balance, tickers
coin, stake_currency, trade_amount, balance
)
except ValueError:
continue
@@ -886,10 +901,10 @@ class RPC:
if amount and amount < trade.amount:
# Partial exit ...
min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount(
trade.pair, current_rate, trade.stop_loss_pct
trade.pair, current_rate, trade.stop_loss_pct or 0.0
)
remaining = (trade.amount - amount) * current_rate
if remaining < min_exit_stake:
if min_exit_stake and remaining < min_exit_stake:
raise RPCException(f"Remaining amount of {remaining} would be too small.")
sub_amount = amount
@@ -1229,7 +1244,7 @@ class RPC:
for pair in add:
if pair not in self._freqtrade.pairlists.blacklist:
try:
expand_pairlist([pair], self._freqtrade.exchange.get_markets().keys())
expand_pairlist([pair], list(self._freqtrade.exchange.get_markets().keys()))
self._freqtrade.pairlists.blacklist.append(pair)
except ValueError:
+65 -6
View File
@@ -90,6 +90,7 @@ class TimeunitMappings:
def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
"""
Decorator to check if the message comes from the correct chat_id
can only be used with Telegram Class to decorate instance methods.
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
@@ -102,13 +103,21 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
# Reject unauthorized messages
if update.callback_query:
cchat_id = int(update.callback_query.message.chat.id)
ctopic_id = update.callback_query.message.message_thread_id
else:
cchat_id = int(update.message.chat_id)
ctopic_id = update.message.message_thread_id
chat_id = int(self._config["telegram"]["chat_id"])
if cchat_id != chat_id:
logger.info(f"Rejected unauthorized message from: {update.message.chat_id}")
return wrapper
logger.info(f"Rejected unauthorized message from: {cchat_id}")
return None
if (topic_id := self._config["telegram"].get("topic_id")) is not None:
if str(ctopic_id) != topic_id:
# This can be quite common in multi-topic environments.
logger.debug(f"Rejected message from wrong channel: {cchat_id}, {ctopic_id}")
return None
# Rollback session to avoid getting data stored in a transaction.
Trade.rollback()
logger.debug("Executing handler: %s for chat_id: %s", command_handler.__name__, chat_id)
@@ -291,6 +300,7 @@ class Telegram(RPCHandler):
CommandHandler("marketdir", self._changemarketdir),
CommandHandler("order", self._order),
CommandHandler("list_custom_data", self._list_custom_data),
CommandHandler("tg_info", self._tg_info),
]
callbacks = [
CallbackQueryHandler(self._status_table, pattern="update_status_table"),
@@ -848,11 +858,14 @@ class Telegram(RPCHandler):
:return: None
"""
fiat_currency = self._config.get("fiat_display_currency", "")
statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
statlist, head, fiat_profit_sum, fiat_total_profit_sum = self._rpc._rpc_status_table(
self._config["stake_currency"], fiat_currency
)
show_total = not isnan(fiat_profit_sum) and len(statlist) > 1
show_total_realized = (
not isnan(fiat_total_profit_sum) and len(statlist) > 1 and fiat_profit_sum
) != fiat_total_profit_sum
max_trades_per_msg = 50
"""
Calculate the number of messages of 50 trades per message
@@ -865,12 +878,22 @@ class Telegram(RPCHandler):
if show_total and i == messages_count - 1:
# append total line
trades.append(["Total", "", "", f"{fiat_profit_sum:.2f} {fiat_currency}"])
if show_total_realized:
trades.append(
[
"Total",
"(incl. realized Profits)",
"",
f"{fiat_total_profit_sum:.2f} {fiat_currency}",
]
)
message = tabulate(trades, headers=head, tablefmt="simple")
if show_total and i == messages_count - 1:
# insert separators line between Total
lines = message.split("\n")
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
offset = 2 if show_total_realized else 1
message = "\n".join(lines[:-offset] + [lines[1]] + lines[-offset:])
await self._send_msg(
f"<pre>{message}</pre>",
parse_mode=ParseMode.HTML,
@@ -1277,7 +1300,7 @@ class Telegram(RPCHandler):
else:
fiat_currency = self._config.get("fiat_display_currency", "")
try:
statlist, _, _ = self._rpc._rpc_status_table(
statlist, _, _, _ = self._rpc._rpc_status_table(
self._config["stake_currency"], fiat_currency
)
except RPCException:
@@ -1806,7 +1829,7 @@ class Telegram(RPCHandler):
"*/fx <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/reload_trade <trade_id>:* `Relade trade from exchange Orders`\n"
"*/reload_trade <trade_id>:* `Reload trade from exchange Orders`\n"
"*/cancel_open_order <trade_id>:* `Cancels open orders for trade. "
"Only valid when the trade has open orders.`\n"
"*/coo <trade_id>|all:* `Alias to /cancel_open_order`\n"
@@ -2054,6 +2077,7 @@ class Telegram(RPCHandler):
parse_mode=parse_mode,
reply_markup=reply_markup,
disable_notification=disable_notification,
message_thread_id=self._config["telegram"].get("topic_id"),
)
except NetworkError as network_err:
# Sometimes the telegram server resets the current connection,
@@ -2067,6 +2091,7 @@ class Telegram(RPCHandler):
parse_mode=parse_mode,
reply_markup=reply_markup,
disable_notification=disable_notification,
message_thread_id=self._config["telegram"].get("topic_id"),
)
except TelegramError as telegram_err:
logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message)
@@ -2112,3 +2137,37 @@ class Telegram(RPCHandler):
"Invalid usage of command /marketdir. \n"
"Usage: */marketdir [short | long | even | none]*"
)
async def _tg_info(self, update: Update, context: CallbackContext) -> None:
"""
Intentionally unauthenticated Handler for /tg_info.
Returns information about the current telegram chat - even if chat_id does not
correspond to this chat.
:param update: message update
:return: None
"""
if not update.message:
return
chat_id = update.message.chat_id
topic_id = update.message.message_thread_id
msg = f"""Freqtrade Bot Info:
```json
{{
"enabled": true,
"token": "********",
"chat_id": "{chat_id}",
{f'"topic_id": "{topic_id}"' if topic_id else ""}
}}
```
"""
try:
await context.bot.send_message(
chat_id=chat_id,
text=msg,
parse_mode=ParseMode.MARKDOWN_V2,
message_thread_id=topic_id,
)
except TelegramError as telegram_err:
logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message)
+6 -13
View File
@@ -5,7 +5,6 @@ This module defines the interface to apply for strategies
import logging
from abc import ABC, abstractmethod
from collections import OrderedDict
from datetime import datetime, timedelta, timezone
from math import isinf, isnan
@@ -141,9 +140,7 @@ class IStrategy(ABC, HyperStrategyMixin):
market_direction: MarketDirection = MarketDirection.NONE
# Global cache dictionary
_cached_grouped_trades_per_pair: dict[
str, OrderedDict[tuple[datetime, datetime], DataFrame]
] = {}
_cached_grouped_trades_per_pair: dict[str, DataFrame] = {}
def __init__(self, config: Config) -> None:
self.config = config
@@ -1163,10 +1160,10 @@ class IStrategy(ABC, HyperStrategyMixin):
logger.warning(f"Empty candle (OHLCV) data for pair {pair}")
return None, None
latest_date = dataframe["date"].max()
latest = dataframe.loc[dataframe["date"] == latest_date].iloc[-1]
latest_date_pd = dataframe["date"].max()
latest = dataframe.loc[dataframe["date"] == latest_date_pd].iloc[-1]
# Explicitly convert to datetime object to ensure the below comparison does not fail
latest_date = latest_date.to_pydatetime()
latest_date: datetime = latest_date_pd.to_pydatetime()
# Check if dataframe is out of date
timeframe_minutes = timeframe_to_minutes(timeframe)
@@ -1604,15 +1601,11 @@ class IStrategy(ABC, HyperStrategyMixin):
if use_public_trades:
trades = self.dp.trades(pair=metadata["pair"], copy=False)
config = self.config
config["timeframe"] = self.timeframe
pair = metadata["pair"]
# TODO: slice trades to size of dataframe for faster backtesting
cached_grouped_trades: OrderedDict[tuple[datetime, datetime], DataFrame] = (
self._cached_grouped_trades_per_pair.get(pair, OrderedDict())
)
cached_grouped_trades: DataFrame | None = self._cached_grouped_trades_per_pair.get(pair)
dataframe, cached_grouped_trades = populate_dataframe_with_trades(
cached_grouped_trades, config, dataframe, trades
cached_grouped_trades, self.config, dataframe, trades
)
# dereference old cache
@@ -216,7 +216,7 @@
"# Get market change (average change of all pairs from start to end of the backtest period)\n",
"print(stats[\"strategy\"][strategy][\"market_change\"])\n",
"# Maximum drawdown ()\n",
"print(stats[\"strategy\"][strategy][\"max_drawdown\"])\n",
"print(stats[\"strategy\"][strategy][\"max_drawdown_abs\"])\n",
"# Maximum drawdown start and end\n",
"print(stats[\"strategy\"][strategy][\"drawdown_start\"])\n",
"print(stats[\"strategy\"][strategy][\"drawdown_end\"])\n",
+2
View File
@@ -11,6 +11,7 @@ from freqtrade.util.datetime_helpers import (
format_ms_time,
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.ft_precise import FtPrecise
from freqtrade.util.measure_time import MeasureTime
@@ -35,6 +36,7 @@ __all__ = [
"dt_utc",
"format_date",
"format_ms_time",
"get_dry_run_wallet",
"FtPrecise",
"PeriodicCache",
"shorten_date",
+12
View File
@@ -0,0 +1,12 @@
from freqtrade.constants import Config
def get_dry_run_wallet(config: Config) -> int | float:
"""
Return dry-run wallet balance in stake currency from configuration.
This setup also supports dictionary mode for dry-run-wallet.
"""
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
return _start_cap
else:
return _start_cap.get("stake_currency")
+68 -35
View File
@@ -2,7 +2,6 @@
"""Wallet"""
import logging
from copy import deepcopy
from datetime import datetime, timedelta
from typing import NamedTuple
@@ -41,7 +40,14 @@ class Wallets:
self._exchange = exchange
self._wallets: dict[str, Wallet] = {}
self._positions: dict[str, PositionWallet] = {}
self._start_cap = config["dry_run_wallet"]
self._start_cap: dict[str, float] = {}
self._stake_currency = config["stake_currency"]
if isinstance(_start_cap := config["dry_run_wallet"], float | int):
self._start_cap[self._stake_currency] = _start_cap
else:
self._start_cap = _start_cap
self._last_wallet_refresh: datetime | None = None
self.update()
@@ -66,6 +72,18 @@ class Wallets:
else:
return 0
def get_collateral(self) -> float:
"""
Get total collateral for liquidation price calculation.
"""
if self._config.get("margin_mode") == "cross":
# free includes all balances and, combined with position collateral,
# is used as "wallet balance".
return self.get_free(self._stake_currency) + sum(
pos.collateral for pos in self._positions.values()
)
return self.get_total(self._stake_currency)
def get_owned(self, pair: str, base_currency: str) -> float:
"""
Get currently owned value.
@@ -109,54 +127,66 @@ class Wallets:
for o in trade.open_orders
if o.amount and o.ft_order_side == trade.exit_side
)
curr_wallet_bal = self._start_cap.get(curr, 0)
_wallets[curr] = Wallet(curr, trade.amount - pending, pending, trade.amount)
current_stake = self._start_cap + tot_profit - tot_in_trades
total_stake = current_stake + used_stake
_wallets[curr] = Wallet(
curr,
curr_wallet_bal + trade.amount - pending,
pending,
trade.amount + curr_wallet_bal,
)
else:
tot_in_trades = 0
for position in open_trades:
# size = self._exchange._contracts_to_amount(position.pair, position['contracts'])
size = position.amount
collateral = position.stake_amount
leverage = position.leverage
tot_in_trades += collateral
_positions[position.pair] = PositionWallet(
position.pair,
position=size,
leverage=leverage,
collateral=collateral,
position=position.amount,
leverage=position.leverage,
collateral=position.stake_amount,
side=position.trade_direction,
)
current_stake = self._start_cap + tot_profit - tot_in_trades
used_stake = tot_in_trades
total_stake = current_stake + tot_in_trades
_wallets[self._config["stake_currency"]] = Wallet(
currency=self._config["stake_currency"],
free=current_stake,
used_stake = tot_in_trades
cross_margin = 0.0
if self._config.get("margin_mode") == "cross":
# In cross-margin mode, the total balance is used as collateral.
# This is moved as "free" into the stake currency balance.
# strongly tied to the get_collateral() implementation.
for curr, bal in self._start_cap.items():
if curr == self._stake_currency:
continue
rate = self._exchange.get_conversion_rate(curr, self._stake_currency)
if rate:
cross_margin += bal * rate
current_stake = self._start_cap.get(self._stake_currency, 0) + tot_profit - tot_in_trades
total_stake = current_stake + used_stake
_wallets[self._stake_currency] = Wallet(
currency=self._stake_currency,
free=current_stake + cross_margin,
used=used_stake,
total=total_stake,
)
for currency, bal in self._start_cap.items():
if currency not in _wallets:
_wallets[currency] = Wallet(currency, bal, 0, bal)
self._wallets = _wallets
self._positions = _positions
def _update_live(self) -> None:
balances = self._exchange.get_balances()
_wallets = {}
for currency in balances:
if isinstance(balances[currency], dict):
self._wallets[currency] = Wallet(
_wallets[currency] = Wallet(
currency,
balances[currency].get("free", 0),
balances[currency].get("used", 0),
balances[currency].get("total", 0),
)
# Remove currencies no longer in get_balances output
for currency in deepcopy(self._wallets):
if currency not in balances:
del self._wallets[currency]
positions = self._exchange.fetch_positions()
_parsed_positions = {}
@@ -176,6 +206,7 @@ class Wallets:
side=position["side"],
)
self._positions = _parsed_positions
self._wallets = _wallets
def update(self, require_update: bool = True) -> None:
"""
@@ -244,8 +275,10 @@ class Wallets:
else:
tot_profit = Trade.get_total_closed_profit()
open_stakes = Trade.total_open_trades_stakes()
available_balance = self.get_free(self._config["stake_currency"])
return available_balance - tot_profit + open_stakes
available_balance = self.get_free(self._stake_currency)
return (available_balance - tot_profit + open_stakes) * self._config[
"tradable_balance_ratio"
]
def get_total_stake_amount(self):
"""
@@ -264,9 +297,9 @@ class Wallets:
# Ensure <tradable_balance_ratio>% is used from the overall balance
# Otherwise we'd risk lowering stakes with each open trade.
# (tied up + current free) * ratio) - tied up
available_amount = (
val_tied_up + self.get_free(self._config["stake_currency"])
) * self._config["tradable_balance_ratio"]
available_amount = (val_tied_up + self.get_free(self._stake_currency)) * self._config[
"tradable_balance_ratio"
]
return available_amount
def get_available_stake_amount(self) -> float:
@@ -277,7 +310,7 @@ class Wallets:
(<open_trade stakes> + free amount) * tradable_balance_ratio - <open_trade stakes>
"""
free = self.get_free(self._config["stake_currency"])
free = self.get_free(self._stake_currency)
return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free)
def _calculate_unlimited_stake_amount(
@@ -316,7 +349,7 @@ class Wallets:
f"lower than stake amount ({stake_amount} {self._config['stake_currency']})"
)
return stake_amount
return max(stake_amount, 0)
def get_trade_stake_amount(
self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True
@@ -336,8 +369,8 @@ class Wallets:
if edge:
stake_amount = edge.stake_amount(
pair,
self.get_free(self._config["stake_currency"]),
self.get_total(self._config["stake_currency"]),
self.get_free(self._stake_currency),
self.get_total(self._stake_currency),
val_tied_up,
)
else:
+1 -1
View File
@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2024.11"
__version__ = "2024.12"
if "dev" in __version__:
from pathlib import Path
+2 -1
View File
@@ -111,6 +111,8 @@ develop = [
"pytest-cov",
"pytest-mock",
"pytest-random-order",
"pytest-timeout",
"pytest-xdist",
"pytest",
"ruff",
"time-machine",
@@ -215,7 +217,6 @@ exclude = [
"build_helpers/*.py",
"ft_client/build/*",
"build/*",
"tests/*",
]
ignore = ["freqtrade/vendor/**"]
pythonPlatform = "All"
+6 -6
View File
@@ -7,11 +7,11 @@
-r docs/requirements-docs.txt
coveralls==4.0.1
ruff==0.8.0
mypy==1.13.0
ruff==0.8.4
mypy==1.14.0
pre-commit==4.0.1
pytest==8.3.3
pytest-asyncio==0.24.0
pytest==8.3.4
pytest-asyncio==0.25.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-random-order==1.1.1
@@ -28,5 +28,5 @@ nbconvert==7.16.4
types-cachetools==5.5.0.20240820
types-filelock==3.2.7
types-requests==2.32.0.20241016
types-tabulate==0.9.0.20240106
types-python-dateutil==2.9.0.20241003
types-tabulate==0.9.0.20241207
types-python-dateutil==2.9.0.20241206
+2 -5
View File
@@ -3,13 +3,10 @@
-r requirements-plot.txt
# Required for freqai
scikit-learn==1.5.2
scikit-learn==1.6.0
joblib==1.4.2
catboost==1.2.7; 'arm' not in platform_machine
# Pin Matplotlib - it's depended on by catboost
# Temporary downgrade of matplotlib due to https://github.com/matplotlib/matplotlib/issues/28551
matplotlib==3.9.2
lightgbm==4.5.0
xgboost==2.0.3
xgboost==2.1.3
tensorboard==2.18.0
datasieve==0.1.7
+1 -1
View File
@@ -3,6 +3,6 @@
# Required for hyperopt
scipy==1.14.1
scikit-learn==1.5.2
scikit-learn==1.6.0
ft-scikit-optimize==0.9.2
filelock==3.16.1
+17 -14
View File
@@ -4,28 +4,31 @@ bottleneck==1.4.2
numexpr==2.10.2
pandas-ta==0.3.14b
ccxt==4.4.35
ccxt==4.4.43
cryptography==42.0.8; platform_machine == 'armv7l'
cryptography==43.0.3; platform_machine != 'armv7l'
cryptography==44.0.0; platform_machine != 'armv7l'
aiohttp==3.10.11
SQLAlchemy==2.0.36
python-telegram-bot==21.7
python-telegram-bot==21.9
# can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1
humanize==4.11.0
cachetools==5.5.0
requests==2.32.3
urllib3==2.2.3
urllib3==2.3.0
jsonschema==4.23.0
TA-Lib==0.4.32
technical==1.4.4
TA-Lib==0.4.34
technical==1.5.0
tabulate==0.9.0
pycoingecko==3.2.0
jinja2==3.1.4
jinja2==3.1.5
tables==3.10.1
# Pin blosc2 to < 3.0 until piwheel has wheels for 3.x
blosc2==2.7.1; platform_machine == 'armv7l' or python_version < '3.11'
blosc2==3.0.0; platform_machine != 'armv7l' and python_version >= '3.11'
joblib==1.4.2
rich==13.9.4
pyarrow==18.0.0; platform_machine != 'armv7l'
pyarrow==18.1.0; platform_machine != 'armv7l'
# find first, C search in arrays
py_find_1st==1.1.6
@@ -39,12 +42,12 @@ orjson==3.10.12
sdnotify==0.3.2
# API Server
fastapi==0.115.5
pydantic==2.10.1
uvicorn==0.32.1
pyjwt==2.10.0
fastapi==0.115.6
pydantic==2.10.4
uvicorn==0.34.0
pyjwt==2.10.1
aiofiles==24.1.0
psutil==6.1.0
psutil==6.1.1
# Building config files interactively
questionary==2.0.1
@@ -58,7 +61,7 @@ schedule==1.2.2
#WS Messages
websockets==14.1
janus==1.1.0
janus==2.0.0
ast-comments==1.2.2
packaging==24.2
+5 -2
View File
@@ -1532,8 +1532,11 @@ def test_hyperopt_list(mocker, capsys, caplog, tmp_path):
assert csv_file.is_file()
line = csv_file.read_text()
assert (
'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,2,0,0.43662" in line
'Best,1,2,-1.25%,-1.2222,-0.00125625,BTC,-2.51,"3,930.0 m",-0.00125625,23.00%,0.43662'
in line
or "Best,1,2,-1.25%,-1.2222,-0.00125625,BTC,-2.51,2 days 17:30:00,2,0,-0.00125625,23.00%,"
"0.43662"
in line
)
csv_file.unlink()
+28 -3
View File
@@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring
import json
import logging
import platform
import re
from copy import deepcopy
from datetime import datetime, timedelta, timezone
@@ -517,6 +518,30 @@ def patch_gc(mocker) -> None:
mocker.patch("freqtrade.main.gc_set_threshold")
def is_arm() -> bool:
machine = platform.machine()
return "arm" in machine or "aarch64" in machine
def is_mac() -> bool:
machine = platform.system()
return "Darwin" in machine
@pytest.fixture(autouse=True)
def patch_torch_initlogs(mocker) -> None:
if is_mac():
# Mock torch import completely
import sys
import types
module_name = "torch"
mocked_module = types.ModuleType(module_name)
sys.modules[module_name] = mocked_module
else:
mocker.patch("torch._logging._init_logs")
@pytest.fixture(autouse=True)
def user_dir(mocker, tmp_path) -> Path:
user_dir = tmp_path / "user_data"
@@ -600,7 +625,7 @@ def get_default_conf(testdatadir):
"telegram": {
"enabled": False,
"token": "token",
"chat_id": "0",
"chat_id": "1235",
"notification_settings": {},
},
"datadir": Path(testdatadir),
@@ -2212,7 +2237,7 @@ def tickers():
"first": None,
"last": 8603.67,
"change": -0.879,
"percentage": None,
"percentage": -8.95,
"average": None,
"baseVolume": 30414.604298,
"quoteVolume": 259629896.48584127,
@@ -2256,7 +2281,7 @@ def tickers():
"first": None,
"last": 129.28,
"change": 1.795,
"percentage": None,
"percentage": -2.5,
"average": None,
"baseVolume": 59698.79897,
"quoteVolume": 29132399.743954,
+102 -8
View File
@@ -1,13 +1,17 @@
from collections import OrderedDict
import numpy as np
import pandas as pd
import pytest
from freqtrade.constants import DEFAULT_TRADES_COLUMNS
from freqtrade.data.converter import populate_dataframe_with_trades
from freqtrade.data.converter.orderflow import trades_to_volumeprofile_with_total_delta_bid_ask
from freqtrade.data.converter.orderflow import (
ORDERFLOW_ADDED_COLUMNS,
timeframe_to_DateOffset,
trades_to_volumeprofile_with_total_delta_bid_ask,
)
from freqtrade.data.converter.trade_converter import trades_list_to_df
from freqtrade.data.dataprovider import DataProvider
from tests.strategy.strats.strategy_test_v3 import StrategyTestV3
BIN_SIZE_SCALE = 0.5
@@ -37,6 +41,7 @@ def populate_dataframe_with_trades_trades(testdatadir):
@pytest.fixture
def candles(testdatadir):
# TODO: this fixture isn't really necessary and could be removed
return pd.read_json(testdatadir / "orderflow/candles.json").copy()
@@ -102,7 +107,7 @@ def test_public_trades_mock_populate_dataframe_with_trades__check_orderflow(
},
}
# Apply the function to populate the data frame with order flow data
df, _ = populate_dataframe_with_trades(OrderedDict(), config, dataframe, trades)
df, _ = populate_dataframe_with_trades(None, config, dataframe, trades)
# Extract results from the first row of the DataFrame
results = df.iloc[0]
t = results["trades"]
@@ -243,7 +248,7 @@ def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
}
# Populate the DataFrame with trades and order flow data
df, _ = populate_dataframe_with_trades(OrderedDict(), config, dataframe, trades)
df, _ = populate_dataframe_with_trades(None, config, dataframe, trades)
# --- DataFrame and Trade Data Validation ---
@@ -401,9 +406,7 @@ def test_public_trades_config_max_trades(
},
}
df, _ = populate_dataframe_with_trades(
OrderedDict(), default_conf | orderflow_config, dataframe, trades
)
df, _ = populate_dataframe_with_trades(None, default_conf | orderflow_config, dataframe, trades)
assert df.delta.count() == 1
@@ -482,3 +485,94 @@ def test_public_trades_testdata_sanity(
"cost",
"date",
]
def test_analyze_with_orderflow(
default_conf_usdt,
mocker,
populate_dataframe_with_trades_dataframe,
populate_dataframe_with_trades_trades,
):
ohlcv_history = populate_dataframe_with_trades_dataframe
# call without orderflow
strategy = StrategyTestV3(config=default_conf_usdt)
strategy.dp = DataProvider(default_conf_usdt, None, None)
mocker.patch.object(strategy.dp, "trades", return_value=populate_dataframe_with_trades_trades)
import freqtrade.data.converter.orderflow as orderflow_module
spy = mocker.spy(orderflow_module, "trades_to_volumeprofile_with_total_delta_bid_ask")
pair = "ETH/BTC"
df = strategy.advise_indicators(ohlcv_history, {"pair:": pair})
assert len(df) == len(ohlcv_history)
assert "open" in df.columns
assert spy.call_count == 0
# Not expected to run - shouldn't have added orderflow columns
for col in ORDERFLOW_ADDED_COLUMNS:
assert col not in df.columns, f"Column {col} found in df.columns"
default_conf_usdt["exchange"]["use_public_trades"] = True
default_conf_usdt["orderflow"] = {
"cache_size": 5,
"max_candles": 5,
"scale": 0.005,
"imbalance_volume": 0,
"imbalance_ratio": 3,
"stacked_imbalance_range": 3,
}
strategy.config = default_conf_usdt
# First round - builds cache
df1 = strategy.advise_indicators(ohlcv_history, {"pair": pair})
assert len(df1) == len(ohlcv_history)
assert "open" in df1.columns
assert spy.call_count == 5
for col in ORDERFLOW_ADDED_COLUMNS:
assert col in df1.columns, f"Column {col} not found in df.columns"
if col not in ("stacked_imbalances_bid", "stacked_imbalances_ask"):
assert df1[col].count() == 5, f"Column {col} has {df1[col].count()} non-NaN values"
assert len(strategy._cached_grouped_trades_per_pair[pair]) == 5
lastval_trades = df1.at[len(df1) - 1, "trades"]
assert isinstance(lastval_trades, list)
assert len(lastval_trades) == 122
lastval_of = df1.at[len(df1) - 1, "orderflow"]
assert isinstance(lastval_of, dict)
spy.reset_mock()
# Ensure caching works - call the same logic again.
df2 = strategy.advise_indicators(ohlcv_history, {"pair": pair})
assert len(df2) == len(ohlcv_history)
assert "open" in df2.columns
assert spy.call_count == 0
for col in ORDERFLOW_ADDED_COLUMNS:
assert col in df2.columns, f"Round2: Column {col} not found in df.columns"
if col not in ("stacked_imbalances_bid", "stacked_imbalances_ask"):
assert (
df2[col].count() == 5
), f"Round2: Column {col} has {df2[col].count()} non-NaN values"
lastval_trade2 = df2.at[len(df2) - 1, "trades"]
assert isinstance(lastval_trade2, list)
assert len(lastval_trade2) == 122
lastval_of2 = df2.at[len(df2) - 1, "orderflow"]
assert isinstance(lastval_of2, dict)
def test_timeframe_to_DateOffset():
assert timeframe_to_DateOffset("1s") == pd.DateOffset(seconds=1)
assert timeframe_to_DateOffset("1m") == pd.DateOffset(minutes=1)
assert timeframe_to_DateOffset("5m") == pd.DateOffset(minutes=5)
assert timeframe_to_DateOffset("1h") == pd.DateOffset(hours=1)
assert timeframe_to_DateOffset("1d") == pd.DateOffset(days=1)
assert timeframe_to_DateOffset("1w") == pd.DateOffset(weeks=1)
assert timeframe_to_DateOffset("1M") == pd.DateOffset(months=1)
assert timeframe_to_DateOffset("1y") == pd.DateOffset(years=1)
+5
View File
@@ -360,6 +360,11 @@ def test_hdf5datahandler_trades_load(testdatadir):
# assert len([t for t in trades2 if t[0] > timerange.stopts * 1000]) == 0
def test_hdf5datahandler_deprecated(testdatadir, caplog):
get_datahandler(testdatadir, "hdf5")
log_has_re(r"DEPRECATED: The hdf5 dataformat is deprecated.*", caplog)
@pytest.mark.parametrize(
"pair,timeframe,candle_type,candle_append,startdt,enddt",
[
+3 -3
View File
@@ -128,8 +128,8 @@ def test_load_data_with_new_pair_1min(
"""
Test load_pair_history() with 1 min timeframe
"""
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
file = tmp_path / "MEME_BTC-1m.feather"
# do not download a new pair if refresh_pairs isn't set
@@ -306,8 +306,8 @@ def test_load_cached_data_for_updating(testdatadir) -> None:
def test_download_pair_history(
ohlcv_history, mocker, default_conf, tmp_path, candle_type, subdir, file_tail
) -> None:
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
file1_1 = tmp_path / f"{subdir}MEME_BTC-1m{file_tail}.feather"
file1_5 = tmp_path / f"{subdir}MEME_BTC-5m{file_tail}.feather"
file2_1 = tmp_path / f"{subdir}CFI_BTC-1m{file_tail}.feather"
@@ -357,8 +357,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir, ohlcv_history
"freqtrade.data.history.datahandlers.featherdatahandler.FeatherDataHandler.ohlcv_store",
return_value=None,
)
mocker.patch(f"{EXMS}.get_historic_ohlcv", return_value=ohlcv_history)
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(exchange, "get_historic_ohlcv", return_value=ohlcv_history)
_download_pair_history(
datadir=testdatadir,
exchange=exchange,
+237 -32
View File
@@ -1,14 +1,17 @@
from datetime import datetime, timezone
from datetime import datetime, timedelta
from random import randint
from unittest.mock import MagicMock, PropertyMock
import ccxt
import pandas as pd
import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_seconds
from freqtrade.persistence import Trade
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts, dt_utc
from tests.conftest import EXMS, get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -290,6 +293,7 @@ def test_liquidation_price_binance(
default_conf["trading_mode"] = trading_mode
default_conf["margin_mode"] = margin_mode
default_conf["liquidation_buffer"] = 0.0
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
def get_maint_ratio(pair_, stake_amount):
@@ -731,42 +735,243 @@ def test__set_leverage_binance(mocker, default_conf):
)
@pytest.mark.parametrize("candle_type", [CandleType.MARK, ""])
async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, candle_type):
ohlcv = [
[
int((datetime.now(timezone.utc).timestamp() - 1000) * 1000),
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
def patch_binance_vision_ohlcv(mocker, start, archive_end, api_end, timeframe):
def make_storage(start: datetime, end: datetime, timeframe: str):
date = pd.date_range(start, end, freq=timeframe.replace("m", "min"))
df = pd.DataFrame(
data=dict(date=date, open=1.0, high=1.0, low=1.0, close=1.0),
)
return df
archive_storage = make_storage(start, archive_end, timeframe)
api_storage = make_storage(start, api_end, timeframe)
ohlcv = [[dt_ts(start), 1, 1, 1, 1]]
# (pair, timeframe, candle_type, ohlcv, True)
candle_history = [None, None, None, ohlcv, None]
def get_historic_ohlcv(
# self,
pair: str,
timeframe: str,
since_ms: int,
candle_type: CandleType,
is_new_pair: bool = False,
until_ms: int | None = None,
):
since = dt_from_ts(since_ms)
until = dt_from_ts(until_ms) if until_ms else api_end + timedelta(seconds=1)
return api_storage.loc[(api_storage["date"] >= since) & (api_storage["date"] < until)]
async def download_archive_ohlcv(
candle_type,
pair,
timeframe,
since_ms,
until_ms,
markets=None,
stop_on_404=False,
):
since = dt_from_ts(since_ms)
until = dt_from_ts(until_ms) if until_ms else archive_end + timedelta(seconds=1)
if since < start:
pass
return archive_storage.loc[
(archive_storage["date"] >= since) & (archive_storage["date"] < until)
]
]
candle_mock = mocker.patch(f"{EXMS}._async_get_candle_history", return_value=candle_history)
api_mock = mocker.patch(f"{EXMS}.get_historic_ohlcv", side_effect=get_historic_ohlcv)
archive_mock = mocker.patch(
"freqtrade.exchange.binance.download_archive_ohlcv", side_effect=download_archive_ohlcv
)
return candle_mock, api_mock, archive_mock
@pytest.mark.parametrize(
"timeframe,is_new_pair,since,until,first_date,last_date,candle_called,archive_called,"
"api_called",
[
(
"1m",
True,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59),
True,
True,
False,
),
(
"1m",
True,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 3),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2, 23, 59),
True,
True,
True,
),
(
"1m",
True,
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 2, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 2, 0, 59),
True,
False,
True,
),
(
"1m",
False,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59),
False,
True,
False,
),
(
"1m",
True,
dt_utc(2019, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59),
True,
True,
False,
),
(
"1m",
False,
dt_utc(2019, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59),
False,
True,
False,
),
(
"1m",
False,
dt_utc(2019, 1, 1),
dt_utc(2019, 1, 2),
None,
None,
False,
True,
True,
),
(
"1m",
True,
dt_utc(2019, 1, 1),
dt_utc(2019, 1, 2),
None,
None,
True,
False,
False,
),
(
"1m",
False,
dt_utc(2021, 1, 1),
dt_utc(2021, 1, 2),
None,
None,
False,
False,
False,
),
(
"1m",
True,
dt_utc(2021, 1, 1),
dt_utc(2021, 1, 2),
None,
None,
True,
False,
False,
),
(
"1h",
False,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23),
False,
False,
True,
),
(
"1m",
False,
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 3, 50, 30),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 3, 50),
False,
True,
False,
),
],
)
def test_get_historic_ohlcv_binance(
mocker,
default_conf,
timeframe,
is_new_pair,
since,
until,
first_date,
last_date,
candle_called,
archive_called,
api_called,
):
exchange = get_patched_exchange(mocker, default_conf, exchange="binance")
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = "ETH/BTC"
respair, restf, restype, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=False, candle_type=candle_type
)
assert respair == pair
assert restf == "5m"
assert restype == candle_type
# Call with very old timestamp - causes tons of requests
assert exchange._api_async.fetch_ohlcv.call_count > 400
# assert res == ohlcv
exchange._api_async.fetch_ohlcv.reset_mock()
_, _, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=True, candle_type=candle_type
start = dt_utc(2020, 1, 1)
archive_end = dt_utc(2020, 1, 2)
api_end = dt_utc(2020, 1, 3)
candle_mock, api_mock, archive_mock = patch_binance_vision_ohlcv(
mocker, start=start, archive_end=archive_end, api_end=api_end, timeframe=timeframe
)
# Called twice - one "init" call - and one to get the actual data.
assert exchange._api_async.fetch_ohlcv.call_count == 2
assert res == ohlcv
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
candle_type = CandleType.SPOT
pair = "BTC/USDT"
since_ms = dt_ts(since)
until_ms = dt_ts(until)
df = exchange.get_historic_ohlcv(pair, timeframe, since_ms, candle_type, is_new_pair, until_ms)
if df.empty:
assert first_date is None
assert last_date is None
else:
assert df["date"].iloc[0] == first_date
assert df["date"].iloc[-1] == last_date
assert (
df["date"].diff().iloc[1:] == timedelta(seconds=timeframe_to_seconds(timeframe))
).all()
if candle_called:
candle_mock.assert_called_once()
if archive_called:
archive_mock.assert_called_once()
if api_called:
api_mock.assert_called_once()
@pytest.mark.parametrize(
+337
View File
@@ -0,0 +1,337 @@
import asyncio
import datetime
import io
import re
import sys
import zipfile
from datetime import timedelta
import aiohttp
import pandas as pd
import pytest
from freqtrade.enums import CandleType
from freqtrade.exchange.binance_public_data import (
BadHttpStatus,
Http404,
binance_vision_zip_name,
download_archive_ohlcv,
get_daily_ohlcv,
)
from freqtrade.util.datetime_helpers import dt_ts, dt_utc
@pytest.fixture(scope="module")
def event_loop_policy(request):
if sys.platform == "win32":
return asyncio.WindowsSelectorEventLoopPolicy()
else:
return asyncio.DefaultEventLoopPolicy()
class MockResponse:
"""AioHTTP response mock"""
def __init__(self, content, status, reason=""):
self._content = content
self.status = status
self.reason = reason
async def read(self):
return self._content
async def __aexit__(self, exc_type, exc, tb):
pass
async def __aenter__(self):
return self
# spot klines archive csv file format, the futures/um klines don't have the header line
#
# open_time,open,high,low,close,volume,close_time,quote_volume,count,taker_buy_volume,taker_buy_quote_volume,ignore # noqa: E501
# 1698364800000,34161.6,34182.5,33977.4,34024.2,409953,1698368399999,1202.97118037,15095,192220,564.12041453,0 # noqa: E501
# 1698368400000,34024.2,34060.1,33776.4,33848.4,740960,1698371999999,2183.75671155,23938,368266,1085.17080793,0 # noqa: E501
# 1698372000000,33848.5,34150.0,33815.1,34094.2,390376,1698375599999,1147.73267094,13854,231446,680.60405822,0 # noqa: E501
def make_response_from_url(start_date, end_date):
def make_daily_df(date, timeframe):
start = dt_utc(date.year, date.month, date.day)
end = start + timedelta(days=1)
date_col = pd.date_range(start, end, freq=timeframe.replace("m", "min"), inclusive="left")
cols = (
"open_time,open,high,low,close,volume,close_time,quote_volume,count,taker_buy_volume,"
"taker_buy_quote_volume,ignore"
)
df = pd.DataFrame(columns=cols.split(","), dtype=float)
df["open_time"] = date_col.astype("int64") // 10**6
df["open"] = df["high"] = df["low"] = df["close"] = df["volume"] = 1.0
return df
def make_daily_zip(asset_type_url_segment, symbol, timeframe, date) -> bytes:
df = make_daily_df(date, timeframe)
if asset_type_url_segment == "spot":
header = True
elif asset_type_url_segment == "futures/um":
header = None
else:
raise ValueError
csv = df.to_csv(index=False, header=header)
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as zipf:
zipf.writestr(binance_vision_zip_name(symbol, timeframe, date), csv)
return zip_buffer.getvalue()
def make_response(url):
pattern = (
r"https://data.binance.vision/data/(?P<asset_type_url_segment>spot|futures/um)"
r"/daily/klines/(?P<symbol>.*?)/(?P<timeframe>.*?)/(?P=symbol)-(?P=timeframe)-"
r"(?P<date>\d{4}-\d{2}-\d{2}).zip"
)
m = re.match(pattern, url)
if not m:
return MockResponse(content="", status=404)
date = datetime.datetime.strptime(m["date"], "%Y-%m-%d").date()
if date < start_date or date > end_date:
return MockResponse(content="", status=404)
zip_file = make_daily_zip(m["asset_type_url_segment"], m["symbol"], m["timeframe"], date)
return MockResponse(content=zip_file, status=200)
return make_response
@pytest.mark.parametrize(
"candle_type,pair,since,until,first_date,last_date,stop_on_404",
[
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59, 59),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 5),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 3, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2019, 12, 25),
dt_utc(2020, 1, 5),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 3, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2019, 1, 1),
dt_utc(2019, 1, 5),
None,
None,
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2021, 1, 1),
dt_utc(2021, 1, 5),
None,
None,
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 2),
None,
dt_utc(2020, 1, 2),
dt_utc(2020, 1, 3, 23),
False,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 5),
dt_utc(2020, 1, 1),
None,
None,
False,
),
(
CandleType.FUTURES,
"BTC/USDT:USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59, 59),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23),
False,
),
(
CandleType.INDEX,
"N/A",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 1, 23, 59, 59),
None,
None,
False,
),
# stop_on_404 = True
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2019, 12, 25),
dt_utc(2020, 1, 5),
None,
None,
True,
),
(
CandleType.SPOT,
"BTC/USDT",
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 5),
dt_utc(2020, 1, 1),
dt_utc(2020, 1, 3, 23),
True,
),
(
CandleType.FUTURES,
"BTC/USDT:USDT",
dt_utc(2019, 12, 25),
dt_utc(2020, 1, 5),
None,
None,
True,
),
],
)
async def test_download_archive_ohlcv(
mocker, candle_type, pair, since, until, first_date, last_date, stop_on_404
):
history_start = dt_utc(2020, 1, 1).date()
history_end = dt_utc(2020, 1, 3).date()
timeframe = "1h"
since_ms = dt_ts(since)
until_ms = dt_ts(until)
mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
side_effect=make_response_from_url(history_start, history_end),
)
markets = {"BTC/USDT": {"id": "BTCUSDT"}, "BTC/USDT:USDT": {"id": "BTCUSDT"}}
df = await download_archive_ohlcv(
candle_type,
pair,
timeframe,
since_ms=since_ms,
until_ms=until_ms,
markets=markets,
stop_on_404=stop_on_404,
)
if df.empty:
assert first_date is None and last_date is None
else:
assert candle_type in [CandleType.SPOT, CandleType.FUTURES]
assert df["date"].iloc[0] == first_date
assert df["date"].iloc[-1] == last_date
async def test_download_archive_ohlcv_exception(mocker):
timeframe = "1h"
pair = "BTC/USDT"
since_ms = dt_ts(dt_utc(2020, 1, 1))
until_ms = dt_ts(dt_utc(2020, 1, 2))
markets = {"BTC/USDT": {"id": "BTCUSDT"}, "BTC/USDT:USDT": {"id": "BTCUSDT"}}
mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get", side_effect=RuntimeError
)
df = await download_archive_ohlcv(
CandleType.SPOT, pair, timeframe, since_ms=since_ms, until_ms=until_ms, markets=markets
)
assert df.empty
async def test_get_daily_ohlcv(mocker, testdatadir):
symbol = "BTCUSDT"
timeframe = "1h"
date = dt_utc(2024, 10, 28).date()
first_date = dt_utc(2024, 10, 28)
last_date = dt_utc(2024, 10, 28, 23)
async with aiohttp.ClientSession() as session:
spot_path = (
testdatadir / "binance/binance_public_data/spot-klines-BTCUSDT-1h-2024-10-28.zip"
)
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(spot_path.read_bytes(), 200),
)
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
assert get.call_count == 1
assert df["date"].iloc[0] == first_date
assert df["date"].iloc[-1] == last_date
futures_path = (
testdatadir / "binance/binance_public_data/futures-um-klines-BTCUSDT-1h-2024-10-28.zip"
)
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(futures_path.read_bytes(), 200),
)
df = await get_daily_ohlcv("futures/um", symbol, timeframe, date, session)
assert get.call_count == 1
assert df["date"].iloc[0] == first_date
assert df["date"].iloc[-1] == last_date
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(b"", 404),
)
with pytest.raises(Http404):
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session, retry_delay=0)
assert get.call_count == 1
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(b"", 500),
)
mocker.patch("asyncio.sleep")
with pytest.raises(BadHttpStatus):
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
assert get.call_count == 4 # 1 + 3 default retries
get = mocker.patch(
"freqtrade.exchange.binance_public_data.aiohttp.ClientSession.get",
return_value=MockResponse(b"nop", 200),
)
with pytest.raises(zipfile.BadZipFile):
df = await get_daily_ohlcv("spot", symbol, timeframe, date, session)
assert get.call_count == 4 # 1 + 3 default retries
+54 -6
View File
@@ -2006,6 +2006,46 @@ def test_get_tickers(default_conf, mocker, exchange_name, caplog):
assert exchange.get_tickers() == {}
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_conversion_rate(default_conf_usdt, mocker, exchange_name):
api_mock = MagicMock()
tick = {
"ETH/USDT": {
"last": 42,
},
"BCH/USDT": {
"last": 41,
},
"ETH/BTC": {
"last": 250,
},
}
tick2 = {
"ADA/USDT:USDT": {
"last": 2.5,
}
}
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
api_mock.fetch_tickers = MagicMock(side_effect=[tick, tick2])
api_mock.fetch_bids_asks = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, exchange=exchange_name)
# retrieve original ticker
assert exchange.get_conversion_rate("USDT", "USDT") == 1
assert api_mock.fetch_tickers.call_count == 0
assert exchange.get_conversion_rate("ETH", "USDT") == 42
assert exchange.get_conversion_rate("ETH", "USDC") is None
assert exchange.get_conversion_rate("ETH", "BTC") == 250
assert exchange.get_conversion_rate("BTC", "ETH") == 0.004
assert api_mock.fetch_tickers.call_count == 1
api_mock.fetch_tickers.reset_mock()
assert exchange.get_conversion_rate("ADA", "USDT") == 2.5
# Only the call to the "others" market
assert api_mock.fetch_tickers.call_count == 1
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_ticker(default_conf, mocker, exchange_name):
api_mock = MagicMock()
@@ -2091,6 +2131,7 @@ def test___now_is_time_to_refresh(default_conf, mocker, exchange_name, time_mach
@pytest.mark.parametrize("candle_type", ["mark", ""])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_type):
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name)
pair = "ETH/BTC"
calls = 0
@@ -2123,7 +2164,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above OHLCV data after truncating the open candle.
assert len(ret) == 2
assert log_has_re(r"Downloaded data for .* with length .*\.", caplog)
assert log_has_re(r"Downloaded data for .* from ccxt with length .*\.", caplog)
caplog.clear()
@@ -2156,7 +2197,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
pair = "ETH/USDT"
respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, candle_type=candle_type, is_new_pair=False
pair, "5m", 1500000000000, candle_type=candle_type
)
assert respair == pair
assert restf == "5m"
@@ -2168,7 +2209,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
end_ts = 1_500_500_000_000
start_ts = 1_500_000_000_000
respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", since_ms=start_ts, candle_type=candle_type, is_new_pair=False, until_ms=end_ts
pair, "5m", since_ms=start_ts, candle_type=candle_type, until_ms=end_ts
)
# Required candles
candles = (end_ts - start_ts) / 300_000
@@ -4078,10 +4119,16 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
)
ex = Exchange(default_conf)
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
assert next(ex.get_valid_pair_combination("ETH", "BTC")) == "ETH/BTC"
assert next(ex.get_valid_pair_combination("BTC", "ETH")) == "ETH/BTC"
multicombs = list(ex.get_valid_pair_combination("ETH", "USDT"))
assert len(multicombs) == 2
assert "ETH/USDT" in multicombs
assert "ETH/USDT:USDT" in multicombs
with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."):
ex.get_valid_pair_combination("NOPAIR", "ETH")
for x in ex.get_valid_pair_combination("NOPAIR", "ETH"):
pass
@pytest.mark.parametrize(
@@ -6130,6 +6177,7 @@ def test_get_liquidation_price(
default_conf_usdt["exchange"]["name"] = exchange_name
default_conf_usdt["margin_mode"] = margin_mode
mocker.patch("freqtrade.exchange.gate.Gate.validate_ordertypes")
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name)
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
-25
View File
@@ -1,4 +1,3 @@
import platform
import sys
from copy import deepcopy
from pathlib import Path
@@ -20,30 +19,6 @@ def is_py12() -> bool:
return sys.version_info >= (3, 12)
def is_mac() -> bool:
machine = platform.system()
return "Darwin" in machine
def is_arm() -> bool:
machine = platform.machine()
return "arm" in machine or "aarch64" in machine
@pytest.fixture(autouse=True)
def patch_torch_initlogs(mocker) -> None:
if is_mac():
# Mock torch import completely
import sys
import types
module_name = "torch"
mocked_module = types.ModuleType(module_name)
sys.modules[module_name] = mocked_module
else:
mocker.patch("torch._logging._init_logs")
@pytest.fixture(scope="function")
def freqai_conf(default_conf, tmp_path):
freqaiconf = deepcopy(default_conf)
+1 -2
View File
@@ -10,11 +10,10 @@ from freqtrade.configuration import TimeRange
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from tests.conftest import get_patched_exchange
from tests.conftest import get_patched_exchange, is_mac
from tests.freqai.conftest import (
get_patched_data_kitchen,
get_patched_freqai_strategy,
is_mac,
make_unfiltered_dataframe,
)
+8 -3
View File
@@ -13,11 +13,16 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import Trade
from freqtrade.plugins.pairlistmanager import PairListManager
from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re
from tests.freqai.conftest import (
get_patched_freqai_strategy,
from tests.conftest import (
EXMS,
create_mock_trades,
get_patched_exchange,
is_arm,
is_mac,
log_has_re,
)
from tests.freqai.conftest import (
get_patched_freqai_strategy,
make_rl_config,
mock_pytorch_mlp_model_training_parameters,
)
+18 -2
View File
@@ -374,7 +374,7 @@ def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -
def test_create_trade(
default_conf_usdt, ticker_usdt, limit_order, fee, mocker, is_short, open_rate
) -> None:
patch_RPCManager(mocker)
send_msg_mock = patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
EXMS,
@@ -387,6 +387,7 @@ def test_create_trade(
whitelist = deepcopy(default_conf_usdt["exchange"]["pair_whitelist"])
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
send_msg_mock.reset_mock()
freqtrade.create_trade("ETH/USDT")
trade = Trade.session.scalars(select(Trade)).first()
@@ -402,6 +403,14 @@ def test_create_trade(
limit_order[entry_side(is_short)], "ADA/USDT", entry_side(is_short)
)
trade.update_trade(oobj)
assert send_msg_mock.call_count == 1
entry_msg = send_msg_mock.call_args_list[0][0][0]
assert entry_msg["type"] == RPCMessageType.ENTRY
assert entry_msg["stake_amount"] == trade.stake_amount
assert entry_msg["stake_currency"] == default_conf_usdt["stake_currency"]
assert entry_msg["pair"] == "ETH/USDT"
assert entry_msg["direction"] == ("Short" if is_short else "Long")
assert entry_msg["sub_trade"] is False
assert trade.open_rate == open_rate
assert trade.amount == 30.0
@@ -4021,7 +4030,7 @@ def test_get_real_amount_fees_order(
default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker
):
tfo_mock = mocker.patch(f"{EXMS}.get_trades_for_order", return_value=[])
mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value="BNB/USDT")
mocker.patch(f"{EXMS}.get_valid_pair_combination", return_value=["BNB/USDT"])
mocker.patch(f"{EXMS}.fetch_ticker", return_value={"last": 200})
trade = Trade(
pair="LTC/USDT",
@@ -5190,6 +5199,13 @@ def test_update_funding_fees(
open_exit_order = limit_order_open[exit_side(is_short)]
bid = 0.11
enter_rate_mock = MagicMock(return_value=bid)
open_order.update(
{
"status": "closed",
"filled": open_order["amount"],
"remaining": 0,
}
)
enter_mm = MagicMock(return_value=open_order)
patch_RPCManager(mocker)
patch_exchange(mocker)
@@ -29,7 +29,7 @@ def test_update_liquidation_prices(mocker, margin_mode, dry_run):
assert trade_mock.set_liquidation_price.call_count == 1
assert wallets.get_total.call_count == (
assert wallets.get_collateral.call_count == (
0 if margin_mode == MarginMode.ISOLATED or not dry_run else 1
)
+5 -3
View File
@@ -371,8 +371,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
mocker.patch("freqtrade.optimize.backtesting.Backtesting.backtest")
mocker.patch("freqtrade.optimize.backtesting.generate_backtest_stats")
mocker.patch("freqtrade.optimize.backtesting.show_backtest_results")
sbs = mocker.patch("freqtrade.optimize.backtesting.store_backtest_stats")
sbc = mocker.patch("freqtrade.optimize.backtesting.store_backtest_analysis_results")
sbs = mocker.patch("freqtrade.optimize.backtesting.store_backtest_results")
mocker.patch(
"freqtrade.plugins.pairlistmanager.PairListManager.whitelist",
PropertyMock(return_value=["UNITTEST/BTC"]),
@@ -397,7 +396,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
assert backtesting.strategy.bot_start.call_count == 1
assert backtesting.strategy.bot_loop_start.call_count == 0
assert sbs.call_count == 1
assert sbc.call_count == 1
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
@@ -568,6 +566,9 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
mocker.patch(f"{EXMS}.get_fee", fee)
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
mocker.patch(
"freqtrade.persistence.trade_model.price_to_precision", lambda p, *args, **kwargs: p
)
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p)
patch_exchange(mocker)
@@ -1842,6 +1843,7 @@ def test_backtest_multi_pair_long_short_switch(
if use_detail:
default_conf_usdt["timeframe_detail"] = "1m"
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float("inf"))
mocker.patch(f"{EXMS}.get_fee", fee)
+74 -11
View File
@@ -39,13 +39,34 @@ def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_res
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
hyperopt_results, 600 + 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600 + 100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
under = hl.hyperopt_loss_function(
hyperopt_results, 600 - 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600 - 100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
assert over > correct
assert under > correct
@@ -58,9 +79,25 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results)
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
longer = hl.hyperopt_loss_function(
hyperopt_results, 100, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
shorter = hl.hyperopt_loss_function(
results=resultsb,
trade_count=100,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": resultsb["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
shorter = hl.hyperopt_loss_function(resultsb, 100, datetime(2019, 1, 1), datetime(2019, 5, 1))
assert shorter < longer
@@ -73,11 +110,34 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
hyperopt_conf.update({"hyperopt_loss": "ShortTradeDurHyperOptLoss"})
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=hyperopt_results,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
results=results_over,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(results_over, 600, datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(
results_under, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)
results=results_under,
trade_count=600,
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=hyperopt_conf,
processed=None,
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
starting_balance=hyperopt_conf["dry_run_wallet"],
)
assert over < correct
assert under > correct
@@ -109,31 +169,34 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
default_conf.update({"hyperopt_loss": lossfunction})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(
hyperopt_results,
results=hyperopt_results,
trade_count=len(hyperopt_results),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
over = hl.hyperopt_loss_function(
results_over,
results=results_over,
trade_count=len(results_over),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": results_over["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
under = hl.hyperopt_loss_function(
results_under,
results=results_under,
trade_count=len(results_under),
min_date=datetime(2019, 1, 1),
max_date=datetime(2019, 5, 1),
config=default_conf,
processed=None,
backtest_stats={"profit_total": results_under["profit_abs"].sum()},
starting_balance=default_conf["dry_run_wallet"],
)
assert over < correct
assert under > correct

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