Compare commits
604 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbf53ce952 | |||
| f6b6b5976d | |||
| 0c15eb4ace | |||
| 3388bc5012 | |||
| 981cf1f6ee | |||
| 2d66f3c022 | |||
| 04122abd17 | |||
| e5a88fdeda | |||
| 9c4aca2b90 | |||
| 67e3ce308b | |||
| a740b9458f | |||
| ffdb5fb790 | |||
| 0cc7039232 | |||
| 0f73e38f98 | |||
| cfe1187cd9 | |||
| a36e131838 | |||
| 94864a6ab3 | |||
| e1e90112ba | |||
| c303d47f26 | |||
| 36d99876c5 | |||
| 2a192b19c3 | |||
| 9eaff301e8 | |||
| 62ce96fd40 | |||
| da7599065f | |||
| 0c7cb29ea1 | |||
| 001c998b26 | |||
| 5934a6e6c3 | |||
| 8ac4f4ac44 | |||
| 03c7e9a022 | |||
| 329b35fc58 | |||
| 6ede7b49e0 | |||
| 5e41f9b42a | |||
| f2b00a54d0 | |||
| 5b25acef52 | |||
| e3d17b98be | |||
| 9da9d48625 | |||
| 1c55432bd1 | |||
| a6a1b0e847 | |||
| 57c23fd9b3 | |||
| d18dea076b | |||
| 4b79481163 | |||
| 9d31566a2d | |||
| 3e44bd073c | |||
| d2a378b706 | |||
| ec2f51dcc9 | |||
| 6d1c1b3e63 | |||
| 4e52380e09 | |||
| f0fc12dfc2 | |||
| 7f4007ecdb | |||
| 5a0f62431f | |||
| 99f7e3bce1 | |||
| e93832117d | |||
| 6d90ceca50 | |||
| 22f1c72517 | |||
| 0f1e66b22a | |||
| b088154c0a | |||
| 4c5f992670 | |||
| f92b8c50dd | |||
| 2d800a1422 | |||
| fb790b2d42 | |||
| b71a44f27c | |||
| 4fefae6f07 | |||
| 3167938d43 | |||
| cf226afb1a | |||
| 7da0174937 | |||
| 4feafb25cf | |||
| 08532e1df1 | |||
| c0f5d31c8e | |||
| 8fa31b35f5 | |||
| f5e0543113 | |||
| 399e308e07 | |||
| aa1dcd1b44 | |||
| 5a9dd79b45 | |||
| 277cc0a523 | |||
| 185c5a779d | |||
| 352f0fdfca | |||
| ee11dae82a | |||
| 3d8dcd1644 | |||
| 30064b4102 | |||
| 8caadc4c5b | |||
| 3eb3596552 | |||
| d4bc736eb1 | |||
| 5de3b9d7ae | |||
| fae742de59 | |||
| fa5f7d290b | |||
| 7577613882 | |||
| 0fab65df03 | |||
| f69a776305 | |||
| 26c89d89e4 | |||
| d97d0e4426 | |||
| 357b04202c | |||
| 6fc1ee9831 | |||
| 780f238904 | |||
| 5d5cc71945 | |||
| 0be2250cf5 | |||
| 53db254cba | |||
| 02ee7f8b5b | |||
| a595074754 | |||
| 7ac9d33c31 | |||
| ac80a69142 | |||
| 46882406be | |||
| ba34318f7a | |||
| 3bc49330ce | |||
| 7bc317fea7 | |||
| b93c6235c1 | |||
| aa756221f6 | |||
| 688018d9f2 | |||
| ea181645b0 | |||
| f607b8abfb | |||
| d439936014 | |||
| 91fd472717 | |||
| 6134807c67 | |||
| 7651f1b1db | |||
| d9fb40ca3e | |||
| 77c7dd8a12 | |||
| 452e1ab016 | |||
| bea6782223 | |||
| 045d8c6fca | |||
| 161ab14ed0 | |||
| 6f347b839a | |||
| 09ec00888f | |||
| 5d3f3fb39f | |||
| 3f5903bad8 | |||
| afcaeafd96 | |||
| 6b11f3063f | |||
| a4842113ce | |||
| d768afed37 | |||
| db9247e78e | |||
| d53b6871ea | |||
| 08bc615826 | |||
| bcc2dd9803 | |||
| d7e9f87b33 | |||
| d7556cd66a | |||
| 018d3a3f6e | |||
| 2f5689b32b | |||
| f4844c1224 | |||
| e6218e8580 | |||
| af64971b86 | |||
| 21cf5fc679 | |||
| 9b6654e81a | |||
| 80c9b1ac7a | |||
| b60fdac9f4 | |||
| bb52778126 | |||
| 92dc6dd8ef | |||
| 7224bdf823 | |||
| 0abfad5586 | |||
| 9a95011b57 | |||
| 2a3157c745 | |||
| 63bc89cff8 | |||
| aed33e759b | |||
| e0915e1638 | |||
| 0722844a6d | |||
| 987a7ddac0 | |||
| ceee57c39c | |||
| 6e79c19fe0 | |||
| f0a50b1f2d | |||
| 4942d01574 | |||
| 4664cc23eb | |||
| 6768641bac | |||
| 90e41274e0 | |||
| 2c5a7ceab5 | |||
| 3ecaedb7d8 | |||
| 1ca3cd086f | |||
| 72bd4e816d | |||
| 716b1cd002 | |||
| 6ce08548fb | |||
| 47adebd872 | |||
| ea257e3cbb | |||
| 20763daa74 | |||
| 7e9389421a | |||
| 4b8569b80e | |||
| 05e1828617 | |||
| 32c3d96760 | |||
| 328a6f791e | |||
| b934644039 | |||
| 4a62ebbf93 | |||
| 2069abe314 | |||
| 78cf8a1c09 | |||
| 565e2699b4 | |||
| 62ad2cca1a | |||
| 46bafa9d5d | |||
| 74cbfcaef4 | |||
| ab156b6ad7 | |||
| 1f23727ff7 | |||
| c88f71c638 | |||
| 05bbc8e7aa | |||
| 5e3e443d27 | |||
| 88d6f70abe | |||
| 9c73e52dd1 | |||
| 33d3c4f7d5 | |||
| 03150ee09a | |||
| 4b07720d0b | |||
| 096df99ba3 | |||
| f16832b07c | |||
| 98310de996 | |||
| d37b2fbada | |||
| ae3c3d81c1 | |||
| 8deedc31c5 | |||
| 8c3a0cf618 | |||
| 67ea6512ef | |||
| 813ace12c9 | |||
| cd6fc1652e | |||
| 7d18261f58 | |||
| 72d9e8a094 | |||
| e174f9640d | |||
| 25602ceac3 | |||
| 065899b426 | |||
| bdf89efd11 | |||
| 23d2bad2a0 | |||
| 9f69a45afd | |||
| a3c6904fbc | |||
| d17bf6350d | |||
| 777d25192c | |||
| 836d7b885a | |||
| 8ebfb731d8 | |||
| 3e77bc3ba2 | |||
| 365766c957 | |||
| 81cd241954 | |||
| 6d6111864e | |||
| 23a2b95994 | |||
| 36b84241b1 | |||
| 51879ffd2c | |||
| 0d71a74d8a | |||
| 78972604d0 | |||
| 2f95c44777 | |||
| 3d3dcc68e0 | |||
| fede847fd9 | |||
| 53c0d30f36 | |||
| fe6deef1bd | |||
| 0e63335d2e | |||
| 494d58e79c | |||
| 3b416223e3 | |||
| d78eb834c4 | |||
| 47850ce1b0 | |||
| bb18e1b45b | |||
| 4b2a6a84f4 | |||
| 505584dc48 | |||
| e36c545258 | |||
| cfdd01d295 | |||
| 53c76160a7 | |||
| 9b924f1f85 | |||
| 261a593ba5 | |||
| 02ec945a96 | |||
| 2b7deb147d | |||
| f2b240b16d | |||
| ac85c3527b | |||
| adbb131dc6 | |||
| 7a6f10f898 | |||
| 482cc615cc | |||
| d037d5d880 | |||
| ae2df4fed3 | |||
| aeb7a8f90e | |||
| b91d7debea | |||
| 1778a6add6 | |||
| 2a0a270f18 | |||
| dc93f122f0 | |||
| e1fd3a6dc3 | |||
| 66adeb0a5c | |||
| 1be1efbc4c | |||
| acd005b726 | |||
| c4d41659f2 | |||
| 3612956b7b | |||
| 78670602dd | |||
| 33eecfa9ef | |||
| 30f6f470d3 | |||
| c836bd8fa5 | |||
| 5c68b0d38e | |||
| f546ee6569 | |||
| 730ae781a9 | |||
| ec10d0706a | |||
| 9d58384eae | |||
| 4fd425260d | |||
| dcdd7d57a6 | |||
| f1fb118d76 | |||
| 61da1877ae | |||
| 78b0609840 | |||
| 6b163af99a | |||
| 8030e65384 | |||
| c20e3a3075 | |||
| d6999fda6f | |||
| 9c39499c5d | |||
| ce574af7c8 | |||
| 74edc202cf | |||
| ddd92a90b7 | |||
| 3ee228428d | |||
| a60fd9c011 | |||
| 370ea273cb | |||
| bc86f12611 | |||
| e5d5c4c50a | |||
| a147f7a041 | |||
| b666adced4 | |||
| 1926e642cd | |||
| 1a1103c239 | |||
| 3148cd39c2 | |||
| 22006ebeea | |||
| d104fb13a0 | |||
| 71737d8792 | |||
| 6659d26131 | |||
| 047de2e0ff | |||
| 15dd8bec1c | |||
| ead9e6f116 | |||
| b17918d8cf | |||
| 79910870a3 | |||
| e7e7a17183 | |||
| 3fa31bfe20 | |||
| b2abdab7cd | |||
| fd7dfc95e3 | |||
| 797617abaa | |||
| 9a400d0e6f | |||
| 8f18a52cdf | |||
| d638a4a0ff | |||
| bbf472e69b | |||
| 83f45d0e65 | |||
| d6122585f7 | |||
| 2fcff78756 | |||
| af1d2ee2a2 | |||
| f56f5179d2 | |||
| c2b40da762 | |||
| 05e4b63091 | |||
| 8b2abf4422 | |||
| 997b80fd7b | |||
| 5a7e822342 | |||
| 1d39cc18bf | |||
| e39af17207 | |||
| 4ce95dd1c3 | |||
| 47fca02ba0 | |||
| ed2485dd57 | |||
| 6dc15fad01 | |||
| 27566a4c8a | |||
| 3860a5a8c5 | |||
| 235721d9d9 | |||
| 5a057e4b94 | |||
| b90e7d75c6 | |||
| 1c26a6600b | |||
| 161446393d | |||
| 9df302f2f0 | |||
| e95fa83ea2 | |||
| b151a2c3e1 | |||
| ce7f7d828d | |||
| 7a69bdff6a | |||
| 1abb71d5a3 | |||
| 2e57ad695f | |||
| bb36c9500f | |||
| 235067c79f | |||
| 11e604d613 | |||
| f7505536ff | |||
| b096c857ae | |||
| b41b966443 | |||
| e01e712b8c | |||
| 327b055468 | |||
| 221f242a4c | |||
| a9d310ca00 | |||
| f26b49ee06 | |||
| 380244f8b1 | |||
| c01c9ee411 | |||
| 0f046ceaf2 | |||
| a7bd6725f5 | |||
| c61b72e5cc | |||
| e2a19402ec | |||
| d909fde4bb | |||
| 2bb4af911c | |||
| 00a392b262 | |||
| 6f5ba27251 | |||
| 5b8800ee18 | |||
| 5bb74e448e | |||
| e4b488cb84 | |||
| 70fa175f57 | |||
| 6ddbc8c00d | |||
| ad428aa9b0 | |||
| a00fcd68f8 | |||
| fb7fb7f59c | |||
| d7916366bd | |||
| ad82ad4407 | |||
| 8dfe43f370 | |||
| 4549fb349c | |||
| 889a732e06 | |||
| 1ab357dc32 | |||
| a33be8a349 | |||
| a5f5293bc8 | |||
| 52ec2324dd | |||
| 8f04225282 | |||
| 4c23771d39 | |||
| 0eddc6b7ad | |||
| 787e94924d | |||
| 955a63725a | |||
| 27a36bfb40 | |||
| e5f01ab2e8 | |||
| 40d7d05e4e | |||
| 14d2e3e88e | |||
| c6ee8fcf54 | |||
| 3552fa431b | |||
| 3dd33cde00 | |||
| ee3b69ea63 | |||
| b0639ab319 | |||
| cfd8b068e7 | |||
| 8621dc96e7 | |||
| 11f24aff97 | |||
| dcc3ef1309 | |||
| 4812bcc28b | |||
| c048e7229a | |||
| 4ea3f41d48 | |||
| 6874123974 | |||
| 8d332fb99e | |||
| f4933a9cff | |||
| 4369e3cdeb | |||
| 9bfe96d4d6 | |||
| 91bf8abf38 | |||
| 9c1fea0e7b | |||
| ac2147727f | |||
| ea45349235 | |||
| e734ab52de | |||
| 75628403b0 | |||
| b3642749fd | |||
| f735973bf8 | |||
| bdb778cb9f | |||
| ab5b272868 | |||
| 49d6b0afb8 | |||
| bb5a12dc11 | |||
| 0539c6bf26 | |||
| f95f954df7 | |||
| 5ee4586989 | |||
| 9c39fd6e92 | |||
| c8ee48bc98 | |||
| ef52a7a328 | |||
| 635ab73706 | |||
| c4d38e6de6 | |||
| dd8a8b0f1f | |||
| 8fd96118a5 | |||
| db0cad04ab | |||
| d64d0e9f94 | |||
| adda506499 | |||
| c8525959ee | |||
| 2b95a0a7e5 | |||
| c64c10e76f | |||
| b9c439cdd9 | |||
| d8ba2b5df3 | |||
| 5e37dd901d | |||
| 031ac01b8d | |||
| 6ccc12f337 | |||
| 8738b8d551 | |||
| f2ff85421a | |||
| cb5a67e9e0 | |||
| ca934c7568 | |||
| 6f0204fcd3 | |||
| c81adf76c2 | |||
| 78de614910 | |||
| 201a8fe0ba | |||
| e5221932e8 | |||
| 3d5889060d | |||
| 68c3c764b7 | |||
| fba89d2082 | |||
| 7823ed6cc0 | |||
| c23ff9d7c2 | |||
| 5f82108aae | |||
| 454baa7b5b | |||
| 69e9691730 | |||
| 8c8b315994 | |||
| e8ff5cb783 | |||
| 256a8c686e | |||
| 69ddbe3944 | |||
| 79f7f82c59 | |||
| 35714c469b | |||
| cf9ba527bb | |||
| d0e0e156b1 | |||
| 8a55423ac7 | |||
| 66131d5103 | |||
| 1fd2a2532d | |||
| 9d36dc7ac6 | |||
| 59cb9e39dd | |||
| d4b282d6f7 | |||
| e57bb6bc97 | |||
| 3ce17b740b | |||
| 7eced953b3 | |||
| 768a7b47ec | |||
| 096cb0d1ee | |||
| 4235ab0c7e | |||
| 6e56f84fe3 | |||
| ff14208105 | |||
| 626ea6b119 | |||
| d61f512e20 | |||
| 77f1584713 | |||
| d8c0621887 | |||
| d1662db813 | |||
| 17296fdf9c | |||
| e4cd29d88c | |||
| eeec1b0b38 | |||
| e043fdba50 | |||
| 7f0e1c27c6 | |||
| bdff34017a | |||
| ffcba45b1b | |||
| 9fb0ce664c | |||
| 5734358d91 | |||
| 7280d6bb3b | |||
| 7d28dad209 | |||
| 061e930eb1 | |||
| 588ffeedc1 | |||
| 49a7de4ebd | |||
| 0c9aa86885 | |||
| 9cb45a3810 | |||
| 240606c5a4 | |||
| 6134764d5e | |||
| 45a9c304b6 | |||
| c970ae8add | |||
| b593205ad9 | |||
| 5399253786 | |||
| 2babb36fc2 | |||
| ec2960a167 | |||
| 1c5d20e9a3 | |||
| a1efd6b783 | |||
| 1d77497f64 | |||
| 586692b73f | |||
| 4d3740d4ce | |||
| 15aa1fd876 | |||
| 578110488c | |||
| 08fdb3a47d | |||
| cca8c4e5b8 | |||
| 3cf419cbcd | |||
| 8ad32e7498 | |||
| 722b5569bd | |||
| e8fe5a4f17 | |||
| a314175565 | |||
| 9505f380bf | |||
| 127ca83dea | |||
| 5acfe2c89c | |||
| aa5a5ce8e1 | |||
| e8eb28996d | |||
| 9af9caae09 | |||
| a0fff43648 | |||
| b7bd1eba6f | |||
| 6d63b9400b | |||
| e0ba2f3a15 | |||
| 7d16012f61 | |||
| 4a422cac5b | |||
| 884594a967 | |||
| c7c5c41f41 | |||
| 12d89a9061 | |||
| 7d117e8ca2 | |||
| f6322bd02d | |||
| a3aafffbd2 | |||
| 52b4a2c921 | |||
| 32857f50ea | |||
| 40945b4eff | |||
| 1dbc294b80 | |||
| 5bc84dca56 | |||
| 8c0e66008a | |||
| 4a1a197943 | |||
| 43f6a7e3c9 | |||
| 1bece11eb3 | |||
| 3c1cd72430 | |||
| 1ca2a8d638 | |||
| 7359a76b77 | |||
| 75e6315aac | |||
| d68eac92b8 | |||
| b4957a2e37 | |||
| 448f02960f | |||
| 5a43dd4766 | |||
| 10f34563f8 | |||
| d8d0a60322 | |||
| 4c6eee8dfe | |||
| a598b8554d | |||
| 511023ee10 | |||
| 8212a5af77 | |||
| af5fc76dc6 | |||
| e6ee55a69b | |||
| 4dda9c6daa | |||
| 5da5369ca4 | |||
| ea04210eb3 | |||
| e64327f353 | |||
| 4d4ec11a8a | |||
| 4f77e3f595 | |||
| 0b68ca6cb3 | |||
| 0c2eb8dc58 | |||
| b2106ef4a2 | |||
| ee1fa34df2 | |||
| c4b0f24cd7 | |||
| a1d50dbfa2 | |||
| 8436f9ade4 | |||
| 2e78f7503e | |||
| 5c0f5588a6 | |||
| 3d6d006e84 | |||
| 1c5ea317e6 | |||
| 31faad776e | |||
| f020daa357 | |||
| dacbcdb710 | |||
| a9e239ca7a | |||
| 65550335ee | |||
| 01db789d42 | |||
| 6fe0895e74 | |||
| c93a27af7d | |||
| 67ce7917cc | |||
| 9b447cdf1e | |||
| 98ba0042d8 | |||
| f64b9503f9 | |||
| e734a664b4 | |||
| 942f0b4fbd | |||
| a2ba466918 | |||
| e6db5bd193 | |||
| 9d3dda4e12 | |||
| b204da3317 | |||
| ce1b90885e | |||
| d534f88d1c | |||
| caca070c1a | |||
| 36b33fb407 | |||
| b2c87c3591 |
@@ -10,7 +10,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
open-pull-requests-limit: 15
|
||||
target-branch: develop
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
|
||||
@@ -461,7 +461,7 @@ jobs:
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.7
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
@@ -469,7 +469,7 @@ jobs:
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.7
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
|
||||
@@ -8,17 +8,17 @@ repos:
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.3.0"
|
||||
rev: "v1.5.0"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.3.0.5
|
||||
- types-cachetools==5.3.0.6
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.31.0.1
|
||||
- types-tabulate==0.9.0.2
|
||||
- types-python-dateutil==2.8.19.13
|
||||
- SQLAlchemy==2.0.17
|
||||
- types-requests==2.31.0.2
|
||||
- types-tabulate==0.9.0.3
|
||||
- types-python-dateutil==2.8.19.14
|
||||
- SQLAlchemy==2.0.20
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
||||
+9
-3
@@ -1,8 +1,14 @@
|
||||
# .readthedocs.yml
|
||||
version: 2
|
||||
|
||||
build:
|
||||
image: latest
|
||||
os: "ubuntu-22.04"
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
python:
|
||||
version: 3.8
|
||||
setup_py_install: false
|
||||
install:
|
||||
- requirements: docs/requirements-docs.txt
|
||||
|
||||
mkdocs:
|
||||
configuration: mkdocs.yml
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,8 +8,9 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||
cd ta-lib \
|
||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||
&& curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
|
||||
&& curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
|
||||
&& echo "Downloading gcc config.guess and config.sub" \
|
||||
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
|
||||
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
|
||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||
&& make
|
||||
if [ $? -ne 0 ]; then
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
# Downloads don't work automatically, since the URL is regenerated via javascript.
|
||||
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
|
||||
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
|
||||
|
||||
python -m pip install --upgrade pip wheel
|
||||
|
||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
if ($pyv -eq '3.8') {
|
||||
pip install build_helpers\TA_Lib-0.4.26-cp38-cp38-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.9') {
|
||||
pip install build_helpers\TA_Lib-0.4.26-cp39-cp39-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.10') {
|
||||
pip install build_helpers\TA_Lib-0.4.26-cp310-cp310-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.11') {
|
||||
pip install build_helpers\TA_Lib-0.4.26-cp311-cp311-win_amd64.whl
|
||||
}
|
||||
|
||||
pip install --find-links=build_helpers\ --prefer-binary TA-Lib
|
||||
|
||||
pip install -r requirements-dev.txt
|
||||
pip install -e .
|
||||
|
||||
BIN
Binary file not shown.
@@ -89,7 +89,6 @@
|
||||
],
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"sandbox": false,
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"password": "",
|
||||
@@ -206,6 +205,6 @@
|
||||
"recursive_strategy_search": false,
|
||||
"add_config_files": [],
|
||||
"reduce_df_footprint": false,
|
||||
"dataformat_ohlcv": "json",
|
||||
"dataformat_trades": "jsongz"
|
||||
"dataformat_ohlcv": "feather",
|
||||
"dataformat_trades": "feather"
|
||||
}
|
||||
|
||||
@@ -32,5 +32,5 @@ services:
|
||||
--logfile /freqtrade/user_data/logs/freqtrade.log
|
||||
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite
|
||||
--config /freqtrade/user_data/config.json
|
||||
--freqai-model XGBoostClassifier
|
||||
--strategy SampleStrategy
|
||||
--freqaimodel XGBoostRegressor
|
||||
--strategy FreqaiExampleStrategy
|
||||
|
||||
@@ -103,6 +103,22 @@ The indicators have to be present in your strategy's main DataFrame (either for
|
||||
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
||||
output.
|
||||
|
||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||
automatically accessible by including them on the indicator-list, and these include:
|
||||
|
||||
- **open_date :** trade open datetime
|
||||
- **close_date :** trade close datetime
|
||||
- **min_rate :** minimum price seen throughout the position
|
||||
- **max_rate :** maxiumum price seen throughout the position
|
||||
- **open :** signal candle open price
|
||||
- **close :** signal candle close price
|
||||
- **high :** signal candle high price
|
||||
- **low :** signal candle low price
|
||||
- **volume :** signal candle volumne
|
||||
- **profit_ratio :** trade profit ratio
|
||||
- **profit_abs :** absolute profit return of the trade
|
||||
|
||||
|
||||
### Filtering the trade output by date
|
||||
|
||||
To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[YYYYMMDD]` format:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
+6
-2
@@ -305,7 +305,7 @@ A backtesting result will look like that:
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy | -0.15 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
@@ -324,6 +324,7 @@ A backtesting result will look like that:
|
||||
| Days win/draw/lose | 12 / 82 / 25 |
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Max Consecutive Wins / Loss | 3 / 4 |
|
||||
| Rejected Entry signals | 3089 |
|
||||
| Entry/Exit Timeouts | 0 / 0 |
|
||||
| Canceled Trade Entries | 34 |
|
||||
@@ -409,7 +410,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy | -0.15 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
@@ -428,6 +429,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Days win/draw/lose | 12 / 82 / 25 |
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Max Consecutive Wins / Loss | 3 / 4 |
|
||||
| Rejected Entry signals | 3089 |
|
||||
| Entry/Exit Timeouts | 0 / 0 |
|
||||
| Canceled Trade Entries | 34 |
|
||||
@@ -467,6 +469,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
|
||||
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||
- `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`.
|
||||
@@ -534,6 +537,7 @@ Since backtesting lacks some detailed information about what happens within a ca
|
||||
- ROI
|
||||
- exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
|
||||
- exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit
|
||||
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
|
||||
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
|
||||
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
|
||||
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` exit reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
|
||||
|
||||
+15
-1
@@ -7,7 +7,7 @@ This page provides you some basic concepts on how Freqtrade works and operates.
|
||||
* **Strategy**: Your trading strategy, telling the bot what to do.
|
||||
* **Trade**: Open position.
|
||||
* **Open Order**: Order which is currently placed on the exchange, and is not yet complete.
|
||||
* **Pair**: Tradable pair, usually in the format of Base/Quote (e.g. XRP/USDT).
|
||||
* **Pair**: Tradable pair, usually in the format of Base/Quote (e.g. `XRP/USDT` for spot, `XRP/USDT:USDT` for futures).
|
||||
* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
|
||||
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
||||
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
||||
@@ -20,6 +20,20 @@ This page provides you some basic concepts on how Freqtrade works and operates.
|
||||
|
||||
All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt / Dry-run modes, the exchange default fee is used (lowest tier on the exchange). For live operations, fees are used as applied by the exchange (this includes BNB rebates etc.).
|
||||
|
||||
## Pair naming
|
||||
|
||||
Freqtrade follows the [ccxt naming convention](https://docs.ccxt.com/#/README?id=consistency-of-base-and-quote-currencies) for currencies.
|
||||
Using the wrong naming convention in the wrong market will usually result in the bot not recognizing the pair, usually resulting in errors like "this pair is not available".
|
||||
|
||||
### Spot pair naming
|
||||
|
||||
For spot pairs, naming will be `base/quote` (e.g. `ETH/USDT`).
|
||||
|
||||
### Futures pair naming
|
||||
|
||||
For futures pairs, naming will be `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||
|
||||
|
||||
## Bot execution logic
|
||||
|
||||
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
This page explains the different parameters of the bot and how to run it.
|
||||
|
||||
!!! Note
|
||||
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
|
||||
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .venv/bin/activate`) before running freqtrade commands.
|
||||
|
||||
!!! Warning "Up-to-date clock"
|
||||
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
||||
|
||||
@@ -188,7 +188,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
|
||||
| | **Exchange**
|
||||
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
||||
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
|
||||
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
@@ -251,8 +250,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> **Datatype:** String, SQLAlchemy connect string
|
||||
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
|
||||
| `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
|
||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `feather`*. <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
|
||||
@@ -682,16 +681,14 @@ To use a proxy for exchange connections - you will have to define the proxies as
|
||||
{
|
||||
"exchange": {
|
||||
"ccxt_config": {
|
||||
"aiohttp_proxy": "http://addr:port",
|
||||
"proxies": {
|
||||
"http": "http://addr:port",
|
||||
"https": "http://addr:port"
|
||||
},
|
||||
"httpsProxy": "http://addr:port",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For more information on available proxy types, please consult the [ccxt proxy documentation](https://docs.ccxt.com/#/README?id=proxy).
|
||||
|
||||
## Next step
|
||||
|
||||
Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).
|
||||
|
||||
@@ -27,7 +27,7 @@ For this to work, first activate your virtual environment and run the following
|
||||
|
||||
``` bash
|
||||
# Activate virtual environment
|
||||
source .env/bin/activate
|
||||
source .venv/bin/activate
|
||||
|
||||
pip install ipykernel
|
||||
ipython kernel install --user --name=freqtrade
|
||||
|
||||
+33
-33
@@ -27,11 +27,11 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[--exchange EXCHANGE]
|
||||
[-t TIMEFRAMES [TIMEFRAMES ...]] [--erase]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
||||
[--data-format-trades {json,jsongz,hdf5}]
|
||||
[--data-format-trades {json,jsongz,hdf5,feather}]
|
||||
[--trading-mode {spot,margin,futures}]
|
||||
[--prepend]
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
@@ -48,8 +48,7 @@ optional arguments:
|
||||
--dl-trades Download trades instead of OHLCV data. The bot will
|
||||
resample trades to the desired timeframe as specified
|
||||
as --timeframes/-t.
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
||||
Specify which tickers to download. Space-separated
|
||||
list. Default: `1m 5m`.
|
||||
@@ -57,17 +56,18 @@ optional arguments:
|
||||
exchange/pairs/timeframes.
|
||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `json`).
|
||||
--data-format-trades {json,jsongz,hdf5}
|
||||
(default: `feather`).
|
||||
--data-format-trades {json,jsongz,hdf5,feather}
|
||||
Storage format for downloaded trades data. (default:
|
||||
`jsongz`).
|
||||
`feather`).
|
||||
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||
Select Trading mode
|
||||
--prepend Allow data prepending. (Data-appending is disabled)
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
--logfile FILE, --log-file FILE
|
||||
Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
@@ -157,7 +157,7 @@ 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 (OHLCV only)
|
||||
* `feather` - a dataformat based on Apache Arrow
|
||||
* `parquet` - columnar datastore (OHLCV only)
|
||||
|
||||
By default, OHLCV data is stored as `json` data, while trades data is stored as `jsongz` data.
|
||||
@@ -255,7 +255,7 @@ usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[--trading-mode {spot,margin,futures}]
|
||||
[--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]]
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
@@ -266,19 +266,20 @@ optional arguments:
|
||||
Destination format for data conversion.
|
||||
--erase Clean all existing data for the selected
|
||||
exchange/pairs/timeframes.
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
||||
Specify which tickers to download. Space-separated
|
||||
list. Default: `1m 5m`.
|
||||
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||
Select Trading mode
|
||||
--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]
|
||||
Select candle type to use
|
||||
Select candle type to convert. Defaults to all
|
||||
available types.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
--logfile FILE, --log-file FILE
|
||||
Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
@@ -291,7 +292,6 @@ Common arguments:
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
```
|
||||
|
||||
### Example converting data
|
||||
@@ -314,7 +314,7 @@ usage: freqtrade convert-trade-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
{json,jsongz,hdf5,feather,parquet}
|
||||
[--erase] [--exchange EXCHANGE]
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
@@ -325,12 +325,12 @@ optional arguments:
|
||||
Destination format for data conversion.
|
||||
--erase Clean all existing data for the selected
|
||||
exchange/pairs/timeframes.
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
--logfile FILE, --log-file FILE
|
||||
Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
@@ -367,9 +367,9 @@ usage: freqtrade trades-to-ohlcv [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-t TIMEFRAMES [TIMEFRAMES ...]]
|
||||
[--exchange EXCHANGE]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
||||
[--data-format-trades {json,jsongz,hdf5}]
|
||||
[--data-format-trades {json,jsongz,hdf5,feather}]
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
@@ -377,18 +377,18 @@ optional arguments:
|
||||
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
||||
Specify which tickers to download. Space-separated
|
||||
list. Default: `1m 5m`.
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `json`).
|
||||
--data-format-trades {json,jsongz,hdf5}
|
||||
(default: `feather`).
|
||||
--data-format-trades {json,jsongz,hdf5,feather}
|
||||
Storage format for downloaded trades data. (default:
|
||||
`jsongz`).
|
||||
`feather`).
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
--logfile FILE, --log-file FILE
|
||||
Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
@@ -422,13 +422,12 @@ usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
[--trading-mode {spot,margin,futures}]
|
||||
[--show-timerange]
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `json`).
|
||||
(default: `feather`).
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
separated.
|
||||
@@ -439,7 +438,8 @@ optional arguments:
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
--logfile FILE, --log-file FILE
|
||||
Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
@@ -474,7 +474,7 @@ ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
||||
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
|
||||
This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes.
|
||||
|
||||
Since this data is large by default, the files use gzip by default. They are stored in your data-directory with the naming convention of `<pair>-trades.json.gz` (`ETH_BTC-trades.json.gz`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository.
|
||||
Since this data is large by default, the files use the feather fileformat by default. They are stored in your data-directory with the naming convention of `<pair>-trades.feather` (`ETH_BTC-trades.feather`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository.
|
||||
|
||||
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally.
|
||||
|
||||
|
||||
+21
-2
@@ -77,7 +77,7 @@ def test_method_to_test(caplog):
|
||||
|
||||
### Debug configuration
|
||||
|
||||
To debug freqtrade, we recommend VSCode with the following launch configuration (located in `.vscode/launch.json`).
|
||||
To debug freqtrade, we recommend VSCode (with the Python extension) with the following launch configuration (located in `.vscode/launch.json`).
|
||||
Details will obviously vary between setups - but this should work to get you started.
|
||||
|
||||
``` json
|
||||
@@ -102,6 +102,19 @@ This method can also be used to debug a strategy, by setting the breakpoints wit
|
||||
|
||||
A similar setup can also be taken for Pycharm - using `freqtrade` as module name, and setting the command line arguments as "parameters".
|
||||
|
||||
??? Tip "Correct venv usage"
|
||||
When using a virtual environment (which you should), make sure that your Editor is using the correct virtual environment to avoid problems or "unknown import" errors.
|
||||
|
||||
#### Vscode
|
||||
|
||||
You can select the correct environment in VSCode with the command "Python: Select Interpreter" - which will show you environments the extension detected.
|
||||
If your environment has not been detected, you can also pick a path manually.
|
||||
|
||||
#### Pycharm
|
||||
|
||||
In pycharm, you can select the appropriate Environment in the "Run/Debug Configurations" window.
|
||||

