Compare commits

...

160 Commits

Author SHA1 Message Date
Matthias
f5a5c2d6b9 Improve imports 2023-04-08 16:44:33 +02:00
Matthias
a102cfdfc9 Add new /profit fields to API 2023-04-08 16:41:25 +02:00
Matthias
be72670ca2 Add documentation about /profit change 2023-04-08 16:40:14 +02:00
Matthias
cf2cb94f8d Add bot start date to /profit output 2023-04-08 16:38:44 +02:00
Matthias
fa3a81b022 convert Keys to enum 2023-04-08 16:28:50 +02:00
Matthias
7ff30c6df8 Add additional, typesafe getters 2023-04-08 16:24:38 +02:00
Matthias
7751768b2e Store initial_time value 2023-04-08 16:13:16 +02:00
Matthias
ac817b7808 Improve docstrings for key-value store 2023-04-08 10:09:31 +02:00
Matthias
4d4f4bf23e Add test for key_value_store 2023-04-08 10:07:21 +02:00
Matthias
c083723698 Add initial version of key value store 2023-04-08 10:07:03 +02:00
Matthias
f8d89c46e5 Don't reset open_order_id if the order didn't cancel 2023-04-07 19:49:13 +02:00
Matthias
1952e453bb Improved formatting for fetch order_or_stop calls 2023-04-07 17:35:11 +02:00
Matthias
77985fa591 Update thread name for uvicorn worker 2023-04-07 14:49:53 +02:00
Matthias
a75d891007 Ensure minimum sqlalchemy version is respected 2023-04-07 14:45:06 +02:00
Matthias
dae3f72be7 Bump Dockerfile to latest 3.10 2023-04-07 14:11:31 +02:00
Matthias
f03a99918a Ensure hyper param file can be loaded
closes #8452
2023-04-04 20:04:28 +02:00
Matthias
fe02f611fb Fix typo in reinforcement learning
closes #8431
2023-04-04 06:46:35 +02:00
Matthias
1b10a3a2bf Merge branch 'develop' of github.com:freqtrade/freqtrade into develop 2023-04-03 20:24:58 +02:00
Matthias
92a060c5b4 Make stop_price_parameter configurable by exchange 2023-04-03 20:18:57 +02:00
Matthias
096fd1916c Merge pull request #8445 from freqtrade/dependabot/pip/develop/tensorboard-2.12.1
Bump tensorboard from 2.12.0 to 2.12.1
2023-04-03 19:14:29 +02:00
Matthias
fb09a16127 Merge pull request #8438 from freqtrade/dependabot/pip/develop/types-tabulate-0.9.0.2
Bump types-tabulate from 0.9.0.1 to 0.9.0.2
2023-04-03 18:12:30 +02:00
Matthias
7fed0782d5 pre-commit types-tabulate 2023-04-03 14:19:11 +02:00
dependabot[bot]
30fc24bd8c Bump types-tabulate from 0.9.0.1 to 0.9.0.2
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.9.0.1 to 0.9.0.2.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 12:18:15 +00:00
Matthias
7e3de178e1 Merge pull request #8447 from freqtrade/dependabot/pip/develop/types-python-dateutil-2.8.19.11
Bump types-python-dateutil from 2.8.19.10 to 2.8.19.11
2023-04-03 14:17:24 +02:00
Matthias
0c9c9fff0e Merge branch 'develop' into dependabot/pip/develop/types-python-dateutil-2.8.19.11 2023-04-03 13:41:10 +02:00
Matthias
b96f6670e3 pre-commit dateutil 2023-04-03 13:28:17 +02:00
Matthias
6e02743256 Merge pull request #8446 from freqtrade/dependabot/pip/develop/types-requests-2.28.11.17
Bump types-requests from 2.28.11.16 to 2.28.11.17
2023-04-03 13:27:31 +02:00
Matthias
2b4fa92d09 Merge pull request #8444 from freqtrade/dependabot/pip/develop/ruff-0.0.260
Bump ruff from 0.0.259 to 0.0.260
2023-04-03 11:40:07 +02:00
Matthias
be250230b6 Merge pull request #8443 from freqtrade/dependabot/pip/develop/plotly-5.14.0
Bump plotly from 5.13.1 to 5.14.0
2023-04-03 11:39:42 +02:00
Matthias
5d33ffc015 Merge pull request #8442 from freqtrade/dependabot/pip/develop/orjson-3.8.9
Bump orjson from 3.8.8 to 3.8.9
2023-04-03 11:04:17 +02:00
Matthias
b48498f27f Types pre-commit 2023-04-03 10:16:56 +02:00
Matthias
e582d8bacb Merge pull request #8434 from freqtrade/dependabot/pip/develop/sqlalchemy-2.0.8
Bump sqlalchemy from 2.0.7 to 2.0.8
2023-04-03 10:16:00 +02:00
dependabot[bot]
ff40ee655b Bump types-python-dateutil from 2.8.19.10 to 2.8.19.11
Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.10 to 2.8.19.11.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 07:49:24 +00:00
dependabot[bot]
57deaad806 Bump types-requests from 2.28.11.16 to 2.28.11.17
Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.16 to 2.28.11.17.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 07:49:21 +00:00
dependabot[bot]
7779b82277 Bump tensorboard from 2.12.0 to 2.12.1
Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.12.0 to 2.12.1.
- [Release notes](https://github.com/tensorflow/tensorboard/releases)
- [Changelog](https://github.com/tensorflow/tensorboard/blob/2.12.1/RELEASE.md)
- [Commits](https://github.com/tensorflow/tensorboard/compare/2.12.0...2.12.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 07:49:18 +00:00
dependabot[bot]
2bd2058afa Bump ruff from 0.0.259 to 0.0.260
Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.259 to 0.0.260.
- [Release notes](https://github.com/charliermarsh/ruff/releases)
- [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md)
- [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.259...v0.0.260)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 07:49:12 +00:00
dependabot[bot]
bf7936b0af Bump plotly from 5.13.1 to 5.14.0
Bumps [plotly](https://github.com/plotly/plotly.py) from 5.13.1 to 5.14.0.
- [Release notes](https://github.com/plotly/plotly.py/releases)
- [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.py/compare/v5.13.1...v5.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 07:48:50 +00:00
dependabot[bot]
8236bbfd48 Bump orjson from 3.8.8 to 3.8.9
Bumps [orjson](https://github.com/ijl/orjson) from 3.8.8 to 3.8.9.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.8.8...3.8.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 07:48:43 +00:00
Matthias
4dc13ac16a Merge pull request #8437 from freqtrade/dependabot/pip/develop/ccxt-3.0.50
Bump ccxt from 3.0.37 to 3.0.50
2023-04-03 09:47:27 +02:00
Matthias
eb5423469a Merge pull request #8435 from freqtrade/dependabot/pip/develop/xgboost-1.7.5
Bump xgboost from 1.7.4 to 1.7.5
2023-04-03 09:47:09 +02:00
Matthias
43496d7929 bump sqlalchemy pre-commit 2023-04-03 09:46:32 +02:00
Matthias
92c70b6b90 Merge pull request #8441 from freqtrade/dependabot/github_actions/develop/pypa/gh-action-pypi-publish-1.8.4
Bump pypa/gh-action-pypi-publish from 1.8.3 to 1.8.4
2023-04-03 09:45:51 +02:00
Matthias
77897c7d6b Merge pull request #8439 from freqtrade/dependabot/pip/develop/mkdocs-material-9.1.5
Bump mkdocs-material from 9.1.4 to 9.1.5
2023-04-03 09:45:26 +02:00
Matthias
531861573a Merge pull request #8436 from freqtrade/dependabot/pip/develop/types-cachetools-5.3.0.5
Bump types-cachetools from 5.3.0.4 to 5.3.0.5
2023-04-03 09:45:10 +02:00
Matthias
c9b904eb0e Fix typos in documentation 2023-04-03 06:49:30 +02:00
Matthias
372f1cb37f Reduce verbosity for stop orders 2023-04-03 06:37:31 +02:00
Matthias
a3acdd5240 apply stop-reserve to minimum limits only when necessary
it's unnecessary for amount - but necessary for Cost / price limits.
2023-04-03 06:37:31 +02:00
Matthias
e6a125719e Slightly refactor _get_stake_amount_limit 2023-04-03 06:37:31 +02:00
Matthias
78a1551798 Reorder get_stake_limit 2023-04-03 06:37:31 +02:00
Matthias
6f79d14c9c pre-commit - bump cachetools 2023-04-03 06:37:15 +02:00
Matthias
28d8722fa7 Merge pull request #8433 from freqtrade/dependabot/pip/develop/websockets-11.0
Bump websockets from 10.4 to 11.0
2023-04-03 06:36:30 +02:00
dependabot[bot]
2715b2ccf0 Bump pypa/gh-action-pypi-publish from 1.8.3 to 1.8.4
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.3 to 1.8.4.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.3...v1.8.4)

---
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>
2023-04-03 03:58:12 +00:00
dependabot[bot]
2ea575cb31 Bump mkdocs-material from 9.1.4 to 9.1.5
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.4 to 9.1.5.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.4...9.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 03:57:30 +00:00
dependabot[bot]
1b31c54162 Bump ccxt from 3.0.37 to 3.0.50
Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.37 to 3.0.50.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/3.0.37...3.0.50)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 03:57:19 +00:00
dependabot[bot]
e289c10b6c Bump types-cachetools from 5.3.0.4 to 5.3.0.5
Bumps [types-cachetools](https://github.com/python/typeshed) from 5.3.0.4 to 5.3.0.5.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 03:57:10 +00:00
dependabot[bot]
26ed1ca07c Bump xgboost from 1.7.4 to 1.7.5
Bumps [xgboost](https://github.com/dmlc/xgboost) from 1.7.4 to 1.7.5.
- [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/v1.7.4...v1.7.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 03:57:05 +00:00
dependabot[bot]
b1e20bcd1e Bump sqlalchemy from 2.0.7 to 2.0.8
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.7 to 2.0.8.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 03:57:00 +00:00
dependabot[bot]
12a73bc151 Bump websockets from 10.4 to 11.0
Bumps [websockets](https://github.com/aaugustin/websockets) from 10.4 to 11.0.
- [Release notes](https://github.com/aaugustin/websockets/releases)
- [Commits](https://github.com/aaugustin/websockets/compare/10.4...11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 03:56:46 +00:00
Matthias
19e112f399 Merge pull request #8427 from initrv/typo-fix-constants
Typo fix constants
2023-04-02 07:42:15 +02:00
initrv
cccf4f305b fix randomize_starting_position typo 2023-04-02 03:42:05 +03:00
Matthias
dc7e834911 Fix some type issues 2023-04-01 20:17:56 +02:00
Matthias
a630799984 Merge pull request #8423 from freqtrade/add-profit-trade-history
make trade_type value more explicit, add profit to trade_history dict
2023-04-01 15:19:54 +02:00
Matthias
916e1bbc7c Merge pull request #8412 from freqtrade/fix/partial_stops
support partially filled stops
2023-04-01 15:18:42 +02:00
Robert Caulk
631cb44f5c ensure python code block renders 2023-04-01 15:16:48 +02:00
Robert Caulk
367186cc34 Update freqai-feature-engineering.md
The `metadata` section of `freqai-feature-engineering.md` had a misplaced whitespace in front of the title. 

This PR removes the whitespace.
2023-04-01 15:16:43 +02:00
robcaulk
92f34f262e make trade_type value more explicit, add profit to trade_history dict 2023-04-01 10:05:58 +02:00
Matthias
5e13b48648 Merge pull request #8386 from freqtrade/feature/price_to_precision_round
price to precision rounding
2023-03-31 07:20:10 +02:00
Matthias
6dfb1a1d14 Improve docker regular build caching 2023-03-31 06:49:12 +02:00
Matthias
f8330800d1 Improve docker arm builds 2023-03-31 06:49:02 +02:00
Matthias
3ec7c72da1 Bump develop version to 2023.4.dev 2023-03-30 07:06:23 +02:00
Matthias
fa7c29fe9f Update producer docs to reflect proper datatype
closes #8419
2023-03-29 20:43:23 +02:00
Matthias
861c577138 Support partially filled stop orders
closes #8374
2023-03-29 07:05:39 +02:00
Matthias
e062a74e70 Add test for partial stop order canceling
part of #8374
2023-03-29 06:57:17 +02:00
Matthias
c330c493d5 test for Handle stop on exchange partial filled
part of #8374
2023-03-29 06:57:17 +02:00
Matthias
8a49d62068 Don't update liquidation price for closed trades 2023-03-29 06:49:22 +02:00
Matthias
a642524928 Improve integration test correctness 2023-03-29 06:48:00 +02:00
Matthias
eb96490c99 Improve some more stoploss tests 2023-03-28 20:28:05 +02:00
Matthias
6282b42741 Remove further Magicmock trade 2023-03-28 19:38:43 +02:00
Matthias
513df4515b Improve stoploss tests 2023-03-28 19:19:55 +02:00
Matthias
411e21f430 Improve stop test 2023-03-28 18:13:26 +02:00
Matthias
f0b5f95fd6 Remove missleading comment 2023-03-28 18:10:26 +02:00
Matthias
736c396d98 Use correct amount for stoploss test 2023-03-28 16:45:54 +02:00
Matthias
2860e817bd Update cached binance leverage Tiers 2023-03-28 07:05:37 +02:00
Matthias
19b78fbc22 Override ccxt's marketOrderRequiresPrice settings for gate 2023-03-28 06:57:18 +02:00
Matthias
cde432fef0 Enable gate market orders
closes #8368
2023-03-28 06:56:11 +02:00
Matthias
8ae44c204e Merge pull request #8361 from TheJoeSchr/feature/trades-feather
featherdatahandler: implement trades_store/_trades_load
2023-03-27 21:05:30 +02:00
Matthias
ed0e7ead31 Fix wrong import 2023-03-27 20:36:05 +02:00
Matthias
3928051baf Revert unneeded formatting changes 2023-03-27 20:35:26 +02:00
Matthias
e35c85000e Excude raspberry from catboost installs
closes #8404
2023-03-27 20:19:23 +02:00
Matthias
85776db692 Merge pull request #8401 from freqtrade/dependabot/pip/develop/ccxt-3.0.37
Bump ccxt from 3.0.36 to 3.0.37
2023-03-27 11:02:44 +02:00
Matthias
ce81af08d8 Merge pull request #8398 from freqtrade/dependabot/pip/develop/mkdocs-material-9.1.4
Bump mkdocs-material from 9.1.3 to 9.1.4
2023-03-27 11:00:57 +02:00
Matthias
5aa6c1dfae Merge pull request #8402 from freqtrade/dependabot/pip/develop/pydantic-1.10.7
Bump pydantic from 1.10.6 to 1.10.7
2023-03-27 11:00:40 +02:00
dependabot[bot]
4f4dfa2a59 Bump pydantic from 1.10.6 to 1.10.7
Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.6 to 1.10.7.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v1.10.7/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.6...v1.10.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 07:50:06 +00:00
dependabot[bot]
90669e0ba9 Bump ccxt from 3.0.36 to 3.0.37
Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.36 to 3.0.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/3.0.36...3.0.37)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 07:49:56 +00:00
Matthias
bc9f6d30c1 Merge pull request #8391 from freqtrade/dependabot/pip/develop/types-requests-2.28.11.16
Bump types-requests from 2.28.11.15 to 2.28.11.16
2023-03-27 09:47:34 +02:00
Matthias
4ae2333306 Merge pull request #8399 from freqtrade/dependabot/pip/develop/filelock-3.10.6
Bump filelock from 3.10.0 to 3.10.6
2023-03-27 09:47:16 +02:00
Matthias
8c63e3dc4f Merge pull request #8396 from freqtrade/dependabot/pip/develop/cryptography-40.0.1
Bump cryptography from 39.0.2 to 40.0.1
2023-03-27 09:47:02 +02:00
Matthias
b0dddd35ca Merge pull request #8395 from freqtrade/dependabot/pip/develop/pre-commit-3.2.1
Bump pre-commit from 3.2.0 to 3.2.1
2023-03-27 09:45:57 +02:00
Matthias
96ba75179b Merge pull request #8400 from freqtrade/dependabot/github_actions/develop/pypa/gh-action-pypi-publish-1.8.3
Bump pypa/gh-action-pypi-publish from 1.8.1 to 1.8.3
2023-03-27 08:28:18 +02:00
Matthias
2589717375 Merge pull request #8397 from freqtrade/dependabot/pip/develop/orjson-3.8.8
Bump orjson from 3.8.7 to 3.8.8
2023-03-27 08:00:46 +02:00
dependabot[bot]
bc0816aa66 Bump cryptography from 39.0.2 to 40.0.1
Bumps [cryptography](https://github.com/pyca/cryptography) from 39.0.2 to 40.0.1.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/39.0.2...40.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 05:15:59 +00:00
dependabot[bot]
1743ad7946 Bump pre-commit from 3.2.0 to 3.2.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.2.0...v3.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 05:14:04 +00:00
Matthias
9367cbcfd3 Merge pull request #8390 from freqtrade/dependabot/pip/develop/ccxt-3.0.36
Bump ccxt from 3.0.23 to 3.0.36
2023-03-27 07:10:39 +02:00
Matthias
43a7b9236b Merge pull request #8393 from freqtrade/dependabot/pip/develop/ruff-0.0.259
Bump ruff from 0.0.257 to 0.0.259
2023-03-27 07:00:38 +02:00
Matthias
4891174a71 list-data should sort pairs also in timerange mode 2023-03-27 06:44:36 +02:00
Matthias
8845f765db pre-commit - bump requests 2023-03-27 06:25:11 +02:00
dependabot[bot]
7e11bce4f4 Bump pypa/gh-action-pypi-publish from 1.8.1 to 1.8.3
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.1 to 1.8.3.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.1...v1.8.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>
2023-03-27 03:58:02 +00:00
dependabot[bot]
8955e09175 Bump filelock from 3.10.0 to 3.10.6
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.10.0 to 3.10.6.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.10.0...3.10.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 03:58:00 +00:00
dependabot[bot]
d13ea71a58 Bump mkdocs-material from 9.1.3 to 9.1.4
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.3 to 9.1.4.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.3...9.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 03:57:55 +00:00
dependabot[bot]
b72f61080b Bump orjson from 3.8.7 to 3.8.8
Bumps [orjson](https://github.com/ijl/orjson) from 3.8.7 to 3.8.8.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.8.7...3.8.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 03:57:46 +00:00
dependabot[bot]
75c31cc8cc Bump ruff from 0.0.257 to 0.0.259
Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.257 to 0.0.259.
- [Release notes](https://github.com/charliermarsh/ruff/releases)
- [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md)
- [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.257...v0.0.259)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 03:57:14 +00:00
dependabot[bot]
1b3d9efedd Bump types-requests from 2.28.11.15 to 2.28.11.16
Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.15 to 2.28.11.16.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 03:56:55 +00:00
dependabot[bot]
2f8f60373e Bump ccxt from 3.0.23 to 3.0.36
Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.23 to 3.0.36.
- [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/3.0.23...3.0.36)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 03:56:49 +00:00
Matthias
72284317c2 Fix failing backtest test 2023-03-26 18:21:21 +02:00
Matthias
80a27bc0db Fix random uvicorn error 2023-03-26 18:18:52 +02:00
Matthias
1c9abd9e35 Properly respect can_short flag in backtesting
closes  #8387
2023-03-26 17:27:52 +02:00
Matthias
c14ac8a205 Properly handle non-replaced first entry orders 2023-03-26 16:46:41 +02:00
Matthias
b09fb5826f don't use "can_short" in backtesting to determine application of leverage 2023-03-26 16:21:51 +02:00
Matthias
fb1541bdf6 Explicitly close loop in async tests 2023-03-26 16:21:51 +02:00
Matthias
444d18aa39 Revert binance PO fix, since ccxt has fixed this bug. 2023-03-26 16:21:51 +02:00
Matthias
91ab4abba8 Merge pull request #8389 from escanoro/patch-1
typo:  above should be below
2023-03-26 15:45:29 +02:00
escanoro
16057da6cc typo: above should be below 2023-03-26 14:09:41 +02:00
Matthias
d97500581d Merge pull request #8379 from xmatthias/type_sendmsg
Type sendmsg
2023-03-26 14:09:01 +02:00
Matthias
31a396bc25 Merge pull request #8272 from paranoidandy/bot-loop-start-every-candle-bt
Make strategy.bot_loop_start run once per candle in backtest
2023-03-26 13:21:08 +02:00
Matthias
7cdcd97c26 Update tests for new logic. 2023-03-26 11:30:44 +02:00
Matthias
73b59df77b Merge branch 'develop' into pr/paranoidandy/8272 2023-03-26 11:22:24 +02:00
Matthias
86aef7cf9d Add current_time to bot_loop_start callbak 2023-03-26 11:22:19 +02:00
Matthias
159090c0e7 Add explicit tests for TRUNCATE mode 2023-03-26 11:14:34 +02:00
Matthias
0cb28f3d82 Use kwarg for rounding_mode, update tests with additional parameter 2023-03-26 11:00:41 +02:00
Matthias
d0d0cbe1d1 Implement price_to_precision logic for stoploss 2023-03-26 10:37:18 +02:00
Matthias
02078456fc Merge branch 'develop' into pr/asuiu/8296 2023-03-26 10:28:02 +02:00
Matthias
01dfb1cba8 Revert having price_rounding_mode as configuration 2023-03-26 10:24:47 +02:00
Matthias
ee205ddc86 Improve trade.from_json when stops are used 2023-03-25 20:26:56 +01:00
Matthias
298f5685ee Reuse existing "cancel_stoploss" call 2023-03-25 20:06:21 +01:00
Matthias
486d8a48a0 Fix docs (buffer_train_data_candles is an integer, not a boolean)
closes #8384
2023-03-25 19:36:28 +01:00
Matthias
d426077445 Merge branch 'develop' of github.com:freqtrade/freqtrade into develop 2023-03-25 16:33:07 +01:00
Matthias
9aa455fcd4 Merge pull request #8364 from freqtrade/robcaulk-patch-1
Update freqai_interface.py
2023-03-25 16:27:25 +01:00
Robert Caulk
d9c8b322ce Update freqai_interface.py 2023-03-25 13:37:07 +01:00
robcaulk
68154a1f52 document why users cant arbitrarily change parameter spaces... 2023-03-25 11:57:52 +01:00
Matthias
f7c1ee6d3e add precision values to api schema 2023-03-25 11:55:47 +01:00
Matthias
9c6a49436b Export amount/price precisions per trade 2023-03-25 11:42:19 +01:00
Matthias
75464c22f5 Merge pull request #8382 from linquanisaac/develop
docs(protections): fix typo
2023-03-25 11:36:35 +01:00
linquanisaac
cdd44a4005 docs(protections): fix typo 2023-03-25 17:19:58 +08:00
Matthias
34313a7af6 Merge remote-tracking branch 'origin/develop' into type_sendmsg 2023-03-25 09:23:00 +01:00
Matthias
c0a57d352f send base_currency with messages that need it. 2023-03-25 08:16:07 +01:00
Matthias
cbdd86d777 Fix test failures due to additional field 2023-03-24 21:05:10 +01:00
Matthias
281dd7785e Fix some remaining type errors 2023-03-24 20:56:18 +01:00
Matthias
ad58bac810 Type WS messagetypes 2023-03-24 20:54:28 +01:00
Matthias
8928d3616a Improve msgtypes 2023-03-24 20:47:53 +01:00
Matthias
e8cffeeffd Update RPCStatusMessage type 2023-03-24 20:36:29 +01:00
Matthias
76d289f0ce Don't overwrite types 2023-03-24 20:35:01 +01:00
Matthias
245ae99273 Further typing ... 2023-03-24 20:33:00 +01:00
Matthias
70ad7b42b1 Improve msg typing 2023-03-24 20:33:00 +01:00
Matthias
0ece73578c Add typedDict for RPC messages
Currently not fully functional.
2023-03-24 20:33:00 +01:00
Robert Caulk
bdf19f1d66 Update freqai_interface.py 2023-03-21 22:44:56 +01:00
Joe Schr
0128b63c1c add 'feather' to AVAILABLE_DATAHANDLERS_TRADES 2023-03-21 19:13:32 +01:00
Joe Schr
e16db814fa featherdatahandler: implement trades_store/_trades_load 2023-03-21 17:56:51 +01:00
ASU
1132fa6093 feat: Added price_rounding modes in config 2023-03-09 02:11:31 +02:00
Andy Lawless
b262f0b374 Update docs re: bot_loop_start in backtest 2023-03-03 20:46:43 +00:00
Andy Lawless
a3dee9350f Move bot_loop_start call to run on every candle 2023-03-03 20:37:05 +00:00
79 changed files with 3283 additions and 1889 deletions

View File

@@ -425,7 +425,7 @@ jobs:
python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.1
uses: pypa/gh-action-pypi-publish@v1.8.4
if: (github.event_name == 'release')
with:
user: __token__
@@ -433,7 +433,7 @@ jobs:
repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.1
uses: pypa/gh-action-pypi-publish@v1.8.4
if: (github.event_name == 'release')
with:
user: __token__

View File

@@ -13,12 +13,12 @@ repos:
- id: mypy
exclude: build_helpers
additional_dependencies:
- types-cachetools==5.3.0.4
- types-cachetools==5.3.0.5
- types-filelock==3.2.7
- types-requests==2.28.11.15
- types-tabulate==0.9.0.1
- types-python-dateutil==2.8.19.10
- SQLAlchemy==2.0.7
- types-requests==2.28.11.17
- types-tabulate==0.9.0.2
- types-python-dateutil==2.8.19.11
- SQLAlchemy==2.0.8
# stages: [push]
- repo: https://github.com/pycqa/isort

View File

@@ -1,4 +1,4 @@
FROM python:3.10.10-slim-bullseye as base
FROM python:3.10.11-slim-bullseye as base
# Setup env
ENV LANG C.UTF-8

View File

@@ -42,9 +42,9 @@ if [ $? -ne 0 ]; then
return 1
fi
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
# Tag image for upload and next build step
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM

View File

@@ -58,9 +58,9 @@ fi
# Tag image for upload and next build step
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI

View File

@@ -60,10 +60,10 @@ This loop will be repeated again and again until the bot is stopped.
* Load historic data for configured pairlist.
* Calls `bot_start()` once.
* Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
* Loops per candle simulating entry and exit points.
* Calls `bot_loop_start()` strategy callback.
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
* Calls `adjust_entry_price()` strategy callback for open entry orders.
* Check for trade entry signals (`enter_long` / `enter_short` columns).

View File

@@ -6,8 +6,8 @@ Low level feature engineering is performed in the user strategy within a set of
| Function | Description |
|---------------|-------------|
| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
@@ -182,11 +182,11 @@ In total, the number of features the user of the presented example strat has cre
$= 3 * 3 * 3 * 2 * 2 = 108$.
### Gain finer control over `feature_engineering_*` functions with `metadata`
### Gain finer control over `feature_engineering_*` functions with `metadata`
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
```py
```python
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
if metadata["tf"] == "1h":
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)

View File

@@ -46,7 +46,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> Default: `False`.
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Boolean. <br> Default: `False`.
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Integer. <br> Default: `0`.
### Data split parameters

View File

@@ -55,7 +55,7 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
dataframe["&-action"] = 0
```
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
```python
def feature_engineering_standard(self, dataframe, **kwargs):
@@ -180,7 +180,7 @@ As you begin to modify the strategy and the prediction model, you will quickly r
# you can use feature values from dataframe
# Assumes the shifted RSI indicator has been generated in the strategy.
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{pair}_"
rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_"
f"{self.config['timeframe']}"].iloc[self._current_tick]
# reward agent for entering trades

View File

@@ -128,6 +128,9 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
???+ danger "Continual learning enforces a constant parameter space"
Since `continual_learning` means that the model parameter space *cannot* change between trainings, `principal_component_analysis` is automatically disabled when `continual_learning` is enabled. Hint: PCA changes the parameter space and the number of features, learn more about PCA [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis).
## Hyperopt
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):

View File

@@ -149,7 +149,7 @@ The below example assumes a timeframe of 1 hour:
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
* Locks all pairs that had 2 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
``` python

View File

@@ -42,14 +42,14 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
| `producers` | **Required.** List of producers <br> **Datatype:** Array.
| `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.<br> **Datatype:** string
| `producers.host` | **Required.** The hostname or IP address from your producer.<br> **Datatype:** string
| `producers.port` | **Required.** The port matching the above host.<br> **Datatype:** string
| `producers.port` | **Required.** The port matching the above host.<br>*Defaults to `8080`.*<br> **Datatype:** Integer
| `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.<br> **Datatype:** string
| `producers.ws_token` | **Required.** `ws_token` as configured on the producer.<br> **Datatype:** string
| | **Optional settings**
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
| `wait_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
| `ping_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `False`.*<br> **Datatype:** Boolean.
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.

View File

@@ -1,6 +1,6 @@
markdown==3.3.7
mkdocs==1.4.2
mkdocs-material==9.1.3
mkdocs-material==9.1.5
mdx_truly_sane_lists==1.3
pymdown-extensions==9.10
jinja2==3.1.2

View File

@@ -51,7 +51,8 @@ During hyperopt, this runs only once at startup.
## Bot loop start
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5
seconds, unless configured differently) or once per candle in backtest/hyperopt mode.
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
``` python
@@ -61,11 +62,12 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods
def bot_loop_start(self, **kwargs) -> None:
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
if self.config['runmode'].value in ('live', 'dry_run'):

View File

@@ -279,6 +279,7 @@ Return a summary of your profit/loss and performance.
> ∙ `33.095 EUR`
>
> **Total Trade Count:** `138`
> **Bot started:** `2022-07-11 18:40:44`
> **First Trade opened:** `3 days ago`
> **Latest Trade opened:** `2 minutes ago`
> **Avg. Duration:** `2:33:45`
@@ -292,6 +293,7 @@ The relative profit of `15.2 Σ%` is be based on the starting capital - so in th
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
### /forceexit <trade_id>

View File

@@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2023.3.dev'
__version__ = '2023.4.dev'
if 'dev' in __version__:
from pathlib import Path

View File

@@ -204,11 +204,14 @@ def start_list_data(args: Dict[str, Any]) -> None:
pair, timeframe, candle_type,
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
) for pair, timeframe, candle_type in paircombs]
print(tabulate([
(pair, timeframe, candle_type,
start.strftime(DATETIME_PRINT_FORMAT),
end.strftime(DATETIME_PRINT_FORMAT))
for pair, timeframe, candle_type, start, end in paircombs1
for pair, timeframe, candle_type, start, end in sorted(
paircombs1,
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
],
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
tablefmt='psql', stralign='right'))

View File

@@ -36,9 +36,10 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5']
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet']
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
BACKTEST_CACHE_DEFAULT = 'day'
@@ -597,7 +598,7 @@ CONF_SCHEMA = {
"model_type": {"type": "string", "default": "PPO"},
"policy_type": {"type": "string", "default": "MlpPolicy"},
"net_arch": {"type": "array", "default": [128, 128]},
"randomize_startinng_position": {"type": "boolean", "default": False},
"randomize_starting_position": {"type": "boolean", "default": False},
"model_reward_parameters": {
"type": "object",
"properties": {

View File

@@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange, timeframe_to_seconds
from freqtrade.exchange.types import OrderBook
from freqtrade.misc import append_candles_to_dataframe
from freqtrade.rpc import RPCManager
from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg
from freqtrade.util import PeriodicCache
@@ -118,8 +119,7 @@ class DataProvider:
:param new_candle: This is a new candle
"""
if self.__rpc:
self.__rpc.send_msg(
{
msg: RPCAnalyzedDFMsg = {
'type': RPCMessageType.ANALYZED_DF,
'data': {
'key': pair_key,
@@ -127,7 +127,7 @@ class DataProvider:
'la': datetime.now(timezone.utc)
}
}
)
self.__rpc.send_msg(msg)
if new_candle:
self.__rpc.send_msg({
'type': RPCMessageType.NEW_CANDLE,

View File

@@ -4,7 +4,7 @@ from typing import Optional
from pandas import DataFrame, read_feather, to_datetime
from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
from freqtrade.enums import CandleType
from .idatahandler import IDataHandler
@@ -92,12 +92,11 @@ class FeatherDataHandler(IDataHandler):
:param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
"""
# filename = self._pair_trades_filename(self._datadir, pair)
filename = self._pair_trades_filename(self._datadir, pair)
self.create_dir_if_needed(filename)
raise NotImplementedError()
# array = pa.array(data)
# array
# feather.write_feather(data, filename)
tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
def trades_append(self, pair: str, data: TradeList):
"""
@@ -116,14 +115,13 @@ class FeatherDataHandler(IDataHandler):
:param timerange: Timerange to load trades for - currently not implemented
:return: List of trades
"""
raise NotImplementedError()
# filename = self._pair_trades_filename(self._datadir, pair)
# tradesdata = misc.file_load_json(filename)
filename = self._pair_trades_filename(self._datadir, pair)
if not filename.exists():
return []
# if not tradesdata:
# return []
tradesdata = read_feather(filename)
# return tradesdata
return tradesdata.values.tolist()
@classmethod
def _get_file_extension(cls):

View File

@@ -8,15 +8,15 @@ from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.bybit import Bybit
from freqtrade.exchange.coinbasepro import Coinbasepro
from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts,
amount_to_precision, available_exchanges,
ccxt_exchanges, contracts_to_amount,
date_minus_candles, is_exchange_known_ccxt,
market_is_active, price_to_precision,
timeframe_to_minutes, timeframe_to_msecs,
timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds, validate_exchange,
validate_exchanges)
from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
amount_to_contracts, amount_to_precision,
available_exchanges, ccxt_exchanges,
contracts_to_amount, date_minus_candles,
is_exchange_known_ccxt, market_is_active,
price_to_precision, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds,
validate_exchange, validate_exchanges)
from freqtrade.exchange.gate import Gate
from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.huobi import Huobi

View File

@@ -7,7 +7,6 @@ from typing import Dict, List, Optional, Tuple
import arrow
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
@@ -49,26 +48,6 @@ class Binance(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'GTC',
) -> Dict:
params = super()._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
if (
time_in_force == 'PO'
and ordertype != 'market'
and self.trading_mode == TradingMode.SPOT
# Only spot can do post only orders
):
params.pop('timeInForce')
params['postOnly'] = True
return params
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
tickers = super().get_tickers(symbols=symbols, cached=cached)
if self.trading_mode == TradingMode.FUTURES:

File diff suppressed because it is too large Load Diff

View File

@@ -30,13 +30,14 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
RetryableOrderError, TemporaryError)
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
retrier_async)
from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision,
amount_to_contracts, amount_to_precision,
contracts_to_amount, date_minus_candles,
is_exchange_known_ccxt, market_is_active,
price_to_precision, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType,
amount_to_contract_precision, amount_to_contracts,
amount_to_precision, contracts_to_amount,
date_minus_candles, is_exchange_known_ccxt,
market_is_active, price_to_precision,
timeframe_to_minutes, timeframe_to_msecs,
timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds)
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
safe_value_fallback2)
@@ -59,6 +60,7 @@ class Exchange:
# or by specifying them in the configuration.
_ft_has_default: Dict = {
"stoploss_on_exchange": False,
"stop_price_param": "stopPrice",
"order_time_in_force": ["GTC"],
"ohlcv_params": {},
"ohlcv_candle_limit": 500,
@@ -80,6 +82,8 @@ class Exchange:
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
"marketOrderRequiresPrice": False,
}
_ft_has: Dict = {}
_ft_has_futures: Dict = {}
@@ -205,6 +209,8 @@ class Exchange:
and self._api_async.session):
logger.debug("Closing async ccxt session.")
self.loop.run_until_complete(self._api_async.close())
if self.loop and not self.loop.is_closed():
self.loop.close()
def validate_config(self, config):
# Check if timeframe is available
@@ -730,12 +736,14 @@ class Exchange:
"""
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
def price_to_precision(self, pair: str, price: float) -> float:
def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
"""
Returns the price rounded up to the precision the Exchange accepts.
Rounds up
Returns the price rounded to the precision the Exchange accepts.
The default price_rounding_mode in conf is ROUND.
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
"""
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
return price_to_precision(price, self.get_precision_price(pair),
self.precisionMode, rounding_mode=rounding_mode)
def price_get_one_pip(self, pair: str, price: float) -> float:
"""
@@ -758,12 +766,12 @@ class Exchange:
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage)
if max_stake_amount is None:
# * Should never be executed
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
'never set max_stake_amount to None')
return max_stake_amount / leverage
return max_stake_amount
def _get_stake_amount_limit(
self,
@@ -781,43 +789,41 @@ class Exchange:
except KeyError:
raise ValueError(f"Can't get market information for symbol {pair}")
if isMin:
# reserve some percent defined in config (5% default) + stoploss
margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent',
DEFAULT_AMOUNT_RESERVE_PERCENT)
stoploss_reserve = (
margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
)
# it should not be more than 50%
stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
else:
margin_reserve = 1.0
stoploss_reserve = 1.0
stake_limits = []
limits = market['limits']
if (limits['cost'][limit] is not None):
stake_limits.append(
self._contracts_to_amount(
pair,
limits['cost'][limit]
)
self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve
)
if (limits['amount'][limit] is not None):
stake_limits.append(
self._contracts_to_amount(
pair,
limits['amount'][limit] * price
)
self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve
)
if not stake_limits:
return None if isMin else float('inf')
# reserve some percent defined in config (5% default) + stoploss
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
DEFAULT_AMOUNT_RESERVE_PERCENT)
amount_reserve_percent = (
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
)
# it should not be more than 50%
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
# The value returned should satisfy both limits: for amount (base currency) and
# for cost (quote, stake currency), so max() is used here.
# See also #2575 at github.
return self._get_stake_amount_considering_leverage(
max(stake_limits) * amount_reserve_percent,
max(stake_limits) if isMin else min(stake_limits),
leverage or 1.0
) if isMin else min(stake_limits)
)
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
"""
@@ -1038,6 +1044,13 @@ class Exchange:
params.update({'reduceOnly': True})
return params
def _order_needs_price(self, ordertype: str) -> bool:
return (
ordertype != 'market'
or self._api.options.get("createMarketBuyOrderRequiresPrice", False)
or self._ft_has.get('marketOrderRequiresPrice', False)
)
def create_order(
self,
*,
@@ -1060,8 +1073,7 @@ class Exchange:
try:
# Set the precision for amount and price(rate) as accepted by the exchange
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
needs_price = (ordertype != 'market'
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
needs_price = self._order_needs_price(ordertype)
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
if not reduceOnly:
@@ -1104,11 +1116,11 @@ class Exchange:
"""
if not self._ft_has.get('stoploss_on_exchange'):
raise OperationalException(f"stoploss is not implemented for {self.name}.")
price_param = self._ft_has['stop_price_param']
return (
order.get('stopPrice', None) is None
or ((side == "sell" and stop_loss > float(order['stopPrice'])) or
(side == "buy" and stop_loss < float(order['stopPrice'])))
order.get(price_param, None) is None
or ((side == "sell" and stop_loss > float(order[price_param])) or
(side == "buy" and stop_loss < float(order[price_param])))
)
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
@@ -1148,8 +1160,8 @@ class Exchange:
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
# Verify if stopPrice works for your exchange!
params.update({'stopPrice': stop_price})
# Verify if stopPrice works for your exchange, else configure stop_price_param
params.update({self._ft_has['stop_price_param']: stop_price})
return params
@retrier(retries=0)
@@ -1175,12 +1187,12 @@ class Exchange:
user_order_type = order_types.get('stoploss', 'market')
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
stop_price_norm = self.price_to_precision(pair, stop_price)
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
limit_rate = None
if user_order_type == 'limit':
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
limit_rate = self.price_to_precision(pair, limit_rate)
limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
if self._config['dry_run']:
dry_order = self.create_dry_run_order(

View File

@@ -2,11 +2,12 @@
Exchange support utils
"""
from datetime import datetime, timedelta, timezone
from math import ceil
from math import ceil, floor
from typing import Any, Dict, List, Optional, Tuple
import ccxt
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
TRUNCATE, decimal_to_precision)
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
from freqtrade.util import FtPrecise
@@ -219,35 +220,51 @@ def amount_to_contract_precision(
return amount
def price_to_precision(price: float, price_precision: Optional[float],
precisionMode: Optional[int]) -> float:
def price_to_precision(
price: float,
price_precision: Optional[float],
precisionMode: Optional[int],
*,
rounding_mode: int = ROUND,
) -> float:
"""
Returns the price rounded up to the precision the Exchange accepts.
Returns the price rounded to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
which does not support rounding up.
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
align with amount_to_precision().
!!! Rounds up
:param price: price to convert
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:param rounding_mode: rounding mode to use. Defaults to ROUND
:return: price rounded up to the precision the Exchange accepts
"""
if price_precision is not None and precisionMode is not None:
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
# precision=price_precision,
# counting_mode=self.precisionMode,
# ))
if precisionMode == TICK_SIZE:
if rounding_mode == ROUND:
ticks = price / price_precision
rounded_ticks = round(ticks)
return rounded_ticks * price_precision
precision = FtPrecise(price_precision)
price_str = FtPrecise(price)
missing = price_str % precision
if not missing == FtPrecise("0"):
price = round(float(str(price_str - missing + precision)), 14)
else:
symbol_prec = price_precision
big_price = price * pow(10, symbol_prec)
price = ceil(big_price) / pow(10, symbol_prec)
return round(float(str(price_str - missing + precision)), 14)
return price
elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
ndigits = round(price_precision)
if rounding_mode == ROUND:
return round(price, ndigits)
ticks = price * (10**ndigits)
if rounding_mode == ROUND_UP:
return ceil(ticks) / (10**ndigits)
if rounding_mode == TRUNCATE:
return int(ticks) / (10**ndigits)
if rounding_mode == ROUND_DOWN:
return floor(ticks) / (10**ndigits)
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
raise ValueError(f"Unknown precisionMode {precisionMode}")
return price

View File

@@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Tuple
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, PriceType, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
from freqtrade.misc import safe_value_fallback2
@@ -28,10 +27,12 @@ class Gate(Exchange):
"order_time_in_force": ['GTC', 'IOC'],
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
"marketOrderRequiresPrice": True,
}
_ft_has_futures: Dict = {
"needs_trading_fees": True,
"marketOrderRequiresPrice": False,
"tickers_have_bid_ask": False,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
@@ -50,14 +51,6 @@ class Gate(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
def validate_ordertypes(self, order_types: Dict) -> None:
if self.trading_mode != TradingMode.FUTURES:
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
super().validate_stop_ordertypes(order_types)
def _get_params(
self,
side: BuySell,

View File

@@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_utils import ROUND_DOWN, ROUND_UP
from freqtrade.exchange.types import Tickers
@@ -109,6 +110,7 @@ class Kraken(Exchange):
if self.trading_mode == TradingMode.FUTURES:
params.update({'reduceOnly': True})
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
if order_types.get('stoploss', 'market') == 'limit':
ordertype = "stop-loss-limit"
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
@@ -116,11 +118,11 @@ class Kraken(Exchange):
limit_rate = stop_price * limit_price_pct
else:
limit_rate = stop_price * (2 - limit_price_pct)
params['price2'] = self.price_to_precision(pair, limit_rate)
params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
else:
ordertype = "stop-loss"
stop_price = self.price_to_precision(pair, stop_price)
stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
if self._config['dry_run']:
dry_order = self.create_dry_run_order(

View File

@@ -28,6 +28,7 @@ class Okx(Exchange):
"funding_fee_timeframe": "8h",
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
"stop_price_param": "stopLossPrice",
}
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
@@ -162,29 +163,12 @@ class Okx(Exchange):
return pair_tiers[-1]['maxNotional'] / leverage
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
# Verify if stopPrice works for your exchange!
params.update({'stopLossPrice': stop_price})
params = super()._get_stop_params(side, ordertype, stop_price)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
params['posSide'] = self._get_posSide(side, True)
return params
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
OKX uses non-default stoploss price naming.
"""
if not self._ft_has.get('stoploss_on_exchange'):
raise OperationalException(f"stoploss is not implemented for {self.name}.")
return (
order.get('stopLossPrice', None) is None
or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
(side == "buy" and stop_loss < float(order['stopLossPrice'])))
)
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
if self._config['dry_run']:
return self.fetch_dry_run_order(order_id)

View File

@@ -66,7 +66,7 @@ class Base3ActionRLEnv(BaseEnvironment):
elif action == Actions.Sell.value and not self.can_short:
self._update_total_profit()
self._position = Positions.Neutral
trade_type = "neutral"
trade_type = "exit"
self._last_trade_tick = None
else:
print("case not defined")
@@ -74,7 +74,7 @@ class Base3ActionRLEnv(BaseEnvironment):
if trade_type is not None:
self.trade_history.append(
{'price': self.current_price(), 'index': self._current_tick,
'type': trade_type})
'type': trade_type, 'profit': self.get_unrealized_profit()})
if (self._total_profit < self.max_drawdown or
self._total_unrealized_profit < self.max_drawdown):

