Compare commits

..

314 Commits

Author SHA1 Message Date
Matthias
77826ebf78 Merge pull request #7806 from freqtrade/new_release
New release 2022.11
2022-11-27 17:10:48 +01:00
Matthias
5c571f565f Version bump 2022.11 2022-11-27 15:34:00 +01:00
Matthias
178e5a195a Merge branch 'stable' into new_release 2022-11-27 15:33:45 +01:00
Matthias
21d7406291 Temporary fix for kraken download
closes #7790
will be removed once the patch is in ccxt.
2022-11-27 15:16:43 +01:00
Matthias
79a7dd5bd1 Merge pull request #7799 from freqtrade/fix-init-extra-rets
Use default `extra_returns_per_train`
2022-11-27 07:58:42 +01:00
robcaulk
dba30393fb ensure extra_returns_per_train are set properly on first hist_preds build 2022-11-26 18:04:47 +01:00
Matthias
9af62ad117 Add note to dev docs about freqUI release 2022-11-26 14:09:05 +01:00
Matthias
ce213b55a2 Bybit fix candle limit 2022-11-26 13:58:22 +01:00
Matthias
756921b16a Update fthypt file 2022-11-25 17:05:49 +01:00
Matthias
79c041b62d Update tests for new export format 2022-11-25 16:57:58 +01:00
Matthias
8c014bd365 Export trade-counts to csv
closes #7789
2022-11-25 16:57:45 +01:00
Matthias
8ee8b6e943 Improve hyperopt list output
closes  #7789
2022-11-25 16:31:21 +01:00
Matthias
0f97ef0d7b Reset stoploss_order_id when order is canceled
closes #7766
2022-11-25 16:08:33 +01:00
Matthias
1b3e62bcbc Lock execute_entry to prevent timing hickups 2022-11-25 14:50:48 +01:00
Matthias
c593cdc438 Improve type hints 2022-11-25 14:48:06 +01:00
Matthias
5e6cda11ef Update method name for trade fee updating 2022-11-25 14:43:56 +01:00
Matthias
048119ad3d Improve doc wording around informative pair candle types
closes #7792
2022-11-25 14:20:41 +01:00
Matthias
b8d1862ca8 Update cached binance leverage tiers
closes #7794
2022-11-25 10:42:19 +01:00
Matthias
c963fd720b Slightly change test setup for dry_run_order_fill 2022-11-23 18:17:14 +01:00
Robert Caulk
12e17b80fe Merge pull request #7791 from freqtrade/fix-m1-wheel-lightgbm
Enable --use-pep517 flag for freqai
2022-11-23 17:52:20 +01:00
Emre
335de760ed Enable --use-pep517 flag for freqai 2022-11-23 18:34:50 +03:00
Matthias
7785c91c5d Merge pull request #7756 from wizrds/feat/secure-ws-conn
Support SSL in WebSocket connection
2022-11-22 19:18:16 +01:00
Matthias
bd05f85c26 Simplify ssl documentation 2022-11-22 18:11:18 +01:00
Timothy Pogue
fff745fd83 add map to nginx config 2022-11-22 07:17:57 -07:00
Matthias
5a489ce71b Fix docs typo 2022-11-22 10:46:38 +01:00
Timothy Pogue
86ff711525 update docs on reverse proxy 2022-11-21 12:52:18 -07:00
Matthias
8cb2b4666d Improve proxy docs
closes #7769
2022-11-21 20:42:07 +01:00
Matthias
0fa5217043 Improve protection setup
lock_pair should be called when the order closes, not when the exit order is placed.
it should also be called for stoploss orders, too.