|
||||
|
||||
!!! Note "Startup directory"
|
||||
This assumes that you have the repository checked out, and the editor is started at the repository root level (so setup.py is at the top level of your repository).
|
||||
|
||||
@@ -453,7 +466,13 @@ Once the PR against stable is merged (best right after merging):
|
||||
* Use the button "Draft a new release" in the Github UI (subsection releases).
|
||||
* Use the version-number specified as tag.
|
||||
* Use "stable" as reference (this step comes after the above PR is merged).
|
||||
* Use the above changelog as release comment (as codeblock)
|
||||
* Use the above changelog as release comment (as codeblock).
|
||||
* Use the below snippet for the new release
|
||||
|
||||
??? Tip "Release template"
|
||||
````
|
||||
--8<-- "includes/release_template.md"
|
||||
````
|
||||
|
||||
## Releases
|
||||
|
||||
|
||||
+19
-12
@@ -14,6 +14,9 @@ Start by downloading and installing Docker / Docker Desktop for your platform:
|
||||
Freqtrade documentation assumes the use of Docker desktop (or the docker compose plugin).
|
||||
While the docker-compose standalone installation still works, it will require changing all `docker compose` commands from `docker compose` to `docker-compose` to work (e.g. `docker compose up -d` will become `docker-compose up -d`).
|
||||
|
||||
??? Warning "Docker on windows"
|
||||
If you just installed docker on a windows system, make sure to reboot your system, otherwise you might encounter unexplainable Problems related to network connectivity to docker containers.
|
||||
|
||||
## Freqtrade with docker
|
||||
|
||||
Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage.
|
||||
@@ -78,7 +81,7 @@ If you've selected to enable FreqUI in the `new-config` step, you will have freq
|
||||
|
||||
You can now access the UI by typing localhost:8080 in your browser.
|
||||
|
||||
??? Note "UI Access on a remote servers"
|
||||
??? Note "UI Access on a remote server"
|
||||
If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot.
|
||||
This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box).
|
||||
Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet.
|
||||
@@ -128,7 +131,7 @@ All freqtrade arguments will be available by running `docker compose run --rm fr
|
||||
!!! Note "`docker compose run --rm`"
|
||||
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
|
||||
|
||||
??? Note "Using docker without docker"
|
||||
??? Note "Using docker without docker compose"
|
||||
"`docker compose run --rm`" will require a compose file to be provided.
|
||||
Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead.
|
||||
For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`.
|
||||
@@ -172,7 +175,7 @@ You can then run `docker compose build --pull` to build the docker image, and ru
|
||||
|
||||
### Plotting with docker
|
||||
|
||||
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
|
||||
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your `docker-compose.yml` file.
|
||||
You can then use these commands as follows:
|
||||
|
||||
``` bash
|
||||
@@ -203,16 +206,20 @@ docker compose -f docker/docker-compose-jupyter.yml build --no-cache
|
||||
|
||||
### Docker on Windows
|
||||
|
||||
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
||||
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
|
||||
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
|
||||
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
|
||||
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
||||
The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
|
||||
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
|
||||
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
|
||||
|
||||
``` bash
|
||||
taskkill /IM "Docker Desktop.exe" /F
|
||||
wsl --shutdown
|
||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
||||
```
|
||||
``` bash
|
||||
taskkill /IM "Docker Desktop.exe" /F
|
||||
wsl --shutdown
|
||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
||||
```
|
||||
|
||||
* Cannot connect to the API (Windows)
|
||||
If you're on windows and just installed Docker (desktop), make sure to reboot your System. Docker can have problems with network connectivity without a restart.
|
||||
You should obviously also make sure to have your [settings](#accessing-the-ui) accordingly.
|
||||
|
||||
!!! Warning
|
||||
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
||||
|
||||
+8
-1
@@ -259,10 +259,17 @@ The configuration parameter `exchange.unknown_fee_rate` can be used to specify t
|
||||
|
||||
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.
|
||||
Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures.
|
||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors.
|
||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors
|
||||
|
||||
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
|
||||
|
||||
API Keys for live futures trading (Subaccount on non-unified) must have the following permissions:
|
||||
* Read-write
|
||||
* Contract - Orders
|
||||
* Contract - Positions
|
||||
|
||||
We do strongly recommend to limit all API keys to the IP you're going to use it from.
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||
|
||||
+10
-2
@@ -20,7 +20,7 @@ Futures trading is supported for selected exchanges. Please refer to the [docume
|
||||
|
||||
* When you work with your strategy & hyperopt file you should use a proper code editor like VSCode or PyCharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely pointed out by Freqtrade during startup).
|
||||
|
||||
## Freqtrade common issues
|
||||
## Freqtrade common questions
|
||||
|
||||
### Can freqtrade open multiple positions on the same pair in parallel?
|
||||
|
||||
@@ -36,7 +36,7 @@ Running the bot with `freqtrade trade --config config.json` shows the output `fr
|
||||
This could be caused by the following reasons:
|
||||
|
||||
* The virtual environment is not active.
|
||||
* Run `source .env/bin/activate` to activate the virtual environment.
|
||||
* Run `source .venv/bin/activate` to activate the virtual environment.
|
||||
* The installation did not complete successfully.
|
||||
* Please check the [Installation documentation](installation.md).
|
||||
|
||||
@@ -78,6 +78,14 @@ Where possible (e.g. on binance), the use of the exchange's dedicated fee curren
|
||||
On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations).
|
||||
Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange.
|
||||
|
||||
### I deposited more funds to the exchange, but my bot doesn't recognize this
|
||||
|
||||
Freqtrade will update the exchange balance when necessary (Before placing an order).
|
||||
RPC calls (Telegram's `/balance`, API calls to `/balance`) can trigger an update at max. once per hour.
|
||||
|
||||
If `adjust_trade_position` is enabled (and the bot has open trades eligible for position adjustments) - then the wallets will be refreshed once per hour.
|
||||
To force an immediate update, you can use `/reload_config` - which will restart the bot.
|
||||
|
||||
### I want to use incomplete candles
|
||||
|
||||
Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened.
|
||||
|
||||
@@ -261,7 +261,7 @@ class MyFreqaiModel(BaseRegressionModel):
|
||||
"""
|
||||
feature_pipeline = Pipeline([
|
||||
('qt', SKLearnWrapper(QuantileTransformer(output_distribution='normal'))),
|
||||
('di', ds.DissimilarityIndex(di_threshold=1)
|
||||
('di', ds.DissimilarityIndex(di_threshold=1))
|
||||
])
|
||||
|
||||
return feature_pipeline
|
||||
|
||||
@@ -42,7 +42,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
| `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Boolean.
|
||||
| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. See details about some select parameters [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm). <br> **Datatype:** Dictionary.
|
||||
| `use_DBSCAN_to_remove_outliers` | Cluster data using the DBSCAN algorithm to identify and remove outliers from training and prediction data. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan). <br> **Datatype:** Boolean.
|
||||
| `inlier_metric_window` | If set, FreqAI adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric). <br> **Datatype:** Integer. <br> Default: `0`.
|
||||
| `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).
|
||||
@@ -101,12 +100,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
|
||||
#### trainer_kwargs
|
||||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary**
|
||||
| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. <br> **Datatype:** int. <br> Default: `100`.
|
||||
| `batch_size` | The size of the batches to use during training.. <br> **Datatype:** int. <br> Default: `64`.
|
||||
| `max_n_eval_batches` | The maximum number batches to use for evaluation.. <br> **Datatype:** int, optional. <br> Default: `None`.
|
||||
| Parameter | Description |
|
||||
|--------------|-------------|
|
||||
| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary**
|
||||
| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset. Overrides `n_steps`. Either `n_epochs` or `n_steps` must be set. <br><br> **Datatype:** int. optional. <br> Default: `10`.
|
||||
| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. Ignored if `n_epochs` is set. A simplified version of the function: <br><br> n_epochs = n_steps / (n_obs / batch_size) <br><br> The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points. <br> <br> **Datatype:** int. optional. <br> Default: `None`.
|
||||
| `batch_size` | The size of the batches to use during training. <br><br> **Datatype:** int. <br> Default: `64`.
|
||||
|
||||
|
||||
### Additional parameters
|
||||
|
||||
@@ -20,7 +20,7 @@ With the current framework, we aim to expose the training environment via the co
|
||||
|
||||
We envision the majority of users focusing their effort on creative design of the `calculate_reward()` function [details here](#creating-a-custom-reward-function), while leaving the rest of the environment untouched. Other users may not touch the environment at all, and they will only play with the configuration settings and the powerful feature engineering that already exists in FreqAI. Meanwhile, we enable advanced users to create their own model classes entirely.
|
||||
|
||||
The framework is built on stable_baselines3 (torch) and OpenAI gym for the base environment class. But generally speaking, the model class is well isolated. Thus, the addition of competing libraries can be easily integrated into the existing framework. For the environment, it is inheriting from `gym.env` which means that it is necessary to write an entirely new environment in order to switch to a different library.
|
||||
The framework is built on stable_baselines3 (torch) and OpenAI gym for the base environment class. But generally speaking, the model class is well isolated. Thus, the addition of competing libraries can be easily integrated into the existing framework. For the environment, it is inheriting from `gym.Env` which means that it is necessary to write an entirely new environment in order to switch to a different library.
|
||||
|
||||
### Important considerations
|
||||
|
||||
@@ -173,7 +173,7 @@ class MyCoolRLModel(ReinforcementLearner):
|
||||
"""
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
"""
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.Env.
|
||||
Users can override any functions from those parent classes. Here is an example
|
||||
of a user customized `calculate_reward()` function.
|
||||
|
||||
@@ -254,7 +254,7 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard
|
||||
```python
|
||||
class MyRLEnv(Base5ActionRLEnv):
|
||||
"""
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||
User made custom environment. This class inherits from BaseEnvironment and gym.Env.
|
||||
Users can override any functions from those parent classes. Here is an example
|
||||
of a user customized `calculate_reward()` function.
|
||||
"""
|
||||
|
||||
+8
-3
@@ -31,7 +31,7 @@ The docker-image includes hyperopt dependencies, no further action needed.
|
||||
### Easy installation script (setup.sh) / Manual installation
|
||||
|
||||
```bash
|
||||
source .env/bin/activate
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements-hyperopt.txt
|
||||
```
|
||||
|
||||
@@ -433,9 +433,14 @@ While this strategy is most likely too simple to provide consistent profit, it s
|
||||
`range` property may also be used with `DecimalParameter` and `CategoricalParameter`. `RealParameter` does not provide this property due to infinite search space.
|
||||
|
||||
??? Hint "Performance tip"
|
||||
During normal hyperopting, indicators are calculated once and supplied to each epoch, linearly increasing RAM usage as a factor of increasing cores. As this also has performance implications, hyperopt provides `--analyze-per-epoch` which will move the execution of `populate_indicators()` to the epoch process, calculating a single value per parameter per epoch instead of using the `.range` functionality. In this case, `.range` functionality will only return the actually used value. This will reduce RAM usage, but increase CPU usage. However, your hyperopting run will be less likely to fail due to Out Of Memory (OOM) issues.
|
||||
During normal hyperopting, indicators are calculated once and supplied to each epoch, linearly increasing RAM usage as a factor of increasing cores. As this also has performance implications, there are two alternatives to reduce RAM usage
|
||||
|
||||
In either case, you should try to use space ranges as small as possible this will improve CPU/RAM usage in both scenarios.
|
||||
* Move `ema_short` and `ema_long` calculations from `populate_indicators()` to `populate_entry_trend()`. Since `populate_entry_trend()` gonna be calculated every epochs, you don't need to use `.range` functionality.
|
||||
* hyperopt provides `--analyze-per-epoch` which will move the execution of `populate_indicators()` to the epoch process, calculating a single value per parameter per epoch instead of using the `.range` functionality. In this case, `.range` functionality will only return the actually used value.
|
||||
|
||||
These alternatives will reduce RAM usage, but increase CPU usage. However, your hyperopting run will be less likely to fail due to Out Of Memory (OOM) issues.
|
||||
|
||||
Whether you are using `.range` functionality or the alternatives above, you should try to use space ranges as small as possible since this will improve CPU/RAM usage.
|
||||
|
||||
|
||||
## Optimizing protections
|
||||
|
||||
@@ -184,6 +184,8 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "RemotePairList",
|
||||
"mode": "whitelist",
|
||||
"processing_mode": "filter",
|
||||
"pairlist_url": "https://example.com/pairlist",
|
||||
"number_assets": 10,
|
||||
"refresh_period": 1800,
|
||||
@@ -194,6 +196,14 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
||||
]
|
||||
```
|
||||
|
||||
The optional `mode` option specifies if the pairlist should be used as a `blacklist` or as a `whitelist`. The default value is "whitelist".
|
||||
|
||||
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append".
|
||||
|
||||
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
||||
|
||||
In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering.
|
||||
|
||||
The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist.
|
||||
|
||||
The user is responsible for providing a server or local file that returns a JSON object with the following structure:
|
||||
@@ -201,7 +211,7 @@ The user is responsible for providing a server or local file that returns a JSON
|
||||
```json
|
||||
{
|
||||
"pairs": ["XRP/USDT", "ETH/USDT", "LTC/USDT"],
|
||||
"refresh_period": 1800,
|
||||
"refresh_period": 1800
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
## Highlighted changes
|
||||
|
||||
- ...
|
||||
|
||||
### How to update
|
||||
|
||||
As always, you can update your bot using one of the following commands:
|
||||
|
||||
#### docker-compose
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Installation via setup script
|
||||
|
||||
```
|
||||
# Deactivate venv and run
|
||||
./setup.sh --update
|
||||
```
|
||||
|
||||
#### Plain native installation
|
||||
|
||||
```
|
||||
git pull
|
||||
pip install -U -r requirements.txt
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Expand full changelog</summary>
|
||||
|
||||
```
|
||||
<Paste your changelog here>
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,11 @@
|
||||
This section will highlight a few projects from members of the community.
|
||||
!!! Note
|
||||
The projects below are for the most part not maintained by the freqtrade , therefore use your own caution before using them.
|
||||
|
||||
- [Example freqtrade strategies](https://github.com/freqtrade/freqtrade-strategies/)
|
||||
- [FrequentHippo - Grafana dashboard with dry/live runs and backtests](http://frequenthippo.ddns.net:3000/) (by hippocritical).
|
||||
- [Online pairlist generator](https://remotepairlist.com/) (by Blood4rc).
|
||||
- [Freqtrade Backtesting Project](https://bt.robot.co.network/) (by Blood4rc).
|
||||
- [Freqtrade analysis notebook](https://github.com/froggleston/freqtrade_analysis_notebook) (by Froggleston).
|
||||
- [TUI for freqtrade](https://github.com/froggleston/freqtrade-frogtrade9000) (by Froggleston).
|
||||
- [Bot Academy](https://botacademy.ddns.net/) (by stash86) - Blog about crypto bot projects.
|
||||
@@ -63,6 +63,10 @@ Exchanges confirmed working by the community:
|
||||
- [X] [Bitvavo](https://bitvavo.com/)
|
||||
- [X] [Kucoin](https://www.kucoin.com/)
|
||||
|
||||
## Community showcase
|
||||
|
||||
--8<-- "includes/showcase.md"
|
||||
|
||||
## Requirements
|
||||
|
||||
### Hardware requirements
|
||||
|
||||
+12
-12
@@ -143,11 +143,11 @@ If you are on Debian, Ubuntu or MacOS, freqtrade provides the script to install
|
||||
|
||||
### Activate your virtual environment
|
||||
|
||||
Each time you open a new terminal, you must run `source .env/bin/activate` to activate your virtual environment.
|
||||
Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment.
|
||||
|
||||
```bash
|
||||
# then activate your .env
|
||||
source ./.env/bin/activate
|
||||
# activate virtual environment
|
||||
source ./.venv/bin/activate
|
||||
```
|
||||
|
||||
### Congratulations
|
||||
@@ -172,7 +172,7 @@ With this option, the script will install the bot and most dependencies:
|
||||
You will need to have git and python3.8+ installed beforehand for this to work.
|
||||
|
||||
* Mandatory software as: `ta-lib`
|
||||
* Setup your virtualenv under `.env/`
|
||||
* Setup your virtualenv under `.venv/`
|
||||
|
||||
This option is a combination of installation tasks and `--reset`
|
||||
|
||||
@@ -225,11 +225,11 @@ rm -rf ./ta-lib*
|
||||
You will run freqtrade in separated `virtual environment`
|
||||
|
||||
```bash
|
||||
# create virtualenv in directory /freqtrade/.env
|
||||
python3 -m venv .env
|
||||
# create virtualenv in directory /freqtrade/.venv
|
||||
python3 -m venv .venv
|
||||
|
||||
# run virtualenv
|
||||
source .env/bin/activate
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
#### Install python dependencies
|
||||
@@ -286,7 +286,7 @@ cd freqtrade
|
||||
#### Freqtrade install: Conda Environment
|
||||
|
||||
```bash
|
||||
conda create --name freqtrade python=3.10
|
||||
conda create --name freqtrade python=3.11
|
||||
```
|
||||
|
||||
!!! Note "Creating Conda Environment"
|
||||
@@ -383,7 +383,7 @@ You've made it this far, so you have successfully installed freqtrade.
|
||||
freqtrade create-userdir --userdir user_data
|
||||
|
||||
# Step 2 - Create a new configuration file
|
||||
freqtrade new-config --config config.json
|
||||
freqtrade new-config --config user_data/config.json
|
||||
```
|
||||
|
||||
You are ready to run, read [Bot Configuration](configuration.md), remember to start with `dry_run: True` and verify that everything is working.
|
||||
@@ -393,7 +393,7 @@ To learn how to setup your configuration, please refer to the [Bot Configuration
|
||||
### Start the Bot
|
||||
|
||||
```bash
|
||||
freqtrade trade --config config.json --strategy SampleStrategy
|
||||
freqtrade trade --config user_data/config.json --strategy SampleStrategy
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
@@ -411,8 +411,8 @@ If you used (1)`Script` or (2)`Manual` installation, you need to run the bot in
|
||||
# if:
|
||||
bash: freqtrade: command not found
|
||||
|
||||
# then activate your .env
|
||||
source ./.env/bin/activate
|
||||
# then activate your virtual environment
|
||||
source ./.venv/bin/activate
|
||||
```
|
||||
|
||||
### MacOS installation error
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
|
||||
|
||||
##### Pair namings
|
||||
|
||||
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future).
|
||||
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/#/README?id=perpetual-swap-perpetual-future).
|
||||
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||
|
||||
### Margin mode
|
||||
|
||||
@@ -21,7 +21,10 @@ It also supports the lookahead-analysis of freqai strategies.
|
||||
|
||||
- `--cache` is forced to "none".
|
||||
- `--max-open-trades` is forced to be at least equal to the number of pairs.
|
||||
- `--dry-run-wallet` is forced to be basically infinite.
|
||||
- `--dry-run-wallet` is forced to be basically infinite (1 billion).
|
||||
- `--stake-amount` is forced to be a static 10000 (10k).
|
||||
|
||||
Those are set to avoid users accidentally generating false positives.
|
||||
|
||||
## Lookahead-analysis command reference
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
markdown==3.3.7
|
||||
mkdocs==1.4.3
|
||||
mkdocs-material==9.1.17
|
||||
markdown==3.4.4
|
||||
mkdocs==1.5.2
|
||||
mkdocs-material==9.2.1
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.0.1
|
||||
pymdown-extensions==10.1
|
||||
jinja2==3.1.2
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# Sandbox API testing
|
||||
|
||||
Some exchanges provide sandboxes or testbeds for risk-free testing, while running the bot against a real exchange.
|
||||
With some configuration, freqtrade (in combination with ccxt) provides access to these.
|
||||
|
||||
This document is an overview to configure Freqtrade to be used with sandboxes.
|
||||
This can be useful to developers and trader alike.
|
||||
|
||||
!!! Warning
|
||||
Sandboxes usually have very low volume, and either a very wide spread, or no orders available at all.
|
||||
Therefore, sandboxes will usually not do a good job of showing you how a strategy would work in real trading.
|
||||
|
||||
## Exchanges known to have a sandbox / testnet
|
||||
|
||||
* [binance](https://testnet.binance.vision/)
|
||||
* [coinbasepro](https://public.sandbox.pro.coinbase.com)
|
||||
* [gemini](https://exchange.sandbox.gemini.com/)
|
||||
* [huobipro](https://www.testnet.huobi.pro/)
|
||||
* [kucoin](https://sandbox.kucoin.com/)
|
||||
* [phemex](https://testnet.phemex.com/)
|
||||
|
||||
!!! Note
|
||||
We did not test correct functioning of all of the above testnets. Please report your experiences with each sandbox.
|
||||
|
||||
---
|
||||
|
||||
## Configure a Sandbox account
|
||||
|
||||
When testing your API connectivity, make sure to use the appropriate sandbox / testnet URL.
|
||||
|
||||
In general, you should follow these steps to enable an exchange's sandbox:
|
||||
|
||||
* Figure out if an exchange has a sandbox (most likely by using google or the exchange's support documents)
|
||||
* Create a sandbox account (often the sandbox-account requires separate registration)
|
||||
* [Add some test assets to account](#add-test-funds)
|
||||
* Create API keys
|
||||
|
||||
### Add test funds
|
||||
|
||||
Usually, sandbox exchanges allow depositing funds directly via web-interface.
|
||||
You should make sure to have a realistic amount of funds available to your test-account, so results are representable of your real account funds.
|
||||
|
||||
!!! Warning
|
||||
Test exchanges will **NEVER** require your real credit card or banking details!
|
||||
|
||||
## Configure freqtrade to use a exchange's sandbox
|
||||
|
||||
### Sandbox URLs
|
||||
|
||||
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
|
||||
These include `['test']` and `['api']`.
|
||||
|
||||
* `[Test]` if available will point to an Exchanges sandbox.
|
||||
* `[Api]` normally used, and resolves to live API target on the exchange.
|
||||
|
||||
To make use of sandbox / test add "sandbox": true, to your config.json
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "coinbasepro",
|
||||
"sandbox": true,
|
||||
"key": "5wowfxemogxeowo;heiohgmd",
|
||||
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
|
||||
"password": "1bkjfkhfhfu6sr",
|
||||
"outdated_offset": 5
|
||||
"pair_whitelist": [
|
||||
"BTC/USD"
|
||||
]
|
||||
},
|
||||
"datadir": "user_data/data/coinbasepro_sandbox"
|
||||
```
|
||||
|
||||
Also the following information:
|
||||
|
||||
* api-key (created for the sandbox webpage)
|
||||
* api-secret (noted earlier)
|
||||
* password (the passphrase - noted earlier)
|
||||
|
||||
!!! Tip "Different data directory"
|
||||
We also recommend to set `datadir` to something identifying downloaded data as sandbox data, to avoid having sandbox data mixed with data from the real exchange.
|
||||
This can be done by adding the `"datadir"` key to the configuration.
|
||||
Now, whenever you use this configuration, your data directory will be set to this directory.
|
||||
|
||||
---
|
||||
|
||||
## You should now be ready to test your sandbox
|
||||
|
||||
Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox. Also make sure to select a pair which shows at least some decent value (which very often is BTC/<somestablecoin>).
|
||||
|
||||
## Common problems with sandbox exchanges
|
||||
|
||||
Sandbox exchange instances often have very low volume, which can cause some problems which usually are not seen on a real exchange instance.
|
||||
|
||||
### Old Candles problem
|
||||
|
||||
Since Sandboxes often have low volume, candles can be quite old and show no volume.
|
||||
To disable the error "Outdated history for pair ...", best increase the parameter `"outdated_offset"` to a number that seems realistic for the sandbox you're using.
|
||||
|
||||
### Unfilled orders
|
||||
|
||||
Sandboxes often have very low volumes - which means that many trades can go unfilled, or can go unfilled for a very long time.
|
||||
|
||||
To mitigate this, you can try to match the first order on the opposite orderbook side using the following configuration:
|
||||
|
||||
``` jsonc
|
||||
"order_types": {
|
||||
"entry": "limit",
|
||||
"exit": "limit"
|
||||
// ...
|
||||
},
|
||||
"entry_pricing": {
|
||||
"price_side": "other",
|
||||
// ...
|
||||
},
|
||||
"exit_pricing":{
|
||||
"price_side": "other",
|
||||
// ...
|
||||
},
|
||||
```
|
||||
|
||||
The configuration is similar to the suggested configuration for market orders - however by using limit-orders you can avoid moving the price too much, and you can set the worst price you might get.
|
||||
@@ -750,7 +750,7 @@ class DigDeeperStrategy(IStrategy):
|
||||
# Hope you have a deep wallet!
|
||||
try:
|
||||
# This returns first order stake size
|
||||
stake_amount = filled_entries[0].cost
|
||||
stake_amount = filled_entries[0].stake_amount
|
||||
# This then calculates current safety order size
|
||||
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
|
||||
return stake_amount
|
||||
|
||||
@@ -287,12 +287,17 @@ Return a summary of your profit/loss and performance.
|
||||
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||
> **Trading volume:** `0.5 BTC`
|
||||
> **Profit factor:** `1.04`
|
||||
> **Win / Loss:** `102 / 36`
|
||||
> **Winrate:** `73.91%`
|
||||
> **Expectancy (Ratio):** `4.87 (1.66)`
|
||||
> **Max Drawdown:** `9.23% (0.01255 BTC)`
|
||||
|
||||
The relative profit of `1.2%` is the average profit per trade.
|
||||
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
||||
Expectancy corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades).
|
||||
Expectancy Ratio is expected profit or loss of a subsequent trade based on the performance of all past trades.
|
||||
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
|
||||
|
||||
|
||||
@@ -141,7 +141,8 @@ Most properties here can be None as they are dependant on the exchange response.
|
||||
`amount` | float | Amount in base currency
|
||||
`filled` | float | Filled amount (in base currency)
|
||||
`remaining` | float | Remaining amount
|
||||
`cost` | float | Cost of the order - usually average * filled
|
||||
`cost` | float | Cost of the order - usually average * filled (*Exchange dependant on futures, may contain the cost with or without leverage and may be in contracts.*)
|
||||
`stake_amount` | float | Stake amount used for this order. *Added in 2023.7.*
|
||||
`order_date` | datetime | Order creation date **use `order_date_utc` instead**
|
||||
`order_date_utc` | datetime | Order creation date (in UTC)
|
||||
`order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead**
|
||||
|
||||
+1
-1
@@ -967,7 +967,7 @@ Print trades with id 2 and 3 as json
|
||||
freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json
|
||||
```
|
||||
|
||||
### Strategy-Updater
|
||||
## Strategy-Updater
|
||||
|
||||
Updates listed strategies or all strategies within the strategies folder to be v3 compliant.
|
||||
If the command runs without --strategy-list then all strategies inside the strategies folder will be converted.
|
||||
|
||||
@@ -80,12 +80,18 @@ When using the Form-Encoded or JSON-Encoded configuration you can configure any
|
||||
|
||||
The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header.
|
||||
|
||||
Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries:
|
||||
## Additional configurations
|
||||
|
||||
The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook.
|
||||
You can also specify `webhook.timeout` - which defines how long the bot will wait until it assumes the other host as unresponsive (defaults to 10s).
|
||||
|
||||
Example configuration for retries:
|
||||
|
||||
```json
|
||||
"webhook": {
|
||||
"enabled": true,
|
||||
"url": "https://<YOURHOOKURL>",
|
||||
"timeout": 10,
|
||||
"retries": 3,
|
||||
"retry_delay": 0.2,
|
||||
"status": {
|
||||
@@ -109,6 +115,8 @@ Custom messages can be sent to Webhook endpoints via the `self.dp.send_msg()` fu
|
||||
|
||||
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
||||
|
||||
## Webhook Message types
|
||||
|
||||
### Entry
|
||||
|
||||
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
||||
|
||||
@@ -31,8 +31,8 @@ Other versions must be downloaded from the above link.
|
||||
|
||||
``` powershell
|
||||
cd \path\freqtrade
|
||||
python -m venv .env
|
||||
.env\Scripts\activate.ps1
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate.ps1
|
||||
# optionally install ta-lib from wheel
|
||||
# Eventually adjust the below filename to match the downloaded wheel
|
||||
pip install --find-links build_helpers\ TA-Lib -U
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2023.6'
|
||||
__version__ = '2023.8'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -67,8 +67,7 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
||||
|
||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode",
|
||||
"candle_types"]
|
||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
||||
|
||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ from typing import Any, Dict, List
|
||||
|
||||
from questionary import Separator, prompt
|
||||
|
||||
from freqtrade.configuration.detect_environment import running_in_docker
|
||||
from freqtrade.configuration.directory_operations import chown_user_directory
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
|
||||
from freqtrade.misc import render_template
|
||||
from freqtrade.util import render_template
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -104,7 +105,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"type": "select",
|
||||
"name": "exchange_name",
|
||||
"message": "Select exchange",
|
||||
"choices": lambda x: [
|
||||
"choices": [
|
||||
"binance",
|
||||
"binanceus",
|
||||
"bittrex",
|
||||
@@ -179,7 +180,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"name": "api_server_listen_addr",
|
||||
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||
"otherwise best left untouched)"),
|
||||
"default": "127.0.0.1",
|
||||
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0",
|
||||
"when": lambda x: x['api_server']
|
||||
},
|
||||
{
|
||||
|
||||
@@ -381,7 +381,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"candle_types": Arg(
|
||||
'--candle-types',
|
||||
help='Select candle type to use',
|
||||
help='Select candle type to convert. Defaults to all available types.',
|
||||
choices=[c.value for c in CandleType],
|
||||
nargs='+',
|
||||
),
|
||||
@@ -435,13 +435,13 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"dataformat_ohlcv": Arg(
|
||||
'--data-format-ohlcv',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `json`).',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `feather`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
),
|
||||
"dataformat_trades": Arg(
|
||||
'--data-format-trades',
|
||||
help='Storage format for downloaded trades data. (default: `jsongz`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS_TRADES,
|
||||
help='Storage format for downloaded trades data. (default: `feather`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
),
|
||||
"show_timerange": Arg(
|
||||
'--show-timerange',
|
||||
@@ -450,14 +450,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"exchange": Arg(
|
||||
'--exchange',
|
||||
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
|
||||
f'Only valid if no config is provided.',
|
||||
help='Exchange name. Only valid if no config is provided.',
|
||||
),
|
||||
"timeframes": Arg(
|
||||
'-t', '--timeframes',
|
||||
help='Specify which tickers to download. Space-separated list. '
|
||||
'Default: `1m 5m`.',
|
||||
default=['1m', '5m'],
|
||||
nargs='+',
|
||||
),
|
||||
"prepend_data": Arg(
|
||||
|
||||
@@ -4,10 +4,10 @@ from collections import defaultdict
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
|
||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||
from freqtrade.data.history import convert_trades_to_ohlcv, download_data_main
|
||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||
from freqtrade.enums import RunMode, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
@@ -57,6 +57,8 @@ def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
"Please check the documentation on how to configure this.")
|
||||
if 'timeframes' not in config:
|
||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
@@ -86,11 +88,10 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
if ohlcv:
|
||||
migrate_binance_futures_data(config)
|
||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
|
||||
for candle_type in candle_types:
|
||||
convert_ohlcv_format(config,
|
||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||
erase=args['erase'], candle_type=candle_type)
|
||||
convert_ohlcv_format(config,
|
||||
convert_from=args['format_from'],
|
||||
convert_to=args['format_to'],
|
||||
erase=args['erase'])
|
||||
else:
|
||||
convert_trades_format(config,
|
||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
||||
|
||||
@@ -10,7 +10,7 @@ from freqtrade.configuration.directory_operations import copy_sample_files, crea
|
||||
from freqtrade.constants import USERPATH_STRATEGIES
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import render_template, render_template_with_fallback
|
||||
from freqtrade.util import render_template, render_template_with_fallback
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -35,6 +35,10 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
||||
Deploy new strategy from template to strategy_path
|
||||
"""
|
||||
fallback = 'full'
|
||||
attributes = render_template_with_fallback(
|
||||
templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2",
|
||||
templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2",
|
||||
)
|
||||
indicators = render_template_with_fallback(
|
||||
templatefile=f"strategy_subtemplates/indicators_{subtemplate}.j2",
|
||||
templatefallbackfile=f"strategy_subtemplates/indicators_{fallback}.j2",
|
||||
@@ -58,6 +62,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
||||
|
||||
strategy_text = render_template(templatefile='base_strategy.py.j2',
|
||||
arguments={"strategy": strategy_name,
|
||||
"attributes": attributes,
|
||||
"indicators": indicators,
|
||||
"buy_trend": buy_trend,
|
||||
"sell_trend": sell_trend,
|
||||
|
||||
@@ -7,9 +7,10 @@ def start_webserver(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Main entry point for webserver mode
|
||||
"""
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.rpc.api_server import ApiServer
|
||||
|
||||
# Initialize configuration
|
||||
config = Configuration(args, RunMode.WEBSERVER).get_config()
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.WEBSERVER)
|
||||
ApiServer(config, standalone=True)
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.configuration.configuration import Configuration
|
||||
from freqtrade.configuration.detect_environment import running_in_docker
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
|
||||
@@ -51,6 +51,8 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
|
||||
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED
|
||||
else:
|
||||
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
|
||||
elif conf.get('runmode', RunMode.OTHER) == RunMode.WEBSERVER:
|
||||
conf_schema['required'] = constants.SCHEMA_MINIMAL_WEBSERVER
|
||||
else:
|
||||
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import os
|
||||
|
||||
|
||||
def running_in_docker() -> bool:
|
||||
"""
|
||||
Check if we are running in a docker container
|
||||
"""
|
||||
return os.environ.get('FT_APP_ENV') == 'docker'
|
||||
@@ -3,6 +3,7 @@ import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from freqtrade.configuration.detect_environment import running_in_docker
|
||||
from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS,
|
||||
USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
@@ -30,8 +31,7 @@ def chown_user_directory(directory: Path) -> None:
|
||||
Use Sudo to change permissions of the home-directory if necessary
|
||||
Only applies when running in docker!
|
||||
"""
|
||||
import os
|
||||
if os.environ.get('FT_APP_ENV') == 'docker':
|
||||
if running_in_docker():
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.check_output(
|
||||
|
||||
@@ -41,7 +41,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
||||
key = env_var.replace(prefix, '')
|
||||
for k in reversed(key.split('__')):
|
||||
val = {k.lower(): get_var_typed(val)
|
||||
if type(val) != dict and k not in no_convert else val}
|
||||
if not isinstance(val, dict) and k not in no_convert else val}
|
||||
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||
return relevant_vars
|
||||
|
||||
|
||||
+18
-8
@@ -10,7 +10,6 @@ from freqtrade.enums import CandleType, PriceType, RPCMessageType
|
||||
|
||||
DOCS_LINK = "https://www.freqtrade.io/en/stable"
|
||||
DEFAULT_CONFIG = 'config.json'
|
||||
DEFAULT_EXCHANGE = 'bittrex'
|
||||
PROCESS_THROTTLE_SECS = 5 # sec
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
@@ -39,8 +38,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
|
||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
||||
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
|
||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
|
||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5', 'feather', 'parquet']
|
||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||
BACKTEST_CACHE_DEFAULT = 'day'
|
||||
@@ -51,6 +49,15 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
||||
# it has wide consequences for stored trades files
|
||||
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
||||
TRADES_DTYPES = {
|
||||
'timestamp': 'int64',
|
||||
'id': 'str',
|
||||
'type': 'str',
|
||||
'side': 'str',
|
||||
'price': 'float64',
|
||||
'amount': 'float64',
|
||||
'cost': 'float64',
|
||||
}
|
||||
TRADING_MODES = ['spot', 'margin', 'futures']
|
||||
MARGIN_MODES = ['cross', 'isolated', '']
|
||||
|
||||
@@ -66,6 +73,7 @@ TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||
FULL_DATAFRAME_THRESHOLD = 100
|
||||
CUSTOM_TAG_MAX_LENGTH = 255
|
||||
DL_DATA_TIMEFRAMES = ['1m', '5m']
|
||||
|
||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||
|
||||
@@ -153,7 +161,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
},
|
||||
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
|
||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True, 'minimum': -1},
|
||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||
'trailing_stop': {'type': 'boolean'},
|
||||
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
@@ -446,12 +454,12 @@ CONF_SCHEMA = {
|
||||
'dataformat_ohlcv': {
|
||||
'type': 'string',
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'json'
|
||||
'default': 'feather'
|
||||
},
|
||||
'dataformat_trades': {
|
||||
'type': 'string',
|
||||
'enum': AVAILABLE_DATAHANDLERS_TRADES,
|
||||
'default': 'jsongz'
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'feather'
|
||||
},
|
||||
'position_adjustment_enable': {'type': 'boolean'},
|
||||
'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1},
|
||||
@@ -461,7 +469,6 @@ CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'sandbox': {'type': 'boolean', 'default': False},
|
||||
'key': {'type': 'string', 'default': ''},
|
||||
'secret': {'type': 'string', 'default': ''},
|
||||
'password': {'type': 'string', 'default': ''},
|
||||
@@ -668,6 +675,9 @@ SCHEMA_MINIMAL_REQUIRED = [
|
||||
'dataformat_ohlcv',
|
||||
'dataformat_trades',
|
||||
]
|
||||
SCHEMA_MINIMAL_WEBSERVER = SCHEMA_MINIMAL_REQUIRED + [
|
||||
'api_server',
|
||||
]
|
||||
|
||||
CANCEL_REASON = {
|
||||
"TIMEOUT": "cancelled due to timeout",
|
||||
|
||||
@@ -5,16 +5,17 @@ import logging
|
||||
from copy import copy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Literal, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import json_load
|
||||
from freqtrade.misc import file_dump_json, json_load
|
||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||
from freqtrade.types import BacktestHistoryEntryType, BacktestResultType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -128,7 +129,7 @@ def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
raise OperationalException('Unexpected error while loading backtest metadata.') from e
|
||||
|
||||
|
||||
def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
|
||||
"""
|
||||
Load backtest statistics file.
|
||||
:param filename: pathlib.Path object, or string pointing to the file.
|
||||
@@ -147,21 +148,21 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
# Legacy list format does not contain metadata.
|
||||
if isinstance(data, dict):
|
||||
data['metadata'] = load_backtest_metadata(filename)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
|
||||
"""
|
||||
Load one strategy from multi-strategy result
|
||||
and merge it with results
|
||||
Load one strategy from multi-strategy result and merge it with results
|
||||
:param strategy_name: Name of the strategy contained in the result
|
||||
:param filename: Backtest-result-filename to load
|
||||
:param results: dict to merge the result to.
|
||||
"""
|
||||
bt_data = load_backtest_stats(filename)
|
||||
for k in ('metadata', 'strategy'):
|
||||
k: Literal['metadata', 'strategy']
|
||||
for k in ('metadata', 'strategy'): # type: ignore
|
||||
results[k][strategy_name] = bt_data[k][strategy_name]
|
||||
results['metadata'][strategy_name]['filename'] = filename.stem
|
||||
comparison = bt_data['strategy_comparison']
|
||||
for i in range(len(comparison)):
|
||||
if comparison[i]['key'] == strategy_name:
|
||||
@@ -170,27 +171,67 @@ def load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
|
||||
|
||||
|
||||
def _get_backtest_files(dirname: Path) -> List[Path]:
|
||||
# Weird glob expression here avoids including .meta.json files.
|
||||
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
||||
|
||||
|
||||
def get_backtest_resultlist(dirname: Path):
|
||||
def get_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||
"""
|
||||
Get backtest result read from metadata file
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'filename': filename.stem,
|
||||
'strategy': s,
|
||||
'notes': v.get('notes', ''),
|
||||
'run_id': v['run_id'],
|
||||
'backtest_start_time': v['backtest_start_time'],
|
||||
} for s, v in load_backtest_metadata(filename).items()
|
||||
]
|
||||
|
||||
|
||||
def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]:
|
||||
"""
|
||||
Get list of backtest results read from metadata files
|
||||
"""
|
||||
results = []
|
||||
for filename in _get_backtest_files(dirname):
|
||||
metadata = load_backtest_metadata(filename)
|
||||
if not metadata:
|
||||
continue
|
||||
for s, v in metadata.items():
|
||||
results.append({
|
||||
'filename': filename.name,
|
||||
'strategy': s,
|
||||
'run_id': v['run_id'],
|
||||
'backtest_start_time': v['backtest_start_time'],
|
||||
return [
|
||||
{
|
||||
'filename': filename.stem,
|
||||
'strategy': s,
|
||||
'run_id': v['run_id'],
|
||||
'notes': v.get('notes', ''),
|
||||
'backtest_start_time': v['backtest_start_time'],
|
||||
}
|
||||
for filename in _get_backtest_files(dirname)
|
||||
for s, v in load_backtest_metadata(filename).items()
|
||||
if v
|
||||
]
|
||||
|
||||
})
|
||||
return results
|
||||
|
||||
def delete_backtest_result(file_abs: Path):
|
||||
"""
|
||||
Delete backtest result file and corresponding metadata file.
|
||||
"""
|
||||
# *.meta.json
|
||||
logger.info(f"Deleting backtest result file: {file_abs.name}")
|
||||
file_abs_meta = file_abs.with_suffix('.meta.json')
|
||||
file_abs.unlink()
|
||||
file_abs_meta.unlink()
|
||||
|
||||
|
||||
def update_backtest_metadata(filename: Path, strategy: str, content: Dict[str, Any]):
|
||||
"""
|
||||
Updates backtest metadata file with new content.
|
||||
:raises: ValueError if metadata file does not exist, or strategy is not in this file.
|
||||
"""
|
||||
metadata = load_backtest_metadata(filename)
|
||||
if not metadata:
|
||||
raise ValueError("File does not exist.")
|
||||
if strategy not in metadata:
|
||||
raise ValueError("Strategy not in metadata.")
|
||||
metadata[strategy].update(content)
|
||||
# Write data again.
|
||||
file_dump_json(get_backtest_metadata_filename(filename), metadata)
|
||||
|
||||
|
||||
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
||||
@@ -211,7 +252,6 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
|
||||
'strategy_comparison': [],
|
||||
}
|
||||
|
||||
# Weird glob expression here avoids including .meta.json files.
|
||||
for filename in _get_backtest_files(dirname):
|
||||
metadata = load_backtest_metadata(filename)
|
||||
if not metadata:
|
||||
|
||||
+83
-51
@@ -1,17 +1,16 @@
|
||||
"""
|
||||
Functions to convert data from one format to another
|
||||
"""
|
||||
import itertools
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, Config, TradeList
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES,
|
||||
Config, TradeList)
|
||||
from freqtrade.enums import CandleType, TradingMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -96,8 +95,14 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
||||
'volume': 'sum'
|
||||
}
|
||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||
resample_interval = f'{timeframe_minutes}min'
|
||||
if timeframe_minutes >= 43200 and timeframe_minutes < 525600:
|
||||
# Monthly candles need special treatment to stick to the 1st of the month
|
||||
resample_interval = f'{timeframe}S'
|
||||
elif timeframe_minutes > 43200:
|
||||
resample_interval = timeframe
|
||||
# Resample to create "NAN" values
|
||||
df = dataframe.resample(f'{timeframe_minutes}min', on='date').agg(ohlcv_dict)
|
||||
df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict)
|
||||
|
||||
# Forwardfill close for missing columns
|
||||
df['close'] = df['close'].fillna(method='ffill')
|
||||
@@ -122,7 +127,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
||||
return df
|
||||
|
||||
|
||||
def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date',
|
||||
def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date',
|
||||
startup_candles: int = 0) -> DataFrame:
|
||||
"""
|
||||
Trim dataframe based on given timerange
|
||||
@@ -189,15 +194,14 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
||||
return frame
|
||||
|
||||
|
||||
def trades_remove_duplicates(trades: List[List]) -> List[List]:
|
||||
def trades_df_remove_duplicates(trades: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Removes duplicates from the trades list.
|
||||
Uses itertools.groupby to avoid converting to pandas.
|
||||
Tests show it as being pretty efficient on lists of 4M Lists.
|
||||
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
|
||||
:return: same format as above, but with duplicates removed
|
||||
Removes duplicates from the trades DataFrame.
|
||||
Uses pandas.DataFrame.drop_duplicates to remove duplicates based on the 'timestamp' column.
|
||||
:param trades: DataFrame with the columns constants.DEFAULT_TRADES_COLUMNS
|
||||
:return: DataFrame with duplicates removed based on the 'timestamp' column
|
||||
"""
|
||||
return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))]
|
||||
return trades.drop_duplicates(subset=['timestamp', 'id'])
|
||||
|
||||
|
||||
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
||||
@@ -209,7 +213,32 @@ def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
||||
return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades]
|
||||
|
||||
|
||||
def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame:
|
||||
def trades_convert_types(trades: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Convert Trades dtypes and add 'date' column
|
||||
"""
|
||||
trades = trades.astype(TRADES_DTYPES)
|
||||
trades['date'] = to_datetime(trades['timestamp'], unit='ms', utc=True)
|
||||
return trades
|
||||
|
||||
|
||||
def trades_list_to_df(trades: TradeList, convert: bool = True):
|
||||
"""
|
||||
convert trades list to dataframe
|
||||
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
|
||||
"""
|
||||
if not trades:
|
||||
df = DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||
else:
|
||||
df = DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
|
||||
|
||||
if convert:
|
||||
df = trades_convert_types(df)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def trades_to_ohlcv(trades: DataFrame, timeframe: str) -> DataFrame:
|
||||
"""
|
||||
Converts trades list to OHLCV list
|
||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||
@@ -219,12 +248,9 @@ def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame:
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||
if not trades:
|
||||
if trades.empty:
|
||||
raise ValueError('Trade-list empty.')
|
||||
df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms',
|
||||
utc=True,)
|
||||
df = df.set_index('timestamp')
|
||||
df = trades.set_index('date', drop=True)
|
||||
|
||||
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
||||
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
||||
@@ -264,7 +290,6 @@ def convert_ohlcv_format(
|
||||
convert_from: str,
|
||||
convert_to: str,
|
||||
erase: bool,
|
||||
candle_type: CandleType
|
||||
):
|
||||
"""
|
||||
Convert OHLCV from one format to another
|
||||
@@ -272,7 +297,6 @@ def convert_ohlcv_format(
|
||||
:param convert_from: Source format
|
||||
:param convert_to: Target format
|
||||
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
from freqtrade.data.history.idatahandler import get_datahandler
|
||||
src = get_datahandler(config['datadir'], convert_from)
|
||||
@@ -280,37 +304,45 @@ def convert_ohlcv_format(
|
||||
timeframes = config.get('timeframes', [config.get('timeframe')])
|
||||
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
||||
|
||||
if 'pairs' not in config:
|
||||
config['pairs'] = []
|
||||
# Check timeframes or fall back to timeframe.
|
||||
for timeframe in timeframes:
|
||||
config['pairs'].extend(src.ohlcv_get_pairs(
|
||||
config['datadir'],
|
||||
timeframe,
|
||||
candle_type=candle_type
|
||||
))
|
||||
config['pairs'] = sorted(set(config['pairs']))
|
||||
logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
|
||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', [
|
||||
c.value for c in CandleType])]
|
||||
logger.info(candle_types)
|
||||
paircombs = src.ohlcv_get_available_data(config['datadir'], TradingMode.SPOT)
|
||||
paircombs.extend(src.ohlcv_get_available_data(config['datadir'], TradingMode.FUTURES))
|
||||
|
||||
for timeframe in timeframes:
|
||||
for pair in config['pairs']:
|
||||
data = src.ohlcv_load(pair=pair, timeframe=timeframe,
|
||||
timerange=None,
|
||||
fill_missing=False,
|
||||
drop_incomplete=False,
|
||||
startup_candles=0,
|
||||
candle_type=candle_type)
|
||||
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
|
||||
if len(data) > 0:
|
||||
trg.ohlcv_store(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
data=data,
|
||||
candle_type=candle_type
|
||||
)
|
||||
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)
|
||||
if 'pairs' in config:
|
||||
# Filter pairs
|
||||
paircombs = [comb for comb in paircombs if comb[0] in config['pairs']]
|
||||
|
||||
if 'timeframes' in config:
|
||||
paircombs = [comb for comb in paircombs if comb[1] in config['timeframes']]
|
||||
paircombs = [comb for comb in paircombs if comb[2] in candle_types]
|
||||
|
||||
paircombs = sorted(paircombs, key=lambda x: (x[0], x[1], x[2].value))
|
||||
|
||||
formatted_paircombs = '\n'.join([f"{pair}, {timeframe}, {candle_type}"
|
||||
for pair, timeframe, candle_type in paircombs])
|
||||
|
||||
logger.info(f"Converting candle (OHLCV) data for the following pair combinations:\n"
|
||||
f"{formatted_paircombs}")
|
||||
for pair, timeframe, candle_type in paircombs:
|
||||
data = src.ohlcv_load(pair=pair, timeframe=timeframe,
|
||||
timerange=None,
|
||||
fill_missing=False,
|
||||
drop_incomplete=False,
|
||||
startup_candles=0,
|
||||
candle_type=candle_type)
|
||||
logger.info(f"Converting {len(data)} {timeframe} {candle_type} candles for {pair}")
|
||||
if len(data) > 0:
|
||||
trg.ohlcv_store(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
data=data,
|
||||
candle_type=candle_type
|
||||
)
|
||||
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:
|
||||
|
||||
@@ -17,7 +17,7 @@ from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWith
|
||||
from freqtrade.data.history import load_pair_history
|
||||
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
||||
from freqtrade.exchange import Exchange, timeframe_to_prev_date, timeframe_to_seconds
|
||||
from freqtrade.exchange.types import OrderBook
|
||||
from freqtrade.misc import append_candles_to_dataframe
|
||||
from freqtrade.rpc import RPCManager
|
||||
@@ -46,6 +46,8 @@ class DataProvider:
|
||||
self.__rpc = rpc
|
||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||
self.__slice_index: Optional[int] = None
|
||||
self.__slice_date: Optional[datetime] = None
|
||||
|
||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||
self.__producer_pairs_df: Dict[str,
|
||||
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {}
|
||||
@@ -64,10 +66,19 @@ class DataProvider:
|
||||
def _set_dataframe_max_index(self, limit_index: int):
|
||||
"""
|
||||
Limit analyzed dataframe to max specified index.
|
||||
Only relevant in backtesting.
|
||||
:param limit_index: dataframe index.
|
||||
"""
|
||||
self.__slice_index = limit_index
|
||||
|
||||
def _set_dataframe_max_date(self, limit_date: datetime):
|
||||
"""
|
||||
Limit infomrative dataframe to max specified index.
|
||||
Only relevant in backtesting.
|
||||
:param limit_date: "current date"
|
||||
"""
|
||||
self.__slice_date = limit_date
|
||||
|
||||
def _set_cached_df(
|
||||
self,
|
||||
pair: str,
|
||||
@@ -284,7 +295,7 @@ class DataProvider:
|
||||
def historic_ohlcv(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: Optional[str] = None,
|
||||
timeframe: str,
|
||||
candle_type: str = ''
|
||||
) -> DataFrame:
|
||||
"""
|
||||
@@ -307,10 +318,10 @@ class DataProvider:
|
||||
timerange.subtract_start(tf_seconds * startup_candles)
|
||||
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
||||
pair=pair,
|
||||
timeframe=timeframe or self._config['timeframe'],
|
||||
timeframe=timeframe,
|
||||
datadir=self._config['datadir'],
|
||||
timerange=timerange,
|
||||
data_format=self._config.get('dataformat_ohlcv', 'json'),
|
||||
data_format=self._config['dataformat_ohlcv'],
|
||||
candle_type=_candle_type,
|
||||
|
||||
)
|
||||
@@ -354,7 +365,13 @@ class DataProvider:
|
||||
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||
else:
|
||||
# Get historical OHLCV data (cached on disk).
|
||||
timeframe = timeframe or self._config['timeframe']
|
||||
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||
# Cut date to timeframe-specific date.
|
||||
# This is necessary to prevent lookahead bias in callbacks through informative pairs.
|
||||
if self.__slice_date:
|
||||
cutoff_date = timeframe_to_prev_date(timeframe, self.__slice_date)
|
||||
data = data.loc[data['date'] < cutoff_date]
|
||||
if len(data) == 0:
|
||||
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
||||
return data
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
from pandas import DataFrame, read_feather, to_datetime
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
from .idatahandler import IDataHandler
|
||||
@@ -82,43 +82,41 @@ class FeatherDataHandler(IDataHandler):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
||||
def _trades_store(self, pair: str, data: DataFrame) -> None:
|
||||
"""
|
||||
Store trades data (list of Dicts) to file
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
self.create_dir_if_needed(filename)
|
||||
data.reset_index(drop=True).to_feather(filename, compression_level=9, compression='lz4')
|
||||
|
||||
tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
|
||||
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
|
||||
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
def trades_append(self, pair: str, data: DataFrame):
|
||||
"""
|
||||
Append data to existing files
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
# TODO: respect timerange ...
|
||||
:param pair: Load trades for this pair
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
:return: Dataframe containing trades
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
if not filename.exists():
|
||||
return []
|
||||
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||
|
||||
tradesdata = read_feather(filename)
|
||||
|
||||
return tradesdata.values.tolist()
|
||||
return tradesdata
|
||||
|
||||
@classmethod
|
||||
def _get_file_extension(cls):
|
||||
|
||||
@@ -5,7 +5,7 @@ import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
from .idatahandler import IDataHandler
|
||||
@@ -100,42 +100,42 @@ class HDF5DataHandler(IDataHandler):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
||||
def _trades_store(self, pair: str, data: pd.DataFrame) -> None:
|
||||
"""
|
||||
Store trades data (list of Dicts) to file
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
key = self._pair_trades_key(pair)
|
||||
|
||||
pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf(
|
||||
data.to_hdf(
|
||||
self._pair_trades_filename(self._datadir, pair), key,
|
||||
mode='a', complevel=9, complib='blosc',
|
||||
format='table', data_columns=['timestamp']
|
||||
)
|
||||
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
def trades_append(self, pair: str, data: pd.DataFrame):
|
||||
"""
|
||||
Append data to existing files
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> pd.DataFrame:
|
||||
"""
|
||||
Load a pair from h5 file.
|
||||
:param pair: Load trades for this pair
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
:return: Dataframe containing trades
|
||||
"""
|
||||
key = self._pair_trades_key(pair)
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
|
||||
if not filename.exists():
|
||||
return []
|
||||
return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||
where = []
|
||||
if timerange:
|
||||
if timerange.starttype == 'date':
|
||||
@@ -145,7 +145,7 @@ class HDF5DataHandler(IDataHandler):
|
||||
|
||||
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||
return trades.values.tolist()
|
||||
return trades
|
||||
|
||||
@classmethod
|
||||
def _get_file_extension(cls):
|
||||
|
||||
@@ -7,16 +7,19 @@ from typing import Dict, List, Optional, Tuple
|
||||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS, Config
|
||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
||||
DL_DATA_TIMEFRAMES, Config)
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||
trades_remove_duplicates, trades_to_ohlcv)
|
||||
trades_df_remove_duplicates, trades_list_to_df,
|
||||
trades_to_ohlcv)
|
||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.misc import format_ms_time
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.util import dt_ts, format_ms_time
|
||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||
from freqtrade.util.datetime_helpers import dt_now
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -68,7 +71,7 @@ def load_data(datadir: Path,
|
||||
fill_up_missing: bool = True,
|
||||
startup_candles: int = 0,
|
||||
fail_without_data: bool = False,
|
||||
data_format: str = 'json',
|
||||
data_format: str = 'feather',
|
||||
candle_type: CandleType = CandleType.SPOT,
|
||||
user_futures_funding_rate: Optional[int] = None,
|
||||
) -> Dict[str, DataFrame]:
|
||||
@@ -348,24 +351,27 @@ def _download_trades_history(exchange: Exchange,
|
||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||
|
||||
if trades and since < trades[0][0]:
|
||||
if not trades.empty and since > 0 and since < trades.iloc[0]['timestamp']:
|
||||
# since is before the first trade
|
||||
logger.info(f"Start earlier than available data. Redownloading trades for {pair}...")
|
||||
trades = []
|
||||
logger.info(f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than "
|
||||
f"available data. Redownloading trades for {pair}...")
|
||||
trades = trades_list_to_df([])
|
||||
|
||||
if not since:
|
||||
since = int((datetime.now() - timedelta(days=-new_pairs_days)).timestamp()) * 1000
|
||||
|
||||
from_id = trades[-1][1] if trades else None
|
||||
if trades and since < trades[-1][0]:
|
||||
from_id = trades.iloc[-1]['id'] if not trades.empty else None
|
||||
if not trades.empty and since < trades.iloc[-1]['timestamp']:
|
||||
# Reset since to the last available point
|
||||
# - 5 seconds (to ensure we're getting all trades)
|
||||
since = trades[-1][0] - (5 * 1000)
|
||||
since = trades.iloc[-1]['timestamp'] - (5 * 1000)
|
||||
logger.info(f"Using last trade date -5s - Downloading trades for {pair} "
|
||||
f"since: {format_ms_time(since)}.")
|
||||
|
||||
logger.debug(f"Current Start: {format_ms_time(trades[0][0]) if trades else 'None'}")
|
||||
logger.debug(f"Current End: {format_ms_time(trades[-1][0]) if trades else 'None'}")
|
||||
if not since:
|
||||
since = dt_ts(dt_now() - timedelta(days=new_pairs_days))
|
||||
|
||||
logger.debug("Current Start: %s", 'None' if trades.empty else
|
||||
f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}")
|
||||
logger.debug("Current End: %s", 'None' if trades.empty else
|
||||
f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}")
|
||||
logger.info(f"Current Amount of trades: {len(trades)}")
|
||||
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
@@ -374,13 +380,16 @@ def _download_trades_history(exchange: Exchange,
|
||||
until=until,
|
||||
from_id=from_id,
|
||||
)
|
||||
trades.extend(new_trades[1])
|
||||
new_trades_df = trades_list_to_df(new_trades[1])
|
||||
trades = concat([trades, new_trades_df], axis=0)
|
||||
# Remove duplicates to make sure we're not storing data we don't need
|
||||
trades = trades_remove_duplicates(trades)
|
||||
trades = trades_df_remove_duplicates(trades)
|
||||
data_handler.trades_store(pair, data=trades)
|
||||
|
||||
logger.debug(f"New Start: {format_ms_time(trades[0][0])}")
|
||||
logger.debug(f"New End: {format_ms_time(trades[-1][0])}")
|
||||
logger.debug("New Start: %s", 'None' if trades.empty else
|
||||
f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}")
|
||||
logger.debug("New End: %s", 'None' if trades.empty else
|
||||
f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}")
|
||||
logger.info(f"New Amount of trades: {len(trades)}")
|
||||
return True
|
||||
|
||||
@@ -393,7 +402,7 @@ def _download_trades_history(exchange: Exchange,
|
||||
|
||||
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
||||
timerange: TimeRange, new_pairs_days: int = 30,
|
||||
erase: bool = False, data_format: str = 'jsongz') -> List[str]:
|
||||
erase: bool = False, data_format: str = 'feather') -> List[str]:
|
||||
"""
|
||||
Refresh stored trades data for backtesting and hyperopt operations.
|
||||
Used by freqtrade download-data subcommand.
|
||||
@@ -426,8 +435,8 @@ def convert_trades_to_ohlcv(
|
||||
datadir: Path,
|
||||
timerange: TimeRange,
|
||||
erase: bool = False,
|
||||
data_format_ohlcv: str = 'json',
|
||||
data_format_trades: str = 'jsongz',
|
||||
data_format_ohlcv: str = 'feather',
|
||||
data_format_trades: str = 'feather',
|
||||
candle_type: CandleType = CandleType.SPOT
|
||||
) -> None:
|
||||
"""
|
||||
@@ -512,6 +521,8 @@ def download_data_main(config: Config) -> None:
|
||||
]
|
||||
|
||||
expanded_pairs = dynamic_expand_pairlist(config, available_pairs)
|
||||
if 'timeframes' not in config:
|
||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
||||
|
||||
# Manual validations of relevant settings
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
|
||||
@@ -15,8 +15,9 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade import misc
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
||||
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
||||
from freqtrade.constants import DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, trades_convert_types,
|
||||
trades_df_remove_duplicates, trim_dataframe)
|
||||
from freqtrade.enums import CandleType, TradingMode
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
|
||||
@@ -170,32 +171,42 @@ class IDataHandler(ABC):
|
||||
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||
|
||||
@abstractmethod
|
||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
||||
def _trades_store(self, pair: str, data: DataFrame) -> None:
|
||||
"""
|
||||
Store trades data (list of Dicts) to file
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
def trades_append(self, pair: str, data: DataFrame):
|
||||
"""
|
||||
Append data to existing files
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
:param pair: Load trades for this pair
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
:return: Dataframe containing trades
|
||||
"""
|
||||
|
||||
def trades_store(self, pair: str, data: DataFrame) -> None:
|
||||
"""
|
||||
Store trades data (list of Dicts) to file
|
||||
:param pair: Pair - used for filename
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
# Filter on expected columns (will remove the actual date column).
|
||||
self._trades_store(pair, data[DEFAULT_TRADES_COLUMNS])
|
||||
|
||||
def trades_purge(self, pair: str) -> bool:
|
||||
"""
|
||||
Remove data for this pair
|
||||
@@ -208,7 +219,7 @@ class IDataHandler(ABC):
|
||||
return True
|
||||
return False
|
||||
|
||||
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
Removes duplicates in the process.
|
||||
@@ -216,7 +227,10 @@ class IDataHandler(ABC):
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
"""
|
||||
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
||||
trades = trades_df_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
||||
|
||||
trades = trades_convert_types(trades)
|
||||
return trades
|
||||
|
||||
@classmethod
|
||||
def create_dir_if_needed(cls, datadir: Path):
|
||||
@@ -427,6 +441,6 @@ def get_datahandler(datadir: Path, data_format: Optional[str] = None,
|
||||
"""
|
||||
|
||||
if not data_handler:
|
||||
HandlerClass = get_datahandlerclass(data_format or 'json')
|
||||
HandlerClass = get_datahandlerclass(data_format or 'feather')
|
||||
data_handler = HandlerClass(datadir)
|
||||
return data_handler
|
||||
|
||||
@@ -6,8 +6,8 @@ from pandas import DataFrame, read_json, to_datetime
|
||||
|
||||
from freqtrade import misc
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
||||
from freqtrade.data.converter import trades_dict_to_list
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
||||
from freqtrade.data.converter import trades_dict_to_list, trades_list_to_df
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
from .idatahandler import IDataHandler
|
||||
@@ -94,45 +94,46 @@ class JsonDataHandler(IDataHandler):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
||||
def _trades_store(self, pair: str, data: DataFrame) -> None:
|
||||
"""
|
||||
Store trades data (list of Dicts) to file
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
misc.file_dump_json(filename, data, is_zip=self._use_zip)
|
||||
trades = data.values.tolist()
|
||||
misc.file_dump_json(filename, trades, is_zip=self._use_zip)
|
||||
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
def trades_append(self, pair: str, data: DataFrame):
|
||||
"""
|
||||
Append data to existing files
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
# TODO: respect timerange ...
|
||||
:param pair: Load trades for this pair
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
:return: Dataframe containing trades
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
tradesdata = misc.file_load_json(filename)
|
||||
|
||||
if not tradesdata:
|
||||
return []
|
||||
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||
|
||||
if isinstance(tradesdata[0], dict):
|
||||
# Convert trades dict to list
|
||||
logger.info("Old trades format detected - converting")
|
||||
tradesdata = trades_dict_to_list(tradesdata)
|
||||
pass
|
||||
return tradesdata
|
||||
return trades_list_to_df(tradesdata, convert=False)
|
||||
|
||||
@classmethod
|
||||
def _get_file_extension(cls):
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
from pandas import DataFrame, read_parquet, to_datetime
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
from .idatahandler import IDataHandler
|
||||
@@ -81,25 +81,22 @@ class ParquetDataHandler(IDataHandler):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
||||
def _trades_store(self, pair: str, data: DataFrame) -> None:
|
||||
"""
|
||||
Store trades data (list of Dicts) to file
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
self.create_dir_if_needed(filename)
|
||||
data.reset_index(drop=True).to_parquet(filename)
|
||||
|
||||
raise NotImplementedError()
|
||||
# array = pa.array(data)
|
||||
# array
|
||||
# feather.write_feather(data, filename)
|
||||
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
def trades_append(self, pair: str, data: DataFrame):
|
||||
"""
|
||||
Append data to existing files
|
||||
:param pair: Pair - used for filename
|
||||
:param data: List of Lists containing trade data,
|
||||
:param data: Dataframe containing trades
|
||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -112,14 +109,13 @@ class ParquetDataHandler(IDataHandler):
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
||||
# tradesdata = misc.file_load_json(filename)
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
if not filename.exists():
|
||||
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||
|
||||
# if not tradesdata:
|
||||
# return []
|
||||
tradesdata = read_parquet(filename)
|
||||
|
||||
# return tradesdata
|
||||
return tradesdata
|
||||
|
||||
@classmethod
|
||||
def _get_file_extension(cls):
|
||||
|
||||
+21
-18
@@ -194,32 +194,35 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
||||
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||
|
||||
|
||||
def calculate_expectancy(trades: pd.DataFrame) -> float:
|
||||
def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]:
|
||||
"""
|
||||
Calculate expectancy
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||
:return: expectancy
|
||||
:return: expectancy, expectancy_ratio
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
return 0
|
||||
|
||||
expectancy = 1
|
||||
expectancy = 0
|
||||
expectancy_ratio = 100
|
||||
|
||||
profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum()
|
||||
loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum())
|
||||
nb_win_trades = len(trades.loc[trades['profit_abs'] > 0])
|
||||
nb_loss_trades = len(trades.loc[trades['profit_abs'] < 0])
|
||||
if len(trades) > 0:
|
||||
winning_trades = trades.loc[trades['profit_abs'] > 0]
|
||||
losing_trades = trades.loc[trades['profit_abs'] < 0]
|
||||
profit_sum = winning_trades['profit_abs'].sum()
|
||||
loss_sum = abs(losing_trades['profit_abs'].sum())
|
||||
nb_win_trades = len(winning_trades)
|
||||
nb_loss_trades = len(losing_trades)
|
||||
|
||||
if (nb_win_trades > 0) and (nb_loss_trades > 0):
|
||||
average_win = profit_sum / nb_win_trades
|
||||
average_loss = loss_sum / nb_loss_trades
|
||||
risk_reward_ratio = average_win / average_loss
|
||||
winrate = nb_win_trades / len(trades)
|
||||
expectancy = ((1 + risk_reward_ratio) * winrate) - 1
|
||||
elif nb_win_trades == 0:
|
||||
expectancy = 0
|
||||
average_win = (profit_sum / nb_win_trades) if nb_win_trades > 0 else 0
|
||||
average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0
|
||||
winrate = (nb_win_trades / len(trades))
|
||||
loserate = (nb_loss_trades / len(trades))
|
||||
|
||||
return expectancy
|
||||
expectancy = (winrate * average_win) - (loserate * average_loss)
|
||||
if (average_loss > 0):
|
||||
risk_reward_ratio = average_win / average_loss
|
||||
expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1
|
||||
|
||||
return expectancy, expectancy_ratio
|
||||
|
||||
|
||||
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||
|
||||
@@ -115,7 +115,7 @@ class Edge:
|
||||
exchange=self.exchange,
|
||||
timeframe=self.strategy.timeframe,
|
||||
timerange=timerange_startup,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
data_format=self.config['dataformat_ohlcv'],
|
||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||
)
|
||||
# Download informative pairs too
|
||||
@@ -132,7 +132,7 @@ class Edge:
|
||||
exchange=self.exchange,
|
||||
timeframe=timeframe,
|
||||
timerange=timerange_startup,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
data_format=self.config['dataformat_ohlcv'],
|
||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||
)
|
||||
|
||||
@@ -142,7 +142,7 @@ class Edge:
|
||||
timeframe=self.strategy.timeframe,
|
||||
timerange=self._timerange,
|
||||
startup_candles=self.strategy.startup_candle_count,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
data_format=self.config['dataformat_ohlcv'],
|
||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||
)
|
||||
|
||||
@@ -172,13 +172,7 @@ class Edge:
|
||||
pair_data = pair_data.sort_values(by=['date'])
|
||||
pair_data = pair_data.reset_index(drop=True)
|
||||
|
||||
df_analyzed = self.strategy.advise_exit(
|
||||
dataframe=self.strategy.advise_entry(
|
||||
dataframe=pair_data,
|
||||
metadata={'pair': pair}
|
||||
),
|
||||
metadata={'pair': pair}
|
||||
)[headers].copy()
|
||||
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})[headers].copy()
|
||||
|
||||
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class Binance(Exchange):
|
||||
"tickers_have_price": False,
|
||||
"floor_leverage": True,
|
||||
"stop_price_type_field": "workingType",
|
||||
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: "CONTRACT_PRICE",
|
||||
PriceType.MARK: "MARK_PRICE",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+10
-25
@@ -7,10 +7,10 @@ import ccxt
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||
from freqtrade.enums.candletype import CandleType
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -27,8 +27,8 @@ class Bybit(Exchange):
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 200,
|
||||
"ohlcv_has_history": False,
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"ohlcv_has_history": True,
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"ohlcv_has_history": True,
|
||||
@@ -91,28 +91,13 @@ class Bybit(Exchange):
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
async def _fetch_funding_rate_history(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
limit: int,
|
||||
since_ms: Optional[int] = None,
|
||||
) -> List[List]:
|
||||
"""
|
||||
Fetch funding rate history
|
||||
Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed.
|
||||
"""
|
||||
params = {}
|
||||
if since_ms:
|
||||
until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit'])
|
||||
params.update({'until': until})
|
||||
# Funding rate
|
||||
data = await self._api_async.fetch_funding_rate_history(
|
||||
pair, since=since_ms,
|
||||
params=params)
|
||||
# Convert funding rate to candle pattern
|
||||
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
||||
return data
|
||||
def ohlcv_candle_limit(
|
||||
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
||||
|
||||
if candle_type in (CandleType.FUNDING_RATE):
|
||||
return 200
|
||||
|
||||
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
|
||||
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||
if self.trading_mode != TradingMode.SPOT:
|
||||
|
||||
@@ -5,6 +5,7 @@ Cryptocurrency Exchanges support
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import signal
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import floor
|
||||
@@ -80,9 +81,8 @@ class Exchange:
|
||||
"mark_ohlcv_price": "mark",
|
||||
"mark_ohlcv_timeframe": "8h",
|
||||
"ccxt_futures_name": "swap",
|
||||
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
|
||||
"marketOrderRequiresPrice": False,
|
||||
}
|
||||
@@ -264,8 +264,6 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
|
||||
|
||||
self.set_sandbox(api, exchange_config, name)
|
||||
|
||||
return api
|
||||
|
||||
@property
|
||||
@@ -466,16 +464,6 @@ class Exchange:
|
||||
return amount_to_contract_precision(amount, self.get_precision_amount(pair),
|
||||
self.precisionMode, contract_size)
|
||||
|
||||
def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None:
|
||||
if exchange_config.get('sandbox'):
|
||||
if api.urls.get('test'):
|
||||
api.urls['api'] = api.urls['test']
|
||||
logger.info("Enabled Sandbox API on %s", name)
|
||||
else:
|
||||
logger.warning(
|
||||
f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json")
|
||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||
|
||||
def _load_async_markets(self, reload: bool = False) -> None:
|
||||
try:
|
||||
if self._api_async:
|
||||
@@ -581,7 +569,7 @@ class Exchange:
|
||||
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
||||
if pair in self.markets and self.markets[pair].get('active'):
|
||||
return pair
|
||||
raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||
|
||||
def validate_timeframes(self, timeframe: Optional[str]) -> None:
|
||||
"""
|
||||
@@ -1859,9 +1847,6 @@ class Exchange:
|
||||
if fee_curr is None:
|
||||
return None
|
||||
fee_cost = float(fee['cost'])
|
||||
if self._ft_has['fee_cost_in_contracts']:
|
||||
# Convert cost via "contracts" conversion
|
||||
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
|
||||
|
||||
# Calculate fee based on order details
|
||||
if fee_curr == self.get_pair_base_currency(symbol):
|
||||
@@ -1880,7 +1865,7 @@ class Exchange:
|
||||
tick = self.fetch_ticker(comb)
|
||||
|
||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
||||
except ExchangeError:
|
||||
except (ValueError, ExchangeError):
|
||||
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
|
||||
if not fee_to_quote_rate:
|
||||
return None
|
||||
@@ -2167,7 +2152,7 @@ class Exchange:
|
||||
except IndexError:
|
||||
logger.exception("Error loading %s. Result was %s.", pair, data)
|
||||
return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
|
||||
logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
|
||||
logger.debug("Done fetching pair %s, %s interval %s...", pair, candle_type, timeframe)
|
||||
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
|
||||
|
||||
except ccxt.NotSupported as e:
|
||||
@@ -2269,20 +2254,24 @@ class Exchange:
|
||||
from_id = t[-1][1]
|
||||
trades.extend(t[:-1])
|
||||
while True:
|
||||
t = await self._async_fetch_trades(pair,
|
||||
params={self._trades_pagination_arg: from_id})
|
||||
if t:
|
||||
# Skip last id since its the key for the next call
|
||||
trades.extend(t[:-1])
|
||||
if from_id == t[-1][1] or t[-1][0] > until:
|
||||
logger.debug(f"Stopping because from_id did not change. "
|
||||
f"Reached {t[-1][0]} > {until}")
|
||||
# Reached the end of the defined-download period - add last trade as well.
|
||||
trades.extend(t[-1:])
|
||||
break
|
||||
try:
|
||||
t = await self._async_fetch_trades(pair,
|
||||
params={self._trades_pagination_arg: from_id})
|
||||
if t:
|
||||
# Skip last id since its the key for the next call
|
||||
trades.extend(t[:-1])
|
||||
if from_id == t[-1][1] or t[-1][0] > until:
|
||||
logger.debug(f"Stopping because from_id did not change. "
|
||||
f"Reached {t[-1][0]} > {until}")
|
||||
# Reached the end of the defined-download period - add last trade as well.
|
||||
trades.extend(t[-1:])
|
||||
break
|
||||
|
||||
from_id = t[-1][1]
|
||||
else:
|
||||
from_id = t[-1][1]
|
||||
else:
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||
break
|
||||
|
||||
return (pair, trades)
|
||||
@@ -2302,16 +2291,20 @@ class Exchange:
|
||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||
while True:
|
||||
t = await self._async_fetch_trades(pair, since=since)
|
||||
if t:
|
||||
since = t[-1][0]
|
||||
trades.extend(t)
|
||||
# Reached the end of the defined-download period
|
||||
if until and t[-1][0] > until:
|
||||
logger.debug(
|
||||
f"Stopping because until was reached. {t[-1][0]} > {until}")
|
||||
try:
|
||||
t = await self._async_fetch_trades(pair, since=since)
|
||||
if t:
|
||||
since = t[-1][0]
|
||||
trades.extend(t)
|
||||
# Reached the end of the defined-download period
|
||||
if until and t[-1][0] > until:
|
||||
logger.debug(
|
||||
f"Stopping because until was reached. {t[-1][0]} > {until}")
|
||||
break
|
||||
else:
|
||||
break
|
||||
else:
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||
break
|
||||
|
||||
return (pair, trades)
|
||||
@@ -2360,9 +2353,16 @@ class Exchange:
|
||||
raise OperationalException("This exchange does not support downloading Trades.")
|
||||
|
||||
with self._loop_lock:
|
||||
return self.loop.run_until_complete(
|
||||
self._async_get_trade_history(pair=pair, since=since,
|
||||
until=until, from_id=from_id))
|
||||
task = asyncio.ensure_future(self._async_get_trade_history(
|
||||
pair=pair, since=since, until=until, from_id=from_id))
|
||||
|
||||
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||
try:
|
||||
self.loop.add_signal_handler(sig, task.cancel)
|
||||
except NotImplementedError:
|
||||
# Not all platforms implement signals (e.g. windows)
|
||||
pass
|
||||
return self.loop.run_until_complete(task)
|
||||
|
||||
@retrier
|
||||
def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
|
||||
|
||||
@@ -33,8 +33,6 @@ class Gate(Exchange):
|
||||
_ft_has_futures: Dict = {
|
||||
"needs_trading_fees": True,
|
||||
"marketOrderRequiresPrice": False,
|
||||
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||
"stop_price_type_field": "price_type",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: 0,
|
||||
|
||||
@@ -32,7 +32,6 @@ class Okx(Exchange):
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"tickers_have_quoteVolume": False,
|
||||
"fee_cost_in_contracts": True,
|
||||
"stop_price_type_field": "slTriggerPxType",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: "last",
|
||||
|
||||
@@ -11,6 +11,8 @@ from gymnasium import spaces
|
||||
from gymnasium.utils import seeding
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -80,8 +82,9 @@ class BaseEnvironment(gym.Env):
|
||||
self.can_short: bool = can_short
|
||||
self.live: bool = live
|
||||
if not self.live and self.add_state_info:
|
||||
self.add_state_info = False
|
||||
logger.warning("add_state_info is not available in backtesting. Deactivating.")
|
||||
raise OperationalException("`add_state_info` is not available in backtesting. Change "
|
||||
"parameter to false in your rl_config. See `add_state_info` "
|
||||
"docs for more info.")
|
||||
self.seed(seed)
|
||||
self.reset_env(df, prices, window_size, reward_kwargs, starting_point)
|
||||
|
||||
|
||||
@@ -375,7 +375,7 @@ class FreqaiDataDrawer:
|
||||
num_keep = self.freqai_info["purge_old_models"]
|
||||
if not num_keep:
|
||||
return
|
||||
elif type(num_keep) == bool:
|
||||
elif isinstance(num_keep, bool):
|
||||
num_keep = 2
|
||||
|
||||
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
||||
@@ -635,7 +635,7 @@ class FreqaiDataDrawer:
|
||||
timeframe=tf,
|
||||
pair=pair,
|
||||
timerange=timerange,
|
||||
data_format=self.config.get("dataformat_ohlcv", "json"),
|
||||
data_format=self.config.get("dataformat_ohlcv", "feather"),
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
|
||||
|
||||
@@ -86,8 +86,6 @@ class IFreqaiModel(ABC):
|
||||
logger.warning("DI threshold is not configured for Keras models yet. Deactivating.")
|
||||
|
||||
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.class_names: List[str] = [] # used in classification subclasses
|
||||
self.pair_it = 0
|
||||
self.pair_it_train = 0
|
||||
@@ -676,15 +674,6 @@ class IFreqaiModel(ABC):
|
||||
hist_preds_df['close_price'] = strat_df['close']
|
||||
hist_preds_df['date_pred'] = strat_df['date']
|
||||
|
||||
# # for keras type models, the conv_window needs to be prepended so
|
||||
# # viewing is correct in frequi
|
||||
if self.ft_params.get('inlier_metric_window', 0):
|
||||
n_lost_points = self.freqai_info.get('conv_width', 2)
|
||||
zeros_df = DataFrame(np.zeros((n_lost_points, len(hist_preds_df.columns))),
|
||||
columns=hist_preds_df.columns)
|
||||
self.dd.historic_predictions[pair] = pd.concat(
|
||||
[zeros_df, hist_preds_df], axis=0, ignore_index=True)
|
||||
|
||||
def fit_live_predictions(self, dk: FreqaiDataKitchen, pair: str) -> None:
|
||||
"""
|
||||
Fit the labels with a gaussian distribution
|
||||
|
||||
@@ -32,8 +32,8 @@ class LightGBMClassifier(BaseClassifierModel):
|
||||
eval_set = None
|
||||
test_weights = None
|
||||
else:
|
||||
eval_set = (data_dictionary["test_features"].to_numpy(),
|
||||
data_dictionary["test_labels"].to_numpy()[:, 0])
|
||||
eval_set = [(data_dictionary["test_features"].to_numpy(),
|
||||
data_dictionary["test_labels"].to_numpy()[:, 0])]
|
||||
test_weights = data_dictionary["test_weights"]
|
||||
X = data_dictionary["train_features"].to_numpy()
|
||||
y = data_dictionary["train_labels"].to_numpy()[:, 0]
|
||||
@@ -42,7 +42,6 @@ class LightGBMClassifier(BaseClassifierModel):
|
||||
init_model = self.get_init_model(dk.pair)
|
||||
|
||||
model = LGBMClassifier(**self.model_training_parameters)
|
||||
|
||||
model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights,
|
||||
eval_sample_weight=[test_weights], init_model=init_model)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class LightGBMRegressor(BaseRegressionModel):
|
||||
eval_set = None
|
||||
eval_weights = None
|
||||
else:
|
||||
eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"])
|
||||
eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])]
|
||||
eval_weights = data_dictionary["test_weights"]
|
||||
X = data_dictionary["train_features"]
|
||||
y = data_dictionary["train_labels"]
|
||||
|
||||
@@ -42,10 +42,10 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel):
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -26,9 +26,9 @@ class PyTorchMLPClassifier(BasePyTorchClassifier):
|
||||
"model_training_parameters" : {
|
||||
"learning_rate": 3e-4,
|
||||
"trainer_kwargs": {
|
||||
"max_iters": 5000,
|
||||
"n_steps": 5000,
|
||||
"batch_size": 64,
|
||||
"max_n_eval_batches": null,
|
||||
"n_epochs": null,
|
||||
},
|
||||
"model_kwargs": {
|
||||
"hidden_dim": 512,
|
||||
|
||||
@@ -27,9 +27,9 @@ class PyTorchMLPRegressor(BasePyTorchRegressor):
|
||||
"model_training_parameters" : {
|
||||
"learning_rate": 3e-4,
|
||||
"trainer_kwargs": {
|
||||
"max_iters": 5000,
|
||||
"n_steps": 5000,
|
||||
"batch_size": 64,
|
||||
"max_n_eval_batches": null,
|
||||
"n_epochs": null,
|
||||
},
|
||||
"model_kwargs": {
|
||||
"hidden_dim": 512,
|
||||
|
||||
@@ -30,9 +30,9 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
||||
"model_training_parameters" : {
|
||||
"learning_rate": 3e-4,
|
||||
"trainer_kwargs": {
|
||||
"max_iters": 5000,
|
||||
"n_steps": 5000,
|
||||
"batch_size": 64,
|
||||
"max_n_eval_batches": null
|
||||
"n_epochs": null
|
||||
},
|
||||
"model_kwargs": {
|
||||
"hidden_dim": 512,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
import torch
|
||||
@@ -12,14 +11,14 @@ class PyTorchDataConvertor(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
||||
def convert_x(self, df: pd.DataFrame, device: str) -> torch.Tensor:
|
||||
"""
|
||||
:param df: "*_features" dataframe.
|
||||
:param device: The device to use for training (e.g. 'cpu', 'cuda').
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
||||
def convert_y(self, df: pd.DataFrame, device: str) -> torch.Tensor:
|
||||
"""
|
||||
:param df: "*_labels" dataframe.
|
||||
:param device: The device to use for training (e.g. 'cpu', 'cuda').
|
||||
@@ -33,8 +32,8 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_tensor_type: Optional[torch.dtype] = None,
|
||||
squeeze_target_tensor: bool = False
|
||||
target_tensor_type: torch.dtype = torch.float32,
|
||||
squeeze_target_tensor: bool = False,
|
||||
):
|
||||
"""
|
||||
:param target_tensor_type: type of target tensor, for classification use
|
||||
@@ -45,23 +44,14 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor):
|
||||
self._target_tensor_type = target_tensor_type
|
||||
self._squeeze_target_tensor = squeeze_target_tensor
|
||||
|
||||
def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
||||
x = torch.from_numpy(df.values).float()
|
||||
if device:
|
||||
x = x.to(device)
|
||||
|
||||
def convert_x(self, df: pd.DataFrame, device: str) -> torch.Tensor:
|
||||
numpy_arrays = df.values
|
||||
x = torch.tensor(numpy_arrays, device=device, dtype=torch.float32)
|
||||
return x
|
||||
|
||||
def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
||||
y = torch.from_numpy(df.values)
|
||||
|
||||
if self._target_tensor_type:
|
||||
y = y.to(self._target_tensor_type)
|
||||
|
||||
def convert_y(self, df: pd.DataFrame, device: str) -> torch.Tensor:
|
||||
numpy_arrays = df.values
|
||||
y = torch.tensor(numpy_arrays, device=device, dtype=self._target_tensor_type)
|
||||
if self._squeeze_target_tensor:
|
||||
y = y.squeeze()
|
||||
|
||||
if device:
|
||||
y = y.to(device)
|
||||
|
||||
return y
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -40,23 +39,27 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
||||
state_dict and model_meta_data saved by self.save() method.
|
||||
:param model_meta_data: Additional metadata about the model (optional).
|
||||
:param data_convertor: convertor from pd.DataFrame to torch.tensor.
|
||||
:param max_iters: The number of training iterations to run.
|
||||
iteration here refers to the number of times we call
|
||||
self.optimizer.step(). used to calculate n_epochs.
|
||||
:param n_steps: used to calculate n_epochs. The number of training iterations to run.
|
||||
iteration here refers to the number of times optimizer.step() is called.
|
||||
ignored if n_epochs is set.
|
||||
:param n_epochs: The maximum number batches to use for evaluation.
|
||||
:param batch_size: The size of the batches to use during training.
|
||||
:param max_n_eval_batches: The maximum number batches to use for evaluation.
|
||||
"""
|
||||
self.model = model
|
||||
self.optimizer = optimizer
|
||||
self.criterion = criterion
|
||||
self.model_meta_data = model_meta_data
|
||||
self.device = device
|
||||
self.max_iters: int = kwargs.get("max_iters", 100)
|
||||
self.n_epochs: Optional[int] = kwargs.get("n_epochs", 10)
|
||||
self.n_steps: Optional[int] = kwargs.get("n_steps", None)
|
||||
if self.n_steps is None and not self.n_epochs:
|
||||
raise Exception("Either `n_steps` or `n_epochs` should be set.")
|
||||
|
||||
self.batch_size: int = kwargs.get("batch_size", 64)
|
||||
self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None)
|
||||
self.data_convertor = data_convertor
|
||||
self.window_size: int = window_size
|
||||
self.tb_logger = tb_logger
|
||||
self.test_batch_counter = 0
|
||||
|
||||
def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]):
|
||||
"""
|
||||
@@ -72,55 +75,46 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
||||
backpropagation.
|
||||
- Updates the model's parameters using an optimizer.
|
||||
"""
|
||||
data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits)
|
||||
epochs = self.calc_n_epochs(
|
||||
n_obs=len(data_dictionary["train_features"]),
|
||||
batch_size=self.batch_size,
|
||||
n_iters=self.max_iters
|
||||
)
|
||||
self.model.train()
|
||||
for epoch in range(1, epochs + 1):
|
||||
for i, batch_data in enumerate(data_loaders_dictionary["train"]):
|
||||
|
||||
data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits)
|
||||
n_obs = len(data_dictionary["train_features"])
|
||||
n_epochs = self.n_epochs or self.calc_n_epochs(n_obs=n_obs)
|
||||
batch_counter = 0
|
||||
for _ in range(n_epochs):
|
||||
for _, batch_data in enumerate(data_loaders_dictionary["train"]):
|
||||
xb, yb = batch_data
|
||||
xb.to(self.device)
|
||||
yb.to(self.device)
|
||||
xb = xb.to(self.device)
|
||||
yb = yb.to(self.device)
|
||||
yb_pred = self.model(xb)
|
||||
loss = self.criterion(yb_pred, yb)
|
||||
|
||||
self.optimizer.zero_grad(set_to_none=True)
|
||||
loss.backward()
|
||||
self.optimizer.step()
|
||||
self.tb_logger.log_scalar("train_loss", loss.item(), i)
|
||||
self.tb_logger.log_scalar("train_loss", loss.item(), batch_counter)
|
||||
batch_counter += 1
|
||||
|
||||
# evaluation
|
||||
if "test" in splits:
|
||||
self.estimate_loss(
|
||||
data_loaders_dictionary,
|
||||
self.max_n_eval_batches,
|
||||
"test"
|
||||
)
|
||||
self.estimate_loss(data_loaders_dictionary, "test")
|
||||
|
||||
@torch.no_grad()
|
||||
def estimate_loss(
|
||||
self,
|
||||
data_loader_dictionary: Dict[str, DataLoader],
|
||||
max_n_eval_batches: Optional[int],
|
||||
split: str,
|
||||
) -> None:
|
||||
self.model.eval()
|
||||
n_batches = 0
|
||||
for i, batch_data in enumerate(data_loader_dictionary[split]):
|
||||
if max_n_eval_batches and i > max_n_eval_batches:
|
||||
n_batches += 1
|
||||
break
|
||||
for _, batch_data in enumerate(data_loader_dictionary[split]):
|
||||
xb, yb = batch_data
|
||||
xb.to(self.device)
|
||||
yb.to(self.device)
|
||||
xb = xb.to(self.device)
|
||||
yb = yb.to(self.device)
|
||||
|
||||
yb_pred = self.model(xb)
|
||||
loss = self.criterion(yb_pred, yb)
|
||||
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), i)
|
||||
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), self.test_batch_counter)
|
||||
self.test_batch_counter += 1
|
||||
|
||||
self.model.train()
|
||||
|
||||
@@ -148,31 +142,30 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
||||
|
||||
return data_loader_dictionary
|
||||
|
||||
@staticmethod
|
||||
def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int:
|
||||
def calc_n_epochs(self, n_obs: int) -> int:
|
||||
"""
|
||||
Calculates the number of epochs required to reach the maximum number
|
||||
of iterations specified in the model training parameters.
|
||||
|
||||
the motivation here is that `max_iters` is easier to optimize and keep stable,
|
||||
the motivation here is that `n_steps` is easier to optimize and keep stable,
|
||||
across different n_obs - the number of data points.
|
||||
"""
|
||||
assert isinstance(self.n_steps, int), "Either `n_steps` or `n_epochs` should be set."
|
||||
n_batches = n_obs // self.batch_size
|
||||
n_epochs = min(self.n_steps // n_batches, 1)
|
||||
if n_epochs <= 10:
|
||||
logger.warning(
|
||||
f"Setting low n_epochs: {n_epochs}. "
|
||||
f"Please consider increasing `n_steps` hyper-parameter."
|
||||
)
|
||||
|
||||
n_batches = math.ceil(n_obs // batch_size)
|
||||
epochs = math.ceil(n_iters // n_batches)
|
||||
if epochs <= 10:
|
||||
logger.warning("User set `max_iters` in such a way that the trainer will only perform "
|
||||
f" {epochs} epochs. Please consider increasing this value accordingly")
|
||||
if epochs <= 1:
|
||||
logger.warning("Epochs set to 1. Please review your `max_iters` value")
|
||||
epochs = 1
|
||||
return epochs
|
||||
return n_epochs
|
||||
|
||||
def save(self, path: Path):
|
||||
"""
|
||||
- Saving any nn.Module state_dict
|
||||
- Saving model_meta_data, this dict should contain any additional data that the
|
||||
user needs to store. e.g class_names for classification models.
|
||||
user needs to store. e.g. class_names for classification models.
|
||||
"""
|
||||
|
||||
torch.save({
|
||||
|
||||
@@ -50,7 +50,7 @@ def download_all_data_for_training(dp: DataProvider, config: Config) -> None:
|
||||
timerange=timerange,
|
||||
new_pairs_days=new_pairs_days,
|
||||
erase=False,
|
||||
data_format=config.get("dataformat_ohlcv", "json"),
|
||||
data_format=config.get("dataformat_ohlcv", "feather"),
|
||||
trading_mode=config.get("trading_mode", "spot"),
|
||||
prepend=config.get("prepend_data", False),
|
||||
)
|
||||
|
||||
@@ -231,7 +231,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.manage_open_orders()
|
||||
|
||||
# Protect from collisions with force_exit.
|
||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||
# Without this, freqtrade may try to recreate stoploss_on_exchange orders
|
||||
# while exiting is in process, since telegram messages arrive in an different thread.
|
||||
with self._exit_lock:
|
||||
trades = Trade.get_open_trades()
|
||||
@@ -613,6 +613,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
for trade in Trade.get_open_trades():
|
||||
# If there is any open orders, wait for them to finish.
|
||||
if trade.open_order_id is None:
|
||||
# Do a wallets update (will be ratelimited to once per hour)
|
||||
self.wallets.update(False)
|
||||
try:
|
||||
self.check_and_call_adjust_trade_position(trade)
|
||||
except DependencyException as exception:
|
||||
@@ -1383,7 +1385,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
latest_candle_close_date = timeframe_to_next_date(self.strategy.timeframe,
|
||||
latest_candle_open_date)
|
||||
# Check if new candle
|
||||
if order_obj and latest_candle_close_date > order_obj.order_date_utc:
|
||||
if (
|
||||
order_obj and order_obj.side == trade.entry_side
|
||||
and latest_candle_close_date > order_obj.order_date_utc
|
||||
):
|
||||
# New candle
|
||||
proposed_rate = self.exchange.get_rate(
|
||||
trade.pair, side='entry', is_short=trade.is_short, refresh=True)
|
||||
@@ -1939,6 +1944,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
Applies the fee to amount (either from Order or from Trades).
|
||||
Can eat into dust if more than the required asset is available.
|
||||
In case of trade adjustment orders, trade.amount will not have been adjusted yet.
|
||||
Can't happen in Futures mode - where Fees are always in settlement currency,
|
||||
never in base currency.
|
||||
"""
|
||||
@@ -1948,6 +1954,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
# check against remaining amount!
|
||||
amount_ = trade.amount - amount
|
||||
|
||||
if trade.nr_of_successful_entries >= 1 and order_obj.ft_order_side == trade.entry_side:
|
||||
# In case of rebuy's, trade.amount doesn't contain the amount of the last entry.
|
||||
amount_ = trade.amount + amount
|
||||
|
||||
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount_:
|
||||
# Eat into dust if we own more than base currency
|
||||
logger.info(f"Fee amount for {trade} was in base currency - "
|
||||
@@ -1977,7 +1987,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Init variables
|
||||
order_amount = safe_value_fallback(order, 'filled', 'amount')
|
||||
# Only run for closed orders
|
||||
if trade.fee_updated(order.get('side', '')) or order['status'] == 'open':
|
||||
if (
|
||||
trade.fee_updated(order.get('side', ''))
|
||||
or order['status'] == 'open'
|
||||
or order_obj.ft_fee_base
|
||||
):
|
||||
return None
|
||||
|
||||
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
|
||||
|
||||
+7
-33
@@ -3,7 +3,6 @@ Various tool function for Freqtrade and scripts
|
||||
"""
|
||||
import gzip
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union
|
||||
from urllib.parse import urlparse
|
||||
@@ -117,20 +116,19 @@ def file_load_json(file: Path):
|
||||
return pairdata
|
||||
|
||||
|
||||
def is_file_in_dir(file: Path, directory: Path) -> bool:
|
||||
"""
|
||||
Helper function to check if file is in directory.
|
||||
"""
|
||||
return file.is_file() and file.parent.samefile(directory)
|
||||
|
||||
|
||||
def pair_to_filename(pair: str) -> str:
|
||||
for ch in ['/', ' ', '.', '@', '$', '+', ':']:
|
||||
pair = pair.replace(ch, '_')
|
||||
return pair
|
||||
|
||||
|
||||
def format_ms_time(date: int) -> str:
|
||||
"""
|
||||
convert MS date to readable format.
|
||||
: epoch-string in ms
|
||||
"""
|
||||
return datetime.fromtimestamp(date / 1000.0).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
|
||||
def deep_merge_dicts(source, destination, allow_null_overrides: bool = True):
|
||||
"""
|
||||
Values from Source override destination, destination is returned (and modified!!)
|
||||
@@ -194,30 +192,6 @@ def plural(num: float, singular: str, plural: Optional[str] = None) -> str:
|
||||
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
||||
|
||||
|
||||
def render_template(templatefile: str, arguments: dict = {}) -> str:
|
||||
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
|
||||
env = Environment(
|
||||
loader=PackageLoader('freqtrade', 'templates'),
|
||||
autoescape=select_autoescape(['html', 'xml'])
|
||||
)
|
||||
template = env.get_template(templatefile)
|
||||
return template.render(**arguments)
|
||||
|
||||
|
||||
def render_template_with_fallback(templatefile: str, templatefallbackfile: str,
|
||||
arguments: dict = {}) -> str:
|
||||
"""
|
||||
Use templatefile if possible, otherwise fall back to templatefallbackfile
|
||||
"""
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
try:
|
||||
return render_template(templatefile, arguments)
|
||||
except TemplateNotFound:
|
||||
return render_template(templatefallbackfile, arguments)
|
||||
|
||||
|
||||
def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]:
|
||||
"""
|
||||
Split lst into chunks of the size n.
|
||||
|
||||
@@ -39,6 +39,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.types import BacktestResultType, get_BacktestResultType_default
|
||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -77,7 +78,7 @@ class Backtesting:
|
||||
|
||||
LoggingMixin.show_output = False
|
||||
self.config = config
|
||||
self.results: Dict[str, Any] = {}
|
||||
self.results: BacktestResultType = get_BacktestResultType_default()
|
||||
self.trade_id_counter: int = 0
|
||||
self.order_id_counter: int = 0
|
||||
|
||||
@@ -239,7 +240,7 @@ class Backtesting:
|
||||
timerange=self.timerange,
|
||||
startup_candles=self.config['startup_candle_count'],
|
||||
fail_without_data=True,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
data_format=self.config['dataformat_ohlcv'],
|
||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
|
||||
)
|
||||
|
||||
@@ -268,7 +269,7 @@ class Backtesting:
|
||||
timerange=self.timerange,
|
||||
startup_candles=0,
|
||||
fail_without_data=True,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
data_format=self.config['dataformat_ohlcv'],
|
||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
|
||||
)
|
||||
else:
|
||||
@@ -282,7 +283,7 @@ class Backtesting:
|
||||
timerange=self.timerange,
|
||||
startup_candles=0,
|
||||
fail_without_data=True,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
data_format=self.config['dataformat_ohlcv'],
|
||||
candle_type=CandleType.FUNDING_RATE
|
||||
)
|
||||
|
||||
@@ -294,7 +295,7 @@ class Backtesting:
|
||||
timerange=self.timerange,
|
||||
startup_candles=0,
|
||||
fail_without_data=True,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
data_format=self.config['dataformat_ohlcv'],
|
||||
candle_type=CandleType.from_string(self.exchange.get_option("mark_ohlcv_price"))
|
||||
)
|
||||
# Combine data to avoid combining the data per trade.
|
||||
@@ -367,18 +368,15 @@ class Backtesting:
|
||||
if not pair_data.empty:
|
||||
# Cleanup from prior runs
|
||||
pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore')
|
||||
|
||||
df_analyzed = self.strategy.advise_exit(
|
||||
self.strategy.advise_entry(pair_data, {'pair': pair}),
|
||||
{'pair': pair}
|
||||
).copy()
|
||||
# Trim startup period from analyzed dataframe
|
||||
df_analyzed = processed[pair] = pair_data = trim_dataframe(
|
||||
df_analyzed, self.timerange, startup_candles=self.required_startup)
|
||||
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})
|
||||
# Update dataprovider cache
|
||||
self.dataprovider._set_cached_df(
|
||||
pair, self.timeframe, df_analyzed, self.config['candle_type_def'])
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
df_analyzed = processed[pair] = pair_data = trim_dataframe(
|
||||
df_analyzed, self.timerange, startup_candles=self.required_startup)
|
||||
|
||||
# Create a copy of the dataframe before shifting, that way the entry signal/tag
|
||||
# remains on the correct candle for callbacks.
|
||||
df_analyzed = df_analyzed.copy()
|
||||
@@ -570,8 +568,7 @@ class Backtesting:
|
||||
pos_trade = self._get_exit_for_signal(trade, row, exit_, amount)
|
||||
if pos_trade is not None:
|
||||
order = pos_trade.orders[-1]
|
||||
if self._get_order_filled(order.ft_price, row):
|
||||
order.close_bt_order(current_date, trade)
|
||||
if self._try_close_open_order(order, trade, current_date, row):
|
||||
trade.recalc_trade_from_orders()
|
||||
self.wallets.update()
|
||||
return pos_trade
|
||||
@@ -582,6 +579,19 @@ class Backtesting:
|
||||
""" Rate is within candle, therefore filled"""
|
||||
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
||||
|
||||
def _try_close_open_order(
|
||||
self, order: Optional[Order], trade: LocalTrade, current_date: datetime,
|
||||
row: Tuple) -> bool:
|
||||
"""
|
||||
Check if an order is open and if it should've filled.
|
||||
:return: True if the order filled.
|
||||
"""
|
||||
if order and self._get_order_filled(order.ft_price, row):
|
||||
order.close_bt_order(current_date, trade)
|
||||
trade.open_order_id = None
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_exit_for_signal(
|
||||
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
|
||||
amount: Optional[float] = None) -> Optional[LocalTrade]:
|
||||
@@ -679,6 +689,7 @@ class Backtesting:
|
||||
remaining=amount,
|
||||
cost=amount * close_rate,
|
||||
)
|
||||
order._trade_bt = trade
|
||||
trade.orders.append(order)
|
||||
return trade
|
||||
|
||||
@@ -901,12 +912,11 @@ class Backtesting:
|
||||
amount=amount,
|
||||
filled=0,
|
||||
remaining=amount,
|
||||
cost=stake_amount + trade.fee_open,
|
||||
cost=amount * propose_rate + trade.fee_open,
|
||||
)
|
||||
order._trade_bt = trade
|
||||
trade.orders.append(order)
|
||||
if pos_adjust and self._get_order_filled(order.ft_price, row):
|
||||
order.close_bt_order(current_time, trade)
|
||||
else:
|
||||
if not self._try_close_open_order(order, trade, current_time, row):
|
||||
trade.open_order_id = str(self.order_id_counter)
|
||||
trade.recalc_trade_from_orders()
|
||||
|
||||
@@ -1122,23 +1132,18 @@ class Backtesting:
|
||||
for trade in list(LocalTrade.bt_trades_open_pp[pair]):
|
||||
# 3. Process entry orders.
|
||||
order = trade.select_order(trade.entry_side, is_open=True)
|
||||
if order and self._get_order_filled(order.ft_price, row):
|
||||
order.close_bt_order(current_time, trade)
|
||||
trade.open_order_id = None
|
||||
if self._try_close_open_order(order, trade, current_time, row):
|
||||
self.wallets.update()
|
||||
|
||||
# 4. Create exit orders (if any)
|
||||
# 4. Create exit orders (if any)
|
||||
if not trade.open_order_id:
|
||||
self._check_trade_exit(trade, row) # Place exit order if necessary
|
||||
|
||||
# 5. Process exit orders.
|
||||
# 5. Process exit orders.
|
||||
order = trade.select_order(trade.exit_side, is_open=True)
|
||||
if order and self._get_order_filled(order.ft_price, row):
|
||||
order.close_bt_order(current_time, trade)
|
||||
trade.open_order_id = None
|
||||
if order and self._try_close_open_order(order, trade, current_time, row):
|
||||
sub_trade = order.safe_amount_after_fee != trade.amount
|
||||
if sub_trade:
|
||||
order.close_bt_order(current_time, trade)
|
||||
trade.recalc_trade_from_orders()
|
||||
else:
|
||||
trade.close_date = current_time
|
||||
@@ -1192,7 +1197,8 @@ class Backtesting:
|
||||
|
||||
row_index += 1
|
||||
indexes[pair] = row_index
|
||||
self.dataprovider._set_dataframe_max_index(row_index)
|
||||
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index)
|
||||
self.dataprovider._set_dataframe_max_date(current_time)
|
||||
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
|
||||
|
||||
@@ -1225,12 +1231,14 @@ class Backtesting:
|
||||
is_first = True
|
||||
current_time_det = current_time
|
||||
for det_row in detail_data[HEADERS].values.tolist():
|
||||
self.dataprovider._set_dataframe_max_date(current_time_det)
|
||||
open_trade_count_start = self.backtest_loop(
|
||||
det_row, pair, current_time_det, end_date,
|
||||
open_trade_count_start, trade_dir, is_first)
|
||||
current_time_det += timedelta(minutes=self.timeframe_detail_min)
|
||||
is_first = False
|
||||
else:
|
||||
self.dataprovider._set_dataframe_max_date(current_time)
|
||||
open_trade_count_start = self.backtest_loop(
|
||||
row, pair, current_time, end_date,
|
||||
open_trade_count_start, trade_dir)
|
||||
@@ -1275,6 +1283,7 @@ class Backtesting:
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
# This only used to determine if trimming would result in an empty dataframe
|
||||
preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup)
|
||||
|
||||
if not preprocessed_tmp:
|
||||
|
||||
@@ -446,6 +446,8 @@ class Hyperopt:
|
||||
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe to get correct dates for output.
|
||||
# This is only used to keep track of min/max date after trimming.
|
||||
# The result is NOT returned from this method, actual trimming happens in backtesting.
|
||||
trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup)
|
||||
self.min_date, self.max_date = get_timerange(trimmed)
|
||||
if not self.market_change:
|
||||
|
||||
@@ -432,12 +432,10 @@ class HyperoptTools:
|
||||
for i in range(len(trials)):
|
||||
if trials.loc[i]['is_profit']:
|
||||
for j in range(len(trials.loc[i]) - 3):
|
||||
trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
|
||||
str(trials.loc[i][j]), Fore.RESET)
|
||||
trials.iat[i, j] = f"{Fore.GREEN}{str(trials.loc[i][j])}{Fore.RESET}"
|
||||
if trials.loc[i]['is_best'] and highlight_best:
|
||||
for j in range(len(trials.loc[i]) - 3):
|
||||
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
|
||||
str(trials.loc[i][j]), Style.RESET_ALL)
|
||||
trials.iat[i, j] = f"{Style.BRIGHT}{str(trials.loc[i][j])}{Style.RESET_ALL}"
|
||||
|
||||
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random'])
|
||||
if remove_header > 0:
|
||||
|
||||
@@ -48,6 +48,7 @@ class LookaheadAnalysis:
|
||||
self.entry_varHolders: List[VarHolder] = []
|
||||
self.exit_varHolders: List[VarHolder] = []
|
||||
self.exchange: Optional[Any] = None
|
||||
self._fee = None
|
||||
|
||||
# pull variables the scope of the lookahead_analysis-instance
|
||||
self.local_config = deepcopy(config)
|
||||
@@ -145,8 +146,13 @@ class LookaheadAnalysis:
|
||||
str(self.dt_to_timestamp(varholder.to_dt)))
|
||||
prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load
|
||||
|
||||
if self._fee is not None:
|
||||
# Don't re-calculate fee per pair, as fee might differ per pair.
|
||||
prepare_data_config['fee'] = self._fee
|
||||
|
||||
backtesting = Backtesting(prepare_data_config, self.exchange)
|
||||
self.exchange = backtesting.exchange
|
||||
self._fee = backtesting.fee
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
|
||||
varholder.data, varholder.timerange = backtesting.load_bt_data()
|
||||
@@ -198,7 +204,7 @@ class LookaheadAnalysis:
|
||||
self.prepare_data(exit_varHolder, [result_row['pair']])
|
||||
|
||||
# now we analyze a full trade of full_varholder and look for analyze its bias
|
||||
def analyze_row(self, idx, result_row):
|
||||
def analyze_row(self, idx: int, result_row):
|
||||
# if force-sold, ignore this signal since here it will unconditionally exit.
|
||||
if result_row.close_date == self.dt_to_timestamp(self.full_varHolder.to_dt):
|
||||
return
|
||||
@@ -209,12 +215,16 @@ class LookaheadAnalysis:
|
||||
# fill entry_varHolder and exit_varHolder
|
||||
self.fill_entry_and_exit_varHolders(result_row)
|
||||
|
||||
# this will trigger a logger-message
|
||||
buy_or_sell_biased: bool = False
|
||||
|
||||
# register if buy signal is broken
|
||||
if not self.report_signal(
|
||||
self.entry_varHolders[idx].result,
|
||||
"open_date",
|
||||
self.entry_varHolders[idx].compared_dt):
|
||||
self.current_analysis.false_entry_signals += 1
|
||||
buy_or_sell_biased = True
|
||||
|
||||
# register if buy or sell signal is broken
|
||||
if not self.report_signal(
|
||||
@@ -222,6 +232,13 @@ class LookaheadAnalysis:
|
||||
"close_date",
|
||||
self.exit_varHolders[idx].compared_dt):
|
||||
self.current_analysis.false_exit_signals += 1
|
||||
buy_or_sell_biased = True
|
||||
|
||||
if buy_or_sell_biased:
|
||||
logger.info(f"found lookahead-bias in trade "
|
||||
f"pair: {result_row['pair']}, "
|
||||
f"timerange:{result_row['open_date']} - {result_row['close_date']}, "
|
||||
f"idx: {idx}")
|
||||
|
||||
# check if the indicators themselves contain biased data
|
||||
self.analyze_indicators(self.full_varHolder, self.entry_varHolders[idx], result_row['pair'])
|
||||
@@ -251,9 +268,33 @@ class LookaheadAnalysis:
|
||||
# starting from the same datetime to avoid miss-reports of bias
|
||||
for idx, result_row in self.full_varHolder.result['results'].iterrows():
|
||||
if self.current_analysis.total_signals == self.targeted_trade_amount:
|
||||
logger.info(f"Found targeted trade amount = {self.targeted_trade_amount} signals.")
|
||||
break
|
||||
if found_signals < self.minimum_trade_amount:
|
||||
logger.info(f"only found {found_signals} "
|
||||
f"which is smaller than "
|
||||
f"minimum trade amount = {self.minimum_trade_amount}. "
|
||||
f"Exiting this lookahead-analysis")
|
||||
return None
|
||||
if "force_exit" in result_row['exit_reason']:
|
||||
logger.info("found force-exit in pair: {result_row['pair']}, "
|
||||
f"timerange:{result_row['open_date']}-{result_row['close_date']}, "
|
||||
f"idx: {idx}, skipping this one to avoid a false-positive.")
|
||||
|
||||
# just to keep the IDs of both full, entry and exit varholders the same
|
||||
# to achieve a better debugging experience
|
||||
self.entry_varHolders.append(VarHolder())
|
||||
self.exit_varHolders.append(VarHolder())
|
||||
continue
|
||||
|
||||
self.analyze_row(idx, result_row)
|
||||
|
||||
if len(self.entry_varHolders) < self.minimum_trade_amount:
|
||||
logger.info(f"only found {found_signals} after skipping forced exits "
|
||||
f"which is smaller than "
|
||||
f"minimum trade amount = {self.minimum_trade_amount}. "
|
||||
f"Exiting this lookahead-analysis")
|
||||
|
||||
# Restore verbosity, so it's not too quiet for the next strategy
|
||||
restore_verbosity_for_bias_tester()
|
||||
# check and report signals
|
||||
|
||||
@@ -137,6 +137,19 @@ class LookaheadAnalysisSubFunctions:
|
||||
'just to avoid false positives')
|
||||
config['dry_run_wallet'] = min_dry_run_wallet
|
||||
|
||||
if 'timerange' not in config:
|
||||
# setting a timerange is enforced here
|
||||
raise OperationalException(
|
||||
"Please set a timerange. "
|
||||
"Usually a few months are enough depending on your needs and strategy."
|
||||
)
|
||||
# fix stake_amount to 10k.
|
||||
# in a combination with a wallet size of 1 billion it should always be able to trade
|
||||
# no matter if they use custom_stake_amount as a small percentage of wallet size
|
||||
# or fixate custom_stake_amount to a certain value.
|
||||
logger.info('fixing stake_amount to 10k')
|
||||
config['stake_amount'] = 10000
|
||||
|
||||
# enforce cache to be 'none', shift it to 'none' if not already
|
||||
# (since the default value is 'day')
|
||||
if config.get('backtest_cache') is None:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.optimize.optimize_reports.bt_output import (generate_edge_table,
|
||||
generate_wins_draws_losses,
|
||||
show_backtest_result,
|
||||
show_backtest_results,
|
||||
show_sorted_pairlist,
|
||||
@@ -14,5 +15,4 @@ from freqtrade.optimize.optimize_reports.optimize_reports import (
|
||||
generate_all_periodic_breakdown_stats, generate_backtest_stats, generate_daily_stats,
|
||||
generate_exit_reason_stats, generate_pair_metrics, generate_periodic_breakdown_stats,
|
||||
generate_rejected_signals, generate_strategy_comparison, generate_strategy_stats,
|
||||
generate_tag_metrics, generate_trade_signal_candles, generate_trading_stats,
|
||||
generate_wins_draws_losses)
|
||||
generate_tag_metrics, generate_trade_signal_candles, generate_trading_stats)
|
||||
|
||||
@@ -5,8 +5,8 @@ from tabulate import tabulate
|
||||
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
|
||||
from freqtrade.misc import decimals_per_coin, round_coin_value
|
||||
from freqtrade.optimize.optimize_reports.optimize_reports import (generate_periodic_breakdown_stats,
|
||||
generate_wins_draws_losses)
|
||||
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
|
||||
from freqtrade.types import BacktestResultType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,6 +30,16 @@ def _get_line_header(first_column: str, stake_currency: str,
|
||||
'Win Draw Loss Win%']
|
||||
|
||||
|
||||
def generate_wins_draws_losses(wins, draws, losses):
|
||||
if wins > 0 and losses == 0:
|
||||
wl_ratio = '100'
|
||||
elif wins == 0:
|
||||
wl_ratio = '0'
|
||||
else:
|
||||
wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100'
|
||||
return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}'
|
||||
|
||||
|
||||
def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
|
||||
"""
|
||||
Generates and returns a text table for the given backtest data and the results dataframe
|
||||
@@ -233,8 +243,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'),
|
||||
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
|
||||
in strat_results else 'N/A'),
|
||||
('Expectancy', f"{strat_results['expectancy']:.2f}" if 'expectancy'
|
||||
in strat_results else 'N/A'),
|
||||
('Expectancy (Ratio)', (
|
||||
f"{strat_results['expectancy']:.2f} ({strat_results['expectancy_ratio']:.2f})" if
|
||||
'expectancy_ratio' in strat_results else 'N/A')),
|
||||
('Trades per day', strat_results['trades_per_day']),
|
||||
('Avg. daily profit %',
|
||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
||||
@@ -260,6 +271,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
||||
('Max Consecutive Wins / Loss',
|
||||
f"{strat_results['max_consecutive_wins']} / {strat_results['max_consecutive_losses']}"
|
||||
if 'max_consecutive_losses' in strat_results else 'N/A'),
|
||||
('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')),
|
||||
('Entry/Exit Timeouts',
|
||||
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
|
||||
@@ -350,7 +364,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
|
||||
print()
|
||||
|
||||
|
||||
def show_backtest_results(config: Config, backtest_stats: Dict):
|
||||
def show_backtest_results(config: Config, backtest_stats: BacktestResultType):
|
||||
stake_currency = config['stake_currency']
|
||||
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
@@ -370,7 +384,7 @@ def show_backtest_results(config: Config, backtest_stats: Dict):
|
||||
print('\nFor more details, please look at the detail tables above')
|
||||
|
||||
|
||||
def show_sorted_pairlist(config: Config, backtest_stats: Dict):
|
||||
def show_sorted_pairlist(config: Config, backtest_stats: BacktestResultType):
|
||||
if config.get('backtest_show_pair_list', False):
|
||||
for strategy, results in backtest_stats['strategy'].items():
|
||||
print(f"Pairs for Strategy {strategy}: \n[")
|
||||
|
||||
@@ -2,18 +2,17 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
from freqtrade.misc import file_dump_joblib, file_dump_json
|
||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||
from freqtrade.types import BacktestResultType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def store_backtest_stats(
|
||||
recordfilename: Path, stats: Dict[str, DataFrame], dtappendix: str) -> None:
|
||||
recordfilename: Path, stats: BacktestResultType, dtappendix: str) -> Path:
|
||||
"""
|
||||
Stores backtest results
|
||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||
@@ -31,13 +30,19 @@ def store_backtest_stats(
|
||||
|
||||
# Store metadata separately.
|
||||
file_dump_json(get_backtest_metadata_filename(filename), stats['metadata'])
|
||||
del stats['metadata']
|
||||
# Don't mutate the original stats dict.
|
||||
stats_copy = {
|
||||
'strategy': stats['strategy'],
|
||||
'strategy_comparison': stats['strategy_comparison'],
|
||||
}
|
||||
|
||||
file_dump_json(filename, stats)
|
||||
file_dump_json(filename, stats_copy)
|
||||
|
||||
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
|
||||
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def _store_backtest_analysis_data(
|
||||
recordfilename: Path, data: Dict[str, Dict],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user