View File

@@ -52,16 +52,6 @@ class Base4ActionRLEnv(BaseEnvironment):
trade_type = None
if self.is_tradesignal(action):
"""
Action: Neutral, position: Long -> Close Long
Action: Neutral, position: Short -> Close Short
Action: Long, position: Neutral -> Open Long
Action: Long, position: Short -> Close Short and Open Long
Action: Short, position: Neutral -> Open Short
Action: Short, position: Long -> Close Long and Open Short
"""
if action == Actions.Neutral.value:
self._position = Positions.Neutral
@@ -69,16 +59,16 @@ class Base4ActionRLEnv(BaseEnvironment):
self._last_trade_tick = None
elif action == Actions.Long_enter.value:
self._position = Positions.Long
trade_type = "long"
trade_type = "enter_long"
self._last_trade_tick = self._current_tick
elif action == Actions.Short_enter.value:
self._position = Positions.Short
trade_type = "short"
trade_type = "enter_short"
self._last_trade_tick = self._current_tick
elif action == Actions.Exit.value:
self._update_total_profit()
self._position = Positions.Neutral
trade_type = "neutral"
trade_type = "exit"
self._last_trade_tick = None
else:
print("case not defined")
@@ -86,7 +76,7 @@ class Base4ActionRLEnv(BaseEnvironment):
if trade_type is not None:
self.trade_history.append(
{'price': self.current_price(), 'index': self._current_tick,
'type': trade_type})
'type': trade_type, 'profit': self.get_unrealized_profit()})
if (self._total_profit < self.max_drawdown or
self._total_unrealized_profit < self.max_drawdown):