closes #7783
2022-11-21 19:30:49 +01:00
Matthias
be80d91ca6 Merge pull request #7779 from freqtrade/dependabot/pip/develop/uvicorn-0.20.0
Bump uvicorn from 0.19.0 to 0.20.0
2022-11-21 18:08:27 +01:00
Matthias
450ebaa2cc Merge pull request #7778 from freqtrade/dependabot/pip/develop/types-requests-2.28.11.5
Bump types-requests from 2.28.11.4 to 2.28.11.5
2022-11-21 13:23:52 +01:00
Matthias
7c00ef8a76 Bump pre-commit requests version 2022-11-21 11:32:31 +01:00
Matthias
74be124a47 Merge pull request #7775 from freqtrade/dependabot/pip/develop/ccxt-2.1.96
Bump ccxt from 2.1.75 to 2.1.96
2022-11-21 11:31:48 +01:00
dependabot[bot]
adc1174d2e Bump types-requests from 2.28.11.4 to 2.28.11.5
Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.4 to 2.28.11.5.
- [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>
2022-11-21 10:30:22 +00:00
Matthias
69b2e31bdb Merge pull request #7773 from freqtrade/dependabot/pip/develop/types-python-dateutil-2.8.19.4
Bump types-python-dateutil from 2.8.19.3 to 2.8.19.4
2022-11-21 11:29:03 +01:00
Matthias
747dd9cb16 Merge pull request #7774 from freqtrade/dependabot/pip/develop/mypy-0.991
Bump mypy from 0.990 to 0.991
2022-11-21 07:12:19 +01:00
Matthias
2df0d613da Bump types-dateutil for precommit 2022-11-21 06:58:59 +01:00
dependabot[bot]
beec9e2d1a Bump mypy from 0.990 to 0.991
Bumps [mypy](https://github.com/python/mypy) from 0.990 to 0.991.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.990...v0.991)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-21 05:36:52 +00:00
Matthias
129f549793 Merge pull request #7781 from freqtrade/dependabot/pip/develop/orjson-3.8.2
Bump orjson from 3.8.1 to 3.8.2
2022-11-21 06:36:34 +01:00
Matthias
1456022dfe Merge pull request #7780 from freqtrade/dependabot/pip/develop/httpx-0.23.1
Bump httpx from 0.23.0 to 0.23.1
2022-11-21 06:35:55 +01:00
Matthias
0d615cfdd8 Merge pull request #7777 from freqtrade/dependabot/pip/develop/nbconvert-7.2.5
Bump nbconvert from 7.2.4 to 7.2.5
2022-11-21 06:35:40 +01:00
Matthias
3dc6a30d65 Merge pull request #7772 from freqtrade/dependabot/pip/develop/numpy-1.23.5
Bump numpy from 1.23.4 to 1.23.5
2022-11-21 06:35:19 +01:00
dependabot[bot]
f09fb2374b Bump orjson from 3.8.1 to 3.8.2
Bumps [orjson](https://github.com/ijl/orjson) from 3.8.1 to 3.8.2.
- [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.1...3.8.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-21 03:02:47 +00:00
dependabot[bot]
8d1ee67ed4 Bump httpx from 0.23.0 to 0.23.1
Bumps [httpx](https://github.com/encode/httpx) from 0.23.0 to 0.23.1.
- [Release notes](https://github.com/encode/httpx/releases)
- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/httpx/compare/0.23.0...0.23.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-21 03:02:38 +00:00
dependabot[bot]
844334a7ea Bump uvicorn from 0.19.0 to 0.20.0
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.19.0...0.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-21 03:02:34 +00:00
dependabot[bot]
ec15ef0398 Bump nbconvert from 7.2.4 to 7.2.5
Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.2.4 to 7.2.5.
- [Release notes](https://github.com/jupyter/nbconvert/releases)
- [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter/nbconvert/compare/v7.2.4...v7.2.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-21 03:02:26 +00:00
dependabot[bot]
3f9dacc9be Bump ccxt from 2.1.75 to 2.1.96
Bumps [ccxt](https://github.com/ccxt/ccxt) from 2.1.75 to 2.1.96.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg)
- [Commits](https://github.com/ccxt/ccxt/compare/2.1.75...2.1.96)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-21 03:02:15 +00:00
dependabot[bot]
5cce8f4f2d Bump types-python-dateutil from 2.8.19.3 to 2.8.19.4
Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.3 to 2.8.19.4.
- [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>
2022-11-21 03:02:00 +00:00
dependabot[bot]
0cb08024f1 Bump numpy from 1.23.4 to 1.23.5
Bumps [numpy](https://github.com/numpy/numpy) from 1.23.4 to 1.23.5.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v1.23.4...v1.23.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-21 03:01:59 +00:00
Timothy Pogue
edb817e2e6 add tutorial for ssl in docs 2022-11-20 19:19:28 -07:00
Timothy Pogue
106ac2ab4d fix tests, change to get call 2022-11-20 16:36:22 -07:00
Matthias
12b471c64b Prevent 2 parallel open orders through forceentry
this leads to forgetting the prior order

closes #7765
2022-11-20 09:28:14 +01:00
Matthias
4de9a46618 Merge pull request #7759 from wizrds/fix/pd-mem-leak
Fix Pandas to_json memory leak
2022-11-18 20:24:16 +01:00
Timothy Pogue
b6a8e421f1 remove redundant timestamp conversion in ws serializer 2022-11-18 09:43:39 -07:00
Matthias
12cd83453c Add warning when queue websocket queue becomes too full 2022-11-18 14:03:56 +01:00
Matthias
4c7bb79c86 Restore prior data transfer format 2022-11-18 13:59:29 +01:00
Matthias
436b314c80 add safe_remaining
fixes #7757
2022-11-17 19:07:47 +00:00
Matthias
1975e942d6 Add test for no remaining
(kucoin case - https://github.com/freqtrade/freqtrade/issues/7757).
2022-11-17 19:07:47 +00:00
Matthias
48e5a45856 rpc_test: dont replicate whole response,
updating  what's changed improves readability
2022-11-17 19:07:47 +00:00
Timothy Pogue
49ecc83061 Merge branch 'develop' into fix/pd-mem-leak 2022-11-17 12:04:49 -07:00
Timothy Pogue
ce43fa5f43 small fix to websocketchannel and relay 2022-11-17 12:03:11 -07:00
Timothy Pogue
875e9ab447 change df serialization to avoid mem leak 2022-11-17 11:59:03 -07:00
Robert Caulk
cd6f87be17 Merge pull request #7728 from freqtrade/improve_timerange
Simplify timerange handling
2022-11-17 19:57:48 +01:00
Matthias
9432bcd065 Fix telegram error on force_enter exception
closes #7727
2022-11-17 19:52:03 +01:00
Timothy Pogue
a993cb512d change to get call in ws_client 2022-11-17 10:22:55 -07:00
Matthias
0a7f4fd3cc fix httpx test warning 2022-11-17 10:36:24 +00:00
Matthias
afcb86f422 Improve migration types 2022-11-17 10:25:51 +00:00
Matthias
93addbe5c3 Improve typechecking 2022-11-17 10:16:38 +00:00
Matthias
097af973d2 improve RPC testcase to cover open orders 2022-11-17 07:10:47 +01:00
Timothy Pogue
1380ddd066 update ws client 2022-11-15 22:44:02 -07:00
Matthias
019577f73d Temporarily Downgrade cryptography until piwheels has the new wheel available 2022-11-16 06:36:26 +01:00
Timothy Pogue
86e094e39b update docs 2022-11-15 22:33:42 -07:00
Timothy Pogue
6a1655c047 support ssl connections in emc 2022-11-15 22:26:54 -07:00
Matthias
6deb2dfb61 Merge pull request #7744 from freqtrade/dependabot/pip/develop/mypy-0.990
Bump mypy from 0.982 to 0.990
2022-11-15 06:24:24 +01:00
Matthias
0a702cdd26 Ensure more methods are typechecked 2022-11-14 20:56:35 +01:00
Matthias
f27be7ada8 Configure mypy to old behavior
based on https://mypy-lang.blogspot.com/2022/11/mypy-0990-released.html release
2022-11-14 20:52:40 +01:00
Matthias
a951b49541 Use Generator when sending initial dataframes 2022-11-14 19:43:59 +01:00
Matthias
30b467906c Delist FTX, following ccxt's delisting. 2022-11-14 19:40:57 +01:00
Matthias
663039835d Merge pull request #7745 from freqtrade/dependabot/pip/develop/fastapi-0.87.0
Bump fastapi from 0.85.1 to 0.87.0
2022-11-14 15:19:52 +01:00
Matthias
c72ffad698 Add starlette test dependency 2022-11-14 13:08:29 +00:00
Matthias
2a1bfb8e57 Merge pull request #7746 from freqtrade/dependabot/pip/develop/types-python-dateutil-2.8.19.3
Bump types-python-dateutil from 2.8.19.2 to 2.8.19.3
2022-11-14 14:05:03 +01:00
Matthias
a689538b9a Merge pull request #7751 from freqtrade/dependabot/pip/develop/sqlalchemy-1.4.44
Bump sqlalchemy from 1.4.43 to 1.4.44
2022-11-14 09:39:47 +01:00
Matthias
e24f644251 Merge pull request #7742 from freqtrade/dependabot/pip/develop/cryptography-38.0.3
Bump cryptography from 38.0.1 to 38.0.3
2022-11-14 09:39:25 +01:00
Matthias
c12dcd9b9b update pre-commit dateutil 2022-11-14 08:40:09 +01:00
dependabot[bot]
4cece8720a Bump mypy from 0.982 to 0.990
Bumps [mypy](https://github.com/python/mypy) from 0.982 to 0.990.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.982...v0.990)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 07:33:06 +00:00
dependabot[bot]
721998521b Bump types-python-dateutil from 2.8.19.2 to 2.8.19.3
Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.2 to 2.8.19.3.
- [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>
2022-11-14 07:33:03 +00:00
dependabot[bot]
60449d9bec Bump cryptography from 38.0.1 to 38.0.3
Bumps [cryptography](https://github.com/pyca/cryptography) from 38.0.1 to 38.0.3.
- [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/38.0.1...38.0.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 07:32:31 +00:00
Matthias
5ca705ae3a Merge pull request #7747 from freqtrade/dependabot/pip/develop/types-requests-2.28.11.4
Bump types-requests from 2.28.11.2 to 2.28.11.4
2022-11-14 08:32:11 +01:00
Matthias
cf6aa0506f Merge pull request #7750 from freqtrade/dependabot/pip/develop/ccxt-2.1.75
Bump ccxt from 2.1.54 to 2.1.75
2022-11-14 08:31:41 +01:00
Matthias
849c028133 Merge pull request #7749 from freqtrade/dependabot/pip/develop/psutil-5.9.4
Bump psutil from 5.9.3 to 5.9.4
2022-11-14 08:28:38 +01:00
Matthias
cf9944c48d Merge pull request #7741 from freqtrade/dependabot/pip/develop/mkdocs-material-8.5.10
Bump mkdocs-material from 8.5.8 to 8.5.10
2022-11-14 08:03:39 +01:00
dependabot[bot]
cf5cda4df5 Bump sqlalchemy from 1.4.43 to 1.4.44
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.43 to 1.4.44.
- [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>
2022-11-14 06:04:54 +00:00
dependabot[bot]
7275d48516 Bump ccxt from 2.1.54 to 2.1.75
Bumps [ccxt](https://github.com/ccxt/ccxt) from 2.1.54 to 2.1.75.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg)
- [Commits](https://github.com/ccxt/ccxt/compare/2.1.54...2.1.75)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 06:04:37 +00:00
dependabot[bot]
60de797dcc Bump psutil from 5.9.3 to 5.9.4
Bumps [psutil](https://github.com/giampaolo/psutil) from 5.9.3 to 5.9.4.
- [Release notes](https://github.com/giampaolo/psutil/releases)
- [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/release-5.9.3...release-5.9.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 06:03:58 +00:00
Matthias
95fd4072fa Merge pull request #7701 from freqtrade/add-single-precision-freqai
add option to force single precision
2022-11-14 07:02:35 +01:00
Matthias
b2de070462 bump types-requests 2022-11-14 07:01:49 +01:00
Matthias
03d3492838 Merge pull request #7739 from freqtrade/dependabot/pip/develop/tensorboard-2.11.0
Bump tensorboard from 2.10.1 to 2.11.0
2022-11-14 06:55:24 +01:00
dependabot[bot]
9843fb2087 Bump mkdocs-material from 8.5.8 to 8.5.10
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.8 to 8.5.10.
- [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/8.5.8...8.5.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 05:55:09 +00:00
Matthias
447635043e Merge pull request #7740 from freqtrade/dependabot/pip/develop/nbconvert-7.2.4
Bump nbconvert from 7.2.3 to 7.2.4
2022-11-14 06:54:47 +01:00
Matthias
5d92008293 Merge pull request #7743 from freqtrade/dependabot/pip/develop/pytest-asyncio-0.20.2
Bump pytest-asyncio from 0.20.1 to 0.20.2
2022-11-14 06:54:27 +01:00
Matthias
d22a22d161 Merge pull request #7748 from freqtrade/dependabot/pip/develop/pymdown-extensions-9.8
Bump pymdown-extensions from 9.7 to 9.8
2022-11-14 06:53:51 +01:00
dependabot[bot]
bbfcaca9e0 Bump pymdown-extensions from 9.7 to 9.8
Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.7 to 9.8.
- [Release notes](https://github.com/facelessuser/pymdown-extensions/releases)
- [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.7...9.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 03:02:26 +00:00
dependabot[bot]
48c4d8d2df Bump types-requests from 2.28.11.2 to 2.28.11.4
Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.2 to 2.28.11.4.
- [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>
2022-11-14 03:02:21 +00:00
dependabot[bot]
ce269b7984 Bump fastapi from 0.85.1 to 0.87.0
Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.85.1 to 0.87.0.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.85.1...0.87.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 03:02:14 +00:00
dependabot[bot]
9d8d18d76b Bump pytest-asyncio from 0.20.1 to 0.20.2
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.20.1 to 0.20.2.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.20.1...v0.20.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 03:01:42 +00:00
dependabot[bot]
001602e034 Bump nbconvert from 7.2.3 to 7.2.4
Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.2.3 to 7.2.4.
- [Release notes](https://github.com/jupyter/nbconvert/releases)
- [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter/nbconvert/compare/v7.2.3...v7.2.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 03:01:11 +00:00
dependabot[bot]
3e6834e3f0 Bump tensorboard from 2.10.1 to 2.11.0
Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.10.1 to 2.11.0.
- [Release notes](https://github.com/tensorflow/tensorboard/releases)
- [Changelog](https://github.com/tensorflow/tensorboard/blob/master/RELEASE.md)
- [Commits](https://github.com/tensorflow/tensorboard/compare/2.10.1...2.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 03:01:08 +00:00
Matthias
42b29cd307 Fix constants type 2022-11-13 19:31:49 +01:00
robcaulk
1e9e7887aa fix constants.py, fix freqai test 2022-11-13 15:38:35 +01:00
Matthias
a59d61472b Add test for dataframe footprint reduction 2022-11-13 15:29:37 +01:00
Matthias
942840da2d Improve setting wording to keep future possibilities open 2022-11-13 15:22:44 +01:00
Matthias
c6013e5819 Fix doc typo 2022-11-13 12:01:20 +01:00
Matthias
535c365b4a Disable ftx ccxt_compat tests
Their API is down due to being insolvent ...
2022-11-13 10:33:54 +01:00
Matthias
fed3bc6730 Simplify Websocket Init 2022-11-13 10:33:54 +01:00
Matthias
914bdbdd83 Remove FTX from list of supported exchanges 2022-11-12 20:32:17 +01:00
Matthias
d39b997489 Merge pull request #7736 from M451z/patch-1
Update README.md
2022-11-12 20:30:50 +01:00
Matthias
954da4fec9 Add "forcebuy error" exception log
part of #7727
2022-11-12 19:52:10 +01:00
Matthias
ee0e59157c Update join relationship of orders table to selectin
closes #6791 by slightly improving performance in this area.
2022-11-12 16:34:00 +01:00
Colt 1911
8c092d457c Update README.md
Removed the FTX option under the "Supported Exchange marketplaces" title, because of FTX bankrupt.
2022-11-12 18:28:51 +03:00
robcaulk
214c622475 move dataframe converter to converter.py 2022-11-12 10:38:25 +01:00
robcaulk
9617d8143d Merge remote-tracking branch 'origin/develop' into add-single-precision-freqai 2022-11-12 10:21:38 +01:00
Matthias
e6172a68d7 Merge pull request #7730 from freqtrade/fix-metric-tracker
fix loading of metric tracker from disk
2022-11-11 20:01:26 +01:00
Robert Caulk
833578716c Merge pull request #7644 from markdregan/multi-target-classifier
Support for multi target multi-class classification (FreqaiMultiOutputRegressor for Classification)
2022-11-11 18:48:38 +01:00
robcaulk
790ff2a84b merge develop into add-single-prec 2022-11-11 18:08:51 +01:00
robcaulk
e46a57bbd0 move mem logs to debug 2022-11-11 18:05:32 +01:00
robcaulk
66514e84e4 add LightGBMClassifierMultiTarget. add test 2022-11-11 17:45:53 +01:00
robcaulk
054133955b fix loading of metric tracker from disk 2022-11-11 17:24:09 +01:00
Matthias
e34f0f60a5 Merge pull request #7724 from wassertim/bugfix/7723
Support git and local changes in dev containers
2022-11-10 21:05:36 +01:00
Matthias
4664d5e1d8 Split installation to onCreateCommand 2022-11-10 18:56:19 +00:00
Matthias
0f9c5f8d41 Simplify timerange handling 2022-11-10 18:28:18 +01:00
Matthias
57313dd961 Update some usages of timerange to new, simplified method 2022-11-10 18:11:39 +01:00
Matthias
3e676dbaa4 Add properties to simplify timerange handling 2022-11-10 16:33:57 +01:00
Matthias
7147f52e02 FreqAI also requires plotting dependencies
cloess #7726
2022-11-10 16:03:30 +01:00
Tim
be83e73411 add pip install 2022-11-10 08:42:47 +00:00
Matthias
88ad3fe43e Remove typo from main page 2022-11-10 07:32:55 +01:00
Matthias
22c419d5c4 Add warning about FTX 2022-11-10 07:14:10 +01:00
Matthias
9e17eabd0a Improve Bybit configuration 2022-11-10 07:09:54 +01:00
Tim
ec6ee7ead9 remove empty space 2022-11-09 21:06:14 +01:00
Tim
7953280513 remove github.copilot extension 2022-11-09 21:05:05 +01:00
Tim
037363f9ee support git and local changes in dev containers #7723 2022-11-09 19:53:09 +01:00
Matthias
d3006f7f3e Bump ccxt to 2.1.54
closes okx: #7720
2022-11-09 17:59:51 +01:00
Matthias
f43f967040 Improve handling of unfilled stoploss orders in edge-cases 2022-11-08 20:34:18 +01:00
Matthias
ce3959a0c6 Merge pull request #7708 from freqtrade/improve_iteration
Improve iteration logic
2022-11-08 19:25:01 +01:00
Robert Caulk
d59b3e2359 Merge pull request #7710 from freqtrade/dependabot/pip/develop/catboost-1.1.1
Bump catboost from 1.1 to 1.1.1
2022-11-08 11:22:29 +01:00
dependabot[bot]
ea489133ac Bump catboost from 1.1 to 1.1.1
Bumps [catboost](https://github.com/catboost/catboost) from 1.1 to 1.1.1.
- [Release notes](https://github.com/catboost/catboost/releases)
- [Changelog](https://github.com/catboost/catboost/blob/master/RELEASE.md)
- [Commits](https://github.com/catboost/catboost/compare/v1.1...v1.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 23:54:59 +00:00
Robert Caulk
c3d2df2f4e Merge pull request #7711 from freqtrade/dependabot/pip/develop/xgboost-1.7.1
Bump xgboost from 1.6.2 to 1.7.1
2022-11-08 00:53:51 +01:00
Matthias
426a26f268 Merge pull request #7475 from freqtrade/backtest_live_models
FreqAI - Backtest live/ready models
2022-11-07 20:39:49 +01:00
Matthias
884014a4b9 Fix some minor typos 2022-11-07 18:35:28 +00:00
Wagner Costa Santos
6559384286 Merge branch 'develop' into backtest_live_models 2022-11-07 15:14:10 -03:00
Matthias
8bc71f2025 Merge pull request #7709 from freqtrade/dependabot/pip/develop/jsonschema-4.17.0
Bump jsonschema from 4.16.0 to 4.17.0
2022-11-07 08:19:12 +01:00
Matthias
24df2d576e Merge pull request #7718 from freqtrade/dependabot/pip/develop/orjson-3.8.1
Bump orjson from 3.8.0 to 3.8.1
2022-11-07 08:18:30 +01:00
Matthias
5ba012c592 Disable "tick" in test_update_funding_fees_schedule
we only want to test run frequency, not time progression.
2022-11-07 07:18:09 +00:00
Matthias
05fc6a5e9f Merge pull request #7717 from freqtrade/dependabot/pip/develop/mkdocs-material-8.5.8
Bump mkdocs-material from 8.5.7 to 8.5.8
2022-11-07 07:54:48 +01:00
dependabot[bot]
850b04357e Bump mkdocs-material from 8.5.7 to 8.5.8
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.7 to 8.5.8.
- [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/8.5.7...8.5.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 06:05:47 +00:00
Matthias
a90d91b576 Merge pull request #7714 from freqtrade/dependabot/pip/develop/progressbar2-4.2.0
Bump progressbar2 from 4.1.1 to 4.2.0
2022-11-07 07:04:56 +01:00
Matthias
95a1827af7 Merge pull request #7712 from freqtrade/dependabot/pip/develop/mkdocs-1.4.2
Bump mkdocs from 1.4.1 to 1.4.2
2022-11-07 07:04:02 +01:00
Matthias
689f936390 Merge pull request #7716 from freqtrade/dependabot/pip/develop/pytest-7.2.0
Bump pytest from 7.1.3 to 7.2.0
2022-11-07 07:03:34 +01:00
Matthias
031c472a23 Merge pull request #7715 from freqtrade/dependabot/pip/develop/prompt-toolkit-3.0.32
Bump prompt-toolkit from 3.0.31 to 3.0.32
2022-11-07 07:03:10 +01:00
Matthias
71580a7159 Merge pull request #7713 from freqtrade/dependabot/pip/develop/sqlalchemy-1.4.43
Bump sqlalchemy from 1.4.42 to 1.4.43
2022-11-07 06:57:46 +01:00
dependabot[bot]
d978ff6bfb Bump orjson from 3.8.0 to 3.8.1
Bumps [orjson](https://github.com/ijl/orjson) from 3.8.0 to 3.8.1.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.8.0...3.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 03:02:15 +00:00
dependabot[bot]
0bb57f738d Bump pytest from 7.1.3 to 7.2.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.3 to 7.2.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.3...7.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 03:01:54 +00:00
dependabot[bot]
37e066bd76 Bump prompt-toolkit from 3.0.31 to 3.0.32
Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.31 to 3.0.32.
- [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases)
- [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG)
- [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.31...3.0.32)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 03:01:47 +00:00
dependabot[bot]
224507dfa0 Bump progressbar2 from 4.1.1 to 4.2.0
Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 4.1.1 to 4.2.0.
- [Release notes](https://github.com/WoLpH/python-progressbar/releases)
- [Changelog](https://github.com/wolph/python-progressbar/blob/develop/CHANGES.rst)
- [Commits](https://github.com/WoLpH/python-progressbar/compare/v4.1.1...v4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 03:01:36 +00:00
dependabot[bot]
f174b41fd7 Bump sqlalchemy from 1.4.42 to 1.4.43
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.42 to 1.4.43.
- [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>
2022-11-07 03:01:30 +00:00
dependabot[bot]
01a31a6e01 Bump mkdocs from 1.4.1 to 1.4.2
Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/mkdocs/mkdocs/releases)
- [Commits](https://github.com/mkdocs/mkdocs/compare/1.4.1...1.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 03:01:08 +00:00
dependabot[bot]
1814f25601 Bump xgboost from 1.6.2 to 1.7.1
Bumps [xgboost](https://github.com/dmlc/xgboost) from 1.6.2 to 1.7.1.
- [Release notes](https://github.com/dmlc/xgboost/releases)
- [Changelog](https://github.com/dmlc/xgboost/blob/master/NEWS.md)
- [Commits](https://github.com/dmlc/xgboost/compare/v1.6.2...v1.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 03:01:03 +00:00
dependabot[bot]
3cbbfde6bc Bump jsonschema from 4.16.0 to 4.17.0
Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.16.0 to 4.17.0.
- [Release notes](https://github.com/python-jsonschema/jsonschema/releases)
- [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.16.0...v4.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-07 03:00:40 +00:00
Matthias
29585b5ecd Improve worker iteration logic 2022-11-06 11:18:13 +01:00
Matthias
2068a44fd0 Add test for new behavior 2022-11-06 11:17:25 +01:00
Matthias
d48a9ae96d Add leverage to backtest results
closes #7574
2022-11-06 09:40:44 +01:00
Matthias
1d2b89bc13 Improve handling of get_fee to not allow impossible combinations 2022-11-06 09:16:03 +01:00
Matthias
3af177d8af bump ccxt to 2.1.33
closes #7700
2022-11-06 09:16:03 +01:00
Robert Caulk
820aad670c Merge pull request #7690 from freqtrade/track-current-candle
Track current candle in FreqAI
2022-11-05 17:35:05 +01:00
Emre
06a2957837 Merge branch 'develop' into add-single-precision-freqai 2022-11-05 19:15:12 +03:00
Emre
43bdd34964 Optimize reduce_dataframe_footprint function 2022-11-05 19:13:02 +03:00
Matthias
25b8d34fe2 Update backtesting test
Had bad behavior before, and didn't properly test what it was supposed to
2022-11-05 17:02:18 +01:00
robcaulk
53df607067 avoid duplicating features with okx/gateio, ensure inference timer gets logged 2022-11-05 15:42:19 +01:00
Matthias
6e09d552ac Properly handle and test ohlcv min_max with empty files 2022-11-05 13:14:35 +01:00
Matthias
a9ea84e2c4 Merge pull request #7702 from freqtrade/move-write-metric-param
move write_metrics_to_disk to proper place in param table
2022-11-04 18:25:34 +01:00
robcaulk
257c833831 add doc for single precision, dont allow half precision, add test 2022-11-04 18:10:04 +01:00
robcaulk
d4cfcbda24 move write_metrics_to_disk to proper place in param table 2022-11-04 17:53:15 +01:00
robcaulk
3ccc120f92 add option to force single precision 2022-11-04 17:42:10 +01:00
robcaulk
8bdc99a3d6 fix self-imposed bug 2022-11-04 16:41:38 +01:00
robcaulk
19d90b813a improve readibility in start_backtesting() 2022-11-04 16:10:46 +01:00
Wagner Costa Santos
8008c63319 Merge branch 'develop' into backtest_live_models 2022-11-04 09:09:39 -03:00
Wagner Costa Santos
a7acfb7ab7 fix dict key error 2022-11-04 09:02:28 -03:00
robcaulk
90c5bfb4b5 add default conv_width 2022-11-03 21:35:12 +01:00
robcaulk
05b309caf2 ensure colon replaced for cached attach 2022-11-03 21:17:48 +01:00
robcaulk
6938ed6584 change default conv_width to 1 2022-11-03 21:11:26 +01:00
robcaulk
444a068481 merge develop into track-current-candle 2022-11-03 21:09:08 +01:00
robcaulk
db942321ad fix bug with lightgbm and colons 2022-11-03 21:03:48 +01:00
Matthias
c2130ed3dd Merge pull request #7695 from freqtrade/fix_issue_7666
Fix inconsistent backtesting results - FreqAI
2022-11-03 19:52:48 +01:00
robcaulk
d721b50230 flake8 2022-11-03 19:13:24 +01:00
robcaulk
3ba1e221eb fix typo 2022-11-03 19:08:33 +01:00
robcaulk
6c4bdb8f67 remove special characters from feature names 2022-11-03 18:49:39 +01:00
Wagner Costa Santos
17798b3397 Merge branch 'develop' into backtest_live_models 2022-11-03 13:29:25 -03:00
Wagner Costa Santos
356d79b38a verify mean and std exists in model metadata 2022-11-03 13:27:56 -03:00
Wagner Costa Santos
cdf12cc541 Merge branch 'develop' into fix_issue_7666 2022-11-03 08:30:46 -03:00
Matthias
0aff8c4823 Merge pull request #7692 from wizrds/fix/ws-memory
Fix Memory Leak in Websockets
2022-11-03 07:17:01 +01:00
Matthias
ff619edebf Improve explanation comment as to why we're waiting ourselfs 2022-11-03 06:50:18 +01:00
Timothy Pogue
b749f3edd6 add latency measure from ping in emc and ws_client 2022-11-02 19:30:35 -06:00
Timothy Pogue
a0965606a5 update ws_client more verbosity, better readable time delta 2022-11-02 18:49:11 -06:00
Timothy Pogue
000b0c2198 prevent memory leaks from error in _broadcast_queue_data 2022-11-02 18:00:10 -06:00
Timothy Pogue
cbede2e27d refactor channel.send to avoid queue.put 2022-11-02 17:57:11 -06:00
Timothy Pogue
2dc55e89e6 better error handling channel send 2022-11-02 15:25:39 -06:00
Timothy Pogue
55bf195bfb remove debugging log calls 2022-11-02 14:21:34 -06:00
Timothy Pogue
c2bdaea84a change exception handling in channel send 2022-11-02 14:19:08 -06:00
Timothy Pogue
d848c27283 add task done to broadcast queue method 2022-11-02 13:30:42 -06:00
robcaulk
b3b756ec14 ensure test pass 2022-11-02 20:30:04 +01:00
Timothy Pogue
e25dea7e0e update channel disconnecting 2022-11-02 13:26:27 -06:00
robcaulk
ce92731132 ensure backwards compatitibility 2022-11-02 20:20:35 +01:00
Wagner Costa Santos
23b6915dde fix issue with different backtesting prediction size 2022-11-02 15:49:51 -03:00
Matthias
09e0a8d4df Merge pull request #7689 from freqtrade/add-pca-dbscan-tests
add integrated tests for PCA and DBSCAN
2022-11-02 19:41:37 +01:00
Matthias
2c3c7e1e3a Merge pull request #7663 from freqtrade/shuffle_list_enhance
Improve ShufflePairlist to shuffle only once per candle
2022-11-02 19:37:48 +01:00
robcaulk
1a38c10fc6 remove old code 2022-11-02 19:37:47 +01:00
robcaulk
255eb71270 start tracking the current candle in FreqAI, add robustness to corr_df caching and inference timer, add test for cache corr_df 2022-11-02 19:32:22 +01:00
robcaulk
63458a6130 isort 2022-11-02 18:40:13 +01:00
robcaulk
2afa185dc6 add integrated tests for PCA and DBSCAN 2022-11-02 18:34:56 +01:00
Matthias
2ed04916ae Merge pull request #7676 from freqtrade/dependabot/pip/develop/pyarrow-10.0.0
Bump pyarrow from 9.0.0 to 10.0.0
2022-11-02 08:14:55 +01:00
Matthias
d9f41e5570 Update pyarrow prebuilt wheel 2022-11-02 06:41:15 +01:00
Matthias
b82fc3fabd Merge pull request #7612 from freqtrade/reduce-indicator-population
avoid redundant indicator population for corr_pair list
2022-10-31 20:24:27 +01:00
Matthias
f5c694213b Merge pull request #7672 from freqtrade/dependabot/pip/develop/scikit-learn-1.1.3
Bump scikit-learn from 1.1.2 to 1.1.3
2022-10-31 19:27:33 +01:00
Matthias
4f1d1d4688 Merge pull request #7683 from freqtrade/add-statement-of-need
add statement of need to FreqAI docs
2022-10-31 18:59:45 +01:00
robcaulk
162056a362 fix flake8 2022-10-31 18:23:35 +01:00
robcaulk
97df232ac6 add a warning to __init__ for get_corr_dataframes 2022-10-31 18:18:00 +01:00
robcaulk
e6a70d95df Merge branch 'develop' into reduce-indicator-population 2022-10-31 18:13:55 +01:00
robcaulk
7b880a969a change elect to decide 2022-10-31 18:13:48 +01:00
robcaulk
fbc281e695 fix grammar, remove hyperlink 2022-10-31 18:10:31 +01:00
robcaulk
c3c6733b2d add statement of need to FreqAI docs 2022-10-31 18:03:53 +01:00
Matthias
0f2e540a64 Update windows documentation about 32bit Windows 2022-10-31 17:39:41 +01:00
Matthias
ef8007fc42 Merge pull request #7675 from freqtrade/dependabot/pip/develop/uvicorn-0.19.0
Bump uvicorn from 0.18.3 to 0.19.0
2022-10-31 17:33:58 +01:00
Matthias
b3f612ecfb Bump ccxt to 2.0.96 2022-10-31 17:21:52 +01:00
Matthias
735546ab89 Merge pull request #7674 from freqtrade/dependabot/pip/develop/nbconvert-7.2.3
Bump nbconvert from 7.2.1 to 7.2.3
2022-10-31 15:26:38 +01:00
robcaulk
66d8ed6c0b Merge remote-tracking branch 'origin/develop' into reduce-indicator-population 2022-10-31 09:42:01 +01:00
dependabot[bot]
aa3d6dc298 Bump uvicorn from 0.18.3 to 0.19.0
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.18.3 to 0.19.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.18.3...0.19.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 08:23:29 +00:00
Matthias
ccba651d37 Merge pull request #7671 from freqtrade/dependabot/pip/develop/plotly-5.11.0
Bump plotly from 5.10.0 to 5.11.0
2022-10-31 09:22:45 +01:00
Matthias
53bc72f27f Merge pull request #7680 from freqtrade/dependabot/pip/develop/psutil-5.9.3
Bump psutil from 5.9.2 to 5.9.3
2022-10-31 09:21:53 +01:00
Matthias
9c7e686db0 Merge pull request #7678 from freqtrade/dependabot/pip/develop/ccxt-2.0.90
Bump ccxt from 2.0.58 to 2.0.90
2022-10-31 08:50:10 +01:00
Matthias
ecd5e22960 Merge pull request #7673 from freqtrade/dependabot/pip/develop/pycoingecko-3.1.0
Bump pycoingecko from 3.0.0 to 3.1.0
2022-10-31 08:40:03 +01:00
Matthias
e010c01446 Merge pull request #7679 from freqtrade/dependabot/pip/develop/websockets-10.4
Bump websockets from 10.3 to 10.4
2022-10-31 08:36:28 +01:00
dependabot[bot]
be67eb9586 Bump psutil from 5.9.2 to 5.9.3
Bumps [psutil](https://github.com/giampaolo/psutil) from 5.9.2 to 5.9.3.
- [Release notes](https://github.com/giampaolo/psutil/releases)
- [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/release-5.9.2...release-5.9.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 07:35:55 +00:00
Matthias
51c97d8099 Merge pull request #7677 from freqtrade/dependabot/pip/develop/colorama-0.4.6
Bump colorama from 0.4.5 to 0.4.6
2022-10-31 08:34:44 +01:00
dependabot[bot]
39f145e7ba Bump websockets from 10.3 to 10.4
Bumps [websockets](https://github.com/aaugustin/websockets) from 10.3 to 10.4.
- [Release notes](https://github.com/aaugustin/websockets/releases)
- [Commits](https://github.com/aaugustin/websockets/compare/10.3...10.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 03:01:55 +00:00
dependabot[bot]
ac86d19459 Bump ccxt from 2.0.58 to 2.0.90
Bumps [ccxt](https://github.com/ccxt/ccxt) from 2.0.58 to 2.0.90.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg)
- [Commits](https://github.com/ccxt/ccxt/compare/2.0.58...2.0.90)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 03:01:52 +00:00
dependabot[bot]
7e12d03225 Bump colorama from 0.4.5 to 0.4.6
Bumps [colorama](https://github.com/tartley/colorama) from 0.4.5 to 0.4.6.
- [Release notes](https://github.com/tartley/colorama/releases)
- [Changelog](https://github.com/tartley/colorama/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tartley/colorama/compare/0.4.5...0.4.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 03:01:42 +00:00
dependabot[bot]
a5824f5cf2 Bump pyarrow from 9.0.0 to 10.0.0
Bumps [pyarrow](https://github.com/apache/arrow) from 9.0.0 to 10.0.0.
- [Release notes](https://github.com/apache/arrow/releases)
- [Commits](https://github.com/apache/arrow/compare/go/v9.0.0...go/v10.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 03:01:38 +00:00
dependabot[bot]
7348a8074e Bump nbconvert from 7.2.1 to 7.2.3
Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.2.1 to 7.2.3.
- [Release notes](https://github.com/jupyter/nbconvert/releases)
- [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter/nbconvert/compare/v7.2.1...v7.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 03:01:31 +00:00
dependabot[bot]
30f0a4dba2 Bump pycoingecko from 3.0.0 to 3.1.0
Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/man-c/pycoingecko/releases)
- [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md)
- [Commits](https://github.com/man-c/pycoingecko/compare/3.0.0...3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 03:01:22 +00:00
dependabot[bot]
eb01bed33a Bump scikit-learn from 1.1.2 to 1.1.3
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.1.2...1.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 03:01:20 +00:00
dependabot[bot]
41c2dc2c68 Bump plotly from 5.10.0 to 5.11.0
Bumps [plotly](https://github.com/plotly/plotly.py) from 5.10.0 to 5.11.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.10.0...v5.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-31 03:01:16 +00:00
robcaulk
a49edfbaee add tests for CatboostClassifier 2022-10-30 18:08:10 +01:00
robcaulk
d59a7fa2f9 remove analysis_lock and realign example hybrid strat 2022-10-30 17:07:33 +01:00
Matthias
32a03a89c6 Merge pull request #7665 from freqtrade/update_ci
Update generic CI's to ubuntu 22.04
2022-10-30 15:55:09 +01:00
Matthias
707b224af5 Update generic CI's to ubuntu 22.04 2022-10-30 15:09:53 +01:00
Matthias
391c3f56f7 Add typehint to corr_pairlist 2022-10-30 13:28:01 +01:00
Matthias
bd051cb205 Remove pandas workaround for pi image 2022-10-30 12:44:39 +01:00
Mark Regan
7053f81fa8 simplified predict and predict_proba using super(). Added duplicate class label check. 2022-10-30 09:48:30 +00:00
Matthias
cf4af2175c Merge pull request #7662 from markdregan/backtest_extra_returns_fix
Fix missing f-string from PR #7611
2022-10-30 10:47:06 +01:00
robcaulk
fc53054d43 leverage list length knowledge, f-string change 2022-10-30 10:12:14 +01:00
Emre
f98c7a2423 Remove loop of normalization from metadata 2022-10-30 10:12:14 +01:00
Matthias
5c14aeddc6 Add "--log-file" alias for "--logfile" 2022-10-30 09:50:54 +01:00
Matthias
5013351143 Rename "shuffle" parameter to "shuffle_freq" 2022-10-30 09:48:55 +01:00
Matthias
a323acf343 Improve ShufflePairlist to shuffle only once per candle 2022-10-30 09:46:12 +01:00
Mark Regan
c26fda282f fix missing f-string from PR #7611 2022-10-30 08:19:59 +00:00
robcaulk
650bb8b7d7 ensure full pair string is used for caching dataframes. If not, revert to old behavior. Update docs. 2022-10-29 22:26:49 +02:00
Matthias
352adaf127 Improve readability of is_time_to_refresh function 2022-10-29 19:45:46 +02:00
Matthias
b7d2c14f2c Improve trading limits docs to mention upper limits
#7654
2022-10-29 09:29:22 +02:00
Matthias
c23a9475e6 Move exchange utilities into separate module 2022-10-29 09:29:17 +02:00
Matthias
801e91c39e Merge pull request #7618 from wizrds/fix/docker-config-record
Update function in FreqAI interface to record FreqAI config params
2022-10-29 08:56:20 +02:00
Matthias
54c7122cc3 Merge pull request #7647 from freqtrade/combine_stop
Combine stop logic
2022-10-28 19:38:40 +02:00
Matthias
777af5517d Version bump develop version to 2022.11-dev 2022-10-28 19:38:19 +02:00
Mark Regan
6ef82dd8b6 minor change to return 2022-10-27 12:41:12 +01:00
Mark Regan
1c98640129 Delete MultiTargetClassifierTestStrategy.py 2022-10-26 13:11:10 +01:00
Mark Regan
a9a3ceadf7 Delete config_test.json 2022-10-26 13:10:18 +01:00
Matthias
255f38537e Simplify stoploss behavior by combining more commonalities 2022-10-26 07:14:33 +02:00
Matthias
6e0ca058f4 Update function-head for _get_stop_params 2022-10-26 07:12:49 +02:00
Matthias
cf6b75a3f3 Use combined stoploss_adjust where possible 2022-10-26 07:12:42 +02:00
Timothy Pogue
b9bf9edb02 update rapidjson opts 2022-10-25 14:12:13 -06:00
Mark Regan
217add70bd add strat and config for testing on PR 2022-10-25 20:07:39 +01:00
Mark Regan
47056eded3 multi target classifier working but not for parallel 2022-10-25 18:24:27 +01:00
Timothy Pogue
51be45547f remove np object, make default str 2022-10-24 12:23:54 -06:00
robcaulk
4d2b7a74f1 move record params to utils, use rapidjson 2022-10-23 20:51:32 +02:00
Timothy Pogue
bb06745227 fix tests 2022-10-23 12:25:39 -06:00
Timothy Pogue
07e813dfa0 change param record to only get certain params 2022-10-23 12:09:07 -06:00
Timothy Pogue
c4a2ee05e7 fix freqai test 2022-10-22 09:31:55 -06:00
Timothy Pogue
4464e91256 use self.identifier in full path 2022-10-21 19:53:33 -06:00
Timothy Pogue
5ee3b8cbbb update config recording to use all configs, fix tests 2022-10-21 19:48:26 -06:00
Wagner Costa Santos
6606a0113f refactoring - remove unnecessary config file 2022-10-20 14:53:25 -03:00
Wagner Costa Santos
52b60c5cbb Merge branch 'develop' into backtest_live_models 2022-10-20 11:59:37 -03:00
rcaulk
a9db668082 avoid redundant indicator population for corr_pair list 2022-10-20 16:30:32 +02:00
Wagner Costa Santos
02fc59d473 Merge branch 'develop' into backtest_live_models 2022-10-13 15:52:19 -03:00
Wagner Costa Santos
4e1bf79239 backtest live models - documentation 2022-10-13 15:47:31 -03:00
Wagner Costa Santos
6919f3aa75 Backtest live models - fix utc date convert issue 2022-10-13 15:03:27 -03:00
Wagner Costa Santos
93fe2b6446 Merge branch 'develop' into backtest_live_models 2022-10-13 11:22:58 -03:00
Wagner Costa Santos
01e3507e4c fix freqai backtest live models 2022-10-10 15:15:43 -03:00
Wagner Costa Santos
88418d524a Merge branch 'develop' into backtest_live_models 2022-10-10 15:14:59 -03:00
Wagner Costa Santos
3081e73f8a Merge branch 'develop' into backtest_live_models 2022-10-10 14:53:45 -03:00
Wagner Costa Santos
6845a5c6ea backtest_live_models - refactoring after PR review 2022-09-29 01:48:38 -03:00
Wagner Costa Santos
df0927cdee Merge branch 'develop' into backtest_live_models 2022-09-28 08:49:15 -03:00
Wagner Costa Santos
55ebbeec18 backtest_live models tests refactoring 2022-09-28 08:48:32 -03:00
Wagner Costa Santos
3c002ff752 Merge branch 'develop' into backtest_live_models 2022-09-27 10:27:47 -03:00
Wagner Costa Santos
0be115de9c backtest_live_models - added new tests and refactoring 2022-09-27 10:26:57 -03:00
Wagner Costa Santos
72aa47fc51 backtest_live_models - fix issue with timerange BT and 2 trainings within same candle (no data) 2022-09-27 00:14:12 -03:00
Wagner Costa Santos
14b96aaa38 backtesting live models - fix ci issues 2022-09-26 19:52:59 -03:00
Wagner Costa Santos
290afd9699 backtest_live_models - fix typo 2022-09-26 19:21:53 -03:00
Wagner Costa Santos
0318ca9f12 backtest_live_models - fix typo 2022-09-26 19:08:25 -03:00
Wagner Costa Santos
22bef71d5d backtest_live_models - add function comments and tests 2022-09-26 19:01:24 -03:00
Wagner Costa Santos
182d9e5426 Merge branch 'develop' into backtest_live_models 2022-09-26 17:23:44 -03:00
Wagner Costa Santos
ec947ad65c remove commented code - backtest_live_models 2022-09-25 23:47:27 -03:00
Wagner Costa Santos
5880f7a638 backtest_live_models - params validation and get timerange from live models in BT 2022-09-25 23:14:00 -03:00
Wagner Costa Santos
f3f3917da3 Merge branch 'develop' into backtest_live_models 2022-09-25 20:05:26 -03:00
Wagner Costa Santos
0ed7b2bfc3 change start_backtesting to handle backtest_live_models 2022-09-25 10:35:55 -03:00
Wagner Costa Santos
7f116db95e added generic function to get timerange from existent models 2022-09-24 13:01:53 -03:00
Wagner Costa Santos
d9c16d4888 Merge branch 'develop' into backtest_live_models 2022-09-24 12:30:55 -03:00
Wagner Costa Santos
3ee7eb63f7 starting backtest live models 2022-09-24 12:28:52 -03:00
114 changed files with 3459 additions and 2831 deletions

View File

@@ -11,12 +11,14 @@
"mounts": [
"source=freqtrade-bashhistory,target=/home/ftuser/commandhistory,type=volume"
],
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/freqtrade,type=bind,consistency=cached",
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "ftuser",
"onCreateCommand": "pip install --user -e .",
"postCreateCommand": "freqtrade create-userdir --userdir user_data/",
"workspaceFolder": "/freqtrade/",
"workspaceFolder": "/workspaces/freqtrade",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",

View File

@@ -258,7 +258,7 @@ jobs:
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
mypy_version_check:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
@@ -283,7 +283,7 @@ jobs:
- uses: pre-commit/action@v3.0.0
docs_check:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
@@ -313,7 +313,7 @@ jobs:
# Notify only once - when CI completes (and after deploy) in case it's successfull
notify-complete:
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check, pre-commit ]
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
# Discord notification can't handle schedule events
if: (github.event_name != 'schedule')
permissions:
@@ -338,7 +338,7 @@ jobs:
deploy:
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check, pre-commit ]
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'

1
.gitignore vendored
View File

@@ -109,7 +109,6 @@ target/
!*.gitkeep
!config_examples/config_binance.example.json
!config_examples/config_bittrex.example.json
!config_examples/config_ftx.example.json
!config_examples/config_full.example.json
!config_examples/config_kraken.example.json
!config_examples/config_freqai.example.json

View File

@@ -15,9 +15,9 @@ repos:
additional_dependencies:
- types-cachetools==5.2.1
- types-filelock==3.2.7
- types-requests==2.28.11.2
- types-requests==2.28.11.5
- types-tabulate==0.9.0.0
- types-python-dateutil==2.8.19.2
- types-python-dateutil==2.8.19.4
# stages: [push]
- repo: https://github.com/pycqa/isort

View File

@@ -28,7 +28,6 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Binance](https://www.binance.com/)
- [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com/#a=2258149)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Huobi](http://huobi.com/)
- [X] [Kraken](https://kraken.com/)
@@ -39,7 +38,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
- [X] [Binance](https://www.binance.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [OKX](https://okx.com/).
- [X] [OKX](https://okx.com/)
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in.

View File

@@ -1,96 +0,0 @@
{
"max_open_trades": 3,
"stake_currency": "USD",
"stake_amount": 50,
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "USD",
"timeframe": "5m",
"dry_run": true,
"cancel_open_orders_on_exit": false,
"unfilledtimeout": {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},
"exchange": {
"name": "ftx",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"ccxt_config": {},
"ccxt_async_config": {},
"pair_whitelist": [
"BTC/USD",
"ETH/USD",
"BNB/USD",
"USDT/USD",
"LTC/USD",
"SRM/USD",
"SXP/USD",
"XRP/USD",
"DOGE/USD",
"1INCH/USD",
"CHZ/USD",
"MATIC/USD",
"LINK/USD",
"OXY/USD",
"SUSHI/USD"
],
"pair_blacklist": [
"FTT/USD"
]
},
"pairlists": [
{"method": "StaticPairList"}
],
"edge": {
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,
"stoploss_range_step": -0.01,
"minimum_winrate": 0.60,
"minimum_expectancy": 0.20,
"min_trade_number": 10,
"max_trade_duration_minute": 1440,
"remove_pumps": false
},
"telegram": {
"enabled": false,
"token": "your_telegram_token",
"chat_id": "your_telegram_chat_id"
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"verbosity": "error",
"jwt_secret_key": "somethingrandom",
"CORS_origins": [],
"username": "freqtrader",
"password": "SuperSecurePassword"
},
"bot_name": "freqtrade",
"initial_state": "running",
"force_entry_enable": false,
"internals": {
"process_throttle_secs": 5
}
}

View File

@@ -204,6 +204,7 @@
"strategy_path": "user_data/strategies/",
"recursive_strategy_search": false,
"add_config_files": [],
"reduce_df_footprint": false,
"dataformat_ohlcv": "json",
"dataformat_trades": "jsongz"
}

View File

@@ -546,8 +546,8 @@ In addition to the above assumptions, strategy authors should carefully read the
### Trading limits in backtesting
Exchanges have certain trading limits, like minimum base currency, or minimum stake (quote) currency.
These limits are usually listed in the exchange documentation as "trading rules" or similar.
Exchanges have certain trading limits, like minimum (and maximum) base currency, or minimum/maximum stake (quote) currency.
These limits are usually listed in the exchange documentation as "trading rules" or similar and can be quite different between different pairs.
Backtesting (as well as live and dry-run) does honor these limits, and will ensure that a stoploss can be placed below this value - so the value will be slightly higher than what the exchange specifies.
Freqtrade has however no information about historic limits.

View File

@@ -253,6 +253,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing in FreqAI). (Currently only affects FreqAI use-cases) <br> **Datatype:** Boolean. <br> Default: `False`.
### Parameters in the strategy
@@ -552,7 +553,7 @@ The possible values are: `GTC` (default), `FOK` or `IOC`.
```
!!! Warning
This is ongoing work. For now, it is supported only for binance, gate, ftx and kucoin.
This is ongoing work. For now, it is supported only for binance, gate and kucoin.
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
### What values can be used for fiat_display_currency?
@@ -664,6 +665,7 @@ You should also make sure to read the [Exchanges](exchanges.md) section of the d
### Using proxy with Freqtrade
To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values.
This will have the proxy settings applied to everything (telegram, coingecko, ...) except exchange requests.
``` bash
export HTTP_PROXY="http://addr:port"
@@ -671,17 +673,20 @@ export HTTPS_PROXY="http://addr:port"
freqtrade
```
#### Proxy just exchange requests
#### Proxy exchange requests
To use a proxy just for exchange connections (skips/ignores telegram and coingecko) - you can also define the proxies as part of the ccxt configuration.
To use a proxy for exchange connections - you will have to define the proxies as part of the ccxt configuration.
``` json
"ccxt_config": {
{
"exchange": {
"ccxt_config": {
"aiohttp_proxy": "http://addr:port",
"proxies": {
"http": "http://addr:port",
"https": "http://addr:port"
"http": "http://addr:port",
"https": "http://addr:port"
},
}
}
```

View File

@@ -177,13 +177,13 @@ freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT --
### Data format
Freqtrade currently supports 3 data-formats for both OHLCV and trades data:
Freqtrade currently supports the following data-formats:
* `json` - plain "text" json files
* `jsongz` - a gzip-zipped version of json files
* `hdf5` - a high performance datastore
* `feather` - a dataformat based on Apache Arrow
* `parquet` - columnar datastore
* `feather` - a dataformat based on Apache Arrow (OHLCV only)
* `parquet` - columnar datastore (OHLCV only)
By default, OHLCV data is stored as `json` data, while trades data is stored as `jsongz` data.

View File

@@ -434,6 +434,11 @@ To keep the release-log short, best wrap the full git changelog into a collapsib
</details>
```
### FreqUI release
If FreqUI has been updated substantially, make sure to create a release before merging the release branch.
Make sure that freqUI CI on the release is finished and passed before merging the release.
### Create github release / tag
Once the PR against stable is merged (best right after merging):

View File

@@ -173,26 +173,6 @@ res = [p for p, x in lm.items() if 'US' in x['info']['prohibitedIn']]
print(res)
```
## FTX
!!! Tip "Stoploss on Exchange"
FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
### Using subaccounts
To use subaccounts with FTX, you need to edit the configuration and add the following:
``` json
"exchange": {
"ccxt_config": {
"headers": {
"FTX-SUBACCOUNT": "name"
}
},
}
```
## Kucoin
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:

View File

@@ -61,7 +61,7 @@ The FreqAI strategy requires including the following lines of code in the standa
"""
Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User controls the indicators
passed to the training/prediction by prepending indicators with `'%-' + coin `
passed to the training/prediction by prepending indicators with `'%-' + pair `
(see convention below). I.e. user should not prepend any supporting metrics
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
model.
@@ -69,20 +69,17 @@ The FreqAI strategy requires including the following lines of code in the standa
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
:param coin: the name of the coin which will modify the feature names.
"""
coin = pair.split('/')[0]
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
@@ -134,7 +131,7 @@ Notice also the location of the labels under `if set_generalized_indicators:` at
(as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`):
```python
def populate_any_indicators(self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False):
def populate_any_indicators(self, pair, df, tf, informative=None, set_generalized_indicators=False):
...

View File

@@ -2,7 +2,10 @@
## Defining the features
Low level feature engineering is performed in the user strategy within a function called `populate_any_indicators()`. That function sets the `base features` such as, `RSI`, `MFI`, `EMA`, `SMA`, time of day, volume, etc. The `base features` can be custom indicators or they can be imported from any technical-analysis library that you can find. One important syntax rule is that all `base features` string names are prepended with `%`, while labels/targets are prepended with `&`.
Low level feature engineering is performed in the user strategy within a function called `populate_any_indicators()`. That function sets the `base features` such as, `RSI`, `MFI`, `EMA`, `SMA`, time of day, volume, etc. The `base features` can be custom indicators or they can be imported from any technical-analysis library that you can find. One important syntax rule is that all `base features` string names are prepended with `%-{pair}`, while labels/targets are prepended with `&`.
!!! Note
Adding the full pair string, e.g. XYZ/USD, in the feature name enables improved performance for dataframe caching on the backend. If you decide *not* to add the full pair string in the feature string, FreqAI will operate in a reduced performance mode.
Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the FreqAI config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles."
@@ -15,7 +18,7 @@ It is advisable to start from the template `populate_any_indicators()` in the so
"""
Function designed to automatically generate, name, and merge features
from user-indicated timeframes in the configuration file. The user controls the indicators
passed to the training/prediction by prepending indicators with `'%-' + coin `
passed to the training/prediction by prepending indicators with `'%-' + pair `
(see convention below). I.e., the user should not prepend any supporting metrics
(e.g., bb_lowerband below) with % unless they explicitly want to pass that metric to the
model.
@@ -23,37 +26,34 @@ It is advisable to start from the template `populate_any_indicators()` in the so
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
:param coin: the name of the coin which will modify the feature names.
"""
coin = pair.split('/')[0]
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(informative), window=t, stds=2.2
)
informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"]
informative[f"{pair}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{pair}bb_upperband-period_{t}"] = bollinger["upper"]
informative[f"%-{coin}bb_width-period_{t}"] = (
informative[f"{coin}bb_upperband-period_{t}"]
- informative[f"{coin}bb_lowerband-period_{t}"]
) / informative[f"{coin}bb_middleband-period_{t}"]
informative[f"%-{coin}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"]
informative[f"%-{pair}bb_width-period_{t}"] = (
informative[f"{pair}bb_upperband-period_{t}"]
- informative[f"{pair}bb_lowerband-period_{t}"]
) / informative[f"{pair}bb_middleband-period_{t}"]
informative[f"%-{pair}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{pair}bb_lowerband-period_{t}"]
)
informative[f"%-{coin}relative_volume-period_{t}"] = (
informative[f"%-{pair}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)

View File

@@ -18,6 +18,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
| | **Feature parameters**
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
@@ -37,7 +38,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
| `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).
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
| | **Data split parameters**
| `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
@@ -50,3 +50,4 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| | **Extraneous parameters**
| `keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.

View File

@@ -73,12 +73,24 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to-
To allow for tweaking your strategy (**not** the features!), FreqAI will automatically save the predictions during backtesting so that they can be reused for future backtests and live runs using the same `identifier` model. This provides a performance enhancement geared towards enabling **high-level hyperopting** of entry/exit criteria.
An additional directory called `predictions`, which contains all the predictions stored in `hdf` format, will be created in the `unique-id` folder.
An additional directory called `backtesting_predictions`, which contains all the predictions stored in `hdf` format, will be created in the `unique-id` folder.
To change your **features**, you **must** set a new `identifier` in the config to signal to FreqAI to train new models.
To save the models generated during a particular backtest so that you can start a live deployment from one of them instead of training a new model, you must set `save_backtest_models` to `True` in the config.
### Backtest live models
FreqAI allow you to reuse ready models through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse models generated in dry/run for comparison or other study. For that, you must set `"purge_old_models"` to `True` in the config.
The `--timerange` parameter must not be informed, as it will be automatically calculated through the training end dates of the models.
Each model has an identifier derived from the training end date. If you have only 1 model trained, FreqAI will backtest from the training end date until the current date. If you have more than 1 model, each model will perform the backtesting according to the training end date until the training end date of the next model and so on. For the last model, the period of the previous model will be used for the execution.
!!! Note
Currently, there is no checking for expired models, even if the `expired_hours` parameter is set.
### Downloading data to cover the full backtest period
For live/dry deployments, FreqAI will download the necessary data automatically. However, to use backtesting functionality, you need to download the necessary data using `download-data` (details [here](data-download.md#data-downloading)). You need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that there is a sufficient amount of training data *before* the start of the backtesting time range. The amount of additional data can be roughly estimated by moving the start date of the time range backwards by `train_period_days` and the `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters) from the beginning of the desired backtesting time range.

View File

@@ -4,7 +4,7 @@
## Introduction
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input features.
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, the FreqAI aims to be a sand-box for easily deploying robust machine-learning libraries on real-time data ([details])(#freqai-position-in-open-source-machine-learning-landscape).
Features include:
@@ -72,6 +72,11 @@ pip install -r requirements-freqai.txt
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker-compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
### FreqAI position in open-source machine learning landscape
Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data-exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data-collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data.
## Common pitfalls
FreqAI cannot be combined with dynamic `VolumePairlists` (or any pairlist filter that adds and removes pairs dynamically).

View File

@@ -268,7 +268,7 @@ This option is disabled by default, and will only apply if set to > 0.
The `max_value` setting removes pairs where the minimum value change is above a specified value.
This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20\$) as the coin has risen sharply since the last limit adaption.
As a result of the above, you can only buy for 20\$, or 40\$ - but not for 25\$.
On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit.
On exchanges that deduct fees from the receiving currency (e.g. binance) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit.
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to > 0.
@@ -286,6 +286,18 @@ Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 -
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
By default, ShuffleFilter will shuffle pairs once per candle.
To shuffle on every iteration, set `"shuffle_frequency"` to `"iteration"` instead of the default of `"candle"`.
``` json
{
"method": "ShuffleFilter",
"shuffle_frequency": "candle",
"seed": 42
}
```
!!! Tip
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. ShuffleFilter will automatically detect runmodes and apply the `seed` only for backtesting modes - if a `seed` value is set.

View File

@@ -32,7 +32,7 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital.
- Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
- Analyze: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
## Supported exchange marketplaces
@@ -40,7 +40,6 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Binance](https://www.binance.com/)
- [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com/#a=2258149)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Huobi](http://huobi.com/)
- [X] [Kraken](https://kraken.com/)
@@ -51,7 +50,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Binance](https://www.binance.com/)
- [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [OKX](https://okx.com/).
- [X] [OKX](https://okx.com/)
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.

View File

@@ -21,6 +21,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
"name": "default", // This can be any name you'd like, default is "default"
"host": "127.0.0.1", // The host from your producer's api_server config
"port": 8080, // The port from your producer's api_server config
"secure": false, // Use a secure websockets connection, default false
"ws_token": "sercet_Ws_t0ken" // The ws_token from your producer's api_server config
}
],
@@ -42,6 +43,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
| `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.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.

View File

@@ -1,6 +1,6 @@
markdown==3.3.7
mkdocs==1.4.1
mkdocs-material==8.5.7
mkdocs==1.4.2
mkdocs-material==8.5.10
mdx_truly_sane_lists==1.3
pymdown-extensions==9.7
pymdown-extensions==9.8
jinja2==3.1.2

View File

@@ -389,6 +389,44 @@ Now anytime those types of RPC messages are sent in the bot, you will receive th
}
```
#### Reverse Proxy setup
When using [Nginx](https://nginx.org/en/docs/), the following configuration is required for WebSockets to work (Note this configuration is incomplete, it's missing some information and can not be used as is):
Please make sure to replace `<freqtrade_listen_ip>` (and the subsequent port) with the IP and Port matching your configuration/setup.
```
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
#...
server {
#...
location / {
proxy_http_version 1.1;
proxy_pass http://<freqtrade_listen_ip>:8080;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}
}
```
To properly configure your reverse proxy (securely), please consult it's documentation for proxying websockets.
- **Traefik**: Traefik supports websockets out of the box, see the [documentation](https://doc.traefik.io/traefik/)
- **Caddy**: Caddy v2 supports websockets out of the box, see the [documentation](https://caddyserver.com/docs/v2-upgrade#proxy)
!!! Tip "SSL certificates"
You can use tools like certbot to setup ssl certificates to access your bot's UI through encrypted connection by using any fo the above reverse proxies.
While this will protect your data in transit, we do not recommend to run the freqtrade API outside of your private network (VPN, SSH tunnel).
### OpenAPI interface
To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration.

View File

@@ -24,7 +24,7 @@ These modes can be configured with these values:
```
!!! Note
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.

View File

@@ -446,15 +446,17 @@ A full sample can be found [in the DataProvider section](#complete-data-provider
??? Note "Alternative candle types"
Informative_pairs can also provide a 3rd tuple element defining the candle type explicitly.
Availability of alternative candle-types will depend on the trading-mode and the exchange. Details about this can be found in the exchange documentation.
Availability of alternative candle-types will depend on the trading-mode and the exchange.
In general, spot pairs cannot be used in futures markets, and futures candles can't be used as informative pairs for spot bots.
Details about this may vary, if they do, this can be found in the exchange documentation.
``` python
def informative_pairs(self):
return [
("ETH/USDT", "5m", ""), # Uses default candletype, depends on trading_mode
("ETH/USDT", "5m", "spot"), # Forces usage of spot candles
("BTC/TUSD", "15m", "futures"), # Uses futures candles
("BTC/TUSD", "15m", "mark"), # Uses mark candles
("ETH/USDT", "5m", ""), # Uses default candletype, depends on trading_mode (recommended)
("ETH/USDT", "5m", "spot"), # Forces usage of spot candles (only valid for bots running on spot markets).
("BTC/TUSD", "15m", "futures"), # Uses futures candles (only bots with `trading_mode=futures`)
("BTC/TUSD", "15m", "mark"), # Uses mark candles (only bots with `trading_mode=futures`)
]
```
***
@@ -723,7 +725,7 @@ if self.dp.runmode.value in ('live', 'dry_run'):
!!! Warning
Although the ticker data structure is a part of the ccxt Unified Interface, the values returned by this method can
vary for different exchanges. For instance, many exchanges do not return `vwap` values, the FTX exchange
vary for different exchanges. For instance, many exchanges do not return `vwap` values, some exchanges
does not always fills in the `last` field (so it can be None), etc. So you need to carefully verify the ticker
data returned from the exchange and add appropriate error handling / defaults.

View File

@@ -263,7 +263,6 @@ equos True missing opt: fetchTicker, fetchTickers
eterbase True
fcoin True missing opt: fetchMyTrades, fetchTickers
fcoinjp True missing opt: fetchMyTrades, fetchTickers
ftx True
gateio True
gemini True
gopax True
@@ -369,7 +368,6 @@ fcoin True missing opt: fetchMyTrades, fetchTickers
fcoinjp True missing opt: fetchMyTrades, fetchTickers
flowbtc False missing: fetchOrder, fetchOHLCV
foxbit False missing: fetchOrder, fetchOHLCV
ftx True
gateio True
gemini True
gopax True

View File

@@ -3,15 +3,16 @@
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
Otherwise, try the instructions below.
Otherwise, please follow the instructions below.
## Install freqtrade manually
!!! Note
Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows.
!!! Note "64bit Python version"
Please make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows.
32bit python versions are no longer supported under Windows.
!!! Hint
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#Anaconda) in this document for more information.
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
### 1. Clone the git repository

View File

@@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2022.10'
__version__ = '2022.11'
if 'dev' in __version__:
try:

View File

@@ -25,7 +25,8 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
"enable_protections", "dry_run_wallet", "timeframe_detail",
"strategy_list", "export", "exportfilename",
"backtest_breakdown", "backtest_cache"]
"backtest_breakdown", "backtest_cache",
"freqai_backtest_live_models"]
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"position_stacking", "use_max_market_positions",

View File

@@ -108,7 +108,6 @@ def ask_user_config() -> Dict[str, Any]:
"binance",
"binanceus",
"bittrex",
"ftx",
"gateio",
"huobi",
"kraken",

View File

@@ -49,7 +49,7 @@ AVAILABLE_CLI_OPTIONS = {
default=0,
),
"logfile": Arg(
'--logfile',
'--logfile', '--log-file',
help="Log to the file specified. Special values are: 'syslog', 'journald'. "
"See the documentation for more details.",
metavar='FILE',
@@ -668,4 +668,9 @@ AVAILABLE_CLI_OPTIONS = {
help='Specify additional lookup path for freqaimodels.',
metavar='PATH',
),
"freqai_backtest_live_models": Arg(
'--freqai-backtest-live-models',
help='Run backtest with ready models.',
action='store_true'
),
}

View File

@@ -86,6 +86,7 @@ def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False)
_validate_unlimited_amount(conf)
_validate_ask_orderbook(conf)
_validate_freqai_hyperopt(conf)
_validate_freqai_backtest(conf)
_validate_freqai_include_timeframes(conf)
_validate_consumers(conf)
validate_migrated_strategy_settings(conf)
@@ -355,6 +356,26 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any]) -> None:
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
if conf.get('runmode', RunMode.OTHER) == RunMode.BACKTEST:
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
timerange = conf.get('timerange')
freqai_backtest_live_models = conf.get('freqai_backtest_live_models', False)
if freqai_backtest_live_models and freqai_enabled and timerange:
raise OperationalException(
'Using timerange parameter is not supported with '
'--freqai-backtest-live-models parameter.')
if freqai_backtest_live_models and not freqai_enabled:
raise OperationalException(
'Using --freqai-backtest-live-models parameter is only '
'supported with a FreqAI strategy.')
if freqai_enabled and not freqai_backtest_live_models and not timerange:
raise OperationalException(
'Please pass --timerange if you intend to use FreqAI for backtesting.')
def _validate_consumers(conf: Dict[str, Any]) -> None:
emc_conf = conf.get('external_message_consumer', {})
if emc_conf.get('enabled', False):

View File

@@ -279,6 +279,9 @@ class Configuration:
self._args_to_config(config, argname='disableparamexport',
logstring='Parameter --disableparamexport detected: {} ...')
self._args_to_config(config, argname='freqai_backtest_live_models',
logstring='Parameter --freqai-backtest-live-models detected ...')
# Edge section:
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args["stoploss_range"])

View File

@@ -3,11 +3,12 @@ This module contains the argument manager class
"""
import logging
import re
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional
import arrow
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import OperationalException
@@ -29,6 +30,52 @@ class TimeRange:
self.startts: int = startts
self.stopts: int = stopts
@property
def startdt(self) -> Optional[datetime]:
if self.startts:
return datetime.fromtimestamp(self.startts, tz=timezone.utc)
return None
@property
def stopdt(self) -> Optional[datetime]:
if self.stopts:
return datetime.fromtimestamp(self.stopts, tz=timezone.utc)
return None
@property
def timerange_str(self) -> str:
"""
Returns a string representation of the timerange as used by parse_timerange.
Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set.
"""
start = ''
stop = ''
if startdt := self.startdt:
start = startdt.strftime('%Y%m%d')
if stopdt := self.stopdt:
stop = stopdt.strftime('%Y%m%d')
return f"{start}-{stop}"
@property
def start_fmt(self) -> str:
"""
Returns a string representation of the start date
"""
val = 'unbounded'
if (startdt := self.startdt) is not None:
val = startdt.strftime(DATETIME_PRINT_FORMAT)
return val
@property
def stop_fmt(self) -> str:
"""
Returns a string representation of the stop date
"""
val = 'unbounded'
if (stopdt := self.stopdt) is not None:
val = stopdt.strftime(DATETIME_PRINT_FORMAT)
return val
def __eq__(self, other):
"""Override the default Equals behavior"""
return (self.starttype == other.starttype and self.stoptype == other.stoptype

View File

@@ -159,6 +159,7 @@ CONF_SCHEMA = {
'ignore_buying_expired_candle_after': {'type': 'number'},
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
'reduce_df_footprint': {'type': 'boolean', 'default': False},
'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99},
'backtest_breakdown': {
'type': 'array',
@@ -511,6 +512,7 @@ CONF_SCHEMA = {
'minimum': 0,
'maximum': 65535
},
'secure': {'type': 'boolean', 'default': False},
'ws_token': {'type': 'string'},
},
'required': ['name', 'host', 'ws_token']
@@ -542,7 +544,7 @@ CONF_SCHEMA = {
"keras": {"type": "boolean", "default": False},
"write_metrics_to_disk": {"type": "boolean", "default": False},
"purge_old_models": {"type": "boolean", "default": True},
"conv_width": {"type": "integer", "default": 2},
"conv_width": {"type": "integer", "default": 1},
"train_period_days": {"type": "integer", "default": 0},
"backtest_period_days": {"type": "number", "default": 7},
"identifier": {"type": "string", "default": "example"},

View File

@@ -26,7 +26,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'profit_ratio', 'profit_abs', 'exit_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
'is_short', 'open_timestamp', 'close_timestamp', 'orders'
'leverage', 'is_short', 'open_timestamp', 'close_timestamp', 'orders'
]
@@ -280,6 +280,8 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
# Compatibility support for pre short Columns
if 'is_short' not in df.columns:
df['is_short'] = 0
if 'leverage' not in df.columns:
df['leverage'] = 1.0
if 'enter_tag' not in df.columns:
df['enter_tag'] = df['buy_tag']
df = df.drop(['buy_tag'], axis=1)

View File

@@ -3,10 +3,10 @@ Functions to convert data from one format to another
"""
import itertools
import logging
from datetime import datetime, timezone
from operator import itemgetter
from typing import Dict, List
import numpy as np
import pandas as pd
from pandas import DataFrame, to_datetime
@@ -137,11 +137,9 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date',
df = df.iloc[startup_candles:, :]
else:
if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
df = df.loc[df[df_date_col] >= start, :]
df = df.loc[df[df_date_col] >= timerange.startdt, :]
if timerange.stoptype == 'date':
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
df = df.loc[df[df_date_col] <= stop, :]
df = df.loc[df[df_date_col] <= timerange.stopdt, :]
return df
@@ -313,3 +311,29 @@ def convert_ohlcv_format(
if erase and convert_from != convert_to:
logger.info(f"Deleting source data for {pair} / {timeframe}")
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
"""
Ensure all values are float32 in the incoming dataframe.
:param df: Dataframe to be converted to float/int 32s
:return: Dataframe converted to float/int 32s
"""
logger.debug(f"Memory usage of dataframe is "
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
df_dtypes = df.dtypes
for column, dtype in df_dtypes.items():
if column in ['open', 'high', 'low', 'close', 'volume']:
continue
if dtype == np.float64:
df_dtypes[column] = np.float32
elif dtype == np.int64:
df_dtypes[column] = np.int32
df = df.astype(df_dtypes)
logger.debug(f"Memory usage after optimization is: "
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
return df

View File

@@ -1,6 +1,6 @@
import logging
import operator
from datetime import datetime, timezone
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
@@ -160,9 +160,9 @@ def _load_cached_data_for_updating(
end = None
if timerange:
if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
start = timerange.startdt
if timerange.stoptype == 'date':
end = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
end = timerange.stopdt
# Intentionally don't pass timerange in - since we need to load the full dataset.
data = data_handler.ohlcv_load(pair, timeframe=timeframe,

View File

@@ -102,6 +102,11 @@ class IDataHandler(ABC):
:return: (min, max)
"""
data = self._ohlcv_load(pair, timeframe, None, candle_type)
if data.empty:
return (
datetime.fromtimestamp(0, tz=timezone.utc),
datetime.fromtimestamp(0, tz=timezone.utc)
)
return data.iloc[0]['date'].to_pydatetime(), data.iloc[-1]['date'].to_pydatetime()
@abstractmethod
@@ -361,13 +366,11 @@ class IDataHandler(ABC):
"""
if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
if pairdata.iloc[0]['date'] > start:
if pairdata.iloc[0]['date'] > timerange.startdt:
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
if timerange.stoptype == 'date':
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
if pairdata.iloc[-1]['date'] < stop:
if pairdata.iloc[-1]['date'] < timerange.stopdt:
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")

View File

@@ -392,7 +392,7 @@ class Edge:
# Returning a list of pairs in order of "expectancy"
return final
def _find_trades_for_stoploss_range(self, df, pair, stoploss_range):
def _find_trades_for_stoploss_range(self, df, pair: str, stoploss_range) -> list:
buy_column = df['enter_long'].values
sell_column = df['exit_long'].values
date_column = df['date'].values
@@ -407,7 +407,7 @@ class Edge:
return result
def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column,
ohlc_columns, stoploss, pair):
ohlc_columns, stoploss, pair: str):
"""
Iterate through ohlc_columns in order to find the next trade
Next trade opens from the first buy signal noticed to

View File

@@ -9,15 +9,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 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.ftx import Ftx
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.gateio import Gateio
from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.huobi import Huobi

View File

@@ -42,24 +42,6 @@ class Binance(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
:param side: "buy" or "sell"
"""
order_types = ('stop_loss_limit', 'stop', 'stop_market')
return (
order.get('stopPrice', None) is None
or (
order['type'] in order_types
and (
(side == "sell" and stop_loss > float(order['stopPrice'])) or
(side == "buy" and stop_loss < float(order['stopPrice']))
)
))
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

@@ -20,8 +20,12 @@ class Bybit(Exchange):
"""
_ft_has: Dict = {
"ohlcv_candle_limit": 200,
"ccxt_futures_name": "linear"
"ohlcv_candle_limit": 1000,
"ccxt_futures_name": "linear",
"ohlcv_has_history": False,
}
_ft_has_futures: Dict = {
"ohlcv_has_history": True,
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [

View File

@@ -52,7 +52,6 @@ MAP_EXCHANGE_CHILDCLASS = {
SUPPORTED_EXCHANGES = [
'binance',
'bittrex',
'ftx',
'gateio',
'huobi',
'kraken',

View File

@@ -8,7 +8,6 @@ import inspect
import logging
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from math import ceil
from threading import Lock
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
@@ -16,7 +15,7 @@ import arrow
import ccxt
import ccxt.async_support as ccxt_async
from cachetools import TTLCache
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
from ccxt import TICK_SIZE
from dateutil import parser
from pandas import DataFrame, concat
@@ -28,17 +27,19 @@ from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError,
RetryableOrderError, TemporaryError)
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
remove_credentials, retrier, retrier_async)
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.types import Ticker, Tickers
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
safe_value_fallback2)
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.util import FtPrecise
CcxtModuleType = Any
logger = logging.getLogger(__name__)
@@ -1076,7 +1077,14 @@ class Exchange:
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
raise OperationalException(f"stoploss is not implemented for {self.name}.")
if not self._ft_has.get('stoploss_on_exchange'):
raise OperationalException(f"stoploss is not implemented for {self.name}.")
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'])))
)
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
@@ -1106,7 +1114,7 @@ class Exchange:
'In stoploss limit order, stop price should be more than limit price')
return limit_rate
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
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})
@@ -1155,7 +1163,8 @@ class Exchange:
return dry_order
try:
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
params = self._get_stop_params(side=side, ordertype=ordertype,
stop_price=stop_price_norm)
if self.trading_mode == TradingMode.FUTURES:
params['reduceOnly'] = True
@@ -1680,6 +1689,17 @@ class Exchange:
@retrier
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float:
"""
Retrieve fee from exchange
:param symbol: Pair
:param type: Type of order (market, limit, ...)
:param side: Side of order (buy, sell)
:param amount: Amount of order
:param price: Price of order
:param taker_or_maker: 'maker' or 'taker' (ignored if "type" is provided)
"""
if type and type == 'market':
taker_or_maker = 'taker'
try:
if self._config['dry_run'] and self._config.get('fee', None) is not None:
return self._config['fee']
@@ -1995,11 +2015,8 @@ class Exchange:
def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
# Timeframe in seconds
interval_in_sec = timeframe_to_seconds(timeframe)
return (
(self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0)
+ interval_in_sec) < arrow.utcnow().int_timestamp
)
plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
return plr < arrow.utcnow().int_timestamp
@retrier_async
async def _async_get_candle_history(
@@ -2802,240 +2819,3 @@ class Exchange:
# describes the min amt for a tier, and the lowest tier will always go down to 0
else:
raise OperationalException(f"Cannot get maintenance ratio using {self.name}")
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module)
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
"""
Return the list of all exchanges known to ccxt
"""
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
"""
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
"""
exchanges = ccxt_exchanges(ccxt_module)
return [x for x in exchanges if validate_exchange(x)[0]]
def validate_exchange(exchange: str) -> Tuple[bool, str]:
ex_mod = getattr(ccxt, exchange.lower())()
if not ex_mod or not ex_mod.has:
return False, ''
missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True]
if missing:
return False, f"missing: {', '.join(missing)}"
missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
if exchange.lower() in BAD_EXCHANGES:
return False, BAD_EXCHANGES.get(exchange.lower(), '')
if missing_opt:
return True, f"missing opt: {', '.join(missing_opt)}"
return True, ''
def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]:
"""
:return: List of tuples with exchangename, valid, reason.
"""
exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
exchanges_valid = [
(e, *validate_exchange(e)) for e in exchanges
]
return exchanges_valid
def timeframe_to_seconds(timeframe: str) -> int:
"""
Translates the timeframe interval value written in the human readable
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
of seconds for one timeframe interval.
"""
return ccxt.Exchange.parse_timeframe(timeframe)
def timeframe_to_minutes(timeframe: str) -> int:
"""
Same as timeframe_to_seconds, but returns minutes.
"""
return ccxt.Exchange.parse_timeframe(timeframe) // 60
def timeframe_to_msecs(timeframe: str) -> int:
"""
Same as timeframe_to_seconds, but returns milliseconds.
"""
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
"""
Use Timeframe and determine the candle start date for this date.
Does not round when given a candle start date.
:param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to now(utc)
:returns: date of previous candle (with utc timezone)
"""
if not date:
date = datetime.now(timezone.utc)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
ROUND_DOWN) // 1000
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
"""
Use Timeframe and determine next candle.
:param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to now(utc)
:returns: date of next candle (with utc timezone)
"""
if not date:
date = datetime.now(timezone.utc)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
ROUND_UP) // 1000
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def date_minus_candles(
timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime:
"""
subtract X candles from a date.
:param timeframe: timeframe in string format (e.g. "5m")
:param candle_count: Amount of candles to subtract.
:param date: date to use. Defaults to now(utc)
"""
if not date:
date = datetime.now(timezone.utc)
tf_min = timeframe_to_minutes(timeframe)
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
return new_date
def market_is_active(market: Dict) -> bool:
"""
Return True if the market is active.
"""
# "It's active, if the active flag isn't explicitly set to false. If it's missing or
# true then it's true. If it's undefined, then it's most likely true, but not 100% )"
# See https://github.com/ccxt/ccxt/issues/4874,
# https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520
return market.get('active', True) is not False
def amount_to_contracts(amount: float, contract_size: Optional[float]) -> float:
"""
Convert amount to contracts.
:param amount: amount to convert
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
:return: num-contracts
"""
if contract_size and contract_size != 1:
return float(FtPrecise(amount) / FtPrecise(contract_size))
else:
return amount
def contracts_to_amount(num_contracts: float, contract_size: Optional[float]) -> float:
"""
Takes num-contracts and converts it to contract size
:param num_contracts: number of contracts
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
:return: Amount
"""
if contract_size and contract_size != 1:
return float(FtPrecise(num_contracts) * FtPrecise(contract_size))
else:
return num_contracts
def amount_to_precision(amount: float, amount_precision: Optional[float],
precisionMode: Optional[int]) -> float:
"""
Returns the amount to buy or sell to a precision the Exchange accepts
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
:param amount: amount to truncate
:param amount_precision: amount precision to use.
should be retrieved from markets[pair]['precision']['amount']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:return: truncated amount
"""
if amount_precision is not None and precisionMode is not None:
precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision
# precision must be an int for non-ticksize inputs.
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=precision,
counting_mode=precisionMode,
))
return amount
def amount_to_contract_precision(
amount, amount_precision: Optional[float], precisionMode: Optional[int],
contract_size: Optional[float]) -> float:
"""
Returns the amount to buy or sell to a precision the Exchange accepts
including calculation to and from contracts.
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
:param amount: amount to truncate
:param amount_precision: amount precision to use.
should be retrieved from markets[pair]['precision']['amount']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
:return: truncated amount
"""
if amount_precision is not None and precisionMode is not None:
contracts = amount_to_contracts(amount, contract_size)
amount_p = amount_to_precision(contracts, amount_precision, precisionMode)
return contracts_to_amount(amount_p, contract_size)
return amount
def price_to_precision(price: float, price_precision: Optional[float],
precisionMode: Optional[int]) -> float:
"""
Returns the price rounded up to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
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
: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:
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 price

View File

@@ -0,0 +1,252 @@
"""
Exchange support utils
"""
from datetime import datetime, timedelta, timezone
from math import ceil
from typing import Any, Dict, List, Optional, Tuple
import ccxt
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
from freqtrade.util import FtPrecise
CcxtModuleType = Any
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module)
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
"""
Return the list of all exchanges known to ccxt
"""
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
"""
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
"""
exchanges = ccxt_exchanges(ccxt_module)
return [x for x in exchanges if validate_exchange(x)[0]]
def validate_exchange(exchange: str) -> Tuple[bool, str]:
ex_mod = getattr(ccxt, exchange.lower())()
if not ex_mod or not ex_mod.has:
return False, ''
missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True]
if missing:
return False, f"missing: {', '.join(missing)}"
missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
if exchange.lower() in BAD_EXCHANGES:
return False, BAD_EXCHANGES.get(exchange.lower(), '')
if missing_opt:
return True, f"missing opt: {', '.join(missing_opt)}"
return True, ''
def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]:
"""
:return: List of tuples with exchangename, valid, reason.
"""
exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
exchanges_valid = [
(e, *validate_exchange(e)) for e in exchanges
]
return exchanges_valid
def timeframe_to_seconds(timeframe: str) -> int:
"""
Translates the timeframe interval value written in the human readable
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
of seconds for one timeframe interval.
"""
return ccxt.Exchange.parse_timeframe(timeframe)
def timeframe_to_minutes(timeframe: str) -> int:
"""
Same as timeframe_to_seconds, but returns minutes.
"""
return ccxt.Exchange.parse_timeframe(timeframe) // 60
def timeframe_to_msecs(timeframe: str) -> int:
"""
Same as timeframe_to_seconds, but returns milliseconds.
"""
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
"""
Use Timeframe and determine the candle start date for this date.
Does not round when given a candle start date.
:param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to now(utc)
:returns: date of previous candle (with utc timezone)
"""
if not date:
date = datetime.now(timezone.utc)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
ROUND_DOWN) // 1000
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
"""
Use Timeframe and determine next candle.
:param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to now(utc)
:returns: date of next candle (with utc timezone)
"""
if not date:
date = datetime.now(timezone.utc)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
ROUND_UP) // 1000
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def date_minus_candles(
timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime:
"""
subtract X candles from a date.
:param timeframe: timeframe in string format (e.g. "5m")
:param candle_count: Amount of candles to subtract.
:param date: date to use. Defaults to now(utc)
"""
if not date:
date = datetime.now(timezone.utc)
tf_min = timeframe_to_minutes(timeframe)
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
return new_date
def market_is_active(market: Dict) -> bool:
"""
Return True if the market is active.
"""
# "It's active, if the active flag isn't explicitly set to false. If it's missing or
# true then it's true. If it's undefined, then it's most likely true, but not 100% )"
# See https://github.com/ccxt/ccxt/issues/4874,
# https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520
return market.get('active', True) is not False
def amount_to_contracts(amount: float, contract_size: Optional[float]) -> float:
"""
Convert amount to contracts.
:param amount: amount to convert
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
:return: num-contracts
"""
if contract_size and contract_size != 1:
return float(FtPrecise(amount) / FtPrecise(contract_size))
else:
return amount
def contracts_to_amount(num_contracts: float, contract_size: Optional[float]) -> float:
"""
Takes num-contracts and converts it to contract size
:param num_contracts: number of contracts
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
:return: Amount
"""
if contract_size and contract_size != 1:
return float(FtPrecise(num_contracts) * FtPrecise(contract_size))
else:
return num_contracts
def amount_to_precision(amount: float, amount_precision: Optional[float],
precisionMode: Optional[int]) -> float:
"""
Returns the amount to buy or sell to a precision the Exchange accepts
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
:param amount: amount to truncate
:param amount_precision: amount precision to use.
should be retrieved from markets[pair]['precision']['amount']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:return: truncated amount
"""
if amount_precision is not None and precisionMode is not None:
precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision
# precision must be an int for non-ticksize inputs.
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=precision,
counting_mode=precisionMode,
))
return amount
def amount_to_contract_precision(
amount, amount_precision: Optional[float], precisionMode: Optional[int],
contract_size: Optional[float]) -> float:
"""
Returns the amount to buy or sell to a precision the Exchange accepts
including calculation to and from contracts.
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
:param amount: amount to truncate
:param amount_precision: amount precision to use.
should be retrieved from markets[pair]['precision']['amount']
:param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
:return: truncated amount
"""
if amount_precision is not None and precisionMode is not None:
contracts = amount_to_contracts(amount, contract_size)
amount_p = amount_to_precision(contracts, amount_precision, precisionMode)
return contracts_to_amount(amount_p, contract_size)
return amount
def price_to_precision(price: float, price_precision: Optional[float],
precisionMode: Optional[int]) -> float:
"""
Returns the price rounded up to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up
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
: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:
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 price

View File

@@ -1,178 +0,0 @@
""" FTX exchange subclass """
import logging
from typing import Any, Dict, List, Optional, Tuple
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__)
class Ftx(Exchange):
_ft_has: Dict = {
"order_time_in_force": ['GTC', 'IOC', 'PO'],
"stoploss_on_exchange": True,
"ohlcv_candle_limit": 1500,
"ohlcv_require_since": True,
"ohlcv_volume_currency": "quote",
"mark_ohlcv_price": "index",
"mark_ohlcv_timeframe": "1h",
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS)
]
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return order['type'] == 'stop' and (
side == "sell" and stop_loss > float(order['price']) or
side == "buy" and stop_loss < float(order['price'])
)
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: BuySell, leverage: float) -> Dict:
"""
Creates a stoploss order.
depending on order_types.stoploss configuration, uses 'market' or limit order.
Limit orders are defined by having orderPrice set, otherwise a market order is used.
"""
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
if side == "sell":
limit_rate = stop_price * limit_price_pct
else:
limit_rate = stop_price * (2 - limit_price_pct)
ordertype = "stop"
stop_price = self.price_to_precision(pair, stop_price)
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
return dry_order
try:
params = self._params.copy()
if order_types.get('stoploss', 'market') == 'limit':
# set orderPrice to place limit order, otherwise it's a market order
params['orderPrice'] = limit_rate
if self.trading_mode == TradingMode.FUTURES:
params.update({'reduceOnly': True})
params['stopPrice'] = stop_price
amount = self.amount_to_precision(pair, amount)
self._lev_prep(pair, leverage, side)
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, params=params)
self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. '
'stop price: %s.', pair, stop_price)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
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)
try:
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
order = [order for order in orders if order['id'] == order_id]
self._log_exchange_response('fetch_stoploss_order', order)
if len(order) == 1:
if order[0].get('status') == 'closed':
# Trigger order was triggered ...
real_order_id: Optional[str] = order[0].get('info', {}).get('orderId')
# OrderId may be None for stoploss-market orders
# So we need to get it through the endpoint
# /conditional_orders/{conditional_order_id}/triggers
if not real_order_id:
res = self._api.privateGetConditionalOrdersConditionalOrderIdTriggers(
params={'conditional_order_id': order_id})
self._log_exchange_response('fetch_stoploss_order2', res)
real_order_id = res['result'][0]['orderId'] if res.get(
'result', []) else None
if real_order_id:
order1 = self._api.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order1)
# Fake type to stop - as this was really a stop order.
order1['id_stop'] = order1['id']
order1['id'] = order_id
order1['type'] = 'stop'
order1['status_stop'] = 'triggered'
return order1
return order[0]
else:
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
if self._config['dry_run']:
return {}
try:
order = self._api.cancel_order(order_id, pair, params={'type': 'stop'})
self._log_exchange_response('cancel_stoploss_order', order)
return order
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not cancel order. Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if order['type'] == 'stop':
return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['id']

View File

@@ -126,13 +126,3 @@ class Gateio(Exchange):
pair=pair,
params={'stop': True}
)
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
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']))
)

View File

@@ -2,6 +2,7 @@
import logging
from typing import Dict
from freqtrade.constants import BuySell
from freqtrade.exchange import Exchange
@@ -22,20 +23,7 @@ class Huobi(Exchange):
"l2_limit_range_required": False,
}
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return (
order.get('stopPrice', None) is None
or (
order['type'] == 'stop'
and stop_loss > float(order['stopPrice'])
)
)
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
params.update({

View File

@@ -218,3 +218,19 @@ class Kraken(Exchange):
fees = sum(df['open_fund'] * df['open_mark'] * amount * time_in_ratio)
return fees if is_short else -fees
def _trades_contracts_to_amount(self, trades: List) -> List:
"""
Fix "last" id issue for kraken data downloads
This whole override can probably be removed once the following
issue is closed in ccxt: https://github.com/ccxt/ccxt/issues/15827
"""
super()._trades_contracts_to_amount(trades)
if (
len(trades) > 0
and isinstance(trades[-1].get('info'), list)
and len(trades[-1].get('info', [])) > 7
):
trades[-1]['id'] = trades[-1].get('info', [])[-1]
return trades

View File

@@ -2,6 +2,7 @@
import logging
from typing import Dict
from freqtrade.constants import BuySell
from freqtrade.exchange import Exchange
@@ -27,17 +28,7 @@ class Kucoin(Exchange):
"ohlcv_candle_limit": 1500,
}
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return (
order.get('stopPrice', None) is None
or stop_loss > float(order['stopPrice'])
)
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
params.update({

View File

@@ -0,0 +1,93 @@
import numpy as np
from joblib import Parallel
from sklearn.base import is_classifier
from sklearn.multioutput import MultiOutputClassifier, _fit_estimator
from sklearn.utils.fixes import delayed
from sklearn.utils.multiclass import check_classification_targets
from sklearn.utils.validation import has_fit_parameter
from freqtrade.exceptions import OperationalException
class FreqaiMultiOutputClassifier(MultiOutputClassifier):
def fit(self, X, y, sample_weight=None, fit_params=None):
"""Fit the model to data, separately for each output variable.
Parameters
----------
X : {array-like, sparse matrix} of shape (n_samples, n_features)
The input data.
y : {array-like, sparse matrix} of shape (n_samples, n_outputs)
Multi-output targets. An indicator matrix turns on multilabel
estimation.
sample_weight : array-like of shape (n_samples,), default=None
Sample weights. If `None`, then samples are equally weighted.
Only supported if the underlying classifier supports sample
weights.
fit_params : A list of dicts for the fit_params
Parameters passed to the ``estimator.fit`` method of each step.
Each dict may contain same or different values (e.g. different
eval_sets or init_models)
.. versionadded:: 0.23
Returns
-------
self : object
Returns a fitted instance.
"""
if not hasattr(self.estimator, "fit"):
raise ValueError("The base estimator should implement a fit method")
y = self._validate_data(X="no_validation", y=y, multi_output=True)
if is_classifier(self):
check_classification_targets(y)
if y.ndim == 1:
raise ValueError(
"y must have at least two dimensions for "
"multi-output regression but has only one."
)
if sample_weight is not None and not has_fit_parameter(
self.estimator, "sample_weight"
):
raise ValueError("Underlying estimator does not support sample weights.")
if not fit_params:
fit_params = [None] * y.shape[1]
self.estimators_ = Parallel(n_jobs=self.n_jobs)(
delayed(_fit_estimator)(
self.estimator, X, y[:, i], sample_weight, **fit_params[i]
)
for i in range(y.shape[1])
)
self.classes_ = []
for estimator in self.estimators_:
self.classes_.extend(estimator.classes_)
if len(set(self.classes_)) != len(self.classes_):
raise OperationalException(f"Class labels must be unique across targets: "
f"{self.classes_}")
if hasattr(self.estimators_[0], "n_features_in_"):
self.n_features_in_ = self.estimators_[0].n_features_in_
if hasattr(self.estimators_[0], "feature_names_in_"):
self.feature_names_in_ = self.estimators_[0].feature_names_in_
return self
def predict_proba(self, X):
"""
Get predict_proba and stack arrays horizontally
"""
results = np.hstack(super().predict_proba(X))
return np.squeeze(results)
def predict(self, X):
"""
Get predict and squeeze into 2D array
"""
results = super().predict(X)
return np.squeeze(results)

View File

@@ -87,6 +87,7 @@ class FreqaiDataDrawer:
self.create_follower_dict()
self.load_drawer_from_disk()
self.load_historic_predictions_from_disk()
self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {}
self.load_metric_tracker_from_disk()
self.training_queue: Dict[str, int] = {}
self.history_lock = threading.Lock()
@@ -97,7 +98,6 @@ class FreqaiDataDrawer:
self.empty_pair_dict: pair_info = {
"model_filename": "", "trained_timestamp": 0,
"data_path": "", "extras": {}}
self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {}
def update_metric_tracker(self, metric: str, value: float, pair: str) -> None:
"""
@@ -153,6 +153,7 @@ class FreqaiDataDrawer:
if exists:
with open(self.metric_tracker_path, "r") as fp:
self.metric_tracker = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
logger.info("Loading existing metric tracker from disk.")
else:
logger.info("Could not find existing metric tracker, starting from scratch")
@@ -636,6 +637,8 @@ class FreqaiDataDrawer:
axis=0,
)
self.current_candle = history_data[dk.pair][self.config['timeframe']].iloc[-1]['date']
def load_all_pair_histories(self, timerange: TimeRange, dk: FreqaiDataKitchen) -> None:
"""
Load pair histories for all whitelist and corr_pairlist pairs.

View File

@@ -1,7 +1,7 @@
import copy
import logging
import shutil
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from math import cos, sin
from pathlib import Path
from typing import Any, Dict, List, Tuple
@@ -19,6 +19,7 @@ from sklearn.neighbors import NearestNeighbors
from freqtrade.configuration import TimeRange
from freqtrade.constants import Config
from freqtrade.data.converter import reduce_dataframe_footprint
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_seconds
from freqtrade.strategy.interface import IStrategy
@@ -80,25 +81,32 @@ class FreqaiDataKitchen:
self.svm_model: linear_model.SGDOneClassSVM = None
self.keras: bool = self.freqai_config.get("keras", False)
self.set_all_pairs()
if not self.live:
if not self.config["timerange"]:
raise OperationalException(
'Please pass --timerange if you intend to use FreqAI for backtesting.')
self.full_timerange = self.create_fulltimerange(
self.config["timerange"], self.freqai_config.get("train_period_days", 0)
)
self.backtest_live_models = config.get("freqai_backtest_live_models", False)
(self.training_timeranges, self.backtesting_timeranges) = self.split_timerange(
self.full_timerange,
config["freqai"]["train_period_days"],
config["freqai"]["backtest_period_days"],
)
if not self.live:
self.full_path = self.get_full_models_path(self.config)
if self.backtest_live_models:
if self.pair:
self.set_timerange_from_ready_models()
(self.training_timeranges,
self.backtesting_timeranges) = self.split_timerange_live_models()
else:
self.full_timerange = self.create_fulltimerange(
self.config["timerange"], self.freqai_config.get("train_period_days", 0)
)
(self.training_timeranges, self.backtesting_timeranges) = self.split_timerange(
self.full_timerange,
config["freqai"]["train_period_days"],
config["freqai"]["backtest_period_days"],
)
self.data['extra_returns_per_train'] = self.freqai_config.get('extra_returns_per_train', {})
self.thread_count = self.freqai_config.get("data_kitchen_thread_count", -1)
self.train_dates: DataFrame = pd.DataFrame()
self.unique_classes: Dict[str, list] = {}
self.unique_class_list: list = []
self.backtest_live_models_data: Dict[str, Any] = {}
def set_paths(
self,
@@ -110,10 +118,7 @@ class FreqaiDataKitchen:
:param metadata: dict = strategy furnished pair metadata
:param trained_timestamp: int = timestamp of most recent training
"""
self.full_path = Path(
self.config["user_data_dir"] / "models" / str(self.freqai_config.get("identifier"))
)
self.full_path = self.get_full_models_path(self.config)
self.data_path = Path(
self.full_path
/ f"sub-train-{pair.split('/')[0]}_{trained_timestamp}"
@@ -244,7 +249,7 @@ class FreqaiDataKitchen:
self.data["filter_drop_index_training"] = drop_index
else:
if len(self.data['constant_features_list']):
if 'constant_features_list' in self.data and len(self.data['constant_features_list']):
filtered_df = self.check_pred_labels(filtered_df)
# we are backtesting so we need to preserve row number to send back to strategy,
# so now we use do_predict to avoid any prediction based on a NaN
@@ -354,13 +359,19 @@ class FreqaiDataKitchen:
:param df: Dataframe to be standardized
"""
for item in df.keys():
df[item] = (
2
* (df[item] - self.data[f"{item}_min"])
/ (self.data[f"{item}_max"] - self.data[f"{item}_min"])
- 1
)
train_max = [None] * len(df.keys())
train_min = [None] * len(df.keys())
for i, item in enumerate(df.keys()):
train_max[i] = self.data[f"{item}_max"]
train_min[i] = self.data[f"{item}_min"]
train_max_series = pd.Series(train_max, index=df.keys())
train_min_series = pd.Series(train_min, index=df.keys())
df = (
2 * (df - train_min_series) / (train_max_series - train_min_series) - 1
)
return df
@@ -422,9 +433,7 @@ class FreqaiDataKitchen:
timerange_train.stopts = timerange_train.startts + train_period_days
first = False
start = datetime.fromtimestamp(timerange_train.startts, tz=timezone.utc)
stop = datetime.fromtimestamp(timerange_train.stopts, tz=timezone.utc)
tr_training_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d"))
tr_training_list.append(timerange_train.timerange_str)
tr_training_list_timerange.append(copy.deepcopy(timerange_train))
# associated backtest period
@@ -436,9 +445,7 @@ class FreqaiDataKitchen:
if timerange_backtest.stopts > config_timerange.stopts:
timerange_backtest.stopts = config_timerange.stopts
start = datetime.fromtimestamp(timerange_backtest.startts, tz=timezone.utc)
stop = datetime.fromtimestamp(timerange_backtest.stopts, tz=timezone.utc)
tr_backtesting_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d"))
tr_backtesting_list.append(timerange_backtest.timerange_str)
tr_backtesting_list_timerange.append(copy.deepcopy(timerange_backtest))
# ensure we are predicting on exactly same amount of data as requested by user defined
@@ -449,6 +456,29 @@ class FreqaiDataKitchen:
# print(tr_training_list, tr_backtesting_list)
return tr_training_list_timerange, tr_backtesting_list_timerange
def split_timerange_live_models(
self
) -> Tuple[list, list]:
tr_backtesting_list_timerange = []
asset = self.pair.split("/")[0]
if asset not in self.backtest_live_models_data["assets_end_dates"]:
raise OperationalException(
f"Model not available for pair {self.pair}. "
"Please, try again after removing this pair from the configuration file."
)
asset_data = self.backtest_live_models_data["assets_end_dates"][asset]
backtesting_timerange = self.backtest_live_models_data["backtesting_timerange"]
model_end_dates = [x for x in asset_data]
model_end_dates.append(backtesting_timerange.stopts)
model_end_dates.sort()
for index, item in enumerate(model_end_dates):
if len(model_end_dates) > (index + 1):
tr_to_add = TimeRange("date", "date", item, model_end_dates[index + 1])
tr_backtesting_list_timerange.append(tr_to_add)
return tr_backtesting_list_timerange, tr_backtesting_list_timerange
def slice_dataframe(self, timerange: TimeRange, df: DataFrame) -> DataFrame:
"""
Given a full dataframe, extract the user desired window
@@ -457,11 +487,9 @@ class FreqaiDataKitchen:
it is sliced down to just the present training period.
"""
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
df = df.loc[df["date"] >= start, :]
df = df.loc[df["date"] >= timerange.startdt, :]
if not self.live:
df = df.loc[df["date"] < stop, :]
df = df.loc[df["date"] < timerange.stopdt, :]
return df
@@ -956,11 +984,13 @@ class FreqaiDataKitchen:
append_df[label] = predictions[label]
if append_df[label].dtype == object:
continue
append_df[f"{label}_mean"] = self.data["labels_mean"][label]
append_df[f"{label}_std"] = self.data["labels_std"][label]
if "labels_mean" in self.data:
append_df[f"{label}_mean"] = self.data["labels_mean"][label]
if "labels_std" in self.data:
append_df[f"{label}_std"] = self.data["labels_std"][label]
for extra_col in self.data["extra_returns_per_train"]:
append_df["{extra_col}"] = self.data["extra_returns_per_train"][extra_col]
append_df[f"{extra_col}"] = self.data["extra_returns_per_train"][extra_col]
append_df["do_predict"] = do_predict
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
@@ -1022,14 +1052,7 @@ class FreqaiDataKitchen:
backtest_timerange.startts = (
backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY
)
start = datetime.fromtimestamp(backtest_timerange.startts, tz=timezone.utc)
stop = datetime.fromtimestamp(backtest_timerange.stopts, tz=timezone.utc)
full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")
self.full_path = Path(
self.config["user_data_dir"] / "models" / f"{self.freqai_config['identifier']}"
)
full_timerange = backtest_timerange.timerange_str
config_path = Path(self.config["config_files"][0])
if not self.full_path.is_dir():
@@ -1112,15 +1135,15 @@ class FreqaiDataKitchen:
return retrain, trained_timerange, data_load_timerange
def set_new_model_names(self, pair: str, trained_timerange: TimeRange):
def set_new_model_names(self, pair: str, timestamp_id: int):
coin, _ = pair.split("/")
self.data_path = Path(
self.full_path
/ f"sub-train-{pair.split('/')[0]}_{int(trained_timerange.stopts)}"
/ f"sub-train-{pair.split('/')[0]}_{timestamp_id}"
)
self.model_filename = f"cb_{coin.lower()}_{int(trained_timerange.stopts)}"
self.model_filename = f"cb_{coin.lower()}_{timestamp_id}"
def set_all_pairs(self) -> None:
@@ -1131,6 +1154,54 @@ class FreqaiDataKitchen:
if pair not in self.all_pairs:
self.all_pairs.append(pair)
def extract_corr_pair_columns_from_populated_indicators(
self,
dataframe: DataFrame
) -> Dict[str, DataFrame]:
"""
Find the columns of the dataframe corresponding to the corr_pairlist, save them
in a dictionary to be reused and attached to other pairs.
:param dataframe: fully populated dataframe (current pair + corr_pairs)
:return: corr_dataframes, dictionary of dataframes to be attached
to other pairs in same candle.
"""
corr_dataframes: Dict[str, DataFrame] = {}
pairs = self.freqai_config["feature_parameters"].get("include_corr_pairlist", [])
for pair in pairs:
pair = pair.replace(':', '') # lightgbm doesnt like colons
valid_strs = [f"%-{pair}", f"%{pair}", f"%_{pair}"]
pair_cols = [col for col in dataframe.columns if
any(substr in col for substr in valid_strs)]
if pair_cols:
pair_cols.insert(0, 'date')
corr_dataframes[pair] = dataframe.filter(pair_cols, axis=1)
return corr_dataframes
def attach_corr_pair_columns(self, dataframe: DataFrame,
corr_dataframes: Dict[str, DataFrame],
current_pair: str) -> DataFrame:
"""
Attach the existing corr_pair dataframes to the current pair dataframe before training
:param dataframe: current pair strategy dataframe, indicators populated already
:param corr_dataframes: dictionary of saved dataframes from earlier in the same candle
:param current_pair: current pair to which we will attach corr pair dataframe
:return:
:dataframe: current pair dataframe of populated indicators, concatenated with corr_pairs
ready for training
"""
pairs = self.freqai_config["feature_parameters"].get("include_corr_pairlist", [])
current_pair = current_pair.replace(':', '')
for pair in pairs:
pair = pair.replace(':', '') # lightgbm doesnt work with colons
if current_pair != pair:
dataframe = dataframe.merge(corr_dataframes[pair], how='left', on='date')
return dataframe
def use_strategy_to_populate_indicators(
self,
strategy: IStrategy,
@@ -1138,6 +1209,7 @@ class FreqaiDataKitchen:
base_dataframes: dict = {},
pair: str = "",
prediction_dataframe: DataFrame = pd.DataFrame(),
do_corr_pairs: bool = True,
) -> DataFrame:
"""
Use the user defined strategy for populating indicators during retrain
@@ -1147,15 +1219,15 @@ class FreqaiDataKitchen:
:param base_dataframes: dict = dict containing the current pair dataframes
(for user defined timeframes)
:param metadata: dict = strategy furnished pair metadata
:returns:
:return:
dataframe: DataFrame = dataframe containing populated indicators
"""
# for prediction dataframe creation, we let dataprovider handle everything in the strategy
# so we create empty dictionaries, which allows us to pass None to
# `populate_any_indicators()`. Signaling we want the dp to give us the live dataframe.
tfs = self.freqai_config["feature_parameters"].get("include_timeframes")
pairs = self.freqai_config["feature_parameters"].get("include_corr_pairlist", [])
tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes")
pairs: List[str] = self.freqai_config["feature_parameters"].get("include_corr_pairlist", [])
if not prediction_dataframe.empty:
dataframe = prediction_dataframe.copy()
for tf in tfs:
@@ -1178,19 +1250,27 @@ class FreqaiDataKitchen:
informative=base_dataframes[tf],
set_generalized_indicators=sgi
)
if pairs:
for i in pairs:
if pair in i:
continue # dont repeat anything from whitelist
# ensure corr pairs are always last
for corr_pair in pairs:
if pair == corr_pair:
continue # dont repeat anything from whitelist
for tf in tfs:
if pairs and do_corr_pairs:
dataframe = strategy.populate_any_indicators(
i,
corr_pair,
dataframe.copy(),
tf,
informative=corr_dataframes[i][tf]
informative=corr_dataframes[corr_pair][tf]
)
self.get_unique_classes_from_labels(dataframe)
dataframe = self.remove_special_chars_from_feature_names(dataframe)
if self.config.get('reduce_df_footprint', False):
dataframe = reduce_dataframe_footprint(dataframe)
return dataframe
def fit_labels(self) -> None:
@@ -1257,14 +1337,16 @@ class FreqaiDataKitchen:
append_df = pd.read_hdf(self.backtesting_results_path)
return append_df
def check_if_backtest_prediction_exists(
self
def check_if_backtest_prediction_is_valid(
self,
len_backtest_df: int
) -> bool:
"""
Check if a backtesting prediction already exists
:param dk: FreqaiDataKitchen
Check if a backtesting prediction already exists and if the predictions
to append have the same size as the backtesting dataframe slice
:param length_backtesting_dataframe: Length of backtesting dataframe slice
:return:
:boolean: whether the prediction file exists or not.
:boolean: whether the prediction file is valid.
"""
path_to_predictionfile = Path(self.full_path /
self.backtest_predictions_folder /
@@ -1272,10 +1354,134 @@ class FreqaiDataKitchen:
self.backtesting_results_path = path_to_predictionfile
file_exists = path_to_predictionfile.is_file()
if file_exists:
logger.info(f"Found backtesting prediction file at {path_to_predictionfile}")
append_df = self.get_backtesting_prediction()
if len(append_df) == len_backtest_df:
logger.info(f"Found backtesting prediction file at {path_to_predictionfile}")
return True
else:
logger.info("A new backtesting prediction file is required. "
"(Number of predictions is different from dataframe length).")
return False
else:
logger.info(
f"Could not find backtesting prediction file at {path_to_predictionfile}"
)
return file_exists
return False
def set_timerange_from_ready_models(self):
backtesting_timerange, \
assets_end_dates = (
self.get_timerange_and_assets_end_dates_from_ready_models(self.full_path))
self.backtest_live_models_data = {
"backtesting_timerange": backtesting_timerange,
"assets_end_dates": assets_end_dates
}
return
def get_full_models_path(self, config: Config) -> Path:
"""
Returns default FreqAI model path
:param config: Configuration dictionary
"""
freqai_config: Dict[str, Any] = config["freqai"]
return Path(
config["user_data_dir"] / "models" / str(freqai_config.get("identifier"))
)
def get_timerange_and_assets_end_dates_from_ready_models(
self, models_path: Path) -> Tuple[TimeRange, Dict[str, Any]]:
"""
Returns timerange information based on a FreqAI model directory
:param models_path: FreqAI model path
:return: a Tuple with (Timerange calculated from directory and
a Dict with pair and model end training dates info)
"""
all_models_end_dates = []
assets_end_dates: Dict[str, Any] = self.get_assets_timestamps_training_from_ready_models(
models_path)
for key in assets_end_dates:
for model_end_date in assets_end_dates[key]:
if model_end_date not in all_models_end_dates:
all_models_end_dates.append(model_end_date)
if len(all_models_end_dates) == 0:
raise OperationalException(
'At least 1 saved model is required to '
'run backtest with the freqai-backtest-live-models option'
)
if len(all_models_end_dates) == 1:
logger.warning(
"Only 1 model was found. Backtesting will run with the "
"timerange from the end of the training date to the current date"
)
finish_timestamp = int(datetime.now(tz=timezone.utc).timestamp())
if len(all_models_end_dates) > 1:
# After last model end date, use the same period from previous model
# to finish the backtest
all_models_end_dates.sort(reverse=True)
finish_timestamp = all_models_end_dates[0] + \
(all_models_end_dates[0] - all_models_end_dates[1])
all_models_end_dates.append(finish_timestamp)
all_models_end_dates.sort()
start_date = (datetime(*datetime.fromtimestamp(min(all_models_end_dates),
timezone.utc).timetuple()[:3], tzinfo=timezone.utc))
end_date = (datetime(*datetime.fromtimestamp(max(all_models_end_dates),
timezone.utc).timetuple()[:3], tzinfo=timezone.utc))
# add 1 day to string timerange to ensure BT module will load all dataframe data
end_date = end_date + timedelta(days=1)
backtesting_timerange = TimeRange(
'date', 'date', int(start_date.timestamp()), int(end_date.timestamp())
)
return backtesting_timerange, assets_end_dates
def get_assets_timestamps_training_from_ready_models(
self, models_path: Path) -> Dict[str, Any]:
"""
Scan the models path and returns all assets end training dates (timestamp)
:param models_path: FreqAI model path
:return: a Dict with asset and model end training dates info
"""
assets_end_dates: Dict[str, Any] = {}
if not models_path.is_dir():
raise OperationalException(
'Model folders not found. Saved models are required '
'to run backtest with the freqai-backtest-live-models option'
)
for model_dir in models_path.iterdir():
if str(model_dir.name).startswith("sub-train"):
model_end_date = int(model_dir.name.split("_")[1])
asset = model_dir.name.split("_")[0].replace("sub-train-", "")
model_file_name = (
f"cb_{str(model_dir.name).replace('sub-train-', '').lower()}"
"_model.joblib"
)
model_path_file = Path(model_dir / model_file_name)
if model_path_file.is_file():
if asset not in assets_end_dates:
assets_end_dates[asset] = []
assets_end_dates[asset].append(model_end_date)
return assets_end_dates
def remove_special_chars_from_feature_names(self, dataframe: pd.DataFrame) -> pd.DataFrame:
"""
Remove all special characters from feature strings (:)
:param dataframe: the dataframe that just finished indicator population. (unfiltered)
:return: dataframe with cleaned featrue names
"""
spec_chars = [':']
for c in spec_chars:
dataframe.columns = dataframe.columns.str.replace(c, "")
return dataframe

View File

@@ -1,12 +1,10 @@
import logging
import shutil
import threading
import time
from abc import ABC, abstractmethod
from collections import deque
from datetime import datetime, timezone
from pathlib import Path
from threading import Lock
from typing import Any, Dict, List, Literal, Tuple
import numpy as np
@@ -15,13 +13,13 @@ from numpy.typing import NDArray
from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
from freqtrade.constants import Config
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_seconds
from freqtrade.freqai.data_drawer import FreqaiDataDrawer
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.freqai.utils import plot_feature_importance
from freqtrade.freqai.utils import plot_feature_importance, record_params
from freqtrade.strategy.interface import IStrategy
@@ -61,6 +59,7 @@ class IFreqaiModel(ABC):
"data_split_parameters", {})
self.model_training_parameters: Dict[str, Any] = config.get("freqai", {}).get(
"model_training_parameters", {})
self.identifier: str = self.freqai_info.get("identifier", "no_id_provided")
self.retrain = False
self.first = True
self.set_full_path()
@@ -69,23 +68,23 @@ class IFreqaiModel(ABC):
if self.save_backtest_models:
logger.info('Backtesting module configured to save all models.')
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode)
self.identifier: str = self.freqai_info.get("identifier", "no_id_provided")
# set current candle to arbitrary historical date
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc)
self.dd.current_candle = self.current_candle
self.scanning = False
self.ft_params = self.freqai_info["feature_parameters"]
self.corr_pairlist: List[str] = self.ft_params.get("include_corr_pairlist", [])
self.keras: bool = self.freqai_info.get("keras", False)
if self.keras and self.ft_params.get("DI_threshold", 0):
self.ft_params["DI_threshold"] = 0
logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
self.CONV_WIDTH = self.freqai_info.get("conv_width", 2)
self.CONV_WIDTH = self.freqai_info.get('conv_width', 1)
if self.ft_params.get("inlier_metric_window", 0):
self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2
self.pair_it = 0
self.pair_it_train = 0
self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist"))
self.train_queue = self._set_train_queue()
self.last_trade_database_summary: DataFrame = {}
self.current_trade_database_summary: DataFrame = {}
self.analysis_lock = Lock()
self.inference_time: float = 0
self.train_time: float = 0
self.begin_time: float = 0
@@ -93,10 +92,15 @@ class IFreqaiModel(ABC):
self.base_tf_seconds = timeframe_to_seconds(self.config['timeframe'])
self.continual_learning = self.freqai_info.get('continual_learning', False)
self.plot_features = self.ft_params.get("plot_feature_importances", 0)
self.corr_dataframes: Dict[str, DataFrame] = {}
# get_corr_dataframes is controlling the caching of corr_dataframes
# for improved performance. Careful with this boolean.
self.get_corr_dataframes: bool = True
self._threads: List[threading.Thread] = []
self._stop_event = threading.Event()
record_params(config, self.full_path)
def __getstate__(self):
"""
Return an empty state to be pickled in hyperopt
@@ -135,7 +139,11 @@ class IFreqaiModel(ABC):
# the concatenated results for the full backtesting period back to the strategy.
elif not self.follow_mode:
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
if self.dk.backtest_live_models:
logger.info(
f"Backtesting {len(self.dk.backtesting_timeranges)} timeranges (live models)")
else:
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
dataframe = self.dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
)
@@ -255,25 +263,20 @@ class IFreqaiModel(ABC):
dataframe_train = dk.slice_dataframe(tr_train, dataframe)
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe)
trained_timestamp = tr_train
tr_train_startts_str = datetime.fromtimestamp(
tr_train.startts,
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
tr_train_stopts_str = datetime.fromtimestamp(
tr_train.stopts,
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
logger.info(
f"Training {pair}, {self.pair_it}/{self.total_pairs} pairs"
f" from {tr_train_startts_str} to {tr_train_stopts_str}, {train_it}/{total_trains} "
"trains"
)
if not self.ensure_data_exists(dataframe_backtest, tr_backtest, pair):
continue
trained_timestamp_int = int(trained_timestamp.stopts)
dk.set_paths(pair, trained_timestamp_int)
self.log_backtesting_progress(tr_train, pair, train_it, total_trains)
dk.set_new_model_names(pair, trained_timestamp)
timestamp_model_id = int(tr_train.stopts)
if dk.backtest_live_models:
timestamp_model_id = int(tr_backtest.startts)
if dk.check_if_backtest_prediction_exists():
dk.set_paths(pair, timestamp_model_id)
dk.set_new_model_names(pair, timestamp_model_id)
if dk.check_if_backtest_prediction_is_valid(len(dataframe_backtest)):
self.dd.load_metadata(dk)
dk.find_features(dataframe_train)
self.check_if_feature_list_matches_strategy(dk)
@@ -285,7 +288,7 @@ class IFreqaiModel(ABC):
dk.find_labels(dataframe_train)
self.model = self.train(dataframe_train, pair, dk)
self.dd.pair_dict[pair]["trained_timestamp"] = int(
trained_timestamp.stopts)
tr_train.stopts)
if self.plot_features:
plot_feature_importance(self.model, pair, dk, self.plot_features)
if self.save_backtest_models:
@@ -337,6 +340,7 @@ class IFreqaiModel(ABC):
if self.dd.historic_data:
self.dd.update_historic_data(strategy, dk)
logger.debug(f'Updating historic data on pair {metadata["pair"]}')
self.track_current_candle()
if not self.follow_mode:
@@ -363,10 +367,10 @@ class IFreqaiModel(ABC):
# load the model and associated data into the data kitchen
self.model = self.dd.load_data(metadata["pair"], dk)
with self.analysis_lock:
dataframe = self.dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
)
dataframe = dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe, pair=metadata["pair"],
do_corr_pairs=self.get_corr_dataframes
)
if not self.model:
logger.warning(
@@ -375,6 +379,9 @@ class IFreqaiModel(ABC):
self.dd.return_null_values_to_strategy(dataframe, dk)
return dk
if self.corr_pairlist:
dataframe = self.cache_corr_pairlist_dfs(dataframe, dk)
dk.find_labels(dataframe)
self.build_strategy_return_arrays(dataframe, dk, metadata["pair"], trained_timestamp)
@@ -526,14 +533,13 @@ class IFreqaiModel(ABC):
return file_exists
def set_full_path(self) -> None:
"""
Creates and sets the full path for the identifier
"""
self.full_path = Path(
self.config["user_data_dir"] / "models" / f"{self.freqai_info['identifier']}"
self.config["user_data_dir"] / "models" / f"{self.identifier}"
)
self.full_path.mkdir(parents=True, exist_ok=True)
shutil.copy(
self.config["config_files"][0],
Path(self.full_path, Path(self.config["config_files"][0]).name),
)
def extract_data_and_train_model(
self,
@@ -559,10 +565,9 @@ class IFreqaiModel(ABC):
data_load_timerange, pair, dk
)
with self.analysis_lock:
unfiltered_dataframe = dk.use_strategy_to_populate_indicators(
strategy, corr_dataframes, base_dataframes, pair
)
unfiltered_dataframe = dk.use_strategy_to_populate_indicators(
strategy, corr_dataframes, base_dataframes, pair
)
unfiltered_dataframe = dk.slice_dataframe(new_trained_timerange, unfiltered_dataframe)
@@ -573,7 +578,7 @@ class IFreqaiModel(ABC):
model = self.train(unfiltered_dataframe, pair, dk)
self.dd.pair_dict[pair]["trained_timestamp"] = new_trained_timerange.stopts
dk.set_new_model_names(pair, new_trained_timerange)
dk.set_new_model_names(pair, new_trained_timerange.stopts)
self.dd.save_data(model, pair, dk)
if self.plot_features:
@@ -624,7 +629,7 @@ class IFreqaiModel(ABC):
hist_preds_df['DI_values'] = 0
for return_str in dk.data['extra_returns_per_train']:
hist_preds_df[return_str] = 0
hist_preds_df[return_str] = dk.data['extra_returns_per_train'][return_str]
hist_preds_df['close_price'] = strat_df['close']
hist_preds_df['date_pred'] = strat_df['date']
@@ -738,6 +743,74 @@ class IFreqaiModel(ABC):
f'Best approximation queue: {best_queue}')
return best_queue
def cache_corr_pairlist_dfs(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame:
"""
Cache the corr_pairlist dfs to speed up performance for subsequent pairs during the
current candle.
:param dataframe: strategy fed dataframe
:param dk: datakitchen object for current asset
:return: dataframe to attach/extract cached corr_pair dfs to/from.
"""
if self.get_corr_dataframes:
self.corr_dataframes = dk.extract_corr_pair_columns_from_populated_indicators(dataframe)
if not self.corr_dataframes:
logger.warning("Couldn't cache corr_pair dataframes for improved performance. "
"Consider ensuring that the full coin/stake, e.g. XYZ/USD, "
"is included in the column names when you are creating features "
"in `populate_any_indicators()`.")
self.get_corr_dataframes = not bool(self.corr_dataframes)
elif self.corr_dataframes:
dataframe = dk.attach_corr_pair_columns(
dataframe, self.corr_dataframes, dk.pair)
return dataframe
def track_current_candle(self):
"""
Checks if the latest candle appended by the datadrawer is
equivalent to the latest candle seen by FreqAI. If not, it
asks to refresh the cached corr_dfs, and resets the pair
counter.
"""
if self.dd.current_candle > self.current_candle:
self.get_corr_dataframes = True
self.pair_it = 1
self.current_candle = self.dd.current_candle
def ensure_data_exists(self, dataframe_backtest: DataFrame,
tr_backtest: TimeRange, pair: str) -> bool:
"""
Check if the dataframe is empty, if not, report useful information to user.
:param dataframe_backtest: the backtesting dataframe, maybe empty.
:param tr_backtest: current backtesting timerange.
:param pair: current pair
:return: if the data exists or not
"""
if self.config.get("freqai_backtest_live_models", False) and len(dataframe_backtest) == 0:
logger.info(f"No data found for pair {pair} from "
f"from { tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. "
"Probably more than one training within the same candle period.")
return False
return True
def log_backtesting_progress(self, tr_train: TimeRange, pair: str,
train_it: int, total_trains: int):
"""
Log the backtesting progress so user knows how many pairs have been trained and
how many more pairs/trains remain.
:param tr_train: the training timerange
:param train_it: the train iteration for the current pair (the sliding window progress)
:param pair: the current pair
:param total_trains: total trains (total number of slides for the sliding window)
"""
if not self.config.get("freqai_backtest_live_models", False):
logger.info(
f"Training {pair}, {self.pair_it}/{self.total_pairs} pairs"
f" from {tr_train.start_fmt} "
f"to {tr_train.stop_fmt}, {train_it}/{total_trains} "
"trains"
)
# Following methods which are overridden by user made prediction models.
# See freqai/prediction_models/CatboostPredictionModel.py for an example.

View File

@@ -0,0 +1,74 @@
import logging
import sys
from pathlib import Path
from typing import Any, Dict
from catboost import CatBoostClassifier, Pool
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
logger = logging.getLogger(__name__)
class CatboostClassifierMultiTarget(BaseClassifierModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), train(), fit(). The class inherits ModelHandler which
has its own DataHandler where data is held, saved, loaded, and managed.
"""
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
cbc = CatBoostClassifier(
allow_writing_files=True,
loss_function='MultiClass',
train_dir=Path(dk.data_path),
**self.model_training_parameters,
)
X = data_dictionary["train_features"]
y = data_dictionary["train_labels"]
sample_weight = data_dictionary["train_weights"]
eval_sets = [None] * y.shape[1]
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0:
eval_sets = [None] * data_dictionary['test_labels'].shape[1]
for i in range(data_dictionary['test_labels'].shape[1]):
eval_sets[i] = Pool(
data=data_dictionary["test_features"],
label=data_dictionary["test_labels"].iloc[:, i],
weight=data_dictionary["test_weights"],
)
init_model = self.get_init_model(dk.pair)
if init_model:
init_models = init_model.estimators_
else:
init_models = [None] * y.shape[1]
fit_params = []
for i in range(len(eval_sets)):
fit_params.append({
'eval_set': eval_sets[i], 'init_model': init_models[i],
'log_cout': sys.stdout, 'log_cerr': sys.stderr,
})
model = FreqaiMultiOutputClassifier(estimator=cbc)
thread_training = self.freqai_info.get('multitarget_parallel_training', False)
if thread_training:
model.n_jobs = y.shape[1]
model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params)
return model

View File

@@ -0,0 +1,64 @@
import logging
from typing import Any, Dict
from lightgbm import LGBMClassifier
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
logger = logging.getLogger(__name__)
class LightGBMClassifierMultiTarget(BaseClassifierModel):
"""
User created prediction model. The class needs to override three necessary
functions, predict(), train(), fit(). The class inherits ModelHandler which
has its own DataHandler where data is held, saved, loaded, and managed.
"""
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
"""
User sets up the training and test data to fit their desired model here
:param data_dictionary: the dictionary constructed by DataHandler to hold
all the training and test data/labels.
"""
lgb = LGBMClassifier(**self.model_training_parameters)
X = data_dictionary["train_features"]
y = data_dictionary["train_labels"]
sample_weight = data_dictionary["train_weights"]
eval_weights = None
eval_sets = [None] * y.shape[1]
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0:
eval_weights = [data_dictionary["test_weights"]]
eval_sets = [(None, None)] * data_dictionary['test_labels'].shape[1] # type: ignore
for i in range(data_dictionary['test_labels'].shape[1]):
eval_sets[i] = ( # type: ignore
data_dictionary["test_features"],
data_dictionary["test_labels"].iloc[:, i]
)
init_model = self.get_init_model(dk.pair)
if init_model:
init_models = init_model.estimators_
else:
init_models = [None] * y.shape[1]
fit_params = []
for i in range(len(eval_sets)):
fit_params.append(
{'eval_set': eval_sets[i], 'eval_sample_weight': eval_weights,
'init_model': init_models[i]})
model = FreqaiMultiOutputClassifier(estimator=lgb)
thread_training = self.freqai_info.get('multitarget_parallel_training', False)
if thread_training:
model.n_jobs = y.shape[1]
model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params)
return model

View File

@@ -1,9 +1,11 @@
import logging
from datetime import datetime, timezone
from typing import Any
from pathlib import Path
from typing import Any, Dict
import numpy as np
import pandas as pd
import rapidjson
from freqtrade.configuration import TimeRange
from freqtrade.constants import Config
@@ -191,3 +193,41 @@ def plot_feature_importance(model: Any, pair: str, dk: FreqaiDataKitchen,
fig.update_layout(title_text=f"Best and worst features by importance {pair}")
label = label.replace('&', '').replace('%', '') # escape two FreqAI specific characters
store_plot_file(fig, f"{dk.model_filename}-{label}.html", dk.data_path)
def record_params(config: Dict[str, Any], full_path: Path) -> None:
"""
Records run params in the full path for reproducibility
"""
params_record_path = full_path / "run_params.json"
run_params = {
"freqai": config.get('freqai', {}),
"timeframe": config.get('timeframe'),
"stake_amount": config.get('stake_amount'),
"stake_currency": config.get('stake_currency'),
"max_open_trades": config.get('max_open_trades'),
"pairs": config.get('exchange', {}).get('pair_whitelist')
}
with open(params_record_path, "w") as handle:
rapidjson.dump(
run_params,
handle,
indent=4,
default=str,
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
)
def get_timerange_backtest_live_models(config: Config) -> str:
"""
Returns a formated timerange for backtest live/ready models
:param config: Configuration dictionary
:return: a string timerange (format example: '20220801-20220822')
"""
dk = FreqaiDataKitchen(config)
models_path = dk.get_full_models_path(config)
timerange, _ = dk.get_timerange_and_assets_end_dates_from_ready_models(models_path)
return timerange.timerange_str

View File

@@ -191,10 +191,10 @@ class FreqtradeBot(LoggingMixin):
# Check whether markets have to be reloaded and reload them when it's needed
self.exchange.reload_markets()
self.update_closed_trades_without_assigned_fees()
self.update_trades_without_assigned_fees()
# Query trades from persistence layer
trades = Trade.get_open_trades()
trades: List[Trade] = Trade.get_open_trades()
self.active_pair_whitelist = self._refresh_active_whitelist(trades)
@@ -354,7 +354,7 @@ class FreqtradeBot(LoggingMixin):
if self.trading_mode == TradingMode.FUTURES:
self._schedule.run_pending()
def update_closed_trades_without_assigned_fees(self):
def update_trades_without_assigned_fees(self) -> None:
"""
Update closed trades without close fees assigned.
Only acts when Orders are in the database, otherwise the last order-id is unknown.
@@ -379,17 +379,18 @@ class FreqtradeBot(LoggingMixin):
stoploss_order=order.ft_order_side == 'stoploss',
send_msg=False)
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
trades = Trade.get_open_trades_without_assigned_fees()
for trade in trades:
if trade.is_open and not trade.fee_updated(trade.entry_side):
order = trade.select_order(trade.entry_side, False)
open_order = trade.select_order(trade.entry_side, True)
if order and open_order is None:
logger.info(
f"Updating {trade.entry_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id, send_msg=False)
with self._exit_lock:
if trade.is_open and not trade.fee_updated(trade.entry_side):
order = trade.select_order(trade.entry_side, False)
open_order = trade.select_order(trade.entry_side, True)
if order and open_order is None:
logger.info(
f"Updating {trade.entry_side}-fee on trade {trade}"
f"for order {order.order_id}."
)
self.update_trade_state(trade, order.order_id, send_msg=False)
def handle_insufficient_funds(self, trade: Trade):
"""
@@ -826,6 +827,8 @@ class FreqtradeBot(LoggingMixin):
co = self.exchange.cancel_stoploss_order_with_result(
trade.stoploss_order_id, trade.pair, trade.amount)
trade.update_order(co)
# Reset stoploss order id.
trade.stoploss_order_id = None
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
return trade
@@ -982,7 +985,7 @@ class FreqtradeBot(LoggingMixin):
# SELL / exit positions / close trades logic and methods
#
def exit_positions(self, trades: List[Any]) -> int:
def exit_positions(self, trades: List[Trade]) -> int:
"""
Tries to execute exit orders for open trades (positions)
"""
@@ -1010,7 +1013,7 @@ class FreqtradeBot(LoggingMixin):
def handle_trade(self, trade: Trade) -> bool:
"""
Sells/exits_short the current pair if the threshold is reached and updates the trade record.
Exits the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold/exited_short, False otherwise
"""
if not trade.is_open:
@@ -1133,10 +1136,8 @@ class FreqtradeBot(LoggingMixin):
trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
stoploss_order=True)
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock')
self._notify_exit(trade, "stoploss", True)
self.handle_protections(trade.pair, trade.trade_direction)
return True
if trade.open_order_id or not trade.is_open:
@@ -1169,7 +1170,6 @@ class FreqtradeBot(LoggingMixin):
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
return False
else:
trade.stoploss_order_id = None
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
# Finally we check if stoploss on exchange should be moved up because of trailing.
@@ -1595,11 +1595,6 @@ class FreqtradeBot(LoggingMixin):
trade.close_rate_requested = limit
trade.exit_reason = exit_reason
if not sub_trade_amt:
# Lock pair for one candle to prevent immediate re-trading
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock')
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
# In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') in ('closed', 'expired'):
@@ -1809,6 +1804,8 @@ class FreqtradeBot(LoggingMixin):
self._notify_enter(trade, order, 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, }

View File

@@ -35,9 +35,5 @@ def interest(
elif exchange_name == "kraken":
# Rounded based on https://kraken-fees-calculator.github.io/
return borrowed * rate * (one + FtPrecise(ceil(hours / four)))
elif exchange_name == "ftx":
# As Explained under #Interest rates section in
# https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer
return borrowed * rate * FtPrecise(ceil(hours)) / twenty_four
else:
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")

View File

@@ -10,7 +10,8 @@ from typing import Any, Dict, Iterator, List, Mapping, Union
from typing.io import IO
from urllib.parse import urlparse
import pandas
import orjson
import pandas as pd
import rapidjson
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
@@ -256,29 +257,37 @@ def parse_db_uri_for_logging(uri: str):
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
def dataframe_to_json(dataframe: pandas.DataFrame) -> str:
def dataframe_to_json(dataframe: pd.DataFrame) -> str:
"""
Serialize a DataFrame for transmission over the wire using JSON
:param dataframe: A pandas DataFrame
:returns: A JSON string of the pandas DataFrame
"""
return dataframe.to_json(orient='split')
# https://github.com/pandas-dev/pandas/issues/24889
# https://github.com/pandas-dev/pandas/issues/40443
# We need to convert to a dict to avoid mem leak
def default(z):
if isinstance(z, pd.Timestamp):
return z.timestamp() * 1e3
raise TypeError
return str(orjson.dumps(dataframe.to_dict(orient='split'), default=default), 'utf-8')
def json_to_dataframe(data: str) -> pandas.DataFrame:
def json_to_dataframe(data: str) -> pd.DataFrame:
"""
Deserialize JSON into a DataFrame
:param data: A JSON string
:returns: A pandas DataFrame from the JSON string
"""
dataframe = pandas.read_json(data, orient='split')
dataframe = pd.read_json(data, orient='split')
if 'date' in dataframe.columns:
dataframe['date'] = pandas.to_datetime(dataframe['date'], unit='ms', utc=True)
dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True)
return dataframe
def remove_entry_exit_signals(dataframe: pandas.DataFrame):
def remove_entry_exit_signals(dataframe: pd.DataFrame):
"""
Remove Entry and Exit signals from a DataFrame

View File

@@ -134,6 +134,10 @@ class Backtesting:
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
self.precision_mode = self.exchange.precisionMode
if self.config.get('freqai_backtest_live_models', False):
from freqtrade.freqai.utils import get_timerange_backtest_live_models
self.config['timerange'] = get_timerange_backtest_live_models(self.config)
self.timerange = TimeRange.parse_timerange(
None if self.config.get('timerange') is None else str(self.config.get('timerange')))
@@ -162,7 +166,7 @@ class Backtesting:
PairLocks.use_db = True
Trade.use_db = True
def init_backtest_detail(self):
def init_backtest_detail(self) -> None:
# Load detail timeframe if specified
self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
if self.timeframe_detail:
@@ -1282,8 +1286,7 @@ class Backtesting:
def _get_min_cached_backtest_date(self):
min_backtest_date = None
backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT)
if self.timerange.stopts == 0 or datetime.fromtimestamp(
self.timerange.stopts, tz=timezone.utc) > datetime.now(tz=timezone.utc):
if self.timerange.stopts == 0 or self.timerange.stopdt > datetime.now(tz=timezone.utc):
logger.warning('Backtest result caching disabled due to use of open-ended timerange.')
elif backtest_cache_age == 'day':
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(days=1)

View File

@@ -17,6 +17,7 @@ from freqtrade.enums import HyperoptState
from freqtrade.exceptions import OperationalException
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
from freqtrade.optimize.optimize_reports import generate_wins_draws_losses
logger = logging.getLogger(__name__)
@@ -325,8 +326,10 @@ class HyperoptTools():
# New mode, using backtest result for metrics
trials['results_metrics.winsdrawslosses'] = trials.apply(
lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} "
f"{x['results_metrics.losses']:>4}", axis=1)
lambda x: generate_wins_draws_losses(
x['results_metrics.wins'], x['results_metrics.draws'],
x['results_metrics.losses']
), axis=1)
trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades',
'results_metrics.winsdrawslosses',
@@ -337,7 +340,7 @@ class HyperoptTools():
'loss', 'is_initial_point', 'is_random', 'is_best']]
trials.columns = [
'Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
'Best', 'Epoch', 'Trades', ' Win Draw Loss Win%', 'Avg profit',
'Total profit', 'Profit', 'Avg duration', 'max_drawdown', 'max_drawdown_account',
'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_random', 'is_best'
]
@@ -467,9 +470,9 @@ class HyperoptTools():
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
'results_metrics.profit_mean', 'results_metrics.profit_median',
'results_metrics.profit_total',
'Stake currency',
'results_metrics.profit_total', 'Stake currency',
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
'results_metrics.trade_count_long', 'results_metrics.trade_count_short',
'loss', 'is_initial_point', 'is_best']
perc_multi = 100
@@ -477,7 +480,9 @@ class HyperoptTools():
trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit',
'Stake currency', 'Profit', 'Avg duration', 'Objective',
'Stake currency', 'Profit', 'Avg duration',
'Trade count long', 'Trade count short',
'Objective',
'is_initial_point', 'is_best']
param_columns = list(results[0]['params_dict'].keys())
trials.columns = base_columns + param_columns

View File

@@ -86,7 +86,7 @@ def _get_line_header(first_column: str, stake_currency: str,
'Win Draw Loss Win%']
def _generate_wins_draws_losses(wins, draws, losses):
def generate_wins_draws_losses(wins, draws, losses):
if wins > 0 and losses == 0:
wl_ratio = '100'
elif wins == 0:
@@ -600,7 +600,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
output = [[
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
t['profit_total_pct'], t['duration_avg'],
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses'])
generate_wins_draws_losses(t['wins'], t['draws'], t['losses'])
] for t in pair_results]
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(output, headers=headers,
@@ -626,7 +626,7 @@ def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_curren
output = [[
t.get('exit_reason', t.get('sell_reason')), t['trades'],
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
t['profit_mean_pct'], t['profit_sum_pct'],
round_coin_value(t['profit_total_abs'], stake_currency, False),
t['profit_total_pct'],
@@ -656,7 +656,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
t['profit_total_abs'],
t['profit_total_pct'],
t['duration_avg'],
_generate_wins_draws_losses(
generate_wins_draws_losses(
t['wins'],
t['draws'],
t['losses'])] for t in tag_results]
@@ -715,7 +715,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
output = [[
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
t['profit_total_pct'], t['duration_avg'],
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown]
for t, drawdown in zip(strategy_results, drawdown)]
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(output, headers=headers,

View File

@@ -1,5 +1,5 @@
import logging
from typing import List
from typing import List, Optional
from sqlalchemy import inspect, select, text, tuple_, update
@@ -31,9 +31,9 @@ def get_backup_name(tabs: List[str], backup_prefix: str):
return table_back_name
def get_last_sequence_ids(engine, trade_back_name, order_back_name):
order_id: int = None
trade_id: int = None
def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str):
order_id: Optional[int] = None
trade_id: Optional[int] = None
if engine.name == 'postgresql':
with engine.begin() as connection:

View File

@@ -90,6 +90,13 @@ class Order(_DECL_BASE):
def safe_filled(self) -> float:
return self.filled if self.filled is not None else self.amount or 0.0
@property
def safe_remaining(self) -> float:
return (
self.remaining if self.remaining is not None else
self.amount - (self.filled or 0.0)
)
@property
def safe_fee_base(self) -> float:
return self.ft_fee_base or 0.0
@@ -667,7 +674,7 @@ class LocalTrade():
self.close(order.safe_price)
else:
self.recalc_trade_from_orders()
elif order.ft_order_side == 'stoploss':
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
@@ -1144,7 +1151,8 @@ class Trade(_DECL_BASE, LocalTrade):
id = Column(Integer, primary_key=True)
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan", lazy="joined")
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan",
lazy="selectin", innerjoin=True)
exchange = Column(String(25), nullable=False)
pair = Column(String(25), nullable=False, index=True)

View File

@@ -36,7 +36,6 @@ class IPairList(LoggingMixin, ABC):
self._pairlistconfig = pairlistconfig
self._pairlist_pos = pairlist_pos
self.refresh_period = self._pairlistconfig.get('refresh_period', 1800)
self._last_refresh = 0
LoggingMixin.__init__(self, logger, self.refresh_period)
@property

View File

@@ -3,16 +3,20 @@ Shuffle pair list filter
"""
import logging
import random
from typing import Any, Dict, List
from typing import Any, Dict, List, Literal
from freqtrade.constants import Config
from freqtrade.enums import RunMode
from freqtrade.exchange import timeframe_to_seconds
from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList
from freqtrade.util.periodic_cache import PeriodicCache
logger = logging.getLogger(__name__)
ShuffleValues = Literal['candle', 'iteration']
class ShuffleFilter(IPairList):
@@ -31,6 +35,9 @@ class ShuffleFilter(IPairList):
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
self._random = random.Random(self._seed)
self._shuffle_freq: ShuffleValues = pairlistconfig.get('shuffle_frequency', 'candle')
self.__pairlist_cache = PeriodicCache(
maxsize=1000, ttl=timeframe_to_seconds(self._config['timeframe']))
@property
def needstickers(self) -> bool:
@@ -45,7 +52,7 @@ class ShuffleFilter(IPairList):
"""
Short whitelist method description - used for startup-messages
"""
return (f"{self.name} - Shuffling pairs" +
return (f"{self.name} - Shuffling pairs every {self._shuffle_freq}" +
(f", seed = {self._seed}." if self._seed is not None else "."))
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
@@ -56,7 +63,13 @@ class ShuffleFilter(IPairList):
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: new whitelist
"""
pairlist_bef = tuple(pairlist)
pairlist_new = self.__pairlist_cache.get(pairlist_bef)
if pairlist_new and self._shuffle_freq == 'candle':
# Use cached pairlist.
return pairlist_new
# Shuffle is done inplace
self._random.shuffle(pairlist)
self.__pairlist_cache[pairlist_bef] = pairlist
return pairlist

View File

@@ -84,11 +84,8 @@ async def _process_consumer_request(
# Limit the amount of candles per dataframe to 'limit' or 1500
limit = max(data.get('limit', 1500), 1500)
# They requested the full historical analyzed dataframes
analyzed_df = rpc._ws_request_analyzed_df(limit)
# For every dataframe, send as a separate message
for _, message in analyzed_df.items():
# For every pair in the generator, send a separate message
for message in rpc._ws_request_analyzed_df(limit):
response = WSAnalyzedDFMessage(data=message)
await channel_manager.send_direct(channel, response.dict(exclude_none=True))
@@ -127,13 +124,6 @@ async def message_endpoint(
except Exception as e:
logger.info(f"Consumer connection failed - {channel}: {e}")
logger.debug(e, exc_info=e)
finally:
await channel_manager.on_disconnect(ws)
else:
if channel:
await channel_manager.on_disconnect(ws)
await ws.close()
except RuntimeError:
# WebSocket was closed
@@ -144,4 +134,5 @@ async def message_endpoint(
# Log tracebacks to keep track of what errors are happening
logger.exception(e)
finally:
await channel_manager.on_disconnect(ws)
if channel:
await channel_manager.on_disconnect(ws)

View File

@@ -2,7 +2,7 @@ import asyncio
import logging
from ipaddress import IPv4Address
from threading import Thread
from typing import Any, Dict
from typing import Any, Dict, Optional
import orjson
import uvicorn
@@ -51,9 +51,9 @@ class ApiServer(RPCHandler):
# Exchange - only available in webserver mode.
_exchange = None
# websocket message queue stuff
_ws_channel_manager = None
_ws_channel_manager: ChannelManager
_ws_thread = None
_ws_loop = None
_ws_loop: Optional[asyncio.AbstractEventLoop] = None
def __new__(cls, *args, **kwargs):
"""
@@ -71,7 +71,7 @@ class ApiServer(RPCHandler):
return
self._standalone: bool = standalone
self._server = None
self._ws_queue = None
self._ws_queue: Optional[ThreadedQueue] = None
self._ws_background_task = None
ApiServer.__initialized = True
@@ -186,7 +186,7 @@ class ApiServer(RPCHandler):
self._ws_background_task = asyncio.run_coroutine_threadsafe(
self._broadcast_queue_data(), loop=self._ws_loop)
async def _broadcast_queue_data(self):
async def _broadcast_queue_data(self) -> None:
# Instantiate the queue in this coroutine so it's attached to our loop
self._ws_queue = ThreadedQueue()
async_queue = self._ws_queue.async_q
@@ -194,9 +194,13 @@ class ApiServer(RPCHandler):
try:
while True:
logger.debug("Getting queue messages...")
if (qsize := async_queue.qsize()) > 20:
# If the queue becomes too big for too long, this may indicate a problem.
logger.warning(f"Queue size now {qsize}")
# Get data from queue
message: WSMessageSchemaType = await async_queue.get()
logger.debug(f"Found message of type: {message.get('type')}")
async_queue.task_done()
# Broadcast it
await self._ws_channel_manager.broadcast(message)
except asyncio.CancelledError:
@@ -209,7 +213,11 @@ class ApiServer(RPCHandler):
finally:
# Disconnect channels and stop the loop on cancel
await self._ws_channel_manager.disconnect_all()
self._ws_loop.stop()
if self._ws_loop:
self._ws_loop.stop()
# Avoid adding more items to the queue if they aren't
# going to get broadcasted.
self._ws_queue = None
def start_api(self):
"""

View File

@@ -1,5 +1,6 @@
import asyncio
import logging
import time
from threading import RLock
from typing import Any, Dict, List, Optional, Type, Union
from uuid import uuid4
@@ -34,8 +35,6 @@ class WebSocketChannel:
# The WebSocket object
self._websocket = WebSocketProxy(websocket)
# The Serializing class for the WebSocket object
self._serializer_cls = serializer_cls
self.drain_timeout = drain_timeout
self.throttle = throttle
@@ -46,10 +45,10 @@ class WebSocketChannel:
self._relay_task = asyncio.create_task(self.relay())
# Internal event to signify a closed websocket
self._closed = False
self._closed = asyncio.Event()
# Wrap the WebSocket in the Serializing class
self._wrapped_ws = self._serializer_cls(self._websocket)
self._wrapped_ws = serializer_cls(self._websocket)
def __repr__(self):
return f"WebSocketChannel({self.channel_id}, {self.remote_addr})"
@@ -73,13 +72,27 @@ class WebSocketChannel:
Add the data to the queue to be sent.
:returns: True if data added to queue, False otherwise
"""
try:
await asyncio.wait_for(
self.queue.put(data),
timeout=self.drain_timeout
)
# This block only runs if the queue is full, it will wait
# until self.drain_timeout for the relay to drain the outgoing queue
# We can't use asyncio.wait_for here because the queue may have been created with a
# different eventloop
if not self.is_closed():
start = time.time()
while self.queue.full():
await asyncio.sleep(1)
if (time.time() - start) > self.drain_timeout:
return False
# If for some reason the queue is still full, just return False
try:
self.queue.put_nowait(data)
except asyncio.QueueFull:
return False
# If we got here everything is ok
return True
except asyncio.TimeoutError:
else:
return False
async def recv(self):
@@ -99,14 +112,19 @@ class WebSocketChannel:
Close the WebSocketChannel
"""
self._closed = True
self._closed.set()
self._relay_task.cancel()
try:
await self.raw_websocket.close()
except Exception:
pass
def is_closed(self) -> bool:
"""
Closed flag
"""
return self._closed
return self._closed.is_set()
def set_subscriptions(self, subscriptions: List[str] = []) -> None:
"""
@@ -129,7 +147,7 @@ class WebSocketChannel:
Relay messages from the channel's queue and send them out. This is started
as a task.
"""
while True:
while not self._closed.is_set():
message = await self.queue.get()
try:
await self._send(message)

View File

@@ -31,6 +31,7 @@ class Producer(TypedDict):
name: str
host: str
port: int
secure: bool
ws_token: str
@@ -180,7 +181,8 @@ class ExternalMessageConsumer:
host, port = producer['host'], producer['port']
token = producer['ws_token']
name = producer['name']
ws_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}"
scheme = 'wss' if producer.get('secure', False) else 'ws'
ws_url = f"{scheme}://{host}:{port}/api/v1/message/ws?token={token}"
# This will raise InvalidURI if the url is bad
async with websockets.connect(
@@ -264,10 +266,10 @@ class ExternalMessageConsumer:
# We haven't received data yet. Check the connection and continue.
try:
# ping
ping = await channel.ping()
pong = await channel.ping()
latency = (await asyncio.wait_for(pong, timeout=self.ping_timeout) * 1000)
await asyncio.wait_for(ping, timeout=self.ping_timeout)
logger.debug(f"Connection to {channel} still alive...")
logger.info(f"Connection to {channel} still alive, latency: {latency}ms")
continue
except (websockets.exceptions.ConnectionClosed):
@@ -276,7 +278,7 @@ class ExternalMessageConsumer:
await asyncio.sleep(self.sleep_time)
break
except Exception as e:
logger.warning(f"Ping error {channel} - retrying in {self.sleep_time}s")
logger.warning(f"Ping error {channel} - {e} - retrying in {self.sleep_time}s")
logger.debug(e, exc_info=e)
await asyncio.sleep(self.sleep_time)

View File

@@ -5,7 +5,7 @@ import logging
from abc import abstractmethod
from datetime import date, datetime, timedelta, timezone
from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
import arrow
import psutil
@@ -218,9 +218,10 @@ class RPC:
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
stoploss_entry_dist=stoploss_entry_dist,
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
open_order='({} {} rem={:.8f})'.format(
order.order_type, order.side, order.remaining
) if order else None,
open_order=(
f'({order.order_type} {order.side} rem={order.safe_remaining:.8f})' if
order else None
),
))
results.append(trade_dict)
return results
@@ -773,6 +774,9 @@ class RPC:
is_short = trade.is_short
if not self._freqtrade.strategy.position_adjustment_enable:
raise RPCException(f'position for {pair} already open - id: {trade.id}')
if trade.open_order_id is not None:
raise RPCException(f'position for {pair} already open - id: {trade.id} '
f'and has open order {trade.open_order_id}')
else:
if Trade.get_open_trade_count() >= self._config['max_open_trades']:
raise RPCException("Maximum number of trades is reached.")
@@ -785,17 +789,18 @@ class RPC:
if not order_type:
order_type = self._freqtrade.strategy.order_types.get(
'force_entry', self._freqtrade.strategy.order_types['entry'])
if self._freqtrade.execute_entry(pair, stake_amount, price,
ordertype=order_type, trade=trade,
is_short=is_short,
enter_tag=enter_tag,
leverage_=leverage,
):
Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade
else:
raise RPCException(f'Failed to enter position for {pair}.')
with self._freqtrade._exit_lock:
if self._freqtrade.execute_entry(pair, stake_amount, price,
ordertype=order_type, trade=trade,
is_short=is_short,
enter_tag=enter_tag,
leverage_=leverage,
):
Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade
else:
raise RPCException(f'Failed to enter position for {pair}.')
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
"""
@@ -1063,23 +1068,20 @@ class RPC:
self,
pairlist: List[str],
limit: Optional[int]
) -> Dict[str, Any]:
) -> Generator[Dict[str, Any], None, None]:
""" Get the analysed dataframes of each pair in the pairlist """
timeframe = self._freqtrade.config['timeframe']
candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT)
_data = {}
for pair in pairlist:
dataframe, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
_data[pair] = {
yield {
"key": (pair, timeframe, candle_type),
"df": dataframe,
"la": last_analyzed
}
return _data
def _ws_request_analyzed_df(self, limit: Optional[int]):
""" Historical Analyzed Dataframes for WebSocket """
whitelist = self._freqtrade.active_pair_whitelist

View File

@@ -1061,7 +1061,8 @@ class Telegram(RPCHandler):
try:
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
except RPCException as e:
self._send_msg(str(e))
logger.exception("Forcebuy error!")
self._send_msg(str(e), ParseMode.HTML)
def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:

View File

@@ -110,8 +110,6 @@ class FreqaiExampleHybridStrategy(IStrategy):
:param informative: the dataframe associated with the informative pair
"""
coin = pair.split('/')[0]
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
@@ -119,13 +117,13 @@ class FreqaiExampleHybridStrategy(IStrategy):
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
informative[f"%-{coin}relative_volume-period_{t}"] = (
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
informative[f"%-{pair}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)

View File

@@ -53,7 +53,7 @@ class FreqaiExampleStrategy(IStrategy):
"""
Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User controls the indicators
passed to the training/prediction by prepending indicators with `'%-' + coin `
passed to the training/prediction by prepending indicators with `f'%-{pair}`
(see convention below). I.e. user should not prepend any supporting metrics
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
model.
@@ -63,8 +63,6 @@ class FreqaiExampleStrategy(IStrategy):
:param informative: the dataframe associated with the informative pair
"""
coin = pair.split('/')[0]
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
@@ -72,36 +70,36 @@ class FreqaiExampleStrategy(IStrategy):
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(informative), window=t, stds=2.2
)
informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"]
informative[f"{pair}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{pair}bb_upperband-period_{t}"] = bollinger["upper"]
informative[f"%-{coin}bb_width-period_{t}"] = (
informative[f"{coin}bb_upperband-period_{t}"]
- informative[f"{coin}bb_lowerband-period_{t}"]
) / informative[f"{coin}bb_middleband-period_{t}"]
informative[f"%-{coin}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"]
informative[f"%-{pair}bb_width-period_{t}"] = (
informative[f"{pair}bb_upperband-period_{t}"]
- informative[f"{pair}bb_lowerband-period_{t}"]
) / informative[f"{pair}bb_middleband-period_{t}"]
informative[f"%-{pair}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{pair}bb_lowerband-period_{t}"]
)
informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
informative[f"%-{coin}relative_volume-period_{t}"] = (
informative[f"%-{pair}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
informative[f"%-{coin}raw_volume"] = informative["volume"]
informative[f"%-{coin}raw_price"] = informative["close"]
informative[f"%-{pair}pct-change"] = informative["close"].pct_change()
informative[f"%-{pair}raw_volume"] = informative["volume"]
informative[f"%-{pair}raw_price"] = informative["close"]
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data

View File

@@ -150,14 +150,20 @@ class Worker:
if timeframe:
next_tf = timeframe_to_next_date(timeframe)
# Maximum throttling should be until new candle arrives
# Offset of 0.2s is added to ensure a new candle has been issued.
next_tf_with_offset = next_tf.timestamp() - time.time() + timeframe_offset
# Offset is added to ensure a new candle has been issued.
next_tft = next_tf.timestamp() - time.time()
next_tf_with_offset = next_tft + timeframe_offset
if next_tft < sleep_duration and sleep_duration < next_tf_with_offset:
# Avoid hitting a new loop between the new candle and the candle with offset
sleep_duration = next_tf_with_offset
sleep_duration = min(sleep_duration, next_tf_with_offset)
sleep_duration = max(sleep_duration, 0.0)
# next_iter = datetime.now(timezone.utc) + timedelta(seconds=sleep_duration)
logger.debug(f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, "
f"last iteration took {time_passed:.2f} s.")
f"last iteration took {time_passed:.2f} s."
# f"next: {next_iter}"
)
self._sleep(sleep_duration)
return result

View File

@@ -30,6 +30,8 @@ asyncio_mode = "auto"
[tool.mypy]
ignore_missing_imports = true
namespace_packages = false
implicit_optional = true
warn_unused_ignores = true
exclude = [
'^build_helpers\.py$'

View File

@@ -8,23 +8,25 @@
coveralls==3.3.1
flake8==5.0.4
flake8-tidy-imports==4.8.0
mypy==0.982
mypy==0.991
pre-commit==2.20.0
pytest==7.1.3
pytest-asyncio==0.20.1
pytest==7.2.0
pytest-asyncio==0.20.2
pytest-cov==4.0.0
pytest-mock==3.10.0
pytest-random-order==1.0.4
isort==5.10.1
# For datetime mocking
time-machine==2.8.2
# fastapi testing
httpx==0.23.1
# Convert jupyter notebooks to markdown documents
nbconvert==7.2.1
nbconvert==7.2.5
# mypy types
types-cachetools==5.2.1
types-filelock==3.2.7
types-requests==2.28.11.2
types-requests==2.28.11.5
types-tabulate==0.9.0.0
types-python-dateutil==2.8.19.2
types-python-dateutil==2.8.19.4

View File

@@ -1,10 +1,11 @@
# Include all requirements to run the bot.
-r requirements.txt
-r requirements-plot.txt
# Required for freqai
scikit-learn==1.1.2
scikit-learn==1.1.3
joblib==1.2.0
catboost==1.1; platform_machine != 'aarch64'
catboost==1.1.1; platform_machine != 'aarch64'
lightgbm==3.3.3
xgboost==1.6.2
tensorboard==2.10.1
xgboost==1.7.1
tensorboard==2.11.0

View File

@@ -3,7 +3,7 @@
# Required for hyperopt
scipy==1.9.3
scikit-learn==1.1.2
scikit-learn==1.1.3
scikit-optimize==0.9.0
filelock==3.8.0
progressbar2==4.1.1
progressbar2==4.2.0

View File

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

View File

@@ -1,29 +1,28 @@
numpy==1.23.4
pandas==1.5.1; platform_machine != 'armv7l'
# Piwheels doesn't have 1.5.0 yet.
pandas==1.4.3; platform_machine == 'armv7l'
numpy==1.23.5
pandas==1.5.1
pandas-ta==0.3.14b
ccxt==2.0.58
ccxt==2.1.96
# Pin cryptography for now due to rust build errors with piwheels
cryptography==38.0.1
cryptography==38.0.1; platform_machine == 'armv7l'
cryptography==38.0.3; platform_machine != 'armv7l'
aiohttp==3.8.3
SQLAlchemy==1.4.42
SQLAlchemy==1.4.44
python-telegram-bot==13.14
arrow==1.2.3
cachetools==4.2.2
requests==2.28.1
urllib3==1.26.12
jsonschema==4.16.0
jsonschema==4.17.0
TA-Lib==0.4.25
technical==1.3.0
tabulate==0.9.0
pycoingecko==3.0.0
pycoingecko==3.1.0
jinja2==3.1.2
tables==3.7.0
blosc==1.10.6
joblib==1.2.0
pyarrow==9.0.0; platform_machine != 'armv7l'
pyarrow==10.0.0; platform_machine != 'armv7l'
# find first, C search in arrays
py_find_1st==1.1.5
@@ -31,24 +30,24 @@ py_find_1st==1.1.5
# Load ticker files 30% faster
python-rapidjson==1.9
# Properly format api responses
orjson==3.8.0
orjson==3.8.2
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.85.1
fastapi==0.87.0
pydantic==1.10.2
uvicorn==0.18.3
uvicorn==0.20.0
pyjwt==2.6.0
aiofiles==22.1.0
psutil==5.9.2
psutil==5.9.4
# Support for colorized terminal output
colorama==0.4.5
colorama==0.4.6
# Building config files interactively
questionary==1.10.0
prompt-toolkit==3.0.31
prompt-toolkit==3.0.32
# Extensions to datetime library
python-dateutil==2.8.2
@@ -56,5 +55,5 @@ python-dateutil==2.8.2
schedule==1.1.0
#WS Messages
websockets==10.3
websockets==10.4
janus==1.0.0

View File

@@ -18,7 +18,6 @@ import orjson
import pandas
import rapidjson
import websockets
from dateutil.relativedelta import relativedelta
logger = logging.getLogger("WebSocketClient")
@@ -28,7 +27,7 @@ logger = logging.getLogger("WebSocketClient")
def setup_logging(filename: str):
logging.basicConfig(
level=logging.INFO,
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(filename),
@@ -75,16 +74,15 @@ def load_config(configfile):
def readable_timedelta(delta):
"""
Convert a dateutil.relativedelta to a readable format
Convert a millisecond delta to a readable format
:param delta: A dateutil.relativedelta
:param delta: A delta between two timestamps in milliseconds
:returns: The readable time difference string
"""
attrs = ['years', 'months', 'days', 'hours', 'minutes', 'seconds', 'microseconds']
return ", ".join([
'%d %s' % (getattr(delta, attr), attr if getattr(delta, attr) > 0 else attr[:-1])
for attr in attrs if getattr(delta, attr)
])
seconds, milliseconds = divmod(delta, 1000)
minutes, seconds = divmod(seconds, 60)
return f"{int(minutes)}:{int(seconds)}.{int(milliseconds)}"
# ----------------------------------------------------------------------------
@@ -170,8 +168,8 @@ class ClientProtocol:
def _calculate_time_difference(self):
old_last_received_at = self._LAST_RECEIVED_AT
self._LAST_RECEIVED_AT = time.time() * 1e6
time_delta = relativedelta(microseconds=(self._LAST_RECEIVED_AT - old_last_received_at))
self._LAST_RECEIVED_AT = time.time() * 1e3
time_delta = self._LAST_RECEIVED_AT - old_last_received_at
return readable_timedelta(time_delta)
@@ -201,6 +199,7 @@ async def create_client(
host,
port,
token,
scheme='ws',
name='default',
protocol=ClientProtocol(),
sleep_time=10,
@@ -213,13 +212,14 @@ async def create_client(
:param host: The host
:param port: The port
:param token: The websocket auth token
:param scheme: `ws` for most connections, `wss` for ssl
:param name: The name of the producer
:param **kwargs: Any extra kwargs passed to websockets.connect
"""
while 1:
try:
websocket_url = f"ws://{host}:{port}/api/v1/message/ws?token={token}"
websocket_url = f"{scheme}://{host}:{port}/api/v1/message/ws?token={token}"
logger.info(f"Attempting to connect to {name} @ {host}:{port}")
async with websockets.connect(websocket_url, **kwargs) as ws:
@@ -242,12 +242,10 @@ async def create_client(
):
# Try pinging
try:
pong = ws.ping()
await asyncio.wait_for(
pong,
timeout=ping_timeout
)
logger.info("Connection still alive...")
pong = await ws.ping()
latency = (await asyncio.wait_for(pong, timeout=ping_timeout) * 1000)
logger.info(f"Connection still alive, latency: {latency}ms")
continue
@@ -272,6 +270,7 @@ async def create_client(
websockets.exceptions.ConnectionClosedError,
websockets.exceptions.ConnectionClosedOK
):
logger.info("Connection was closed")
# Just keep trying to connect again indefinitely
await asyncio.sleep(sleep_time)
@@ -307,6 +306,7 @@ async def _main(args):
producer['host'],
producer['port'],
producer['ws_token'],
'wss' if producer.get('secure', False) else 'ws',
producer['name'],
sleep_time=sleep_time,
ping_timeout=ping_timeout,

View File

@@ -82,7 +82,7 @@ function updateenv() {
dev=$REPLY
if [[ $REPLY =~ ^[Yy]$ ]]
then
REQUIREMENTS_FREQAI="-r requirements-freqai.txt"
REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517"
fi
${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI}

View File

@@ -30,7 +30,7 @@ def test_validate_is_int():
assert not validate_is_int('-ee')
@pytest.mark.parametrize('exchange', ['bittrex', 'binance', 'kraken', 'ftx'])
@pytest.mark.parametrize('exchange', ['bittrex', 'binance', 'kraken'])
def test_start_new_config(mocker, caplog, exchange):
wt_mock = mocker.patch.object(Path, "write_text", MagicMock())
mocker.patch.object(Path, "exists", MagicMock(return_value=True))

View File

@@ -1271,7 +1271,7 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir):
assert csv_file.is_file()
line = csv_file.read_text()
assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line)
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,2,0,0.43662" in line)
csv_file.unlink()

View File

@@ -1748,28 +1748,7 @@ def limit_buy_order_canceled_empty(request):
# https://docs.pytest.org/en/latest/example/parametrize.html#apply-indirect-on-particular-arguments
exchange_name = request.param
if exchange_name == 'ftx':
return {
'info': {},
'id': '1234512345',
'clientOrderId': None,
'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 34.3225,
'amount': 0.55,
'cost': 0.0,
'average': None,
'filled': 0.0,
'remaining': 0.0,
'status': 'closed',
'fee': None,
'trades': None
}
elif exchange_name == 'kraken':
if exchange_name == 'kraken':
return {
'info': {},
'id': 'AZNPFF-4AC4N-7MKTAT',
@@ -2700,7 +2679,7 @@ def saved_hyperopt_results():
'params_dict': {
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
'results_metrics': {'total_trades': 2, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'max_drawdown': 0.23, 'max_drawdown_abs': -0.00125625, 'holding_avg': timedelta(minutes=3930.0), 'stake_currency': 'BTC', 'strategy_name': 'SampleStrategy'}, # noqa: E501
'results_metrics': {'total_trades': 2, 'trade_count_long': 2, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 2, 'profit_mean': -0.01254995, 'profit_median': -0.012222, 'profit_total': -0.00125625, 'profit_total_abs': -2.50999, 'max_drawdown': 0.23, 'max_drawdown_abs': -0.00125625, 'holding_avg': timedelta(minutes=3930.0), 'stake_currency': 'BTC', 'strategy_name': 'SampleStrategy'}, # noqa: E501
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
'total_profit': -0.00125625,
'current_epoch': 1,
@@ -2717,7 +2696,7 @@ def saved_hyperopt_results():
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
'stoploss': {'stoploss': -0.338070047333259}},
'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'max_drawdown': 0.23, 'max_drawdown_abs': -0.00125625, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501
'results_metrics': {'total_trades': 1, 'trade_count_long': 1, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 1, 'profit_mean': 0.012357, 'profit_median': -0.012222, 'profit_total': 6.185e-05, 'profit_total_abs': 0.12357, 'max_drawdown': 0.23, 'max_drawdown_abs': -0.00125625, 'holding_avg': timedelta(minutes=1200.0)}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
'total_profit': 6.185e-05,
'current_epoch': 2,
@@ -2728,7 +2707,7 @@ def saved_hyperopt_results():
'loss': 14.241196856510731,
'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501
'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501
'results_metrics': {'total_trades': 621, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'max_drawdown': 0.25, 'max_drawdown_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501
'results_metrics': {'total_trades': 621, 'trade_count_long': 621, 'trade_count_short': 0, 'wins': 320, 'draws': 0, 'losses': 301, 'profit_mean': -0.043883302093397747, 'profit_median': -0.012222, 'profit_total': -0.13639474, 'profit_total_abs': -272.515306, 'max_drawdown': 0.25, 'max_drawdown_abs': -272.515306, 'holding_avg': timedelta(minutes=1691.207729468599)}, # noqa: E501
'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501
'total_profit': -0.13639474,
'current_epoch': 3,
@@ -2739,14 +2718,14 @@ def saved_hyperopt_results():
'loss': 100000,
'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501
'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501
'results_metrics': {'total_trades': 0, 'trade_count_long': 0, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_random': False, 'is_best': False # noqa: E501
}, {
'loss': 0.22195522184191518,
'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501
'results_metrics': {'total_trades': 14, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'max_drawdown': 0.34, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501
'results_metrics': {'total_trades': 14, 'trade_count_long': 14, 'trade_count_short': 0, 'wins': 6, 'draws': 0, 'losses': 8, 'profit_mean': -0.003539515, 'profit_median': -0.012222, 'profit_total': -0.002480140000000001, 'profit_total_abs': -4.955321, 'max_drawdown': 0.34, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=3402.8571428571427)}, # noqa: E501
'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501
'total_profit': -0.002480140000000001,
'current_epoch': 5,
@@ -2757,7 +2736,7 @@ def saved_hyperopt_results():
'loss': 0.545315889154162,
'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501
'results_metrics': {'total_trades': 39, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'max_drawdown': 0.45, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501
'results_metrics': {'total_trades': 39, 'trade_count_long': 39, 'trade_count_short': 0, 'wins': 20, 'draws': 0, 'losses': 19, 'profit_mean': -0.0021400679487179478, 'profit_median': -0.012222, 'profit_total': -0.0041773, 'profit_total_abs': -8.346264999999997, 'max_drawdown': 0.45, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=636.9230769230769)}, # noqa: E501
'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501
'total_profit': -0.0041773,
'current_epoch': 6,
@@ -2770,7 +2749,7 @@ def saved_hyperopt_results():
'params_details': {
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
'results_metrics': {'total_trades': 318, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'max_drawdown': 0.50, 'max_drawdown_abs': -200.955321, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501
'results_metrics': {'total_trades': 318, 'trade_count_long': 318, 'trade_count_short': 0, 'wins': 100, 'draws': 0, 'losses': 218, 'profit_mean': -0.0039833954716981146, 'profit_median': -0.012222, 'profit_total': -0.06339929, 'profit_total_abs': -126.67197600000004, 'max_drawdown': 0.50, 'max_drawdown_abs': -200.955321, 'holding_avg': timedelta(minutes=3140.377358490566)}, # noqa: E501
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
'total_profit': -0.06339929,
'current_epoch': 7,
@@ -2781,7 +2760,7 @@ def saved_hyperopt_results():
'loss': 20.0, # noqa: E501
'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501
'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501
'results_metrics': {'total_trades': 1, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'max_drawdown': 0.0, 'max_drawdown_abs': 0.52, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501
'results_metrics': {'total_trades': 1, 'trade_count_long': 1, 'trade_count_short': 0, 'wins': 0, 'draws': 1, 'losses': 0, 'profit_mean': 0.0, 'profit_median': 0.0, 'profit_total': 0.0, 'profit_total_abs': 0.0, 'max_drawdown': 0.0, 'max_drawdown_abs': 0.52, 'holding_avg': timedelta(minutes=5340.0)}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501
'total_profit': 0.0,
'current_epoch': 8,
@@ -2792,7 +2771,7 @@ def saved_hyperopt_results():
'loss': 2.4731817780991223,
'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501
'results_metrics': {'total_trades': 229, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'max_drawdown': 0.41, 'max_drawdown_abs': -150.955321, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501
'results_metrics': {'total_trades': 229, 'trade_count_long': 229, 'trade_count_short': 0, 'wins': 150, 'draws': 0, 'losses': 79, 'profit_mean': -0.0038433433624454144, 'profit_median': -0.012222, 'profit_total': -0.044050070000000004, 'profit_total_abs': -88.01256299999999, 'max_drawdown': 0.41, 'max_drawdown_abs': -150.955321, 'holding_avg': timedelta(minutes=6505.676855895196)}, # noqa: E501
'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501
'total_profit': -0.044050070000000004, # noqa: E501
'current_epoch': 9,
@@ -2803,7 +2782,7 @@ def saved_hyperopt_results():
'loss': -0.2604606005845212, # noqa: E501
'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501
'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501
'results_metrics': {'total_trades': 4, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'max_drawdown': 0.13, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501
'results_metrics': {'total_trades': 4, 'trade_count_long': 4, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 4, 'profit_mean': 0.001080385, 'profit_median': -0.012222, 'profit_total': 0.00021629, 'profit_total_abs': 0.432154, 'max_drawdown': 0.13, 'max_drawdown_abs': -4.955321, 'holding_avg': timedelta(minutes=2850.0)}, # noqa: E501
'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501
'total_profit': 0.00021629,
'current_epoch': 10,
@@ -2815,7 +2794,7 @@ def saved_hyperopt_results():
'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501
'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501
# New Hyperopt mode!
'results_metrics': {'total_trades': 117, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'max_drawdown': 0.52, 'max_drawdown_abs': -224.955321, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501
'results_metrics': {'total_trades': 117, 'trade_count_long': 117, 'trade_count_short': 0, 'wins': 67, 'draws': 0, 'losses': 50, 'profit_mean': -0.012698609145299145, 'profit_median': -0.012222, 'profit_total': -0.07436117, 'profit_total_abs': -148.573727, 'max_drawdown': 0.52, 'max_drawdown_abs': -224.955321, 'holding_avg': timedelta(minutes=4282.5641025641025)}, # noqa: E501
'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501
'total_profit': -0.07436117,
'current_epoch': 11,
@@ -2826,7 +2805,7 @@ def saved_hyperopt_results():
'loss': 100000,
'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501
'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501
'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'max_drawdown': 0.0, 'max_drawdown_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501
'results_metrics': {'total_trades': 0, 'trade_count_long': 0, 'trade_count_short': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit_total_abs': 0.0, 'max_drawdown': 0.0, 'max_drawdown_abs': 0.0, 'holding_avg': timedelta()}, # noqa: E501
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
'total_profit': 0,
'current_epoch': 12,

View File

@@ -3,18 +3,19 @@ import logging
from pathlib import Path
from shutil import copyfile
import numpy as np
import pytest
from freqtrade.configuration.timerange import TimeRange
from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format,
ohlcv_fill_up_missing_data, ohlcv_to_dataframe,
trades_dict_to_list, trades_remove_duplicates,
trades_to_ohlcv, trim_dataframe)
reduce_dataframe_footprint, trades_dict_to_list,
trades_remove_duplicates, trades_to_ohlcv, trim_dataframe)
from freqtrade.data.history import (get_timerange, load_data, load_pair_history,
validate_backtest_data)
from freqtrade.data.history.idatahandler import IDataHandler
from freqtrade.enums import CandleType
from tests.conftest import log_has, log_has_re
from tests.conftest import generate_test_data, log_has, log_has_re
from tests.data.test_history import _clean_test_file
@@ -344,3 +345,33 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand
assert file.exists()
for file in (files_new):
assert not file.exists()
def test_reduce_dataframe_footprint():
data = generate_test_data('15m', 40)
data['open_copy'] = data['open']
data['close_copy'] = data['close']
data['close_copy'] = data['close']
assert data['open'].dtype == np.float64
assert data['open_copy'].dtype == np.float64
assert data['close_copy'].dtype == np.float64
df2 = reduce_dataframe_footprint(data)
# Does not modify original dataframe
assert data['open'].dtype == np.float64
assert data['open_copy'].dtype == np.float64
assert data['close_copy'].dtype == np.float64
# skips ohlcv columns
assert df2['open'].dtype == np.float64
assert df2['high'].dtype == np.float64
assert df2['low'].dtype == np.float64
assert df2['close'].dtype == np.float64
assert df2['volume'].dtype == np.float64
# Changes dtype of returned dataframe
assert df2['open_copy'].dtype == np.float32
assert df2['close_copy'].dtype == np.float32

View File

@@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103
import re
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import MagicMock
@@ -69,7 +70,7 @@ def test_datahandler_ohlcv_regex(filename, pair, timeframe, candletype):
('BTC_USDT_USDT', 'BTC/USDT:USDT'), # Futures
('XRP_USDT_USDT', 'XRP/USDT:USDT'), # futures
('BTC-PERP', 'BTC-PERP'),
('BTC-PERP_USDT', 'BTC-PERP:USDT'), # potential FTX case
('BTC-PERP_USDT', 'BTC-PERP:USDT'),
('UNITTEST_USDT', 'UNITTEST/USDT'),
])
def test_rebuild_pair_from_filename(input, expected):
@@ -154,6 +155,23 @@ def test_jsondatahandler_ohlcv_load(testdatadir, caplog):
assert df.columns.equals(df1.columns)
def test_datahandler_ohlcv_data_min_max(testdatadir):
dh = JsonDataHandler(testdatadir)
min_max = dh.ohlcv_data_min_max('UNITTEST/BTC', '5m', 'spot')
assert len(min_max) == 2
# Empty pair
min_max = dh.ohlcv_data_min_max('UNITTEST/BTC', '8m', 'spot')
assert len(min_max) == 2
assert min_max[0] == datetime.fromtimestamp(0, tz=timezone.utc)
assert min_max[0] == min_max[1]
# Empty pair2
min_max = dh.ohlcv_data_min_max('NOPAIR/XXX', '4m', 'spot')
assert len(min_max) == 2
assert min_max[0] == datetime.fromtimestamp(0, tz=timezone.utc)
assert min_max[0] == min_max[1]
def test_datahandler__check_empty_df(testdatadir, caplog):
dh = JsonDataHandler(testdatadir)
expected_text = r"Price jump in UNITTEST/USDT, 1h, spot between"

View File

@@ -162,9 +162,6 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
}
assert exchange.stoploss_adjust(sl1, order, side=side)
assert not exchange.stoploss_adjust(sl2, order, side=side)
# Test with invalid order case
order['type'] = 'stop_loss'
assert not exchange.stoploss_adjust(sl3, order, side=side)
def test_fill_leverage_tiers_binance(default_conf, mocker):

View File

@@ -45,16 +45,6 @@ EXCHANGES = {
'leverage_tiers_public': False,
'leverage_in_spot_market': True,
},
'ftx': {
'pair': 'BTC/USD',
'stake_currency': 'USD',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures_pair': 'BTC/USD:USD',
'futures': False,
'leverage_tiers_public': False, # TODO: Set to True once implemented on CCXT
'leverage_in_spot_market': True,
},
'kucoin': {
'pair': 'XRP/USDT',
'stake_currency': 'USDT',

View File

@@ -27,7 +27,7 @@ from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_e
# Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio']
EXCHANGES = ['bittrex', 'binance', 'kraken', 'gateio']
get_entry_rate_data = [
('other', 20, 19, 10, 0.0, 20), # Full ask side
@@ -1207,12 +1207,17 @@ def test_create_dry_run_order_fees(
assert order1['fee']['rate'] == fee
@pytest.mark.parametrize("side,startprice,endprice", [
("buy", 25.563, 25.566),
("sell", 25.566, 25.563)
@pytest.mark.parametrize("side,price,filled", [
# order_book_l2_usd spread:
# best ask: 25.566
# best bid: 25.563
("buy", 25.563, False),
("buy", 25.566, True),
("sell", 25.566, False),
("sell", 25.563, True),
])
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, endprice,
def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled,
exchange_name, order_book_l2_usd):
default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
@@ -1226,7 +1231,7 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
ordertype='limit',
side=side,
amount=1,
rate=startprice,
rate=price,
leverage=1.0
)
assert order_book_l2_usd.call_count == 1
@@ -1235,22 +1240,17 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
assert order["side"] == side
assert order["type"] == "limit"
assert order["symbol"] == "LTC/USDT"
assert order["average"] == price
assert order['status'] == 'open' if not filled else 'closed'
order_book_l2_usd.reset_mock()
# fetch order again...
order_closed = exchange.fetch_dry_run_order(order['id'])
assert order_book_l2_usd.call_count == 1
assert order_closed['status'] == 'open'
assert not order['fee']
assert order_closed['filled'] == 0
assert order_book_l2_usd.call_count == (1 if not filled else 0)
assert order_closed['status'] == ('open' if not filled else 'closed')
assert order_closed['filled'] == (0 if not filled else 1)
order_book_l2_usd.reset_mock()
order_closed['price'] = endprice
order_closed = exchange.fetch_dry_run_order(order['id'])
assert order_closed['status'] == 'closed'
assert order['fee']
assert order_closed['filled'] == 1
assert order_closed['filled'] == order_closed['amount']
# Empty orderbook test
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
@@ -3162,19 +3162,16 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123})
mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', return_value={'for': 123})
mocker.patch('freqtrade.exchange.Gateio.fetch_stoploss_order', return_value={'for': 123})
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
res = {'fee': {}, 'status': 'canceled', 'amount': 1234}
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value=res)
mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', return_value=res)
mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', return_value=res)
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co == res
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value='canceled')
mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', return_value='canceled')
mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', return_value='canceled')
# Fall back to fetch_stoploss_order
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
@@ -3182,7 +3179,6 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
exc = InvalidOrderException("")
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.Gateio.fetch_stoploss_order', side_effect=exc)
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co['amount'] == 555
@@ -3191,7 +3187,6 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
with pytest.raises(InvalidOrderException):
exc = InvalidOrderException("Did not find order")
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', side_effect=exc)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
@@ -3253,9 +3248,6 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
# Don't test FTX here - that needs a separate test
if exchange_name == 'ftx':
return
default_conf['dry_run'] = True
order = MagicMock()
order.myid = 123
@@ -3699,16 +3691,6 @@ def test_date_minus_candles():
# no darkpools
("BTC/EUR.d", 'BTC', 'EUR', "kraken", True, False, False, 'spot',
{"darkpool": True}, False),
("BTC/USD", 'BTC', 'USD', "ftx", True, False, False, 'spot', {}, True),
("USD/BTC", 'USD', 'BTC', "ftx", True, False, False, 'spot', {}, True),
# Can only trade spot markets
("BTC/USD", 'BTC', 'USD', "ftx", False, False, True, 'spot', {}, False),
("BTC/USD", 'BTC', 'USD', "ftx", False, False, True, 'futures', {}, True),
# Can only trade spot markets
("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'spot', {}, False),
("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'margin', {}, False),
("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'futures', {}, True),
("BTC/USDT:USDT", 'BTC', 'USD', "okx", False, False, True, 'spot', {}, False),
("BTC/USDT:USDT", 'BTC', 'USD', "okx", False, False, True, 'margin', {}, False),
("BTC/USDT:USDT", 'BTC', 'USD', "okx", False, False, True, 'futures', {}, True),
@@ -3841,7 +3823,7 @@ def test_calculate_backoff(retrycount, max_retries, expected):
assert calculate_backoff(retrycount, max_retries) == expected
@pytest.mark.parametrize("exchange_name", ['binance', 'ftx'])
@pytest.mark.parametrize("exchange_name", ['binance'])
def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
api_mock = MagicMock()
api_mock.fetch_funding_history = MagicMock(return_value=[
@@ -3909,7 +3891,7 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
)
@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx'])
@pytest.mark.parametrize('exchange', ['binance', 'kraken'])
@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [
(9.0, 3.0, 3.0),
(20.0, 5.0, 4.0),
@@ -3930,8 +3912,6 @@ def test_get_stake_amount_considering_leverage(
@pytest.mark.parametrize("exchange_name,trading_mode", [
("binance", TradingMode.FUTURES),
("ftx", TradingMode.MARGIN),
("ftx", TradingMode.FUTURES)
])
def test__set_leverage(mocker, default_conf, exchange_name, trading_mode):
@@ -3982,9 +3962,6 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
("kraken", TradingMode.SPOT, None, False),
("kraken", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("kraken", TradingMode.FUTURES, MarginMode.ISOLATED, True),
("ftx", TradingMode.SPOT, None, False),
("ftx", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("ftx", TradingMode.FUTURES, MarginMode.ISOLATED, True),
("bittrex", TradingMode.SPOT, None, False),
("bittrex", TradingMode.MARGIN, MarginMode.CROSS, True),
("bittrex", TradingMode.MARGIN, MarginMode.ISOLATED, True),
@@ -4005,8 +3982,6 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
("binance", TradingMode.FUTURES, MarginMode.CROSS, True),
("kraken", TradingMode.MARGIN, MarginMode.CROSS, True),
("kraken", TradingMode.FUTURES, MarginMode.CROSS, True),
("ftx", TradingMode.MARGIN, MarginMode.CROSS, True),
("ftx", TradingMode.FUTURES, MarginMode.CROSS, True),
("gateio", TradingMode.MARGIN, MarginMode.CROSS, True),
("gateio", TradingMode.FUTURES, MarginMode.CROSS, True),
@@ -4015,8 +3990,6 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
# ("binance", TradingMode.FUTURES, MarginMode.CROSS, False),
# ("kraken", TradingMode.MARGIN, MarginMode.CROSS, False),
# ("kraken", TradingMode.FUTURES, MarginMode.CROSS, False),
# ("ftx", TradingMode.MARGIN, MarginMode.CROSS, False),
# ("ftx", TradingMode.FUTURES, MarginMode.CROSS, False),
# ("gateio", TradingMode.MARGIN, MarginMode.CROSS, False),
# ("gateio", TradingMode.FUTURES, MarginMode.CROSS, False),
])
@@ -4046,7 +4019,6 @@ def test_validate_trading_mode_and_margin_mode(
("bibox", "futures", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "swap"}}),
("bybit", "spot", {"options": {"defaultType": "spot"}}),
("bybit", "futures", {"options": {"defaultType": "linear"}}),
("ftx", "futures", {"options": {"defaultType": "swap"}}),
("gateio", "futures", {"options": {"defaultType": "swap"}}),
("hitbtc", "futures", {"options": {"defaultType": "swap"}}),
("kraken", "futures", {"options": {"defaultType": "swap"}}),
@@ -4223,11 +4195,6 @@ def test_combine_funding_and_mark(
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0012443999999999999),
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759),
# ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289),
('ftx', 0, 2, "2021-09-01 00:10:00", "2021-09-01 00:30:00", 30.0, 0.0),
('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008),
('ftx', 0, 13, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691),
('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.001668),
('ftx', 1, 9, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0019932),
('gateio', 0, 2, "2021-09-01 00:10:00", "2021-09-01 04:00:00", 30.0, 0.0),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999),
@@ -4289,7 +4256,6 @@ def test__fetch_and_calculate_funding_fees(
d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z')
funding_rate_history = {
'binance': funding_rate_history_octohourly,
'ftx': funding_rate_history_hourly,
'gateio': funding_rate_history_octohourly,
}[exchange][rate_start:rate_end]
api_mock = MagicMock()
@@ -5056,7 +5022,7 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
exchange.get_max_leverage("BTC/USDT", 1000000000.01)
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx'])
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gateio', 'okx'])
def test__get_params(mocker, default_conf, exchange_name):
api_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)

View File

@@ -1,272 +0,0 @@
from random import randint
from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade.exceptions import DependencyException, InvalidOrderException
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT
from tests.conftest import get_patched_exchange
from .test_exchange import ccxt_exceptionhandlers
STOPLOSS_ORDERTYPE = 'stop'
@pytest.mark.parametrize('order_price,exchangelimitratio,side', [
(217.8, 1.05, "sell"),
(222.2, 0.95, "buy"),
])
def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitratio, side):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=190,
side=side,
order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio},
leverage=1.0
)
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
assert api_mock.create_order.call_args_list[0][1]['side'] == side
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params']
assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 190
assert api_mock.create_order.call_count == 1
api_mock.create_order.reset_mock()
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
assert api_mock.create_order.call_args_list[0][1]['side'] == side
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
api_mock.create_order.reset_mock()
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={'stoploss': 'limit'}, side=side,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
assert api_mock.create_order.call_args_list[0][1]['side'] == side
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params']
assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == order_price
assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
@pytest.mark.parametrize('side', [("sell"), ("buy")])
def test_stoploss_order_dry_run_ftx(default_conf, mocker, side):
api_mock = MagicMock()
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
api_mock.create_order.reset_mock()
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == STOPLOSS_ORDERTYPE
assert order['price'] == 220
assert order['amount'] == 1
@pytest.mark.parametrize('sl1,sl2,sl3,side', [
(1501, 1499, 1501, "sell"),
(1499, 1501, 1499, "buy")
])
def test_stoploss_adjust_ftx(mocker, default_conf, sl1, sl2, sl3, side):
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
order = {
'type': STOPLOSS_ORDERTYPE,
'price': 1500,
}
assert exchange.stoploss_adjust(sl1, order, side=side)
assert not exchange.stoploss_adjust(sl2, order, side=side)
# Test with invalid order case ...
order['type'] = 'stop_loss_limit'
assert not exchange.stoploss_adjust(sl3, order, side=side)
@pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order, limit_buy_order):
default_conf['dry_run'] = True
order = MagicMock()
order.myid = 123
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
exchange._dry_run_open_orders['X'] = order
assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.fetch_stoploss_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}])
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
assert exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] == '456'
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}])
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
exchange.fetch_stoploss_order('X', 'TKN/BTC')['status']
# stoploss Limit order
api_mock.fetch_orders = MagicMock(return_value=[
{'id': 'X', 'status': 'closed',
'info': {
'orderId': 'mocked_limit_sell',
}}])
api_mock.fetch_order = MagicMock(return_value=limit_sell_order.copy())
# No orderId field - no call to fetch_order
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
assert resp
assert api_mock.fetch_order.call_count == 1
assert resp['id_stop'] == 'mocked_limit_sell'
assert resp['id'] == 'X'
assert resp['type'] == 'stop'
assert resp['status_stop'] == 'triggered'
# Stoploss market order
# Contains no new Order, but "average" instead
order = {'id': 'X', 'status': 'closed', 'info': {'orderId': None}, 'average': 0.254}
api_mock.fetch_orders = MagicMock(return_value=[order])
api_mock.fetch_order.reset_mock()
api_mock.privateGetConditionalOrdersConditionalOrderIdTriggers = MagicMock(
return_value={'result': [
{'orderId': 'mocked_market_sell', 'type': 'market', 'side': 'sell', 'price': 0.254}
]})
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
assert resp
# fetch_order not called (no regular order ID)
assert api_mock.fetch_order.call_count == 1
api_mock.privateGetConditionalOrdersConditionalOrderIdTriggers.call_count == 1
expected_resp = limit_sell_order.copy()
expected_resp.update({
'id_stop': 'X',
'id': 'X',
'type': 'stop',
'status_stop': 'triggered',
})
assert expected_resp == resp
with pytest.raises(InvalidOrderException):
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_orders.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
'fetch_stoploss_order', 'fetch_orders',
retries=API_FETCH_ORDER_RETRY_COUNT + 1,
order_id='_', pair='TKN/BTC')
def test_get_order_id(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
order = {
'type': STOPLOSS_ORDERTYPE,
'price': 1500,
'id': '1111',
'id_stop': '1234',
'info': {
}
}
assert exchange.get_order_id_conditional(order) == '1234'
order = {
'type': 'limit',
'price': 1500,
'id': '1111',
'id_stop': '1234',
'info': {
}
}
assert exchange.get_order_id_conditional(order) == '1111'

View File

@@ -113,5 +113,4 @@ def test_stoploss_adjust_huobi(mocker, default_conf):
assert exchange.stoploss_adjust(1501, order, 'sell')
assert not exchange.stoploss_adjust(1499, order, 'sell')
# Test with invalid order case
order['type'] = 'stop_loss'
assert not exchange.stoploss_adjust(1501, order, 'sell')
assert exchange.stoploss_adjust(1501, order, 'sell')

View File

@@ -3,8 +3,11 @@ from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import PropertyMock
import pytest
from freqtrade.commands.optimize_commands import setup_optimize_configuration
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.backtesting import Backtesting
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has_re, patch_exchange,
patched_configuration_load_config_file)
@@ -51,3 +54,32 @@ def test_freqai_backtest_load_data(freqai_conf, mocker, caplog):
assert log_has_re('Increasing startup_candle_count for freqai to.*', caplog)
Backtesting.cleanup()
def test_freqai_backtest_live_models_model_not_found(freqai_conf, mocker, testdatadir, caplog):
patch_exchange(mocker)
now = datetime.now(timezone.utc)
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT']))
mocker.patch('freqtrade.optimize.backtesting.history.load_data')
mocker.patch('freqtrade.optimize.backtesting.history.get_timerange', return_value=(now, now))
freqai_conf["timerange"] = ""
patched_configuration_load_config_file(mocker, freqai_conf)
args = [
'backtesting',
'--config', 'config.json',
'--datadir', str(testdatadir),
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
'--timeframe', '5m',
'--freqai-backtest-live-models'
]
args = get_args(args)
bt_config = setup_optimize_configuration(args, RunMode.BACKTEST)
with pytest.raises(OperationalException,
match=r".* Saved models are required to run backtest .*"):
Backtesting(bt_config)
Backtesting.cleanup()

View File

@@ -22,6 +22,7 @@ def test_update_historic_data(mocker, freqai_conf):
historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"])
dp_candles = len(strategy.dp.get_pair_dataframe("ADA/BTC", "5m"))
candle_difference = dp_candles - historic_candles
freqai.dk.pair = "ADA/BTC"
freqai.dd.update_historic_data(strategy, freqai.dk)
updated_historic_candles = len(freqai.dd.historic_data["ADA/BTC"]["5m"])

View File

@@ -1,13 +1,18 @@
import shutil
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from freqtrade.configuration import TimeRange
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
from tests.conftest import log_has_re
from tests.freqai.conftest import (get_patched_data_kitchen, make_data_dictionary,
make_unfiltered_dataframe)
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
from freqtrade.freqai.utils import get_timerange_backtest_live_models
from tests.conftest import get_patched_exchange, log_has_re
from tests.freqai.conftest import (get_patched_data_kitchen, get_patched_freqai_strategy,
make_data_dictionary, make_unfiltered_dataframe)
@pytest.mark.parametrize(
@@ -159,3 +164,98 @@ def test_make_train_test_datasets(mocker, freqai_conf):
assert data_dictionary
assert len(data_dictionary) == 7
assert len(data_dictionary['train_features'].index) == 1916
def test_get_pairs_timestamp_validation(mocker, freqai_conf):
exchange = get_patched_exchange(mocker, freqai_conf)
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
strategy.freqai_info = freqai_conf.get("freqai", {})
freqai = strategy.freqai
freqai.live = True
freqai.dk = FreqaiDataKitchen(freqai_conf)
freqai_conf['freqai'].update({"identifier": "invalid_id"})
model_path = freqai.dk.get_full_models_path(freqai_conf)
with pytest.raises(
OperationalException,
match=r'.*required to run backtest with the freqai-backtest-live-models.*'
):
freqai.dk.get_assets_timestamps_training_from_ready_models(model_path)
@pytest.mark.parametrize('model', [
'LightGBMRegressor'
])
def test_get_timerange_from_ready_models(mocker, freqai_conf, model):
freqai_conf.update({"freqaimodel": model})
freqai_conf.update({"timerange": "20180110-20180130"})
freqai_conf.update({"strategy": "freqai_test_strat"})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
strategy.freqai_info = freqai_conf.get("freqai", {})
freqai = strategy.freqai
freqai.live = True
freqai.dk = FreqaiDataKitchen(freqai_conf)
timerange = TimeRange.parse_timerange("20180101-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
freqai.dd.pair_dict = MagicMock()
data_load_timerange = TimeRange.parse_timerange("20180101-20180130")
# 1516233600 (2018-01-18 00:00) - Start Training 1
# 1516406400 (2018-01-20 00:00) - End Training 1 (Backtest slice 1)
# 1516579200 (2018-01-22 00:00) - End Training 2 (Backtest slice 2)
# 1516838400 (2018-01-25 00:00) - End Timerange
new_timerange = TimeRange("date", "date", 1516233600, 1516406400)
freqai.extract_data_and_train_model(
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
new_timerange = TimeRange("date", "date", 1516406400, 1516579200)
freqai.extract_data_and_train_model(
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
model_path = freqai.dk.get_full_models_path(freqai_conf)
(backtesting_timerange,
pairs_end_dates) = freqai.dk.get_timerange_and_assets_end_dates_from_ready_models(
models_path=model_path)
assert len(pairs_end_dates["ADA"]) == 2
assert backtesting_timerange.startts == 1516406400
assert backtesting_timerange.stopts == 1516838400
backtesting_string_timerange = get_timerange_backtest_live_models(freqai_conf)
assert backtesting_string_timerange == '20180120-20180125'
@pytest.mark.parametrize('model', [
'LightGBMRegressor'
])
def test_get_full_model_path(mocker, freqai_conf, model):
freqai_conf.update({"freqaimodel": model})
freqai_conf.update({"timerange": "20180110-20180130"})
freqai_conf.update({"strategy": "freqai_test_strat"})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
strategy.freqai_info = freqai_conf.get("freqai", {})
freqai = strategy.freqai
freqai.live = True
freqai.dk = FreqaiDataKitchen(freqai_conf)
timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
freqai.dd.pair_dict = MagicMock()
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
new_timerange = TimeRange.parse_timerange("20180120-20180130")
freqai.extract_data_and_train_model(
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
model_path = freqai.dk.get_full_models_path(freqai_conf)
assert model_path.is_dir() is True

View File

@@ -27,13 +27,13 @@ def is_mac() -> bool:
return "Darwin" in machine
@pytest.mark.parametrize('model', [
'LightGBMRegressor',
'XGBoostRegressor',
'XGBoostRFRegressor',
'CatboostRegressor',
@pytest.mark.parametrize('model, pca, dbscan, float32', [
('LightGBMRegressor', True, False, True),
('XGBoostRegressor', False, True, False),
('XGBoostRFRegressor', False, False, False),
('CatboostRegressor', False, False, False),
])
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model):
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32):
if is_arm() and model == 'CatboostRegressor':
pytest.skip("CatBoost is not supported on ARM")
@@ -41,6 +41,9 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model):
freqai_conf.update({"freqaimodel": model})
freqai_conf.update({"timerange": "20180110-20180130"})
freqai_conf.update({"strategy": "freqai_test_strat"})
freqai_conf['freqai']['feature_parameters'].update({"principal_component_analysis": pca})
freqai_conf['freqai']['feature_parameters'].update({"use_DBSCAN_to_remove_outliers": dbscan})
freqai_conf.update({"reduce_df_footprint": float32})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
@@ -75,17 +78,19 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model):
shutil.rmtree(Path(freqai.dk.full_path))
@pytest.mark.parametrize('model', [
'LightGBMRegressorMultiTarget',
'XGBoostRegressorMultiTarget',
'CatboostRegressorMultiTarget',
@pytest.mark.parametrize('model, strat', [
('LightGBMRegressorMultiTarget', "freqai_test_multimodel_strat"),
('XGBoostRegressorMultiTarget', "freqai_test_multimodel_strat"),
('CatboostRegressorMultiTarget', "freqai_test_multimodel_strat"),
('LightGBMClassifierMultiTarget', "freqai_test_multimodel_classifier_strat"),
('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat")
])
def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model):
if is_arm() and model == 'CatboostRegressorMultiTarget':
def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat):
if is_arm() and 'Catboost' in model:
pytest.skip("CatBoost is not supported on ARM")
freqai_conf.update({"timerange": "20180110-20180130"})
freqai_conf.update({"strategy": "freqai_test_multimodel_strat"})
freqai_conf.update({"strategy": strat})
freqai_conf.update({"freqaimodel": model})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
@@ -192,6 +197,7 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
df = freqai.cache_corr_pairlist_dfs(df, freqai.dk)
for i in range(5):
df[f'%-constant_{i}'] = i
# df.loc[:, f'%-constant_{i}'] = i
@@ -234,6 +240,7 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
metadata = {"pair": "LTC/BTC"}
freqai.start_backtesting(df, metadata, freqai.dk)
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
assert len(model_folders) == 9
shutil.rmtree(Path(freqai.dk.full_path))
@@ -336,6 +343,7 @@ def test_follow_mode(mocker, freqai_conf):
df = strategy.dp.get_pair_dataframe('ADA/BTC', '5m')
freqai.dk.pair = "ADA/BTC"
freqai.start_live(df, metadata, strategy, freqai.dk)
assert len(freqai.dk.return_dataframe.index) == 5702

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