View File

@@ -53,16 +53,6 @@ class Base5ActionRLEnv(BaseEnvironment):
trade_type = None
if self.is_tradesignal(action):
"""
Action: Neutral, position: Long -> Close Long
Action: Neutral, position: Short -> Close Short
Action: Long, position: Neutral -> Open Long
Action: Long, position: Short -> Close Short and Open Long
Action: Short, position: Neutral -> Open Short
Action: Short, position: Long -> Close Long and Open Short
"""
if action == Actions.Neutral.value:
self._position = Positions.Neutral
@@ -70,21 +60,21 @@ class Base5ActionRLEnv(BaseEnvironment):
self._last_trade_tick = None
elif action == Actions.Long_enter.value:
self._position = Positions.Long
trade_type = "long"
trade_type = "enter_long"
self._last_trade_tick = self._current_tick
elif action == Actions.Short_enter.value:
self._position = Positions.Short
trade_type = "short"
trade_type = "enter_short"
self._last_trade_tick = self._current_tick
elif action == Actions.Long_exit.value:
self._update_total_profit()
self._position = Positions.Neutral
trade_type = "neutral"
trade_type = "exit_long"
self._last_trade_tick = None
elif action == Actions.Short_exit.value:
self._update_total_profit()
self._position = Positions.Neutral
trade_type = "neutral"
trade_type = "exit_short"
self._last_trade_tick = None
else:
print("case not defined")
@@ -92,7 +82,7 @@ class Base5ActionRLEnv(BaseEnvironment):
if trade_type is not None:
self.trade_history.append(
{'price': self.current_price(), 'index': self._current_tick,
'type': trade_type})
'type': trade_type, 'profit': self.get_unrealized_profit()})
if (self._total_profit < self.max_drawdown or
self._total_unrealized_profit < self.max_drawdown):

View File

@@ -105,6 +105,9 @@ class IFreqaiModel(ABC):
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
self.can_short = True # overridden in start() with strategy.can_short
self.model: Any = None
if self.ft_params.get('principal_component_analysis', False) and self.continual_learning:
self.ft_params.update({'principal_component_analysis': False})
logger.warning('User tried to use PCA with continual learning. Deactivating PCA.')
record_params(config, self.full_path)
@@ -154,8 +157,7 @@ class IFreqaiModel(ABC):
dk = self.start_backtesting(dataframe, metadata, self.dk, strategy)
dataframe = dk.remove_features_from_df(dk.return_dataframe)
else:
logger.info(
"Backtesting using historic predictions (live models)")
logger.info("Backtesting using historic predictions (live models)")
dk = self.start_backtesting_from_historic_predictions(
dataframe, metadata, self.dk)
dataframe = dk.return_dataframe

View File

@@ -21,15 +21,19 @@ from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode,
State, TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, timeframe_to_next_date,
timeframe_to_seconds)
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, init_db
from freqtrade.persistence.key_value_store import set_startup_time
from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg,
RPCSellMsg)
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise
@@ -179,6 +183,7 @@ class FreqtradeBot(LoggingMixin):
performs startup tasks
"""
migrate_binance_futures_names(self.config)
set_startup_time()
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
# Update older trades with precision and precision mode
@@ -212,7 +217,8 @@ class FreqtradeBot(LoggingMixin):
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
self.strategy.gather_informative_pairs())
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
current_time=datetime.now(timezone.utc))
self.strategy.analyze(self.active_pair_whitelist)
@@ -850,11 +856,13 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Canceling stoploss on exchange for {trade}")
co = self.exchange.cancel_stoploss_order_with_result(
trade.stoploss_order_id, trade.pair, trade.amount)
trade.update_order(co)
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
# Reset stoploss order id.
trade.stoploss_order_id = None
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} "
f"for pair {trade.pair}")
return trade
def get_valid_enter_price_and_stake(
@@ -941,12 +949,11 @@ class FreqtradeBot(LoggingMixin):
return enter_limit_requested, stake_amount, leverage
def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None,
def _notify_enter(self, trade: Trade, order: Order, order_type: str,
fill: bool = False, sub_trade: bool = False) -> None:
"""
Sends rpc notification when a entry order occurred.
"""
msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
open_rate = order.safe_price
if open_rate is None:
@@ -957,9 +964,9 @@ class FreqtradeBot(LoggingMixin):
current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
msg = {
msg: RPCBuyMsg = {
'trade_id': trade.id,
'type': msg_type,
'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag,
'exchange': trade.exchange.capitalize(),
@@ -971,6 +978,7 @@ class FreqtradeBot(LoggingMixin):
'order_type': order_type,
'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'],
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
'fiat_currency': self.config.get('fiat_display_currency', None),
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
'open_date': trade.open_date or datetime.utcnow(),
@@ -989,7 +997,7 @@ class FreqtradeBot(LoggingMixin):
current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
msg = {
msg: RPCCancelMsg = {
'trade_id': trade.id,
'type': RPCMessageType.ENTRY_CANCEL,
'buy_tag': trade.enter_tag,
@@ -1001,7 +1009,9 @@ class FreqtradeBot(LoggingMixin):
'limit': trade.open_rate,
'order_type': order_type,
'stake_amount': trade.stake_amount,
'open_rate': trade.open_rate,
'stake_currency': self.config['stake_currency'],
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
'fiat_currency': self.config.get('fiat_display_currency', None),
'amount': trade.amount,
'open_date': trade.open_date,
@@ -1165,7 +1175,8 @@ class FreqtradeBot(LoggingMixin):
logger.warning('Unable to fetch stoploss order: %s', exception)
if stoploss_order:
trade.update_order(stoploss_order)
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
stoploss_order=True)
# We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
@@ -1229,7 +1240,9 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order
:return: None
"""
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation)
stoploss_norm = self.exchange.price_to_precision(
trade.pair, trade.stoploss_or_liquidation,
rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP)
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
# we check if the update is necessary
@@ -1239,13 +1252,8 @@ class FreqtradeBot(LoggingMixin):
# cancelling the current stoploss on exchange first
logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
f"(orderid:{order['id']}) in order to add another one ...")
try:
co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair,
trade.amount)
trade.update_order(co)
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {order['id']} "
f"for pair {trade.pair}")
self.cancel_stoploss_on_exchange(trade)
# Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
@@ -1477,8 +1485,8 @@ class FreqtradeBot(LoggingMixin):
return False
try:
order = self.exchange.cancel_order_with_result(order['id'], trade.pair,
trade.amount)
order = self.exchange.cancel_order_with_result(
order['id'], trade.pair, trade.amount)
except InvalidOrderException:
logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
@@ -1490,17 +1498,18 @@ class FreqtradeBot(LoggingMixin):
# Order might be filled above in odd timing issues.
if order.get('status') in ('canceled', 'cancelled'):
trade.exit_reason = None
trade.open_order_id = None
else:
trade.exit_reason = exit_reason_prev
cancelled = True
else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
trade.exit_reason = None
trade.open_order_id = None
self.update_trade_state(trade, trade.open_order_id, order)
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
trade.open_order_id = None
trade.close_rate = None
trade.close_rate_requested = None
@@ -1666,7 +1675,7 @@ class FreqtradeBot(LoggingMixin):
amount = trade.amount
gain = "profit" if profit_ratio > 0 else "loss"
msg = {
msg: RPCSellMsg = {
'type': (RPCMessageType.EXIT_FILL if fill
else RPCMessageType.EXIT),
'trade_id': trade.id,
@@ -1692,6 +1701,7 @@ class FreqtradeBot(LoggingMixin):
'close_date': trade.close_date or datetime.utcnow(),
'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'],
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
'fiat_currency': self.config.get('fiat_display_currency'),
'sub_trade': sub_trade,
'cumulative_profit': trade.realized_profit,
@@ -1722,7 +1732,7 @@ class FreqtradeBot(LoggingMixin):
profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss"
msg = {
msg: RPCSellCancelMsg = {
'type': RPCMessageType.EXIT_CANCEL,
'trade_id': trade.id,
'exchange': trade.exchange.capitalize(),
@@ -1744,6 +1754,7 @@ class FreqtradeBot(LoggingMixin):
'open_date': trade.open_date,
'close_date': trade.close_date or datetime.now(timezone.utc),
'stake_currency': self.config['stake_currency'],
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
'fiat_currency': self.config.get('fiat_display_currency', None),
'reason': reason,
'sub_trade': sub_trade,
@@ -1775,11 +1786,11 @@ class FreqtradeBot(LoggingMixin):
return False
# Update trade with order values
logger.info(f'Found open order for {trade}')
if not stoploss_order:
logger.info(f'Found open order for {trade}')
try:
order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id,
trade.pair,
stoploss_order)
order = action_order or self.exchange.fetch_order_or_stoploss_order(
order_id, trade.pair, stoploss_order)
except InvalidOrderException as exception:
logger.warning('Unable to fetch order %s: %s', order_id, exception)
return False
@@ -1808,7 +1819,7 @@ class FreqtradeBot(LoggingMixin):
# TODO: should shorting/leverage be supported by Edge,
# then this will need to be fixed.
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
if order.get('side') == trade.entry_side or trade.amount > 0:
if order.get('side') == trade.entry_side or (trade.amount > 0 and trade.is_open):
# Must also run for partial exits
# TODO: Margin will need to use interest_rate as well.
# interest_rate = self.exchange.get_interest_rate()
@@ -1844,21 +1855,27 @@ class FreqtradeBot(LoggingMixin):
self.handle_protections(trade.pair, trade.trade_direction)
elif send_msg and not trade.open_order_id and not stoploss_order:
# Enter fill
self._notify_enter(trade, order, fill=True, sub_trade=sub_trade)
self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
def handle_protections(self, pair: str, side: LongShort) -> None:
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock')
prot_trig = self.protections.stop_per_pair(pair, side=side)
if prot_trig:
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
msg.update(prot_trig.to_json())
msg: RPCProtectionMsg = {
'type': RPCMessageType.PROTECTION_TRIGGER,
'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair),
**prot_trig.to_json() # type: ignore
}
self.rpc.send_msg(msg)
prot_trig_glb = self.protections.global_stop(side=side)
if prot_trig_glb:
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
msg.update(prot_trig_glb.to_json())
msg = {
'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair),
**prot_trig_glb.to_json() # type: ignore
}
self.rpc.send_msg(msg)
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,

View File

@@ -203,9 +203,10 @@ class Backtesting:
# since a "perfect" stoploss-exit is assumed anyway
# And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False
# Update can_short flag
self._can_short = self.trading_mode != TradingMode.SPOT and strategy.can_short
self.strategy.ft_bot_start()
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
def _load_protections(self, strategy: IStrategy):
if self.config.get('enable_protections', False):
@@ -740,7 +741,7 @@ class Backtesting:
proposed_leverage=1.0,
max_leverage=max_leverage,
side=direction, entry_tag=entry_tag,
) if self._can_short else 1.0
) if self.trading_mode != TradingMode.SPOT else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
@@ -1030,6 +1031,9 @@ class Backtesting:
requested_stake=(
order.safe_remaining * order.ft_price / trade.leverage),
direction='short' if trade.is_short else 'long')
# Delete trade if no successful entries happened (if placing the new order failed)
if trade.open_order_id is None and trade.nr_of_successful_entries == 0:
return True
self.replaced_entry_orders += 1
else:
# assumption: there can't be multiple open entry orders at any given time
@@ -1155,6 +1159,8 @@ class Backtesting:
while current_time <= end_date:
open_trade_count_start = LocalTrade.bt_open_open_trade_count
self.check_abort()
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
current_time=current_time)
for i, pair in enumerate(data):
row_index = indexes[pair]
row = self.validate_row(data, pair, row_index, current_time)

View File

@@ -23,6 +23,8 @@ logger = logging.getLogger(__name__)
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN
def hyperopt_serializer(x):
if isinstance(x, np.integer):
@@ -76,9 +78,18 @@ class HyperoptTools():
with filename.open('w') as f:
rapidjson.dump(final_params, f, indent=2,
default=hyperopt_serializer,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
number_mode=HYPER_PARAMS_FILE_FORMAT
)
@staticmethod
def load_params(filename: Path) -> Dict:
"""
Load parameters from file
"""
with filename.open('r') as f:
params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT)
return params
@staticmethod
def try_export_params(config: Config, strategy_name: str, params: Dict):
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
@@ -189,7 +200,7 @@ class HyperoptTools():
for s in ['buy', 'sell', 'protection',
'roi', 'stoploss', 'trailing', 'max_open_trades']:
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT))
else:
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:",

View File

@@ -1,5 +1,6 @@
# flake8: noqa: F401
from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore
from freqtrade.persistence.models import init_db
from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.persistence.trade_model import LocalTrade, Order, Trade

View File

@@ -0,0 +1,179 @@
from datetime import datetime, timezone
from enum import Enum
from typing import ClassVar, Optional, Union
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from freqtrade.persistence.base import ModelBase, SessionType
ValueTypes = Union[str, datetime, float, int]
class ValueTypesEnum(str, Enum):
STRING = 'str'
DATETIME = 'datetime'
FLOAT = 'float'
INT = 'int'
class KeyStoreKeys(str, Enum):
BOT_START_TIME = 'bot_start_time'
STARTUP_TIME = 'startup_time'
class _KeyValueStoreModel(ModelBase):
"""
Pair Locks database model.
"""
__tablename__ = 'KeyValueStore'
session: ClassVar[SessionType]
id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True)
value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
string_value: Mapped[Optional[str]]
datetime_value: Mapped[Optional[datetime]]
float_value: Mapped[Optional[float]]
int_value: Mapped[Optional[int]]
class KeyValueStore():
"""
Generic bot-wide, persistent key-value store
Can be used to store generic values, e.g. very first bot startup time.
Supports the types str, datetime, float and int.
"""
@staticmethod
def store_value(key: KeyStoreKeys, value: ValueTypes) -> None:
"""
Store the given value for the given key.
:param key: Key to store the value for - can be used in get-value to retrieve the key
:param value: Value to store - can be str, datetime, float or int
"""
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
_KeyValueStoreModel.key == key).first()
if kv is None:
kv = _KeyValueStoreModel(key=key)
if isinstance(value, str):
kv.value_type = ValueTypesEnum.STRING
kv.string_value = value
elif isinstance(value, datetime):
kv.value_type = ValueTypesEnum.DATETIME
kv.datetime_value = value
elif isinstance(value, float):
kv.value_type = ValueTypesEnum.FLOAT
kv.float_value = value
elif isinstance(value, int):
kv.value_type = ValueTypesEnum.INT
kv.int_value = value
else:
raise ValueError(f'Unknown value type {kv.value_type}')
_KeyValueStoreModel.session.add(kv)
_KeyValueStoreModel.session.commit()
@staticmethod
def delete_value(key: KeyStoreKeys) -> None:
"""
Delete the value for the given key.
:param key: Key to delete the value for
"""
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
_KeyValueStoreModel.key == key).first()
if kv is not None:
_KeyValueStoreModel.session.delete(kv)
_KeyValueStoreModel.session.commit()
@staticmethod
def get_value(key: KeyStoreKeys) -> Optional[ValueTypes]:
"""
Get the value for the given key.
:param key: Key to get the value for
"""
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
_KeyValueStoreModel.key == key).first()
if kv is None:
return None
if kv.value_type == ValueTypesEnum.STRING:
return kv.string_value
if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None:
return kv.datetime_value.replace(tzinfo=timezone.utc)
if kv.value_type == ValueTypesEnum.FLOAT:
return kv.float_value
if kv.value_type == ValueTypesEnum.INT:
return kv.int_value
# This should never happen unless someone messed with the database manually
raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover
@staticmethod
def get_string_value(key: KeyStoreKeys) -> Optional[str]:
"""
Get the value for the given key.
:param key: Key to get the value for
"""
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
_KeyValueStoreModel.key == key,
_KeyValueStoreModel.value_type == ValueTypesEnum.STRING).first()
if kv is None:
return None
return kv.string_value
@staticmethod
def get_datetime_value(key: KeyStoreKeys) -> Optional[datetime]:
"""
Get the value for the given key.
:param key: Key to get the value for
"""
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
_KeyValueStoreModel.key == key,
_KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME).first()
if kv is None or kv.datetime_value is None:
return None
return kv.datetime_value.replace(tzinfo=timezone.utc)
@staticmethod
def get_float_value(key: KeyStoreKeys) -> Optional[float]:
"""
Get the value for the given key.
:param key: Key to get the value for
"""
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
_KeyValueStoreModel.key == key,
_KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT).first()
if kv is None:
return None
return kv.float_value
@staticmethod
def get_int_value(key: KeyStoreKeys) -> Optional[int]:
"""
Get the value for the given key.
:param key: Key to get the value for
"""
kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter(
_KeyValueStoreModel.key == key,
_KeyValueStoreModel.value_type == ValueTypesEnum.INT).first()
if kv is None:
return None
return kv.int_value
def set_startup_time():
"""
sets bot_start_time to the first trade open date - or "now" on new databases.
sets startup_time to "now"
"""
st = KeyValueStore.get_value('bot_start_time')
if st is None:
from freqtrade.persistence import Trade
t = Trade.session.query(Trade).order_by(Trade.open_date.asc()).first()
if t is not None:
KeyValueStore.store_value('bot_start_time', t.open_date_utc)
else:
KeyValueStore.store_value('bot_start_time', datetime.now(timezone.utc))
KeyValueStore.store_value('startup_time', datetime.now(timezone.utc))

View File

@@ -13,6 +13,7 @@ from sqlalchemy.pool import StaticPool
from freqtrade.exceptions import OperationalException
from freqtrade.persistence.base import ModelBase
from freqtrade.persistence.key_value_store import _KeyValueStoreModel
from freqtrade.persistence.migrations import check_migrate
from freqtrade.persistence.pairlock import PairLock
from freqtrade.persistence.trade_model import Order, Trade
@@ -76,6 +77,7 @@ def init_db(db_url: str) -> None:
bind=engine, autoflush=False), scopefunc=get_request_or_thread_id)
Order.session = Trade.session
PairLock.session = Trade.session
_KeyValueStoreModel.session = Trade.session
previous_tables = inspect(engine).get_table_names()
ModelBase.metadata.create_all(engine)

View File

@@ -15,7 +15,8 @@ from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPE
BuySell, LongShort)
from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import amount_to_contract_precision, price_to_precision
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
price_to_precision)
from freqtrade.leverage import interest
from freqtrade.persistence.base import ModelBase, SessionType
from freqtrade.util import FtPrecise
@@ -560,6 +561,9 @@ class LocalTrade():
'trading_mode': self.trading_mode,
'funding_fees': self.funding_fees,
'open_order_id': self.open_order_id,
'amount_precision': self.amount_precision,
'price_precision': self.price_precision,
'precision_mode': self.precision_mode,
'orders': orders,
}
@@ -594,7 +598,8 @@ class LocalTrade():
"""
Method used internally to set self.stop_loss.
"""
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode)
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode,
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
if not self.stop_loss:
self.initial_stop_loss = stop_loss_norm
self.stop_loss = stop_loss_norm
@@ -625,7 +630,8 @@ class LocalTrade():
if self.initial_stop_loss_pct is None or refresh:
self.__set_stop_loss(new_loss, stoploss)
self.initial_stop_loss = price_to_precision(
new_loss, self.price_precision, self.precision_mode)
new_loss, self.price_precision, self.precision_mode,
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
self.initial_stop_loss_pct = -1 * abs(stoploss)
# evaluate if the stop loss needs to be updated
@@ -689,21 +695,24 @@ class LocalTrade():
else:
logger.warning(
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
if self.is_open:
logger.info(f'{order.order_type.upper()} is hit for {self}.')
else:
raise ValueError(f'Unknown order type: {order.order_type}')
if order.ft_order_side != self.entry_side:
amount_tr = amount_to_contract_precision(self.amount, self.amount_precision,
self.precision_mode, self.contract_size)
if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC):
self.close(order.safe_price)
else:
self.recalc_trade_from_orders()
elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'):
self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
if self.is_open:
logger.info(f'{order.order_type.upper()} is hit for {self}.')
self.close(order.safe_price)
else:
raise ValueError(f'Unknown order type: {order.order_type}')
Trade.commit()
def close(self, rate: float, *, show_msg: bool = True) -> None:
@@ -1660,8 +1669,10 @@ class Trade(ModelBase, LocalTrade):
stop_loss=data["stop_loss_abs"],
stop_loss_pct=data["stop_loss_ratio"],
stoploss_order_id=data["stoploss_order_id"],
stoploss_last_update=(datetime.fromtimestamp(data["stoploss_last_update"] // 1000,
tz=timezone.utc) if data["stoploss_last_update"] else None),
stoploss_last_update=(
datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
tz=timezone.utc)
if data["stoploss_last_update_timestamp"] else None),
initial_stop_loss=data["initial_stop_loss_abs"],
initial_stop_loss_pct=data["initial_stop_loss_ratio"],
min_rate=data["min_rate"],

View File

@@ -1,4 +1,5 @@
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional
@@ -635,7 +636,7 @@ def load_and_plot_trades(config: Config):
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange)
strategy.ft_bot_start()
strategy.bot_loop_start()
strategy.bot_loop_start(datetime.now(timezone.utc))
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements['timerange']
trades = plot_elements['trades']

View File

@@ -6,6 +6,7 @@ from typing import Any, Dict, Optional
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import ROUND_UP
from freqtrade.exchange.types import Ticker
from freqtrade.plugins.pairlist.IPairList import IPairList
@@ -61,9 +62,10 @@ class PrecisionFilter(IPairList):
stop_price = ticker['last'] * self._stoploss
# Adjust stop-prices to precision
sp = self._exchange.price_to_precision(pair, stop_price)
sp = self._exchange.price_to_precision(pair, stop_price, rounding_mode=ROUND_UP)
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99)
stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99,
rounding_mode=ROUND_UP)
logger.debug(f"{pair} - {sp} : {stop_gap_price}")
if sp <= stop_gap_price:

View File

@@ -108,6 +108,8 @@ class Profit(BaseModel):
max_drawdown: float
max_drawdown_abs: float
trading_volume: Optional[float]
bot_start_timestamp: int
bot_start_date: str
class SellReason(BaseModel):
@@ -276,6 +278,10 @@ class TradeSchema(BaseModel):
funding_fees: Optional[float]
trading_mode: Optional[TradingMode]
amount_precision: Optional[float]
price_precision: Optional[float]
precision_mode: Optional[int]
class OpenTradeSchema(TradeSchema):
stoploss_current_dist: Optional[float]

View File

@@ -55,7 +55,7 @@ class UvicornServer(uvicorn.Server):
@contextlib.contextmanager
def run_in_thread(self):
self.thread = threading.Thread(target=self.run)
self.thread = threading.Thread(target=self.run, name='FTUvicorn')
self.thread.start()
while not self.started:
time.sleep(1e-3)

View File

@@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
from freqtrade.rpc.rpc_types import RPCSendMsg
logger = logging.getLogger(__name__)
@@ -108,7 +109,7 @@ class ApiServer(RPCHandler):
cls._has_rpc = False
cls._rpc = None
def send_msg(self, msg: Dict[str, Any]) -> None:
def send_msg(self, msg: RPCSendMsg) -> None:
"""
Publish the message to the message stream
"""

View File

@@ -26,10 +26,11 @@ from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler
from freqtrade.misc import decimals_per_coin, shorten_date
from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, 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.wallets import PositionWallet, Wallet
@@ -79,7 +80,7 @@ class RPCHandler:
""" Cleanup pending module resources """
@abstractmethod
def send_msg(self, msg: Dict[str, str]) -> None:
def send_msg(self, msg: RPCSendMsg) -> None:
""" Sends a message to all registered rpc modules """
@@ -542,6 +543,7 @@ class RPC:
first_date = trades[0].open_date if trades else None
last_date = trades[-1].open_date if trades else None
num = float(len(durations) or 1)
bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME)
return {
'profit_closed_coin': profit_closed_coin_sum,
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
@@ -575,6 +577,8 @@ class RPC:
'max_drawdown': max_drawdown,
'max_drawdown_abs': max_drawdown_abs,
'trading_volume': trading_volume,
'bot_start_timestamp': int(bot_start.timestamp() * 1000) if bot_start else 0,
'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '',
}
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:

View File

@@ -3,11 +3,12 @@ This module contains class to manage RPC communications (Telegram, API, ...)
"""
import logging
from collections import deque
from typing import Any, Dict, List
from typing import List
from freqtrade.constants import Config
from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType
from freqtrade.rpc import RPC, RPCHandler
from freqtrade.rpc.rpc_types import RPCSendMsg
logger = logging.getLogger(__name__)
@@ -58,7 +59,7 @@ class RPCManager:
mod.cleanup()
del mod
def send_msg(self, msg: Dict[str, Any]) -> None:
def send_msg(self, msg: RPCSendMsg) -> None:
"""
Send given message to all registered rpc modules.
A message consists of one or more key value pairs of strings.
@@ -69,10 +70,6 @@ class RPCManager:
"""
if msg.get('type') not in NO_ECHO_MESSAGES:
logger.info('Sending rpc message: %s', msg)
if 'pair' in msg:
msg.update({
'base_currency': self._rpc._freqtrade.exchange.get_pair_base_currency(msg['pair'])
})
for mod in self.registered_modules:
logger.debug('Forwarding message to rpc.%s', mod.name)
try:

128
freqtrade/rpc/rpc_types.py Normal file
View File

@@ -0,0 +1,128 @@
from datetime import datetime
from typing import Any, List, Literal, Optional, TypedDict, Union
from freqtrade.constants import PairWithTimeframe
from freqtrade.enums import RPCMessageType
class RPCSendMsgBase(TypedDict):
pass
# ty1pe: Literal[RPCMessageType]
class RPCStatusMsg(RPCSendMsgBase):
"""Used for Status, Startup and Warning messages"""
type: Literal[RPCMessageType.STATUS, RPCMessageType.STARTUP, RPCMessageType.WARNING]
status: str
class RPCStrategyMsg(RPCSendMsgBase):
"""Used for Status, Startup and Warning messages"""
type: Literal[RPCMessageType.STRATEGY_MSG]
msg: str
class RPCProtectionMsg(RPCSendMsgBase):
type: Literal[RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL]
id: int
pair: str
base_currency: Optional[str]
lock_time: str
lock_timestamp: int
lock_end_time: str
lock_end_timestamp: int
reason: str
side: str
active: bool
class RPCWhitelistMsg(RPCSendMsgBase):
type: Literal[RPCMessageType.WHITELIST]
data: List[str]
class __RPCBuyMsgBase(RPCSendMsgBase):
trade_id: int
buy_tag: Optional[str]
enter_tag: Optional[str]
exchange: str
pair: str
base_currency: str
leverage: Optional[float]
direction: str
limit: float
open_rate: float
order_type: str
stake_amount: float
stake_currency: str
fiat_currency: Optional[str]
amount: float
open_date: datetime
current_rate: Optional[float]
sub_trade: bool
class RPCBuyMsg(__RPCBuyMsgBase):
type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]
class RPCCancelMsg(__RPCBuyMsgBase):
type: Literal[RPCMessageType.ENTRY_CANCEL]
reason: str
class RPCSellMsg(__RPCBuyMsgBase):
type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]
cumulative_profit: float
gain: str # Literal["profit", "loss"]
close_rate: float
profit_amount: float
profit_ratio: float
sell_reason: Optional[str]
exit_reason: Optional[str]
close_date: datetime
# current_rate: Optional[float]
order_rate: Optional[float]
class RPCSellCancelMsg(__RPCBuyMsgBase):
type: Literal[RPCMessageType.EXIT_CANCEL]
reason: str
gain: str # Literal["profit", "loss"]
profit_amount: float
profit_ratio: float
sell_reason: Optional[str]
exit_reason: Optional[str]
close_date: datetime
class _AnalyzedDFData(TypedDict):
key: PairWithTimeframe
df: Any
la: datetime
class RPCAnalyzedDFMsg(RPCSendMsgBase):
"""New Analyzed dataframe message"""
type: Literal[RPCMessageType.ANALYZED_DF]
data: _AnalyzedDFData
class RPCNewCandleMsg(RPCSendMsgBase):
"""New candle ping message, issued once per new candle/pair"""
type: Literal[RPCMessageType.NEW_CANDLE]
data: PairWithTimeframe
RPCSendMsg = Union[
RPCStatusMsg,
RPCStrategyMsg,
RPCProtectionMsg,
RPCWhitelistMsg,
RPCBuyMsg,
RPCCancelMsg,
RPCSellMsg,
RPCSellCancelMsg,
RPCAnalyzedDFMsg,
RPCNewCandleMsg
]

View File

@@ -30,6 +30,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.misc import chunks, plural, round_coin_value
from freqtrade.persistence import Trade
from freqtrade.rpc import RPC, RPCException, RPCHandler
from freqtrade.rpc.rpc_types import RPCSendMsg
logger = logging.getLogger(__name__)
@@ -429,14 +430,14 @@ class Telegram(RPCHandler):
return None
return message
def send_msg(self, msg: Dict[str, Any]) -> None:
def send_msg(self, msg: RPCSendMsg) -> None:
""" Send a message to telegram channel """
default_noti = 'on'
msg_type = msg['type']
noti = ''
if msg_type == RPCMessageType.EXIT:
if msg['type'] == RPCMessageType.EXIT:
sell_noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), {})
# For backward compatibility sell still can be string
@@ -453,7 +454,7 @@ class Telegram(RPCHandler):
# Notification disabled
return
message = self.compose_message(deepcopy(msg), msg_type)
message = self.compose_message(deepcopy(msg), msg_type) # type: ignore
if message:
self._send_msg(message, disable_notification=(noti == 'silent'))
@@ -818,7 +819,7 @@ class Telegram(RPCHandler):
best_pair = stats['best_pair']
best_pair_profit_ratio = stats['best_pair_profit_ratio']
if stats['trade_count'] == 0:
markdown_msg = 'No trades yet.'
markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`"
else:
# Message to display
if stats['closed_trade_count'] > 0:
@@ -837,6 +838,7 @@ class Telegram(RPCHandler):
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
f"*Total Trade Count:* `{trade_count}`\n"
f"*Bot started:* `{stats['bot_start_date']}`\n"
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}`\n"

View File

@@ -10,6 +10,7 @@ from requests import RequestException, post
from freqtrade.constants import Config
from freqtrade.enums import RPCMessageType
from freqtrade.rpc import RPC, RPCHandler
from freqtrade.rpc.rpc_types import RPCSendMsg
logger = logging.getLogger(__name__)
@@ -41,7 +42,7 @@ class Webhook(RPCHandler):
"""
pass
def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]:
whconfig = self._config['webhook']
# Deprecated 2022.10 - only keep generic method.
if msg['type'] in [RPCMessageType.ENTRY]:
@@ -75,7 +76,7 @@ class Webhook(RPCHandler):
return None
return valuedict
def send_msg(self, msg: Dict[str, Any]) -> None:
def send_msg(self, msg: RPCSendMsg) -> None:
""" Send a message to telegram channel """
try:

View File

@@ -8,7 +8,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.misc import deep_merge_dicts
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.strategy.parameters import BaseParameter
@@ -124,8 +124,7 @@ class HyperStrategyMixin:
if filename.is_file():
logger.info(f"Loading parameters from file {filename}")
try:
with filename.open('r') as f:
params = json_load(f)
params = HyperoptTools.load_params(filename)
if params.get('strategy_name') != self.__class__.__name__:
raise OperationalException('Invalid parameter file provided.')
return params

View File

@@ -251,11 +251,12 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
pass
def bot_loop_start(self, **kwargs) -> None:
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass

View File

@@ -1,5 +1,5 @@
def bot_loop_start(self, **kwargs) -> None:
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
@@ -8,6 +8,7 @@ def bot_loop_start(self, **kwargs) -> None:
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, this simply does nothing.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass

View File

@@ -7,9 +7,9 @@
-r docs/requirements-docs.txt
coveralls==3.3.1
ruff==0.0.257
ruff==0.0.260
mypy==1.1.1
pre-commit==3.2.0
pre-commit==3.2.1
pytest==7.2.2
pytest-asyncio==0.21.0
pytest-cov==4.0.0
@@ -25,8 +25,8 @@ httpx==0.23.3
nbconvert==7.2.10
# mypy types
types-cachetools==5.3.0.4
types-cachetools==5.3.0.5
types-filelock==3.2.7
types-requests==2.28.11.15
types-tabulate==0.9.0.1
types-python-dateutil==2.8.19.10
types-requests==2.28.11.17
types-tabulate==0.9.0.2
types-python-dateutil==2.8.19.11

View File

@@ -5,7 +5,7 @@
# Required for freqai
scikit-learn==1.1.3
joblib==1.2.0
catboost==1.1.1; platform_machine != 'aarch64' and python_version < '3.11'
catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11'
lightgbm==3.3.5
xgboost==1.7.4
tensorboard==2.12.0
xgboost==1.7.5
tensorboard==2.12.1

View File

@@ -5,5 +5,5 @@
scipy==1.10.1
scikit-learn==1.1.3
scikit-optimize==0.9.0
filelock==3.10.0
filelock==3.10.6
progressbar2==4.2.0

View File

@@ -1,4 +1,4 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==5.13.1
plotly==5.14.0

View File

@@ -2,10 +2,10 @@ numpy==1.24.2
pandas==1.5.3
pandas-ta==0.3.14b
ccxt==3.0.23
cryptography==39.0.2
ccxt==3.0.50
cryptography==40.0.1
aiohttp==3.8.4
SQLAlchemy==2.0.7
SQLAlchemy==2.0.8
python-telegram-bot==13.15
arrow==1.2.3
cachetools==4.2.2
@@ -28,14 +28,14 @@ py_find_1st==1.1.5
# Load ticker files 30% faster
python-rapidjson==1.10
# Properly format api responses
orjson==3.8.7
orjson==3.8.9
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.95.0
pydantic==1.10.6
pydantic==1.10.7
uvicorn==0.21.1
pyjwt==2.6.0
aiofiles==23.1.0
@@ -53,7 +53,7 @@ python-dateutil==2.8.2
schedule==1.1.0
#WS Messages
websockets==10.4
websockets==11.0
janus==1.0.0
ast-comments==1.0.1

View File

@@ -59,7 +59,7 @@ setup(
install_requires=[
# from requirements.txt
'ccxt>=2.6.26',
'SQLAlchemy',
'SQLAlchemy>=2.0.6',
'python-telegram-bot>=13.4',
'arrow>=0.17.0',
'cachetools',

View File

@@ -252,7 +252,7 @@ def test_datahandler__check_empty_df(testdatadir, caplog):
assert log_has_re(expected_text, caplog)
@pytest.mark.parametrize('datahandler', ['feather', 'parquet'])
@pytest.mark.parametrize('datahandler', ['parquet'])
def test_datahandler_trades_not_supported(datahandler, testdatadir, ):
dh = get_datahandler(testdatadir, datahandler)
with pytest.raises(NotImplementedError):
@@ -496,6 +496,58 @@ def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir):
assert unlinkmock.call_count == 2
def test_featherdatahandler_trades_load(testdatadir):
dh = get_datahandler(testdatadir, 'feather')
trades = dh.trades_load('XRP/ETH')
assert isinstance(trades, list)
assert trades[0][0] == 1570752011620
assert trades[-1][-1] == 0.1986231
trades1 = dh.trades_load('UNITTEST/NONEXIST')
assert trades1 == []
def test_featherdatahandler_trades_store(testdatadir, tmpdir):
tmpdir1 = Path(tmpdir)
dh = get_datahandler(testdatadir, 'feather')
trades = dh.trades_load('XRP/ETH')
dh1 = get_datahandler(tmpdir1, 'feather')
dh1.trades_store('XRP/NEW', trades)
file = tmpdir1 / 'XRP_NEW-trades.feather'
assert file.is_file()
# Load trades back
trades_new = dh1.trades_load('XRP/NEW')
assert len(trades_new) == len(trades)
assert trades[0][0] == trades_new[0][0]
assert trades[0][1] == trades_new[0][1]
# assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense
assert trades[0][3] == trades_new[0][3]
assert trades[0][4] == trades_new[0][4]
assert trades[0][5] == trades_new[0][5]
assert trades[0][6] == trades_new[0][6]
assert trades[-1][0] == trades_new[-1][0]
assert trades[-1][1] == trades_new[-1][1]
# assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense
assert trades[-1][3] == trades_new[-1][3]
assert trades[-1][4] == trades_new[-1][4]
assert trades[-1][5] == trades_new[-1][5]
assert trades[-1][6] == trades_new[-1][6]
def test_featherdatahandler_trades_purge(mocker, testdatadir):
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
dh = get_datahandler(testdatadir, 'feather')
assert not dh.trades_purge('UNITTEST/NONEXIST')
assert unlinkmock.call_count == 0
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
assert dh.trades_purge('UNITTEST/NONEXIST')
assert unlinkmock.call_count == 1
def test_gethandlerclass():
cl = get_datahandlerclass('json')
assert cl == JsonDataHandler

View File

@@ -15,8 +15,8 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
('buy', 'limit', 'gtc', {'timeInForce': 'GTC'}),
('buy', 'limit', 'IOC', {'timeInForce': 'IOC'}),
('buy', 'market', 'IOC', {}),
('buy', 'limit', 'PO', {'postOnly': True}),
('sell', 'limit', 'PO', {'postOnly': True}),
('buy', 'limit', 'PO', {'timeInForce': 'PO'}),
('sell', 'limit', 'PO', {'timeInForce': 'PO'}),
('sell', 'market', 'PO', {}),
])
def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected):
@@ -48,7 +48,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte
default_conf['margin_mode'] = MarginMode.ISOLATED
default_conf['trading_mode'] = trademode
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
@@ -127,7 +127,7 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
order_type = 'stop_loss_limit'
default_conf['dry_run'] = True
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')

View File

@@ -8,6 +8,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
import arrow
import ccxt
import pytest
from ccxt import DECIMAL_PLACES, ROUND, ROUND_UP, TICK_SIZE, TRUNCATE
from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, TradingMode
@@ -113,18 +114,21 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
exchange.close()
with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
exchange.close()
with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
exchange.close()
def test_init(default_conf, mocker, caplog):
@@ -312,35 +316,54 @@ def test_amount_to_precision(amount, precision_mode, precision, expected,):
assert amount_to_precision(amount, precision, precision_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
(2.34559, 2, 4, 2.3456),
(2.34559, 2, 5, 2.34559),
(2.34559, 2, 3, 2.346),
(2.9999, 2, 3, 3.000),
(2.9909, 2, 3, 2.991),
# Tests for Tick_size
(2.34559, 4, 0.0001, 2.3456),
(2.34559, 4, 0.00001, 2.34559),
(2.34559, 4, 0.001, 2.346),
(2.9999, 4, 0.001, 3.000),
(2.9909, 4, 0.001, 2.991),
(2.9909, 4, 0.005, 2.995),
(2.9973, 4, 0.005, 3.0),
(2.9977, 4, 0.005, 3.0),
(234.43, 4, 0.5, 234.5),
(234.53, 4, 0.5, 235.0),
(0.891534, 4, 0.0001, 0.8916),
(64968.89, 4, 0.01, 64968.89),
(0.000000003483, 4, 1e-12, 0.000000003483),
@pytest.mark.parametrize("price,precision_mode,precision,expected,rounding_mode", [
# Tests for DECIMAL_PLACES, ROUND_UP
(2.34559, 2, 4, 2.3456, ROUND_UP),
(2.34559, 2, 5, 2.34559, ROUND_UP),
(2.34559, 2, 3, 2.346, ROUND_UP),
(2.9999, 2, 3, 3.000, ROUND_UP),
(2.9909, 2, 3, 2.991, ROUND_UP),
# Tests for DECIMAL_PLACES, ROUND
(2.345600000000001, DECIMAL_PLACES, 4, 2.3456, ROUND),
(2.345551, DECIMAL_PLACES, 4, 2.3456, ROUND),
(2.49, DECIMAL_PLACES, 0, 2., ROUND),
(2.51, DECIMAL_PLACES, 0, 3., ROUND),
(5.1, DECIMAL_PLACES, -1, 10., ROUND),
(4.9, DECIMAL_PLACES, -1, 0., ROUND),
# Tests for TICK_SIZE, ROUND_UP
(2.34559, TICK_SIZE, 0.0001, 2.3456, ROUND_UP),
(2.34559, TICK_SIZE, 0.00001, 2.34559, ROUND_UP),
(2.34559, TICK_SIZE, 0.001, 2.346, ROUND_UP),
(2.9999, TICK_SIZE, 0.001, 3.000, ROUND_UP),
(2.9909, TICK_SIZE, 0.001, 2.991, ROUND_UP),
(2.9909, TICK_SIZE, 0.005, 2.995, ROUND_UP),
(2.9973, TICK_SIZE, 0.005, 3.0, ROUND_UP),
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND_UP),
(234.43, TICK_SIZE, 0.5, 234.5, ROUND_UP),
(234.53, TICK_SIZE, 0.5, 235.0, ROUND_UP),
(0.891534, TICK_SIZE, 0.0001, 0.8916, ROUND_UP),
(64968.89, TICK_SIZE, 0.01, 64968.89, ROUND_UP),
(0.000000003483, TICK_SIZE, 1e-12, 0.000000003483, ROUND_UP),
# Tests for TICK_SIZE, ROUND
(2.49, TICK_SIZE, 1., 2., ROUND),
(2.51, TICK_SIZE, 1., 3., ROUND),
(2.000000051, TICK_SIZE, 0.0000001, 2.0000001, ROUND),
(2.000000049, TICK_SIZE, 0.0000001, 2., ROUND),
(2.9909, TICK_SIZE, 0.005, 2.990, ROUND),
(2.9973, TICK_SIZE, 0.005, 2.995, ROUND),
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND),
(234.24, TICK_SIZE, 0.5, 234., ROUND),
(234.26, TICK_SIZE, 0.5, 234.5, ROUND),
# Tests for TRUNCATTE
(2.34559, 2, 4, 2.3455, TRUNCATE),
(2.34559, 2, 5, 2.34559, TRUNCATE),
(2.34559, 2, 3, 2.345, TRUNCATE),
(2.9999, 2, 3, 2.999, TRUNCATE),
(2.9909, 2, 3, 2.990, TRUNCATE),
])
def test_price_to_precision(price, precision_mode, precision, expected):
# digits counting mode
# DECIMAL_PLACES = 2
# SIGNIFICANT_DIGITS = 3
# TICK_SIZE = 4
assert price_to_precision(price, precision, precision_mode) == expected
def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode):
assert price_to_precision(
price, precision, precision_mode, rounding_mode=rounding_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
@@ -414,7 +437,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
}
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result = 2 * 2 * (1 + 0.05) / (1 - abs(stoploss))
expected_result = 2 * 2 * (1 + 0.05)
assert pytest.approx(result) == expected_result
# With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0)
@@ -423,14 +446,14 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
assert result == 20000
# min amount and cost are set (cost is minimal)
# min amount and cost are set (cost is minimal and therefore ignored)
markets["ETH/BTC"]["limits"] = {
'cost': {'min': 2, 'max': None},
'amount': {'min': 2, 'max': None},
}
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result = max(2, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss))
expected_result = max(2, 2 * 2) * (1 + 0.05)
assert pytest.approx(result) == expected_result
# With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
@@ -473,6 +496,9 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
assert result == 1000
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2, 12.0)
assert result == 1000 / 12
markets["ETH/BTC"]["contractSize"] = '0.01'
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
@@ -1436,7 +1462,10 @@ def test_buy_prod(default_conf, mocker, exchange_name):
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'buy'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] is None
if exchange._order_needs_price(order_type):
assert api_mock.create_order.call_args[0][4] == 200
else:
assert api_mock.create_order.call_args[0][4] is None
api_mock.create_order.reset_mock()
order_type = 'limit'
@@ -1541,7 +1570,10 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'buy'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] is None
if exchange._order_needs_price(order_type):
assert api_mock.create_order.call_args[0][4] == 200
else:
assert api_mock.create_order.call_args[0][4] is None
# Market orders should not send timeInForce!!
assert "timeInForce" not in api_mock.create_order.call_args[0][5]
@@ -1585,7 +1617,10 @@ def test_sell_prod(default_conf, mocker, exchange_name):
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'sell'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] is None
if exchange._order_needs_price(order_type):
assert api_mock.create_order.call_args[0][4] == 200
else:
assert api_mock.create_order.call_args[0][4] is None
api_mock.create_order.reset_mock()
order_type = 'limit'
@@ -1679,7 +1714,10 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'sell'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] is None
if exchange._order_needs_price(order_type):
assert api_mock.create_order.call_args[0][4] == 200
else:
assert api_mock.create_order.call_args[0][4] is None
# Market orders should not send timeInForce!!
assert "timeInForce" not in api_mock.create_order.call_args[0][5]
@@ -2248,7 +2286,6 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach
assert res[pair2].at[0, 'open']
@pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
ohlcv = [
@@ -2277,7 +2314,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
assert res[3] == ohlcv
assert exchange._api_async.fetch_ohlcv.call_count == 1
assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog)
exchange.close()
# exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(),
"_async_get_candle_history", "fetch_ohlcv",
@@ -2292,15 +2329,17 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT,
(arrow.utcnow().int_timestamp - 2000) * 1000)
exchange.close()
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
r'historical candle \(OHLCV\) data\..*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT,
(arrow.utcnow().int_timestamp - 2000) * 1000)
exchange.close()
@pytest.mark.asyncio
async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog):
from freqtrade.exchange.common import _reset_logging_mixin
_reset_logging_mixin()
@@ -2341,9 +2380,9 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog):
# Expect the "returned exception" message 12 times (4 retries * 3 (loop))
assert num_log_has_re(msg, caplog) == 12
assert num_log_has_re(msg2, caplog) == 9
exchange.close()
@pytest.mark.asyncio
async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
""" Test empty exchange result """
ohlcv = []
@@ -2363,6 +2402,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
assert res[2] == CandleType.SPOT
assert res[3] == ohlcv
assert exchange._api_async.fetch_ohlcv.call_count == 1
exchange.close()
def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
@@ -2757,7 +2797,6 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
assert res_ohlcv[9][5] == 2.31452783
@pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
fetch_trades_result):
@@ -2785,8 +2824,8 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
assert exchange._api_async.fetch_trades.call_args[1]['limit'] == 1000
assert exchange._api_async.fetch_trades.call_args[1]['params'] == {'from': '123'}
assert log_has_re(f"Fetching trades for pair {pair}, params: .*", caplog)
exchange.close()
exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(),
"_async_fetch_trades", "fetch_trades",
pair='ABCD/BTC', since=None)
@@ -2796,15 +2835,16 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
exchange.close()
with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching '
r'historical trade data\..*'):
api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
exchange.close()
@pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, exchange_name,
fetch_trades_result):
@@ -2839,6 +2879,7 @@ async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, e
pair = 'ETH/USDT:USDT'
res = await exchange._async_fetch_trades(pair, since=None, params=None)
assert res[0][5] == 300
exchange.close()
@pytest.mark.asyncio
@@ -4807,7 +4848,6 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name
)
@pytest.mark.asyncio
@pytest.mark.parametrize('exchange_name', EXCHANGES)
async def test_get_market_leverage_tiers(mocker, default_conf, exchange_name):
default_conf['exchange']['name'] = exchange_name
@@ -5264,7 +5304,7 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
})
default_conf['dry_run'] = False
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_contract_size = MagicMock(return_value=contract_size)
@@ -5284,3 +5324,10 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
assert order['cost'] == 100
assert order['filled'] == 100
assert order['remaining'] == 100
def test_price_to_precision_with_default_conf(default_conf, mocker):
conf = copy.deepcopy(default_conf)
patched_ex = get_patched_exchange(mocker, conf)
prec_price = patched_ex.price_to_precision("XRP/USDT", 1.0000000101)
assert prec_price == 1.00000001

View File

@@ -4,42 +4,9 @@ from unittest.mock import MagicMock
import pytest
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Gate
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import EXMS, get_patched_exchange
def test_validate_order_types_gate(default_conf, mocker):
default_conf['exchange']['name'] = 'gate'
mocker.patch(f'{EXMS}._init_ccxt')
mocker.patch(f'{EXMS}._load_markets', return_value={})
mocker.patch(f'{EXMS}.validate_pairs')
mocker.patch(f'{EXMS}.validate_timeframes')
mocker.patch(f'{EXMS}.validate_stakecurrency')
mocker.patch(f'{EXMS}.validate_pricing')
mocker.patch(f'{EXMS}.name', 'Gate')
exch = ExchangeResolver.load_exchange('gate', default_conf, True)
assert isinstance(exch, Gate)
default_conf['order_types'] = {
'entry': 'market',
'exit': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
with pytest.raises(OperationalException,
match=r'Exchange .* does not support market orders.'):
ExchangeResolver.load_exchange('gate', default_conf, True)
# market-orders supported on futures markets.
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
ex = ExchangeResolver.load_exchange('gate', default_conf, True)
assert ex
@pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_gate(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='gate')

View File

@@ -27,7 +27,7 @@ def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected,
})
default_conf['dry_run'] = False
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
@@ -80,7 +80,7 @@ def test_create_stoploss_order_dry_run_huobi(default_conf, mocker):
order_type = 'stop-limit'
default_conf['dry_run'] = True
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')

View File

@@ -29,7 +29,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
default_conf['dry_run'] = False
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
order = exchange.create_order(
@@ -192,7 +192,7 @@ def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adj
default_conf['dry_run'] = False
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
@@ -263,7 +263,7 @@ def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side):
api_mock = MagicMock()
default_conf['dry_run'] = True
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')

View File

@@ -27,7 +27,7 @@ def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected
})
default_conf['dry_run'] = False
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
if order_type == 'limit':
@@ -88,7 +88,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
order_type = 'market'
default_conf['dry_run'] = True
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')

View File

@@ -5,7 +5,7 @@ from unittest.mock import MagicMock
import pytest
from freqtrade.data.history import get_timerange
from freqtrade.enums import ExitType
from freqtrade.enums import ExitType, TradingMode
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence.trade_model import LocalTrade
from tests.conftest import EXMS, patch_exchange
@@ -925,12 +925,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
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_max_leverage", return_value=100)
mocker.patch(f"{EXMS}.calculate_funding_fees", return_value=0)
patch_exchange(mocker)
frame = _build_backtest_dataframe(data.data)
backtesting = Backtesting(default_conf)
# TODO: Should we initialize this properly??
backtesting._can_short = True
backtesting.trading_mode = TradingMode.MARGIN
backtesting._set_strategy(backtesting.strategylist[0])
backtesting._can_short = True
backtesting.required_startup = 0
backtesting.strategy.advise_entry = lambda a, m: frame
backtesting.strategy.advise_exit = lambda a, m: frame

View File

@@ -344,7 +344,7 @@ def test_backtest_abort(default_conf, mocker, testdatadir) -> None:
assert backtesting.progress.progress == 0
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
def test_backtesting_start(default_conf, mocker, caplog) -> None:
def get_timerange(input1):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
@@ -367,6 +367,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.bot_loop_start = MagicMock()
backtesting.strategy.bot_start = MagicMock()
backtesting.start()
# check the logs, that will contain the backtest result
exists = [
@@ -376,7 +377,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
for line in exists:
assert log_has(line, caplog)
assert backtesting.strategy.dp._pairlists is not None
assert backtesting.strategy.bot_loop_start.call_count == 1
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

View File

@@ -10,7 +10,7 @@ from arrow import Arrow
from freqtrade.configuration import TimeRange
from freqtrade.data import history
from freqtrade.data.history import get_timerange
from freqtrade.enums import ExitType
from freqtrade.enums import ExitType, TradingMode
from freqtrade.optimize.backtesting import Backtesting
from tests.conftest import EXMS, patch_exchange
@@ -108,9 +108,10 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera
default_conf.update({
"stake_amount": 100.0,
"dry_run_wallet": 1000.0,
"strategy": "StrategyTestV3"
"strategy": "StrategyTestV3",
})
backtesting = Backtesting(default_conf)
backtesting.trading_mode = TradingMode.FUTURES
backtesting._can_short = True
backtesting._set_strategy(backtesting.strategylist[0])
pair = 'XRP/USDT'

View File

@@ -872,7 +872,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
assert hyperopt.backtesting.strategy.bot_loop_started is True
assert hyperopt.backtesting.strategy.bot_started is True
assert hyperopt.backtesting.strategy.bot_loop_started is False
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
@@ -922,7 +923,8 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir,
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
assert hyperopt.backtesting.strategy.bot_loop_started is True
assert hyperopt.backtesting.strategy.bot_started is True
assert hyperopt.backtesting.strategy.bot_loop_started is False
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
@@ -959,7 +961,8 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee)
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
assert hyperopt.backtesting.strategy.bot_loop_started is True
assert hyperopt.backtesting.strategy.bot_loop_started is False
assert hyperopt.backtesting.strategy.bot_started is True
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
assert hyperopt.backtesting.strategy.buy_rsi.value == 35

View File

@@ -0,0 +1,69 @@
from datetime import datetime, timedelta, timezone
import pytest
from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time
from tests.conftest import create_mock_trades_usdt
@pytest.mark.usefixtures("init_persistence")
def test_key_value_store(time_machine):
start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc)
time_machine.move_to(start, tick=False)
KeyValueStore.store_value("test", "testStringValue")
KeyValueStore.store_value("test_dt", datetime.now(timezone.utc))
KeyValueStore.store_value("test_float", 22.51)
KeyValueStore.store_value("test_int", 15)
assert KeyValueStore.get_value("test") == "testStringValue"
assert KeyValueStore.get_value("test") == "testStringValue"
assert KeyValueStore.get_string_value("test") == "testStringValue"
assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc)
assert KeyValueStore.get_datetime_value("test_dt") == datetime.now(timezone.utc)
assert KeyValueStore.get_string_value("test_dt") is None
assert KeyValueStore.get_float_value("test_dt") is None
assert KeyValueStore.get_int_value("test_dt") is None
assert KeyValueStore.get_value("test_float") == 22.51
assert KeyValueStore.get_float_value("test_float") == 22.51
assert KeyValueStore.get_value("test_int") == 15
assert KeyValueStore.get_int_value("test_int") == 15
assert KeyValueStore.get_datetime_value("test_int") is None
time_machine.move_to(start + timedelta(days=20, hours=5), tick=False)
assert KeyValueStore.get_value("test_dt") != datetime.now(timezone.utc)
assert KeyValueStore.get_value("test_dt") == start
# Test update works
KeyValueStore.store_value("test_dt", datetime.now(timezone.utc))
assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc)
KeyValueStore.store_value("test_float", 23.51)
assert KeyValueStore.get_value("test_float") == 23.51
# test deleting
KeyValueStore.delete_value("test_float")
assert KeyValueStore.get_value("test_float") is None
# Delete same value again (should not fail)
KeyValueStore.delete_value("test_float")
with pytest.raises(ValueError, match=r"Unknown value type"):
KeyValueStore.store_value("test_float", {'some': 'dict'})
@pytest.mark.usefixtures("init_persistence")
def test_set_startup_time(fee, time_machine):
create_mock_trades_usdt(fee)
start = datetime.now(timezone.utc)
time_machine.move_to(start, tick=False)
set_startup_time()
assert KeyValueStore.get_value("startup_time") == start
initial_time = KeyValueStore.get_value("bot_start_time")
assert initial_time <= start
# Simulate bot restart
new_start = start + timedelta(days=5)
time_machine.move_to(new_start, tick=False)
set_startup_time()
assert KeyValueStore.get_value("startup_time") == new_start
assert KeyValueStore.get_value("bot_start_time") == initial_time

View File

@@ -1330,71 +1330,78 @@ def test_to_json(fee):
open_rate=0.123,
exchange='binance',
enter_tag=None,
open_order_id='dry_run_buy_12345'
open_order_id='dry_run_buy_12345',
precision_mode=1,
amount_precision=8.0,
price_precision=7.0,
)
result = trade.to_json()
assert isinstance(result, dict)
assert result == {'trade_id': None,
'pair': 'ADA/USDT',
'base_currency': 'ADA',
'quote_currency': 'USDT',
'is_open': None,
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'open_order_id': 'dry_run_buy_12345',
'close_date': None,
'close_timestamp': None,
'open_rate': 0.123,
'open_rate_requested': None,
'open_trade_value': 15.1668225,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'close_rate': None,
'close_rate_requested': None,
'amount': 123.0,
'amount_requested': 123.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': None,
'trade_duration_s': None,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'exit_reason': None,
'exit_order_status': None,
'stop_loss_abs': None,
'stop_loss_ratio': None,
'stop_loss_pct': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'min_rate': None,
'max_rate': None,
'strategy': None,
'enter_tag': None,
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None,
'orders': [],
}
assert result == {
'trade_id': None,
'pair': 'ADA/USDT',
'base_currency': 'ADA',
'quote_currency': 'USDT',
'is_open': None,
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'open_order_id': 'dry_run_buy_12345',
'close_date': None,
'close_timestamp': None,
'open_rate': 0.123,
'open_rate_requested': None,
'open_trade_value': 15.1668225,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'close_rate': None,
'close_rate_requested': None,
'amount': 123.0,
'amount_requested': 123.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': None,
'trade_duration_s': None,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'exit_reason': None,
'exit_order_status': None,
'stop_loss_abs': None,
'stop_loss_ratio': None,
'stop_loss_pct': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'min_rate': None,
'max_rate': None,
'strategy': None,
'enter_tag': None,
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None,
'amount_precision': 8.0,
'price_precision': 7.0,
'precision_mode': 1,
'orders': [],
}
# Simulate dry_run entries
trade = Trade(
@@ -1410,70 +1417,77 @@ def test_to_json(fee):
close_rate=0.125,
enter_tag='buys_signal_001',
exchange='binance',
precision_mode=2,
amount_precision=7.0,
price_precision=8.0,
)
result = trade.to_json()
assert isinstance(result, dict)
assert result == {'trade_id': None,
'pair': 'XRP/BTC',
'base_currency': 'XRP',
'quote_currency': 'BTC',
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'close_timestamp': int(trade.close_date.timestamp() * 1000),
'open_rate': 0.123,
'close_rate': 0.125,
'amount': 100.0,
'amount_requested': 101.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': 60,
'trade_duration_s': 3600,
'stop_loss_abs': None,
'stop_loss_pct': None,
'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'close_rate_requested': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': None,
'max_rate': None,
'min_rate': None,
'open_order_id': None,
'open_rate_requested': None,
'open_trade_value': 12.33075,
'exit_reason': None,
'exit_order_status': None,
'strategy': None,
'enter_tag': 'buys_signal_001',
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None,
'orders': [],
}
assert result == {
'trade_id': None,
'pair': 'XRP/BTC',
'base_currency': 'XRP',
'quote_currency': 'BTC',
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'close_timestamp': int(trade.close_date.timestamp() * 1000),
'open_rate': 0.123,
'close_rate': 0.125,
'amount': 100.0,
'amount_requested': 101.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': 60,
'trade_duration_s': 3600,
'stop_loss_abs': None,
'stop_loss_pct': None,
'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'close_rate_requested': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': None,
'max_rate': None,
'min_rate': None,
'open_order_id': None,
'open_rate_requested': None,
'open_trade_value': 12.33075,
'exit_reason': None,
'exit_order_status': None,
'strategy': None,
'enter_tag': 'buys_signal_001',
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None,
'amount_precision': 7.0,
'price_precision': 8.0,
'precision_mode': 2,
'orders': [],
}
def test_stoploss_reinitialization(default_conf, fee):

View File

@@ -50,8 +50,8 @@ def test_trade_fromjson():
"stop_loss_ratio": -0.216,
"stop_loss_pct": -21.6,
"stoploss_order_id": null,
"stoploss_last_update": null,
"stoploss_last_update_timestamp": null,
"stoploss_last_update": "2022-10-18 09:13:42",
"stoploss_last_update_timestamp": 1666077222000,
"initial_stop_loss_abs": 0.1981,
"initial_stop_loss_ratio": -0.216,
"initial_stop_loss_pct": -21.6,

View File

@@ -88,6 +88,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT,
'amount_precision': 8.0,
'price_precision': 8.0,
'precision_mode': 2,
'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',

View File

@@ -1,6 +1,7 @@
"""
Unit test file for rpc/api_server.py
"""
import asyncio
import logging
import time
from datetime import datetime, timedelta, timezone
@@ -299,10 +300,6 @@ def test_api_UvicornServer(mocker):
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert thread_mock.call_count == 0
s.install_signal_handlers()
# Original implementation starts a thread - make sure that's not the case
assert thread_mock.call_count == 0
# Fake started to avoid sleeping forever
s.started = True
s.run_in_thread()
@@ -318,10 +315,6 @@ def test_api_UvicornServer_run(mocker):
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert serve_mock.call_count == 0
s.install_signal_handlers()
# Original implementation starts a thread - make sure that's not the case
assert serve_mock.call_count == 0
# Fake started to avoid sleeping forever
s.started = True
s.run()
@@ -331,13 +324,10 @@ def test_api_UvicornServer_run(mocker):
def test_api_UvicornServer_run_no_uvloop(mocker, import_fails):
serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve',
get_mock_coro(None))
asyncio.set_event_loop(asyncio.new_event_loop())
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert serve_mock.call_count == 0
s.install_signal_handlers()
# Original implementation starts a thread - make sure that's not the case
assert serve_mock.call_count == 0
# Fake started to avoid sleeping forever
s.started = True
s.run()
@@ -893,6 +883,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
'max_drawdown': ANY,
'max_drawdown_abs': ANY,
'trading_volume': expected['trading_volume'],
'bot_start_timestamp': 0,
'bot_start_date': '',
}
@@ -1066,6 +1058,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'liquidation_price': None,
'funding_fees': None,
'trading_mode': ANY,
'amount_precision': None,
'price_precision': None,
'precision_mode': None,
'orders': [ANY],
}
@@ -1271,6 +1266,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
'liquidation_price': None,
'funding_fees': None,
'trading_mode': 'spot',
'amount_precision': None,
'price_precision': None,
'precision_mode': None,
'orders': [],
}

View File

@@ -50,6 +50,7 @@ class HyperoptableStrategy(StrategyTestV3):
return prot
bot_loop_started = False
bot_started = False
def bot_loop_start(self):
self.bot_loop_started = True
@@ -58,6 +59,7 @@ class HyperoptableStrategy(StrategyTestV3):
"""
Parameters can also be defined here ...
"""
self.bot_started = True
self.buy_rsi = IntParameter([0, 50], default=30, space='buy')
def informative_pairs(self):

View File

@@ -986,7 +986,8 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
}
}
}
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
return_value=expected_result)
PairLocks.timeframe = default_conf['timeframe']
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.stoploss == -0.05
@@ -1005,11 +1006,13 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
}
}
mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
return_value=expected_result)
with pytest.raises(OperationalException, match="Invalid parameter file provided."):
StrategyResolver.load_strategy(default_conf)
mocker.patch('freqtrade.strategy.hyper.json_load', MagicMock(side_effect=ValueError()))
mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
MagicMock(side_effect=ValueError()))
StrategyResolver.load_strategy(default_conf)
assert log_has("Invalid parameter file format.", caplog)

View File

@@ -356,7 +356,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke
@pytest.mark.parametrize("is_short", [False, True])
@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
(5.0, True, True, 99),
(0.049, True, False, 99), # Amount will be adjusted to min - which is 0.051
(0.042, True, False, 99), # Amount will be adjusted to min - which is 0.051
(0, False, True, 99),
(UNLIMITED_STAKE_AMOUNT, False, True, 0),
])
@@ -1060,9 +1060,19 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order,
@pytest.mark.parametrize("is_short", [False, True])
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None:
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short, fee) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={
'bid': 1.9,
'ask': 2.2,
'last': 1.9
}),
create_order=MagicMock(return_value=limit_order[entry_side(is_short)]),
get_fee=fee,
)
order = limit_order[entry_side(is_short)]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch(f'{EXMS}.fetch_order', return_value=order)
@@ -1074,8 +1084,10 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# TODO: should not be magicmock
trade = MagicMock()
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
freqtrade.enter_positions()
trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short
trade.open_order_id = None
trade.stoploss_order_id = None
@@ -1091,7 +1103,8 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short,
limit_order) -> None:
stoploss = MagicMock(return_value={'id': 13434334})
stop_order_dict = {'id': "13434334"}
stoploss = MagicMock(return_value=stop_order_dict)
enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)]
patch_RPCManager(mocker)
@@ -1116,8 +1129,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
# First case: when stoploss is not yet set but the order is open
# should get the stoploss order id immediately
# and should return false as no trade actually happened
# TODO: should not be magicmock
trade = MagicMock()
freqtrade.enter_positions()
trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short
trade.is_open = True
trade.open_order_id = None
@@ -1129,44 +1143,62 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
# Second case: when stoploss is set but it is not yet hit
# should do nothing and return false
stop_order_dict.update({'id': "102"})
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = "100"
trade.stoploss_order_id = "102"
trade.orders.append(
Order(
ft_order_side='stoploss',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.stop_loss,
order_id='102',
status='open',
)
)
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id == "100"
assert trade.stoploss_order_id == "102"
# Third case: when stoploss was set but it was canceled for some reason
# should set a stoploss immediately and return False
caplog.clear()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = "100"
trade.stoploss_order_id = "102"
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
canceled_stoploss_order = MagicMock(return_value={'id': '103_1', 'status': 'canceled'})
mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order)
stoploss.reset_mock()
amount_before = trade.amount
stop_order_dict.update({'id': "103_1"})
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 1
assert trade.stoploss_order_id == "13434334"
assert trade.stoploss_order_id == "103_1"
assert trade.amount == amount_before
# Fourth case: when stoploss is set and it is hit
# should unset stoploss_order_id and return true
# as a trade actually happened
caplog.clear()
freqtrade.enter_positions()
stop_order_dict.update({'id': "104"})
trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = "100"
trade.stoploss_order_id = "104"
trade.orders.append(Order(
ft_order_side='stoploss',
order_id='100',
order_id='104',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
@@ -1175,7 +1207,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
assert trade
stoploss_order_hit = MagicMock(return_value={
'id': "100",
'id': "104",
'status': 'closed',
'type': 'stop_loss_limit',
'price': 3,
@@ -1197,7 +1229,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
# Fifth case: fetch_order returns InvalidOrder
# It should try to add stoploss order
trade.stoploss_order_id = 100
stop_order_dict.update({'id': "105"})
trade.stoploss_order_id = "105"
stoploss.reset_mock()
mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException())
mocker.patch(f'{EXMS}.create_stoploss', stoploss)
@@ -1217,21 +1250,36 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
# Seventh case: emergency exit triggered
# Trailing stop should not act anymore
stoploss_order_cancelled = MagicMock(side_effect=[{
'id': "100",
'id': "107",
'status': 'canceled',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'amount': enter_order['amount'],
'filled': 0,
'remaining': enter_order['amount'],
'info': {'stopPrice': 22},
}])
trade.stoploss_order_id = 100
trade.stoploss_order_id = "107"
trade.is_open = True
trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime
trade.stop_loss = 24
trade.exit_reason = None
trade.orders.append(
Order(
ft_order_side='stoploss',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.stop_loss,
order_id='107',
status='open',
)
)
freqtrade.config['trailing_stop'] = True
stoploss = MagicMock(side_effect=InvalidOrderException())
Trade.commit()
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result',
side_effect=InvalidOrderException())
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled)
@@ -1242,6 +1290,137 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_stoploss_on_exchange_partial(
mocker, default_conf_usdt, fee, is_short, limit_order) -> None:
stop_order_dict = {'id': "101", "status": "open"}
stoploss = MagicMock(return_value=stop_order_dict)
enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)]
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={
'bid': 1.9,
'ask': 2.2,
'last': 1.9
}),
create_order=MagicMock(side_effect=[
enter_order,
exit_order,
]),
get_fee=fee,
create_stoploss=stoploss
)
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
freqtrade.enter_positions()
trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = None
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 1
assert trade.stoploss_order_id == "101"
assert trade.amount == 30
stop_order_dict.update({'id': "102"})
# Stoploss on exchange is cancelled on exchange, but filled partially.
# Must update trade amount to guarantee successful exit.
stoploss_order_hit = MagicMock(return_value={
'id': "101",
'status': 'canceled',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'filled': trade.amount / 2,
'remaining': trade.amount / 2,
'amount': enter_order['amount'],
})
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# Stoploss filled partially ...
assert trade.amount == 15
assert trade.stoploss_order_id == "102"
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_stoploss_on_exchange_partial_cancel_here(
mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None:
stop_order_dict = {'id': "101", "status": "open"}
default_conf_usdt['trailing_stop'] = True
stoploss = MagicMock(return_value=stop_order_dict)
enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)]
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={
'bid': 1.9,
'ask': 2.2,
'last': 1.9
}),
create_order=MagicMock(side_effect=[
enter_order,
exit_order,
]),
get_fee=fee,
create_stoploss=stoploss
)
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
freqtrade.enter_positions()
trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = None
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 1
assert trade.stoploss_order_id == "101"
assert trade.amount == 30
stop_order_dict.update({'id': "102"})
# Stoploss on exchange is open.
# Freqtrade cancels the stop - but cancel returns a partial filled order.
stoploss_order_hit = MagicMock(return_value={
'id': "101",
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'filled': 0,
'remaining': trade.amount,
'amount': enter_order['amount'],
})
stoploss_order_cancel = MagicMock(return_value={
'id': "101",
'status': 'canceled',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'filled': trade.amount / 2,
'remaining': trade.amount / 2,
'amount': enter_order['amount'],
})
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel)
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-10).datetime
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# Canceled Stoploss filled partially ...
assert log_has_re('Cancelling current stoploss on exchange.*', caplog)
assert trade.stoploss_order_id == "102"
assert trade.amount == 15
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
limit_order) -> None:
@@ -1273,10 +1452,21 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
freqtrade.enter_positions()
trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short
assert trade.is_short == is_short
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
trade.stoploss_order_id = "100"
trade.orders.append(
Order(
ft_order_side='stoploss',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.stop_loss,
order_id='100',
status='open',
)
)
assert trade
assert freqtrade.handle_stoploss_on_exchange(trade) is False
@@ -1395,7 +1585,7 @@ def test_handle_stoploss_on_exchange_trailing(
# When trailing stoploss is set
enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)]
stoploss = MagicMock(return_value={'id': 13434334})
stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'})
patch_RPCManager(mocker)
mocker.patch.multiple(
EXMS,
@@ -1440,11 +1630,21 @@ def test_handle_stoploss_on_exchange_trailing(
trade.is_short = is_short
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
trade.stoploss_order_id = '100'
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-20).datetime
trade.orders.append(
Order(
ft_order_side='stoploss',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.stop_loss,
order_id='100',
)
)
stoploss_order_hanging = MagicMock(return_value={
'id': 100,
'id': '100',
'status': 'open',
'type': 'stop_loss_limit',
'price': hang_price,
@@ -1471,7 +1671,7 @@ def test_handle_stoploss_on_exchange_trailing(
)
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock(return_value={'id': 'so1'})
stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
@@ -1483,13 +1683,14 @@ def test_handle_stoploss_on_exchange_trailing(
assert freqtrade.handle_trade(trade) is False
assert trade.stop_loss == stop_price[1]
trade.stoploss_order_id = '100'
# setting stoploss_on_exchange_interval to 0 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
cancel_order_mock.assert_called_once_with('100', 'ETH/USDT')
stoploss_order_mock.assert_called_once_with(
amount=pytest.approx(amt),
pair='ETH/USDT',
@@ -1519,7 +1720,7 @@ def test_handle_stoploss_on_exchange_trailing_error(
enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)]
# When trailing stoploss is set
stoploss = MagicMock(return_value={'id': 13434334})
stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
patch_exchange(mocker)
mocker.patch.multiple(
@@ -1601,7 +1802,7 @@ def test_stoploss_on_exchange_price_rounding(
EXMS,
get_fee=fee,
)
price_mock = MagicMock(side_effect=lambda p, s: int(s))
price_mock = MagicMock(side_effect=lambda p, s, **kwargs: int(s))
stoploss_mock = MagicMock(return_value={'id': '13434334'})
adjust_mock = MagicMock(return_value=False)
mocker.patch.multiple(
@@ -1628,7 +1829,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)]
# When trailing stoploss is set
stoploss = MagicMock(return_value={'id': 13434334})
stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'})
patch_RPCManager(mocker)
mocker.patch.multiple(
EXMS,
@@ -1673,11 +1874,21 @@ def test_handle_stoploss_on_exchange_custom_stop(
trade.is_short = is_short
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
trade.stoploss_order_id = '100'
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime
trade.orders.append(
Order(
ft_order_side='stoploss',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.stop_loss,
order_id='100',
)
)
stoploss_order_hanging = MagicMock(return_value={
'id': 100,
'id': '100',
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
@@ -1703,9 +1914,10 @@ def test_handle_stoploss_on_exchange_custom_stop(
)
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock(return_value={'id': 'so1'})
stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
trade.stoploss_order_id = '100'
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@@ -1722,7 +1934,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
cancel_order_mock.assert_called_once_with('100', 'ETH/USDT')
# Long uses modified ask - offset, short modified bid + offset
stoploss_order_mock.assert_called_once_with(
amount=pytest.approx(trade.amount),
@@ -1751,7 +1963,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
exit_order = limit_order['sell']
# When trailing stoploss is set
stoploss = MagicMock(return_value={'id': 13434334})
stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_edge(mocker)
@@ -1800,11 +2012,21 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
trade = Trade.session.scalars(select(Trade)).first()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
trade.stoploss_last_update = arrow.utcnow()
trade.stoploss_order_id = '100'
trade.stoploss_last_update = arrow.utcnow().datetime
trade.orders.append(
Order(
ft_order_side='stoploss',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.stop_loss,
order_id='100',
)
)
stoploss_order_hanging = MagicMock(return_value={
'id': 100,
'id': '100',
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
@@ -1851,7 +2073,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
# stoploss should be set to 1% as trailing is on
assert trade.stop_loss == 4.4 * 0.99
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
cancel_order_mock.assert_called_once_with('100', 'NEO/BTC')
stoploss_order_mock.assert_called_once_with(
amount=pytest.approx(11.41438356),
pair='NEO/BTC',
@@ -2733,6 +2955,9 @@ def test_manage_open_orders_exit_usercustom(
assert rpc_mock.call_count == 2
assert freqtrade.strategy.check_exit_timeout.call_count == 1
assert freqtrade.strategy.check_entry_timeout.call_count == 0
trade = Trade.session.scalars(select(Trade)).first()
# cancelling didn't succeed - order-id remains open.
assert trade.open_order_id is not None
# 2nd canceled trade - Fail execute exit
caplog.clear()
@@ -3243,6 +3468,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
# TODO: should not be magicmock
trade = MagicMock()
trade.open_order_id = '125'
reason = CANCEL_REASON['TIMEOUT']
order = {'remaining': 1,
'id': '125',
@@ -3250,6 +3476,10 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
'status': "open"}
assert not freqtrade.handle_cancel_exit(trade, order, reason)
# mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order)
# assert not freqtrade.handle_cancel_exit(trade, order, reason)
# assert trade.open_order_id == '125'
@pytest.mark.parametrize("is_short, open_rate, amt", [
(False, 2.0, 30.0),
@@ -3326,6 +3556,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
'profit_ratio': 0.00493809 if is_short else 0.09451372,
'stake_currency': 'USDT',
'fiat_currency': 'USD',
'base_currency': 'ETH',
'sell_reason': ExitType.ROI.value,
'exit_reason': ExitType.ROI.value,
'open_date': ANY,
@@ -3389,6 +3620,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
'profit_amount': -5.65990099 if is_short else -0.00075,
'profit_ratio': -0.0945681 if is_short else -1.247e-05,
'stake_currency': 'USDT',
'base_currency': 'ETH',
'fiat_currency': 'USD',
'sell_reason': ExitType.STOP_LOSS.value,
'exit_reason': ExitType.STOP_LOSS.value,
@@ -3474,6 +3706,7 @@ def test_execute_trade_exit_custom_exit_price(
'profit_amount': pytest.approx(profit_amount),
'profit_ratio': profit_ratio,
'stake_currency': 'USDT',
'base_currency': 'ETH',
'fiat_currency': 'USD',
'sell_reason': 'foo',
'exit_reason': 'foo',
@@ -3547,6 +3780,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
'profit_ratio': -0.00501253 if is_short else -0.01493766,
'stake_currency': 'USDT',
'fiat_currency': 'USD',
'base_currency': 'ETH',
'sell_reason': ExitType.STOP_LOSS.value,
'exit_reason': ExitType.STOP_LOSS.value,
'open_date': ANY,
@@ -3588,7 +3822,7 @@ def test_execute_trade_exit_sloe_cancel_exception(
freqtrade.execute_trade_exit(trade=trade, limit=1234,
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
assert create_order_mock.call_count == 2
assert log_has('Could not cancel stoploss order abcd', caplog)
assert log_has('Could not cancel stoploss order abcd for pair ETH/USDT', caplog)
@pytest.mark.parametrize("is_short", [False, True])
@@ -3600,10 +3834,12 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
patch_exchange(mocker)
stoploss = MagicMock(return_value={
'id': 123,
'status': 'open',
'info': {
'foo': 'bar'
}
})
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
cancel_order = MagicMock(return_value=True)
mocker.patch.multiple(
@@ -3701,12 +3937,12 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
"lastTradeTimestamp": None,
"symbol": "BTC/USDT",
"type": "stop_loss_limit",
"side": "sell",
"side": "buy" if is_short else "sell",
"price": 1.08801,
"amount": 90.99181074,
"cost": 99.0000000032274,
"amount": trade.amount,
"cost": 1.08801 * trade.amount,
"average": 1.08801,
"filled": 90.99181074,
"filled": trade.amount,
"remaining": 0.0,
"status": "closed",
"fee": None,
@@ -3811,6 +4047,7 @@ def test_execute_trade_exit_market_order(
'profit_amount': pytest.approx(profit_amount),
'profit_ratio': profit_ratio,
'stake_currency': 'USDT',
'base_currency': 'ETH',
'fiat_currency': 'USD',
'sell_reason': ExitType.ROI.value,
'exit_reason': ExitType.ROI.value,

View File

@@ -35,7 +35,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
"type": "stop_loss_limit",
"side": "sell",
"price": 1.08801,
"amount": 90.99181074,
"amount": 91.07468123,
"cost": 0.0,
"average": 0.0,
"filled": 0.0,
@@ -49,8 +49,9 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
stoploss_order_closed['filled'] = stoploss_order_closed['amount']
# Sell first trade based on stoploss, keep 2nd and 3rd trade open
stop_orders = [stoploss_order_closed, stoploss_order_open, stoploss_order_open]
stoploss_order_mock = MagicMock(
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
side_effect=stop_orders)
# Sell 3rd trade (not called for the first trade)
should_sell_mock = MagicMock(side_effect=[
[],
@@ -93,13 +94,14 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
wallets_mock.reset_mock()
trades = Trade.session.scalars(select(Trade)).all()
# Make sure stoploss-order is open and trade is bought (since we mock update_trade_state)
for trade in trades:
stoploss_order_closed['id'] = '3'
oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss')
# Make sure stoploss-order is open and trade is bought
for idx, trade in enumerate(trades):
stop_order = stop_orders[idx]
stop_order['id'] = f"stop{idx}"
oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss')
trade.orders.append(oobj)
trade.stoploss_order_id = '3'
trade.stoploss_order_id = f"stop{idx}"
trade.open_order_id = None
n = freqtrade.exit_positions(trades)

BIN
tests/testdata/XRP_ETH-trades.feather vendored Normal file

Binary file not shown.