Compare commits
982 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 | |||
| eea95f79aa | |||
| f020daa357 | |||
| dacbcdb710 | |||
| a9e239ca7a | |||
| 65550335ee | |||
| 01db789d42 | |||
| 6fe0895e74 | |||
| c93a27af7d | |||
| 67ce7917cc | |||
| 9b447cdf1e | |||
| 98ba0042d8 | |||
| f64b9503f9 | |||
| e734a664b4 | |||
| 942f0b4fbd | |||
| a2ba466918 | |||
| 7ba459db88 | |||
| ab39144af8 | |||
| 092e30a159 | |||
| e6db5bd193 | |||
| 5cd08ce554 | |||
| e51085ebc6 | |||
| 817b6f9bde | |||
| b204a93d1c | |||
| 977bfa08b7 | |||
| ba6cba31be | |||
| 1880f9ffa1 | |||
| 9d3dda4e12 | |||
| 0310a26b80 | |||
| 86956908d0 | |||
| b204da3317 | |||
| e16c433cb8 | |||
| c5510491e5 | |||
| 29725440c8 | |||
| accc1b509b | |||
| 4b06b4772d | |||
| a90b7c0bf5 | |||
| c4168055f9 | |||
| b12dbd2bea | |||
| be07ea5d4f | |||
| e729c5f9fd | |||
| 5c8181fdd3 | |||
| 3329279b71 | |||
| 6b201d525e | |||
| 8c2098c262 | |||
| 6274197f85 | |||
| ae42d57a26 | |||
| 2d2699b0ad | |||
| fec4cb3cf9 | |||
| 4a886e1b97 | |||
| 2c36a09b4f | |||
| 1717f86702 | |||
| 72504e62ad | |||
| 65e8359908 | |||
| 794bca1379 | |||
| 5e084ad2e5 | |||
| fca73531cf | |||
| 9da28e5328 | |||
| fd420738cd | |||
| 48e8965322 | |||
| ce1b90885e | |||
| 5f98530ef9 | |||
| 69087c30e7 | |||
| d534f88d1c | |||
| caca070c1a | |||
| 36b33fb407 | |||
| 6e143d4a5d | |||
| 757c6dc5ca | |||
| 01dfca80ab | |||
| 2f7b29ed34 | |||
| b49a118764 | |||
| c7683a7b61 | |||
| 5d60c62645 | |||
| 96c2ca67e9 | |||
| b0e5fb3940 | |||
| 8c54036fa5 | |||
| 859f7ff3de | |||
| b2c87c3591 | |||
| 62f4bd27ec | |||
| 26c06e38be | |||
| 78451447ac | |||
| 0f7720dec0 | |||
| b9fe364d9c | |||
| 97e7b60656 | |||
| 936e49e8ee | |||
| 6bc3439cb7 | |||
| ec72c91b17 | |||
| 2809379b56 | |||
| e965b2e454 | |||
| d82a0ad7b5 | |||
| f04598c5e5 | |||
| f82d52c6d3 | |||
| fc0548ce0b | |||
| 8cc763b664 | |||
| ed90e77ea0 | |||
| 1eb691d461 | |||
| 571dea6e9c | |||
| 02071df8fa | |||
| cca4fa1178 | |||
| 7e2f857aa5 | |||
| 3d72d32845 | |||
| 52db6ac7d7 | |||
| d94f3e7679 | |||
| 7af14d1985 | |||
| 44a38e8362 | |||
| 0be4084eac | |||
| 937734365f | |||
| 66b34edc0b | |||
| 6f0f954686 | |||
| 7453ff2fb5 | |||
| b8ab6fe42b | |||
| e0d5242a45 | |||
| 402a247c92 | |||
| 886b86f7c5 | |||
| b0ab400ff3 | |||
| bf872e8ed4 | |||
| 447feb16b4 | |||
| 636f5753e1 | |||
| 11ff454b3b | |||
| 6bb75f0dd4 | |||
| 1567cd2849 | |||
| 34e7e3efea | |||
| 2c7aa9f721 | |||
| 24e806f081 | |||
| b0396af4c4 | |||
| efaa959bfa | |||
| 7939716a5e | |||
| 4f834c8964 | |||
| ffd7394adb | |||
| 2107dce2cd | |||
| 72101f059d | |||
| 75ec19062c | |||
| 64fcb1ed11 | |||
| dec3c0f374 | |||
| 1b86bf8a1d | |||
| 2cd9043c51 | |||
| b3ef024e9e | |||
| 964bf76469 | |||
| ad74e65673 | |||
| ac36ba6592 | |||
| ca88cac08b | |||
| d211bf47f1 | |||
| 11d7e7925e | |||
| bc4d1c5326 | |||
| 10b93f080a | |||
| 876ce85cd8 | |||
| 9a7794c520 | |||
| 1a4d94a6f3 | |||
| 1e44cfe2fc | |||
| 502090c199 | |||
| 385d9d30b7 | |||
| e763e2ad35 | |||
| a89c647255 | |||
| 1beaf6f05c | |||
| 21949c0446 | |||
| 6e6bccfd3e | |||
| a7f882a7fe | |||
| 0c4dab37d7 | |||
| 1af4fa0419 | |||
| 37495884d4 | |||
| 2e087750e0 | |||
| 64e803356a | |||
| 7172bc0af3 | |||
| feb6e5c466 | |||
| 8b27b408c7 | |||
| a9515dee81 | |||
| 71064c02e5 | |||
| 66dc1fd339 | |||
| 7542909e18 | |||
| a39f23a5c7 | |||
| d748cf6531 | |||
| 663cfc6211 | |||
| bdb535d0e6 | |||
| 5dee86eda7 | |||
| c36547a563 | |||
| afd54d39a5 | |||
| 5844756ba1 | |||
| 4a800fe467 | |||
| fd940dbba2 | |||
| 320b3e20a6 | |||
| fc11c79b77 | |||
| 87e144a95a | |||
| 9ef814689e | |||
| 2bd66fbb47 | |||
| 9eceb2f38c | |||
| 1da1972c18 | |||
| e332fbfb47 | |||
| 2806110869 | |||
| cfe88f06d2 | |||
| ad8a4897ce | |||
| 4f15b30339 | |||
| 229ee643cd | |||
| 41e37f9d32 | |||
| d9bdd879ab | |||
| f8d7c2e21d | |||
| 4cdd6bc6c3 | |||
| e246259792 | |||
| 3523f564bd | |||
| 265d782af8 | |||
| 94ca2988a0 | |||
| 6656740f21 | |||
| 99842402f7 | |||
| 16b3363970 | |||
| b89390c06b | |||
| c8e827d483 | |||
| fc8c6b06ad | |||
| e3056b141a | |||
| 05ea36f03b | |||
| 61f1701e56 | |||
| beaaa94406 | |||
| 6b736c49d4 | |||
| 33b028b104 | |||
| 88337b6c5e | |||
| e39e40dc60 | |||
| 317e0b5f2b | |||
| 4404d112a9 | |||
| f81139b97c | |||
| 14557f2d32 | |||
| f10f00f5e8 | |||
| 675a97c1cb | |||
| c066f014e3 | |||
| 6d39adc739 | |||
| 135aaa2be2 | |||
| dc577d2a1a | |||
| 4d4589becd | |||
| f7f88aa14d | |||
| 17d74429b5 | |||
| 5ac141f72b | |||
| 0b8ef1b880 | |||
| c269eef77e | |||
| 21172802de | |||
| 17b8cb2f7c | |||
| 6ec91d11ae | |||
| 984dec12df | |||
| 11f2bbdd08 | |||
| 41f5c32526 | |||
| 4dcb6395ef | |||
| 46ad8afeb9 | |||
| 49891967f2 | |||
| 4acb0830e3 | |||
| e61659a2bc | |||
| c2125698a7 | |||
| 093181e8f2 | |||
| bdaf230bd1 | |||
| 9dcab313b5 | |||
| c9bbeedd88 | |||
| 94bc91ef57 | |||
| 7a726da691 | |||
| 71b81ee7cd | |||
| f61ae9c7e2 | |||
| 12e31208e1 | |||
| ac7419e975 | |||
| 72f4e1475c | |||
| 74254bb893 | |||
| 54bf1634c7 | |||
| 6f928b826f | |||
| cc04f3279a | |||
| fcb960185e | |||
| 250ae2d006 | |||
| b5d1017779 | |||
| 26ed17fa02 | |||
| e890bc0718 | |||
| 10ea2b44c7 | |||
| d9d1735333 | |||
| 48328fb29d | |||
| 49c0fdf367 | |||
| ac046d6a2d | |||
| af16ce874c | |||
| e2594e7494 | |||
| 77e3e9e899 | |||
| cafc9479b7 | |||
| 8f02050fde | |||
| 565c0496d9 | |||
| d0f900f567 | |||
| 30dd63fcb9 | |||
| 8aee368f60 | |||
| e0d9603e99 | |||
| 88ecb935b9 | |||
| 9c6fee3841 | |||
| 5fc8426b9b | |||
| 430cd24bbc | |||
| 08d040db14 | |||
| 5311614d54 | |||
| 193d88c9c8 | |||
| 1f543666f4 | |||
| fd955028a8 | |||
| 7bccf2129f | |||
| f6a32f4ffd | |||
| b666c418bb | |||
| af1dbf7dff | |||
| f074383d6a | |||
| 785f0d396f | |||
| 6315516d50 | |||
| 6237806817 | |||
| e572653616 | |||
| 12e8e29b4e | |||
| 5ecf93e84b | |||
| 9f1bdc19aa | |||
| 35836479de | |||
| e03e8547c0 | |||
| bcd27f5517 | |||
| ce02a3ff33 | |||
| 85de8ca63f | |||
| 4c54640800 | |||
| cb7a0f9bff | |||
| 90808683e2 | |||
| 8fdec5f3a6 | |||
| 4e9a43ebf6 | |||
| 3bc390cb2e | |||
| 12af6ea766 | |||
| 51ba4d14e9 | |||
| 6b3b5f201d | |||
| fc887efd4b | |||
| 0874b1a959 | |||
| eec7837167 | |||
| 1317de8c1c | |||
| dbb92f686f | |||
| aa8eb14461 | |||
| 3d05669f61 | |||
| 8a86169256 | |||
| 8ec0469b11 | |||
| 9bb25be880 | |||
| 0ed84fbcc1 | |||
| a7426755bc | |||
| df5e6409a4 | |||
| 5649d1d4da | |||
| 36c82ad67c | |||
| 35a388bf9a | |||
| ee37693729 | |||
| 05f0b32e3b | |||
| 636298bb71 | |||
| 31e19add27 | |||
| eb31b574c1 | |||
| 9366c77e42 | |||
| af7afa80a9 | |||
| 9e75c768c0 | |||
| 4c52109fa3 | |||
| c2010d160f | |||
| 33e25434b4 | |||
| 756e1f5d5b | |||
| 01984a06af | |||
| 7cc8da23c2 | |||
| bf9a6dd6e7 | |||
| 818a3342b9 | |||
| 680e7ba98f | |||
| 5ad6652e55 | |||
| 70a0c2e625 | |||
| 3e6a2bf9b0 | |||
| 104fa9e32d | |||
| e73cd1487e | |||
| 9869a21951 | |||
| 3f5c18a035 | |||
| e183707979 | |||
| ceddcd9242 | |||
| d8af0dc9c4 | |||
| 1c4a7c7a05 | |||
| 7b9f82c71a | |||
| 5142b6bc0d | |||
| 209eb63ede | |||
| 2e675efa13 | |||
| 073dac8d5f | |||
| a0edbe4797 | |||
| 2e79aaae00 | |||
| 5488789bc4 | |||
| b2ecfd28a7 | |||
| 7a5f457b2f | |||
| 36f14249d4 | |||
| 7d871faf04 | |||
| 91ce1cb2ae | |||
| 9aac367534 | |||
| b8357c36ca | |||
| b252bdd3c7 | |||
| ac4aa8ed2b | |||
| 2306c74dc1 | |||
| 5abd616ae9 | |||
| ce979b21f9 | |||
| 8e0788cf5f | |||
| 877d53f439 | |||
| 3d4be92cc6 | |||
| c5bf029701 | |||
| 9e4f9798e6 | |||
| 3ef2a57bca | |||
| e20b94d836 | |||
| 4636de30cd | |||
| 2ea157d9d3 | |||
| 987da010c9 | |||
| 5ad352fdf1 | |||
| 2df80fc49a | |||
| e990b9fb13 | |||
| 2b416d3b62 | |||
| d5c98a3c39 | |||
| 46b97d2be4 | |||
| 767442198e | |||
| a9ef4c3ab0 | |||
| e5e63d5bee | |||
| 0fb155d6ee | |||
| bad2cdabf2 | |||
| 7bd55971dc | |||
| efefcb240b | |||
| f57787882d | |||
| d12a7ff18b |
@@ -1,11 +1,12 @@
|
|||||||
FROM freqtradeorg/freqtrade:develop
|
FROM freqtradeorg/freqtrade:develop_freqairl
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements-dev.txt /freqtrade/
|
COPY requirements-dev.txt /freqtrade/
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install git mercurial sudo vim build-essential \
|
&& apt-get -y install --no-install-recommends apt-utils dialog \
|
||||||
|
&& apt-get -y install --no-install-recommends git sudo vim build-essential \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \
|
&& mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \
|
||||||
&& echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \
|
&& echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"postCreateCommand": "freqtrade create-userdir --userdir user_data/",
|
"postCreateCommand": "freqtrade create-userdir --userdir user_data/",
|
||||||
|
|
||||||
"workspaceFolder": "/workspaces/freqtrade",
|
"workspaceFolder": "/workspaces/freqtrade",
|
||||||
|
"customizations": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"terminal.integrated.shell.linux": "/bin/bash",
|
"terminal.integrated.shell.linux": "/bin/bash",
|
||||||
"editor.insertSpaces": true,
|
"editor.insertSpaces": true,
|
||||||
@@ -38,4 +38,5 @@
|
|||||||
"ms-azuretools.vscode-docker",
|
"ms-azuretools.vscode-docker",
|
||||||
"vscode-icons-team.vscode-icons",
|
"vscode-icons-team.vscode-icons",
|
||||||
],
|
],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 15
|
||||||
target-branch: develop
|
target-branch: develop
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ jobs:
|
|||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Cache_dependencies
|
- name: Cache_dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
@@ -159,7 +160,8 @@ jobs:
|
|||||||
- name: Installation - macOS
|
- name: Installation - macOS
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
brew update
|
# brew update
|
||||||
|
# TODO: Should be the brew upgrade
|
||||||
# homebrew fails to update python due to unlinking failures
|
# homebrew fails to update python due to unlinking failures
|
||||||
# https://github.com/actions/runner-images/issues/6817
|
# https://github.com/actions/runner-images/issues/6817
|
||||||
rm /usr/local/bin/2to3 || true
|
rm /usr/local/bin/2to3 || true
|
||||||
@@ -459,7 +461,7 @@ jobs:
|
|||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.6
|
uses: pypa/gh-action-pypi-publish@v1.8.10
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
@@ -467,7 +469,7 @@ jobs:
|
|||||||
repository_url: https://test.pypi.org/legacy/
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.6
|
uses: pypa/gh-action-pypi-publish@v1.8.10
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ repos:
|
|||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.0.1"
|
rev: "v1.5.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.5
|
- types-cachetools==5.3.0.6
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.30.0.0
|
- types-requests==2.31.0.2
|
||||||
- types-tabulate==0.9.0.2
|
- types-tabulate==0.9.0.3
|
||||||
- types-python-dateutil==2.8.19.13
|
- types-python-dateutil==2.8.19.14
|
||||||
- SQLAlchemy==2.0.15
|
- SQLAlchemy==2.0.20
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
@@ -30,7 +30,7 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.0.263'
|
rev: 'v0.0.270'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -1,8 +1,14 @@
|
|||||||
# .readthedocs.yml
|
# .readthedocs.yml
|
||||||
|
version: 2
|
||||||
|
|
||||||
build:
|
build:
|
||||||
image: latest
|
os: "ubuntu-22.04"
|
||||||
|
tools:
|
||||||
|
python: "3.11"
|
||||||
|
|
||||||
python:
|
python:
|
||||||
version: 3.8
|
install:
|
||||||
setup_py_install: false
|
- requirements: docs/requirements-docs.txt
|
||||||
|
|
||||||
|
mkdocs:
|
||||||
|
configuration: mkdocs.yml
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10.11-slim-bullseye as base
|
FROM python:3.11.4-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|||||||
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
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib \
|
cd ta-lib \
|
||||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
&& 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 \
|
&& echo "Downloading gcc config.guess and config.sub" \
|
||||||
&& curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o 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}/ \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make
|
&& make
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
# Downloads don't work automatically, since the URL is regenerated via javascript.
|
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
|
||||||
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
|
|
||||||
|
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
|
|
||||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||||
|
|
||||||
if ($pyv -eq '3.8') {
|
|
||||||
pip install build_helpers\TA_Lib-0.4.26-cp38-cp38-win_amd64.whl
|
pip install --find-links=build_helpers\ --prefer-binary TA-Lib
|
||||||
}
|
|
||||||
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 -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -89,7 +89,6 @@
|
|||||||
],
|
],
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "binance",
|
"name": "binance",
|
||||||
"sandbox": false,
|
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"password": "",
|
"password": "",
|
||||||
@@ -206,6 +205,6 @@
|
|||||||
"recursive_strategy_search": false,
|
"recursive_strategy_search": false,
|
||||||
"add_config_files": [],
|
"add_config_files": [],
|
||||||
"reduce_df_footprint": false,
|
"reduce_df_footprint": false,
|
||||||
"dataformat_ohlcv": "json",
|
"dataformat_ohlcv": "feather",
|
||||||
"dataformat_trades": "jsongz"
|
"dataformat_trades": "feather"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ services:
|
|||||||
--logfile /freqtrade/user_data/logs/freqtrade.log
|
--logfile /freqtrade/user_data/logs/freqtrade.log
|
||||||
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite
|
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite
|
||||||
--config /freqtrade/user_data/config.json
|
--config /freqtrade/user_data/config.json
|
||||||
--freqai-model XGBoostClassifier
|
--freqaimodel XGBoostRegressor
|
||||||
--strategy SampleStrategy
|
--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
|
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
||||||
output.
|
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
|
### 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:
|
To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[YYYYMMDD]` format:
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
### Dynamic parameters
|
### Dynamic parameters
|
||||||
|
|
||||||
Parameters can also be defined dynamically, but must be available to the instance once the * [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
Parameters can also be defined dynamically, but must be available to the instance once the [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
|
||||||
|
|||||||
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 |
|
| Sharpe | 2.97 |
|
||||||
| Calmar | 6.29 |
|
| Calmar | 6.29 |
|
||||||
| Profit factor | 1.11 |
|
| Profit factor | 1.11 |
|
||||||
| Expectancy | -0.15 |
|
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 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 |
|
| Days win/draw/lose | 12 / 82 / 25 |
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
|
| Max Consecutive Wins / Loss | 3 / 4 |
|
||||||
| Rejected Entry signals | 3089 |
|
| Rejected Entry signals | 3089 |
|
||||||
| Entry/Exit Timeouts | 0 / 0 |
|
| Entry/Exit Timeouts | 0 / 0 |
|
||||||
| Canceled Trade Entries | 34 |
|
| Canceled Trade Entries | 34 |
|
||||||
@@ -409,7 +410,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
| Sharpe | 2.97 |
|
| Sharpe | 2.97 |
|
||||||
| Calmar | 6.29 |
|
| Calmar | 6.29 |
|
||||||
| Profit factor | 1.11 |
|
| Profit factor | 1.11 |
|
||||||
| Expectancy | -0.15 |
|
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 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 |
|
| Days win/draw/lose | 12 / 82 / 25 |
|
||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
|
| Max Consecutive Wins / Loss | 3 / 4 |
|
||||||
| Rejected Entry signals | 3089 |
|
| Rejected Entry signals | 3089 |
|
||||||
| Entry/Exit Timeouts | 0 / 0 |
|
| Entry/Exit Timeouts | 0 / 0 |
|
||||||
| Canceled Trade Entries | 34 |
|
| 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.
|
- `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).
|
- `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.
|
- `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.
|
- `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).
|
- `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`.
|
- `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
|
- 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 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
|
- 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)
|
- 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 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
|
- 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.
|
* **Strategy**: Your trading strategy, telling the bot what to do.
|
||||||
* **Trade**: Open position.
|
* **Trade**: Open position.
|
||||||
* **Open Order**: Order which is currently placed on the exchange, and is not yet complete.
|
* **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"`, ...).
|
* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
|
||||||
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
||||||
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
* **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.).
|
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
|
## Bot execution logic
|
||||||
|
|
||||||
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
|
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.
|
This page explains the different parameters of the bot and how to run it.
|
||||||
|
|
||||||
!!! Note
|
!!! 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"
|
!!! 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.
|
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
|
| `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**
|
||||||
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
| `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.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.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
|
| `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
|
| `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
|
| `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
|
| `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_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 `jsongz`*. <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`.
|
| `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
|
### Parameters in the strategy
|
||||||
@@ -682,16 +681,14 @@ To use a proxy for exchange connections - you will have to define the proxies as
|
|||||||
{
|
{
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"ccxt_config": {
|
"ccxt_config": {
|
||||||
"aiohttp_proxy": "http://addr:port",
|
"httpsProxy": "http://addr:port",
|
||||||
"proxies": {
|
|
||||||
"http": "http://addr:port",
|
|
||||||
"https": "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
|
## Next step
|
||||||
|
|
||||||
Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).
|
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
|
``` bash
|
||||||
# Activate virtual environment
|
# Activate virtual environment
|
||||||
source .env/bin/activate
|
source .venv/bin/activate
|
||||||
|
|
||||||
pip install ipykernel
|
pip install ipykernel
|
||||||
ipython kernel install --user --name=freqtrade
|
ipython kernel install --user --name=freqtrade
|
||||||
|
|||||||
+101
-97
@@ -6,7 +6,7 @@ To download data (candles / OHLCV) needed for backtesting and hyperoptimization
|
|||||||
|
|
||||||
If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes for the last 30 days.
|
If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes for the last 30 days.
|
||||||
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
|
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
|
||||||
Otherwise `--exchange` becomes mandatory.
|
Without provided configuration, `--exchange` becomes mandatory.
|
||||||
|
|
||||||
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
|
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
|
||||||
|
|
||||||
@@ -27,11 +27,11 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--exchange EXCHANGE]
|
[--exchange EXCHANGE]
|
||||||
[-t TIMEFRAMES [TIMEFRAMES ...]] [--erase]
|
[-t TIMEFRAMES [TIMEFRAMES ...]] [--erase]
|
||||||
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
[--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}]
|
[--trading-mode {spot,margin,futures}]
|
||||||
[--prepend]
|
[--prepend]
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Limit command to these pairs. Pairs are space-
|
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
|
--dl-trades Download trades instead of OHLCV data. The bot will
|
||||||
resample trades to the desired timeframe as specified
|
resample trades to the desired timeframe as specified
|
||||||
as --timeframes/-t.
|
as --timeframes/-t.
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
||||||
Specify which tickers to download. Space-separated
|
Specify which tickers to download. Space-separated
|
||||||
list. Default: `1m 5m`.
|
list. Default: `1m 5m`.
|
||||||
@@ -57,17 +56,18 @@ optional arguments:
|
|||||||
exchange/pairs/timeframes.
|
exchange/pairs/timeframes.
|
||||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `json`).
|
(default: `feather`).
|
||||||
--data-format-trades {json,jsongz,hdf5}
|
--data-format-trades {json,jsongz,hdf5,feather}
|
||||||
Storage format for downloaded trades data. (default:
|
Storage format for downloaded trades data. (default:
|
||||||
`jsongz`).
|
`feather`).
|
||||||
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||||
Select Trading mode
|
Select Trading mode
|
||||||
--prepend Allow data prepending. (Data-appending is disabled)
|
--prepend Allow data prepending. (Data-appending is disabled)
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-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
|
'syslog', 'journald'. See the documentation for more
|
||||||
details.
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
@@ -83,40 +83,47 @@ Common arguments:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! Tip "Downloading all data for one quote currency"
|
||||||
|
Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
|
||||||
|
`freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
|
||||||
|
To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
|
||||||
|
|
||||||
!!! Note "Startup period"
|
!!! Note "Startup period"
|
||||||
`download-data` is a strategy-independent command. The idea is to download a big chunk of data once, and then iteratively increase the amount of data stored.
|
`download-data` is a strategy-independent command. The idea is to download a big chunk of data once, and then iteratively increase the amount of data stored.
|
||||||
|
|
||||||
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
|
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
|
||||||
|
|
||||||
### Pairs file
|
### Start download
|
||||||
|
|
||||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
A very simple command (assuming an available `config.json` file) can look as follows.
|
||||||
If you are using Binance for example:
|
|
||||||
|
|
||||||
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
|
||||||
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p user_data/data/binance
|
freqtrade download-data --exchange binance
|
||||||
touch user_data/data/binance/pairs.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The format of the `pairs.json` file is a simple json list.
|
This will download historical candle (OHLCV) data for all the currency pairs defined in the configuration.
|
||||||
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
|
||||||
|
|
||||||
``` json
|
Alternatively, specify the pairs directly
|
||||||
[
|
|
||||||
"ETH/BTC",
|
```bash
|
||||||
"ETH/USDT",
|
freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
|
||||||
"BTC/USDT",
|
|
||||||
"XRP/ETH"
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Tip "Downloading all data for one quote currency"
|
or as regex (in this case, to download all active USDT pairs)
|
||||||
Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
|
|
||||||
`freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
|
```bash
|
||||||
To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
|
freqtrade download-data --exchange binance --pairs .*/USDT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Notes
|
||||||
|
|
||||||
|
* To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||||
|
* To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
||||||
|
* To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||||
|
* To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||||
|
* To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020.
|
||||||
|
* Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||||
|
* To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||||
|
|
||||||
??? Note "Permission denied errors"
|
??? Note "Permission denied errors"
|
||||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||||
@@ -131,39 +138,7 @@ Mixing different stake-currencies is allowed for this file, since it's only used
|
|||||||
sudo chown -R $UID:$GID user_data
|
sudo chown -R $UID:$GID user_data
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start download
|
### Download additional data before the current timerange
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade download-data --exchange binance
|
|
||||||
```
|
|
||||||
|
|
||||||
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
|
|
||||||
|
|
||||||
Alternatively, specify the pairs directly
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
|
|
||||||
```
|
|
||||||
|
|
||||||
or as regex (to download all active USDT pairs)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade download-data --exchange binance --pairs .*/USDT
|
|
||||||
```
|
|
||||||
|
|
||||||
### Other Notes
|
|
||||||
|
|
||||||
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
|
||||||
- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
|
||||||
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
|
||||||
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
|
||||||
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020.
|
|
||||||
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
|
||||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
|
||||||
|
|
||||||
#### Download additional data before the current timerange
|
|
||||||
|
|
||||||
Assuming you downloaded all data from 2022 (`--timerange 20220101-`) - but you'd now like to also backtest with earlier data.
|
Assuming you downloaded all data from 2022 (`--timerange 20220101-`) - but you'd now like to also backtest with earlier data.
|
||||||
You can do so by using the `--prepend` flag, combined with `--timerange` - specifying an end-date.
|
You can do so by using the `--prepend` flag, combined with `--timerange` - specifying an end-date.
|
||||||
@@ -182,7 +157,7 @@ Freqtrade currently supports the following data-formats:
|
|||||||
* `json` - plain "text" json files
|
* `json` - plain "text" json files
|
||||||
* `jsongz` - a gzip-zipped version of json files
|
* `jsongz` - a gzip-zipped version of json files
|
||||||
* `hdf5` - a high performance datastore
|
* `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)
|
* `parquet` - columnar datastore (OHLCV only)
|
||||||
|
|
||||||
By default, OHLCV data is stored as `json` data, while trades data is stored as `jsongz` data.
|
By default, OHLCV data is stored as `json` data, while trades data is stored as `jsongz` data.
|
||||||
@@ -238,7 +213,36 @@ Size has been taken from the BTC/USDT 1m spot combination for the timerange spec
|
|||||||
|
|
||||||
To have a best performance/size mix, we recommend the use of either feather or parquet.
|
To have a best performance/size mix, we recommend the use of either feather or parquet.
|
||||||
|
|
||||||
#### Sub-command convert data
|
### Pairs file
|
||||||
|
|
||||||
|
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||||
|
If you are using Binance for example:
|
||||||
|
|
||||||
|
* create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
||||||
|
* update the `pairs.json` file to contain the currency pairs you are interested in.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p user_data/data/binance
|
||||||
|
touch user_data/data/binance/pairs.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The format of the `pairs.json` file is a simple json list.
|
||||||
|
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
[
|
||||||
|
"ETH/BTC",
|
||||||
|
"ETH/USDT",
|
||||||
|
"BTC/USDT",
|
||||||
|
"XRP/ETH"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
The `pairs.json` file is only used when no configuration is loaded (implicitly by naming, or via `--config` flag).
|
||||||
|
You can force the usage of this file via `--pairs-file pairs.json` - however we recommend to use the pairlist from within the configuration, either via `exchange.pair_whitelist` or `pairs` setting in the configuration.
|
||||||
|
|
||||||
|
## Sub-command convert data
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
@@ -251,7 +255,7 @@ usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--trading-mode {spot,margin,futures}]
|
[--trading-mode {spot,margin,futures}]
|
||||||
[--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]]
|
[--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
|
-h, --help show this help message and exit
|
||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Limit command to these pairs. Pairs are space-
|
Limit command to these pairs. Pairs are space-
|
||||||
@@ -262,19 +266,20 @@ optional arguments:
|
|||||||
Destination format for data conversion.
|
Destination format for data conversion.
|
||||||
--erase Clean all existing data for the selected
|
--erase Clean all existing data for the selected
|
||||||
exchange/pairs/timeframes.
|
exchange/pairs/timeframes.
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
||||||
Specify which tickers to download. Space-separated
|
Specify which tickers to download. Space-separated
|
||||||
list. Default: `1m 5m`.
|
list. Default: `1m 5m`.
|
||||||
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||||
Select Trading mode
|
Select Trading mode
|
||||||
--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]
|
--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:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-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
|
'syslog', 'journald'. See the documentation for more
|
||||||
details.
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
@@ -287,10 +292,9 @@ Common arguments:
|
|||||||
Path to directory with historical backtesting data.
|
Path to directory with historical backtesting data.
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Example converting data
|
### Example converting data
|
||||||
|
|
||||||
The following command will convert all candle (OHLCV) data available in `~/.freqtrade/data/binance` from json to jsongz, saving diskspace in the process.
|
The following command will convert all candle (OHLCV) data available in `~/.freqtrade/data/binance` from json to jsongz, saving diskspace in the process.
|
||||||
It'll also remove original json data files (`--erase` parameter).
|
It'll also remove original json data files (`--erase` parameter).
|
||||||
@@ -299,7 +303,7 @@ It'll also remove original json data files (`--erase` parameter).
|
|||||||
freqtrade convert-data --format-from json --format-to jsongz --datadir ~/.freqtrade/data/binance -t 5m 15m --erase
|
freqtrade convert-data --format-from json --format-to jsongz --datadir ~/.freqtrade/data/binance -t 5m 15m --erase
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Sub-command convert trade data
|
## Sub-command convert trade data
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade convert-trade-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
usage: freqtrade convert-trade-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
@@ -310,7 +314,7 @@ usage: freqtrade convert-trade-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
{json,jsongz,hdf5,feather,parquet}
|
{json,jsongz,hdf5,feather,parquet}
|
||||||
[--erase] [--exchange EXCHANGE]
|
[--erase] [--exchange EXCHANGE]
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Limit command to these pairs. Pairs are space-
|
Limit command to these pairs. Pairs are space-
|
||||||
@@ -321,12 +325,12 @@ optional arguments:
|
|||||||
Destination format for data conversion.
|
Destination format for data conversion.
|
||||||
--erase Clean all existing data for the selected
|
--erase Clean all existing data for the selected
|
||||||
exchange/pairs/timeframes.
|
exchange/pairs/timeframes.
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-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
|
'syslog', 'journald'. See the documentation for more
|
||||||
details.
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
@@ -342,7 +346,7 @@ Common arguments:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Example converting trades
|
### Example converting trades
|
||||||
|
|
||||||
The following command will convert all available trade-data in `~/.freqtrade/data/kraken` from jsongz to json.
|
The following command will convert all available trade-data in `~/.freqtrade/data/kraken` from jsongz to json.
|
||||||
It'll also remove original jsongz data files (`--erase` parameter).
|
It'll also remove original jsongz data files (`--erase` parameter).
|
||||||
@@ -351,7 +355,7 @@ It'll also remove original jsongz data files (`--erase` parameter).
|
|||||||
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sub-command trades to ohlcv
|
## Sub-command trades to ohlcv
|
||||||
|
|
||||||
When you need to use `--dl-trades` (kraken only) to download data, conversion of trades data to ohlcv data is the last step.
|
When you need to use `--dl-trades` (kraken only) to download data, conversion of trades data to ohlcv data is the last step.
|
||||||
This command will allow you to repeat this last step for additional timeframes without re-downloading the data.
|
This command will allow you to repeat this last step for additional timeframes without re-downloading the data.
|
||||||
@@ -363,9 +367,9 @@ usage: freqtrade trades-to-ohlcv [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[-t TIMEFRAMES [TIMEFRAMES ...]]
|
[-t TIMEFRAMES [TIMEFRAMES ...]]
|
||||||
[--exchange EXCHANGE]
|
[--exchange EXCHANGE]
|
||||||
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
[--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
|
-h, --help show this help message and exit
|
||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Limit command to these pairs. Pairs are space-
|
Limit command to these pairs. Pairs are space-
|
||||||
@@ -373,18 +377,18 @@ optional arguments:
|
|||||||
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
-t TIMEFRAMES [TIMEFRAMES ...], --timeframes TIMEFRAMES [TIMEFRAMES ...]
|
||||||
Specify which tickers to download. Space-separated
|
Specify which tickers to download. Space-separated
|
||||||
list. Default: `1m 5m`.
|
list. Default: `1m 5m`.
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `json`).
|
(default: `feather`).
|
||||||
--data-format-trades {json,jsongz,hdf5}
|
--data-format-trades {json,jsongz,hdf5,feather}
|
||||||
Storage format for downloaded trades data. (default:
|
Storage format for downloaded trades data. (default:
|
||||||
`jsongz`).
|
`feather`).
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-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
|
'syslog', 'journald'. See the documentation for more
|
||||||
details.
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
@@ -400,13 +404,13 @@ Common arguments:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Example trade-to-ohlcv conversion
|
### Example trade-to-ohlcv conversion
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade trades-to-ohlcv --exchange kraken -t 5m 1h 1d --pairs BTC/EUR ETH/EUR
|
freqtrade trades-to-ohlcv --exchange kraken -t 5m 1h 1d --pairs BTC/EUR ETH/EUR
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sub-command list-data
|
## Sub-command list-data
|
||||||
|
|
||||||
You can get a list of downloaded data using the `list-data` sub-command.
|
You can get a list of downloaded data using the `list-data` sub-command.
|
||||||
|
|
||||||
@@ -418,13 +422,12 @@ usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
[--trading-mode {spot,margin,futures}]
|
[--trading-mode {spot,margin,futures}]
|
||||||
[--show-timerange]
|
[--show-timerange]
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `json`).
|
(default: `feather`).
|
||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Limit command to these pairs. Pairs are space-
|
Limit command to these pairs. Pairs are space-
|
||||||
separated.
|
separated.
|
||||||
@@ -435,7 +438,8 @@ optional arguments:
|
|||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-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
|
'syslog', 'journald'. See the documentation for more
|
||||||
details.
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
@@ -451,7 +455,7 @@ Common arguments:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Example list-data
|
### Example list-data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
> freqtrade list-data --userdir ~/.freqtrade/user_data/
|
> freqtrade list-data --userdir ~/.freqtrade/user_data/
|
||||||
@@ -465,12 +469,12 @@ ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
|||||||
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
||||||
```
|
```
|
||||||
|
|
||||||
### Trades (tick) data
|
## Trades (tick) data
|
||||||
|
|
||||||
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
|
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.
|
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.
|
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
|
### 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.
|
Details will obviously vary between setups - but this should work to get you started.
|
||||||
|
|
||||||
``` json
|
``` 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".
|
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"
|
!!! 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).
|
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 button "Draft a new release" in the Github UI (subsection releases).
|
||||||
* Use the version-number specified as tag.
|
* Use the version-number specified as tag.
|
||||||
* Use "stable" as reference (this step comes after the above PR is merged).
|
* 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
|
## Releases
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
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`).
|
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 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.
|
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.
|
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.
|
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).
|
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.
|
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`"
|
!!! 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).
|
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.
|
"`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.
|
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`.
|
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
|
### 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:
|
You can then use these commands as follows:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
@@ -204,7 +207,7 @@ docker compose -f docker/docker-compose-jupyter.yml build --no-cache
|
|||||||
### Docker on Windows
|
### Docker on Windows
|
||||||
|
|
||||||
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
* 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.
|
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).
|
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.
|
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.
|
||||||
|
|
||||||
@@ -214,6 +217,10 @@ docker compose -f docker/docker-compose-jupyter.yml build --no-cache
|
|||||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
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
|
!!! 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.
|
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
||||||
Best use a linux-VPS for running freqtrade reliably.
|
Best use a linux-VPS for running freqtrade reliably.
|
||||||
|
|||||||
+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.
|
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.
|
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.
|
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"
|
!!! 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.
|
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.
|
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).
|
* 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?
|
### 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:
|
This could be caused by the following reasons:
|
||||||
|
|
||||||
* The virtual environment is not active.
|
* 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.
|
* The installation did not complete successfully.
|
||||||
* Please check the [Installation documentation](installation.md).
|
* 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).
|
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.
|
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
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
# the model will return all labels created by user in `set_freqai_labels()`
|
# the model will return all labels created by user in `set_freqai_targets()`
|
||||||
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
||||||
# the target mean/std values for each of the labels created by user in
|
# the target mean/std values for each of the labels created by user in
|
||||||
# `feature_engineering_*` for each training period.
|
# `set_freqai_targets()` for each training period.
|
||||||
|
|
||||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ Below are the values you can expect to include/use inside a typical strategy dat
|
|||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| `df['&*']` | Any dataframe column prepended with `&` in `set_freqai_targets()` is treated as a training target (label) inside FreqAI (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. FreqAI makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`. <br> **Datatype:** Depends on the output of the model.
|
| `df['&*']` | Any dataframe column prepended with `&` in `set_freqai_targets()` is treated as a training target (label) inside FreqAI (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. FreqAI makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`. <br> **Datatype:** Depends on the output of the model.
|
||||||
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
||||||
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
||||||
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
||||||
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`. <br> **Datatype:** Depends on the output of the model.
|
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`. <br> **Datatype:** Depends on the output of the model.
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ You can ask for each of the defined features to be included also for informative
|
|||||||
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
||||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||||
|
|
||||||
|
!!! note "Learn more about creative feature engineering"
|
||||||
|
Check out our [medium article](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665) geared toward helping users learn how to creatively engineer features.
|
||||||
|
|
||||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||||
|
|
||||||
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
||||||
@@ -209,41 +212,7 @@ Another example, where the user wants to use live metrics from the trade databas
|
|||||||
|
|
||||||
You need to set the standard dictionary in the config so that FreqAI can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the pre-set values are what will be returned.
|
You need to set the standard dictionary in the config so that FreqAI can return proper dataframe shapes. These values will likely be overridden by the prediction model, but in the case where the model has yet to set them, or needs a default initial value, the pre-set values are what will be returned.
|
||||||
|
|
||||||
## Feature normalization
|
### Weighting features for temporal importance
|
||||||
|
|
||||||
FreqAI is strict when it comes to data normalization. The train features, $X^{train}$, are always normalized to [-1, 1] using a shifted min-max normalization:
|
|
||||||
|
|
||||||
$$X^{train}_{norm} = 2 * \frac{X^{train} - X^{train}.min()}{X^{train}.max() - X^{train}.min()} - 1$$
|
|
||||||
|
|
||||||
All other data (test data and unseen prediction data in dry/live/backtest) is always automatically normalized to the training feature space according to industry standards. FreqAI stores all the metadata required to ensure that test and prediction features will be properly normalized and that predictions are properly denormalized. For this reason, it is not recommended to eschew industry standards and modify FreqAI internals - however - advanced users can do so by inheriting `train()` in their custom `IFreqaiModel` and using their own normalization functions.
|
|
||||||
|
|
||||||
## Data dimensionality reduction with Principal Component Analysis
|
|
||||||
|
|
||||||
You can reduce the dimensionality of your features by activating the `principal_component_analysis` in the config:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"freqai": {
|
|
||||||
"feature_parameters" : {
|
|
||||||
"principal_component_analysis": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This will perform PCA on the features and reduce their dimensionality so that the explained variance of the data set is >= 0.999. Reducing data dimensionality makes training the model faster and hence allows for more up-to-date models.
|
|
||||||
|
|
||||||
## Inlier metric
|
|
||||||
|
|
||||||
The `inlier_metric` is a metric aimed at quantifying how similar the features of a data point are to the most recent historical data points.
|
|
||||||
|
|
||||||
You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
FreqAI adds the `inlier_metric` to the training features and hence gives the model access to a novel type of temporal information.
|
|
||||||
|
|
||||||
This function does **not** remove outliers from the data set.
|
|
||||||
|
|
||||||
## Weighting features for temporal importance
|
|
||||||
|
|
||||||
FreqAI allows you to set a `weight_factor` to weight recent data more strongly than past data via an exponential function:
|
FreqAI allows you to set a `weight_factor` to weight recent data more strongly than past data via an exponential function:
|
||||||
|
|
||||||
@@ -253,13 +222,103 @@ where $W_i$ is the weight of data point $i$ in a total set of $n$ data points. B
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Building the data pipeline
|
||||||
|
|
||||||
|
By default, FreqAI builds a dynamic pipeline based on user congfiguration settings. The default settings are robust and designed to work with a variety of methods. These two steps are a `MinMaxScaler(-1,1)` and a `VarianceThreshold` which removes any column that has 0 variance. Users can activate other steps with more configuration parameters. For example if users add `use_SVM_to_remove_outliers: true` to the `freqai` config, then FreqAI will automatically add the [`SVMOutlierExtractor`](#identifying-outliers-using-a-support-vector-machine-svm) to the pipeline. Likewise, users can add `principal_component_analysis: true` to the `freqai` config to activate PCA. The [DissimilarityIndex](#identifying-outliers-with-the-dissimilarity-index-di) is activated with `DI_threshold: 1`. Finally, noise can also be added to the data with `noise_standard_deviation: 0.1`. Finally, users can add [DBSCAN](#identifying-outliers-with-dbscan) outlier removal with `use_DBSCAN_to_remove_outliers: true`.
|
||||||
|
|
||||||
|
!!! note "More information available"
|
||||||
|
Please review the [parameter table](freqai-parameter-table.md) for more information on these parameters.
|
||||||
|
|
||||||
|
|
||||||
|
### Customizing the pipeline
|
||||||
|
|
||||||
|
Users are encouraged to customize the data pipeline to their needs by building their own data pipeline. This can be done by simply setting `dk.feature_pipeline` to their desired `Pipeline` object inside their `IFreqaiModel` `train()` function, or if they prefer not to touch the `train()` function, they can override `define_data_pipeline`/`define_label_pipeline` functions in their `IFreqaiModel`:
|
||||||
|
|
||||||
|
!!! note "More information available"
|
||||||
|
FreqAI uses the the [`DataSieve`](https://github.com/emergentmethods/datasieve) pipeline, which follows the SKlearn pipeline API, but adds, among other features, coherence between the X, y, and sample_weight vector point removals, feature removal, feature name following.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datasieve.transforms import SKLearnWrapper, DissimilarityIndex
|
||||||
|
from datasieve.pipeline import Pipeline
|
||||||
|
from sklearn.preprocessing import QuantileTransformer, StandardScaler
|
||||||
|
from freqai.base_models import BaseRegressionModel
|
||||||
|
|
||||||
|
|
||||||
|
class MyFreqaiModel(BaseRegressionModel):
|
||||||
|
"""
|
||||||
|
Some cool custom model
|
||||||
|
"""
|
||||||
|
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
|
"""
|
||||||
|
My custom fit function
|
||||||
|
"""
|
||||||
|
model = cool_model.fit()
|
||||||
|
return model
|
||||||
|
|
||||||
|
def define_data_pipeline(self) -> Pipeline:
|
||||||
|
"""
|
||||||
|
User defines their custom feature pipeline here (if they wish)
|
||||||
|
"""
|
||||||
|
feature_pipeline = Pipeline([
|
||||||
|
('qt', SKLearnWrapper(QuantileTransformer(output_distribution='normal'))),
|
||||||
|
('di', ds.DissimilarityIndex(di_threshold=1))
|
||||||
|
])
|
||||||
|
|
||||||
|
return feature_pipeline
|
||||||
|
|
||||||
|
def define_label_pipeline(self) -> Pipeline:
|
||||||
|
"""
|
||||||
|
User defines their custom label pipeline here (if they wish)
|
||||||
|
"""
|
||||||
|
label_pipeline = Pipeline([
|
||||||
|
('qt', SKLearnWrapper(StandardScaler())),
|
||||||
|
])
|
||||||
|
|
||||||
|
return label_pipeline
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, you are defining the exact pipeline that will be used for your feature set during training and prediction. You can use *most* SKLearn transformation steps by wrapping them in the `SKLearnWrapper` class as shown above. In addition, you can use any of the transformations available in the [`DataSieve` library](https://github.com/emergentmethods/datasieve).
|
||||||
|
|
||||||
|
You can easily add your own transformation by creating a class that inherits from the datasieve `BaseTransform` and implementing your `fit()`, `transform()` and `inverse_transform()` methods:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datasieve.transforms.base_transform import BaseTransform
|
||||||
|
# import whatever else you need
|
||||||
|
|
||||||
|
class MyCoolTransform(BaseTransform):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.param1 = kwargs.get('param1', 1)
|
||||||
|
|
||||||
|
def fit(self, X, y=None, sample_weight=None, feature_list=None, **kwargs):
|
||||||
|
# do something with X, y, sample_weight, or/and feature_list
|
||||||
|
return X, y, sample_weight, feature_list
|
||||||
|
|
||||||
|
def transform(self, X, y=None, sample_weight=None,
|
||||||
|
feature_list=None, outlier_check=False, **kwargs):
|
||||||
|
# do something with X, y, sample_weight, or/and feature_list
|
||||||
|
return X, y, sample_weight, feature_list
|
||||||
|
|
||||||
|
def inverse_transform(self, X, y=None, sample_weight=None, feature_list=None, **kwargs):
|
||||||
|
# do/dont do something with X, y, sample_weight, or/and feature_list
|
||||||
|
return X, y, sample_weight, feature_list
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note "Hint"
|
||||||
|
You can define this custom class in the same file as your `IFreqaiModel`.
|
||||||
|
|
||||||
|
### Migrating a custom `IFreqaiModel` to the new Pipeline
|
||||||
|
|
||||||
|
If you have created your own custom `IFreqaiModel` with a custom `train()`/`predict()` function, *and* you still rely on `data_cleaning_train/predict()`, then you will need to migrate to the new pipeline. If your model does *not* rely on `data_cleaning_train/predict()`, then you do not need to worry about this migration.
|
||||||
|
|
||||||
|
More details about the migration can be found [here](strategy_migration.md#freqai---new-data-pipeline).
|
||||||
|
|
||||||
## Outlier detection
|
## Outlier detection
|
||||||
|
|
||||||
Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. FreqAI implements a variety of methods to identify such outliers and hence mitigate risk.
|
Equity and crypto markets suffer from a high level of non-patterned noise in the form of outlier data points. FreqAI implements a variety of methods to identify such outliers and hence mitigate risk.
|
||||||
|
|
||||||
### Identifying outliers with the Dissimilarity Index (DI)
|
### Identifying outliers with the Dissimilarity Index (DI)
|
||||||
|
|
||||||
The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model.
|
The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each prediction made by the model.
|
||||||
|
|
||||||
You can tell FreqAI to remove outlier data points from the training/test data sets using the DI by including the following statement in the config:
|
You can tell FreqAI to remove outlier data points from the training/test data sets using the DI by including the following statement in the config:
|
||||||
|
|
||||||
@@ -271,7 +330,7 @@ You can tell FreqAI to remove outlier data points from the training/test data se
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The DI allows predictions which are outliers (not existent in the model feature space) to be thrown out due to low levels of certainty. To do so, FreqAI measures the distance between each training data point (feature vector), $X_{a}$, and all other training data points:
|
Which will add `DissimilarityIndex` step to your `feature_pipeline` and set the threshold to 1. The DI allows predictions which are outliers (not existent in the model feature space) to be thrown out due to low levels of certainty. To do so, FreqAI measures the distance between each training data point (feature vector), $X_{a}$, and all other training data points:
|
||||||
|
|
||||||
$$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$
|
$$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$
|
||||||
|
|
||||||
@@ -305,9 +364,9 @@ You can tell FreqAI to remove outlier data points from the training/test data se
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The SVM will be trained on the training data and any data point that the SVM deems to be beyond the feature space will be removed.
|
Which will add `SVMOutlierExtractor` step to your `feature_pipeline`. The SVM will be trained on the training data and any data point that the SVM deems to be beyond the feature space will be removed.
|
||||||
|
|
||||||
FreqAI uses `sklearn.linear_model.SGDOneClassSVM` (details are available on scikit-learn's webpage [here](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDOneClassSVM.html) (external website)) and you can elect to provide additional parameters for the SVM, such as `shuffle`, and `nu`.
|
You can elect to provide additional parameters for the SVM, such as `shuffle`, and `nu` via the `feature_parameters.svm_params` dictionary in the config.
|
||||||
|
|
||||||
The parameter `shuffle` is by default set to `False` to ensure consistent results. If it is set to `True`, running the SVM multiple times on the same data set might result in different outcomes due to `max_iter` being to low for the algorithm to reach the demanded `tol`. Increasing `max_iter` solves this issue but causes the procedure to take longer time.
|
The parameter `shuffle` is by default set to `False` to ensure consistent results. If it is set to `True`, running the SVM multiple times on the same data set might result in different outcomes due to `max_iter` being to low for the algorithm to reach the demanded `tol`. Increasing `max_iter` solves this issue but causes the procedure to take longer time.
|
||||||
|
|
||||||
@@ -325,7 +384,7 @@ You can configure FreqAI to use DBSCAN to cluster and remove outliers from the t
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
DBSCAN is an unsupervised machine learning algorithm that clusters data without needing to know how many clusters there should be.
|
Which will add the `DataSieveDBSCAN` step to your `feature_pipeline`. This is an unsupervised machine learning algorithm that clusters data without needing to know how many clusters there should be.
|
||||||
|
|
||||||
Given a number of data points $N$, and a distance $\varepsilon$, DBSCAN clusters the data set by setting all data points that have $N-1$ other data points within a distance of $\varepsilon$ as *core points*. A data point that is within a distance of $\varepsilon$ from a *core point* but that does not have $N-1$ other data points within a distance of $\varepsilon$ from itself is considered an *edge point*. A cluster is then the collection of *core points* and *edge points*. Data points that have no other data points at a distance $<\varepsilon$ are considered outliers. The figure below shows a cluster with $N = 3$.
|
Given a number of data points $N$, and a distance $\varepsilon$, DBSCAN clusters the data set by setting all data points that have $N-1$ other data points within a distance of $\varepsilon$ as *core points*. A data point that is within a distance of $\varepsilon$ from a *core point* but that does not have $N-1$ other data points within a distance of $\varepsilon$ from itself is considered an *edge point*. A cluster is then the collection of *core points* and *edge points*. Data points that have no other data points at a distance $<\varepsilon$ are considered outliers. The figure below shows a cluster with $N = 3$.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
| `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.
|
| `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.
|
| `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`.
|
| `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`.
|
| `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).
|
| `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).
|
||||||
@@ -102,11 +101,11 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
#### trainer_kwargs
|
#### trainer_kwargs
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|--------------|-------------|
|
||||||
| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary**
|
| | **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`.
|
| `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`.
|
||||||
| `batch_size` | The size of the batches to use during training.. <br> **Datatype:** int. <br> Default: `64`.
|
| `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`.
|
||||||
| `max_n_eval_batches` | The maximum number batches to use for evaluation.. <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
|
### 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.
|
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
|
### Important considerations
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class MyCoolRLModel(ReinforcementLearner):
|
|||||||
"""
|
"""
|
||||||
class MyRLEnv(Base5ActionRLEnv):
|
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
|
Users can override any functions from those parent classes. Here is an example
|
||||||
of a user customized `calculate_reward()` function.
|
of a user customized `calculate_reward()` function.
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard
|
|||||||
```python
|
```python
|
||||||
class MyRLEnv(Base5ActionRLEnv):
|
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
|
Users can override any functions from those parent classes. Here is an example
|
||||||
of a user customized `calculate_reward()` function.
|
of a user customized `calculate_reward()` function.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+8
-1
@@ -76,7 +76,7 @@ pip install -r requirements-freqai.txt
|
|||||||
|
|
||||||
### Usage with docker
|
### Usage with docker
|
||||||
|
|
||||||
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. If you would like to use PyTorch or Reinforcement learning, you should use the torch or RL tags, `image: freqtradeorg/freqtrade:develop_freqaitorch`, `image: freqtradeorg/freqtrade:develop_freqairl`.
|
||||||
|
|
||||||
!!! note "docker-compose-freqai.yml"
|
!!! note "docker-compose-freqai.yml"
|
||||||
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||||
@@ -107,6 +107,13 @@ This is for performance reasons - FreqAI relies on making quick predictions/retr
|
|||||||
it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends
|
it needs to download all the training data at the beginning of a dry/live instance. FreqAI stores and appends
|
||||||
new candles automatically for future retrains. This means that if new pairs arrive later in the dry run due to a volume pairlist, it will not have the data ready. However, FreqAI does work with the `ShufflePairlist` or a `VolumePairlist` which keeps the total pairlist constant (but reorders the pairs according to volume).
|
new candles automatically for future retrains. This means that if new pairs arrive later in the dry run due to a volume pairlist, it will not have the data ready. However, FreqAI does work with the `ShufflePairlist` or a `VolumePairlist` which keeps the total pairlist constant (but reorders the pairs according to volume).
|
||||||
|
|
||||||
|
## Additional learning materials
|
||||||
|
|
||||||
|
Here we compile some external materials that provide deeper looks into various components of FreqAI:
|
||||||
|
|
||||||
|
- [Real-time head-to-head: Adaptive modeling of financial market data using XGBoost and CatBoost](https://emergentmethods.medium.com/real-time-head-to-head-adaptive-modeling-of-financial-market-data-using-xgboost-and-catboost-995a115a7495)
|
||||||
|
- [FreqAI - from price to prediction](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665)
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
||||||
|
|||||||
+8
-3
@@ -31,7 +31,7 @@ The docker-image includes hyperopt dependencies, no further action needed.
|
|||||||
### Easy installation script (setup.sh) / Manual installation
|
### Easy installation script (setup.sh) / Manual installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source .env/bin/activate
|
source .venv/bin/activate
|
||||||
pip install -r requirements-hyperopt.txt
|
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.
|
`range` property may also be used with `DecimalParameter` and `CategoricalParameter`. `RealParameter` does not provide this property due to infinite search space.
|
||||||
|
|
||||||
??? Hint "Performance tip"
|
??? 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
|
## Optimizing protections
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
|||||||
"pairlists": [
|
"pairlists": [
|
||||||
{
|
{
|
||||||
"method": "RemotePairList",
|
"method": "RemotePairList",
|
||||||
|
"mode": "whitelist",
|
||||||
|
"processing_mode": "filter",
|
||||||
"pairlist_url": "https://example.com/pairlist",
|
"pairlist_url": "https://example.com/pairlist",
|
||||||
"number_assets": 10,
|
"number_assets": 10,
|
||||||
"refresh_period": 1800,
|
"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 `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:
|
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
|
```json
|
||||||
{
|
{
|
||||||
"pairs": ["XRP/USDT", "ETH/USDT", "LTC/USDT"],
|
"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] [Bitvavo](https://bitvavo.com/)
|
||||||
- [X] [Kucoin](https://www.kucoin.com/)
|
- [X] [Kucoin](https://www.kucoin.com/)
|
||||||
|
|
||||||
|
## Community showcase
|
||||||
|
|
||||||
|
--8<-- "includes/showcase.md"
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Hardware 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
|
### 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
|
```bash
|
||||||
# then activate your .env
|
# activate virtual environment
|
||||||
source ./.env/bin/activate
|
source ./.venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Congratulations
|
### 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.
|
You will need to have git and python3.8+ installed beforehand for this to work.
|
||||||
|
|
||||||
* Mandatory software as: `ta-lib`
|
* 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`
|
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`
|
You will run freqtrade in separated `virtual environment`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# create virtualenv in directory /freqtrade/.env
|
# create virtualenv in directory /freqtrade/.venv
|
||||||
python3 -m venv .env
|
python3 -m venv .venv
|
||||||
|
|
||||||
# run virtualenv
|
# run virtualenv
|
||||||
source .env/bin/activate
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install python dependencies
|
#### Install python dependencies
|
||||||
@@ -286,7 +286,7 @@ cd freqtrade
|
|||||||
#### Freqtrade install: Conda Environment
|
#### Freqtrade install: Conda Environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conda create --name freqtrade python=3.10
|
conda create --name freqtrade python=3.11
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note "Creating Conda Environment"
|
!!! 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
|
freqtrade create-userdir --userdir user_data
|
||||||
|
|
||||||
# Step 2 - Create a new configuration file
|
# 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.
|
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
|
### Start the Bot
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade trade --config config.json --strategy SampleStrategy
|
freqtrade trade --config user_data/config.json --strategy SampleStrategy
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
@@ -411,8 +411,8 @@ If you used (1)`Script` or (2)`Manual` installation, you need to run the bot in
|
|||||||
# if:
|
# if:
|
||||||
bash: freqtrade: command not found
|
bash: freqtrade: command not found
|
||||||
|
|
||||||
# then activate your .env
|
# then activate your virtual environment
|
||||||
source ./.env/bin/activate
|
source ./.venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### MacOS installation error
|
### MacOS installation error
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
|
|||||||
|
|
||||||
##### Pair namings
|
##### 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`).
|
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||||
|
|
||||||
### Margin mode
|
### Margin mode
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Lookahead analysis
|
||||||
|
|
||||||
|
This page explains how to validate your strategy in terms of look ahead bias.
|
||||||
|
|
||||||
|
Checking look ahead bias is the bane of any strategy since it is sometimes very easy to introduce backtest bias -
|
||||||
|
but very hard to detect.
|
||||||
|
|
||||||
|
Backtesting initializes all timestamps at once and calculates all indicators in the beginning.
|
||||||
|
This means that if your indicators or entry/exit signals could look into future candles and falsify your backtest.
|
||||||
|
|
||||||
|
Lookahead-analysis requires historic data to be available.
|
||||||
|
To learn how to get data for the pairs and exchange you're interested in,
|
||||||
|
head over to the [Data Downloading](data-download.md) section of the documentation.
|
||||||
|
|
||||||
|
This command is built upon backtesting since it internally chains backtests and pokes at the strategy to provoke it to show look ahead bias.
|
||||||
|
This is done by not looking at the strategy itself - but at the results it returned.
|
||||||
|
The results are things like changed indicator-values and moved entries/exits compared to the full backtest.
|
||||||
|
|
||||||
|
You can use commands of [Backtesting](backtesting.md).
|
||||||
|
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 (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
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade lookahead-analysis [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH] [-s NAME]
|
||||||
|
[--strategy-path PATH]
|
||||||
|
[--recursive-strategy-search]
|
||||||
|
[--freqaimodel NAME]
|
||||||
|
[--freqaimodel-path PATH] [-i TIMEFRAME]
|
||||||
|
[--timerange TIMERANGE]
|
||||||
|
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
||||||
|
[--max-open-trades INT]
|
||||||
|
[--stake-amount STAKE_AMOUNT]
|
||||||
|
[--fee FLOAT] [-p PAIRS [PAIRS ...]]
|
||||||
|
[--enable-protections]
|
||||||
|
[--dry-run-wallet DRY_RUN_WALLET]
|
||||||
|
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||||
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
|
[--export {none,trades,signals}]
|
||||||
|
[--export-filename PATH]
|
||||||
|
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||||
|
[--cache {none,day,week,month}]
|
||||||
|
[--freqai-backtest-live-models]
|
||||||
|
[--minimum-trade-amount INT]
|
||||||
|
[--targeted-trade-amount INT]
|
||||||
|
[--lookahead-analysis-exportfilename LOOKAHEAD_ANALYSIS_EXPORTFILENAME]
|
||||||
|
|
||||||
|
options:
|
||||||
|
--minimum-trade-amount INT
|
||||||
|
Minimum trade amount for lookahead-analysis
|
||||||
|
--targeted-trade-amount INT
|
||||||
|
Targeted trade amount for lookahead analysis
|
||||||
|
--lookahead-analysis-exportfilename LOOKAHEAD_ANALYSIS_EXPORTFILENAME
|
||||||
|
Use this csv-filename to store lookahead-analysis-
|
||||||
|
results
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note ""
|
||||||
|
The above Output was reduced to options `lookahead-analysis` adds on top of regular backtesting commands.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Checks a given strategy for look ahead bias via lookahead-analysis
|
||||||
|
Look ahead bias means that the backtest uses data from future candles thereby not making it viable beyond backtesting
|
||||||
|
and producing false hopes for the one backtesting.
|
||||||
|
|
||||||
|
### Introduction
|
||||||
|
|
||||||
|
Many strategies - without the programmer knowing - have fallen prey to look ahead bias.
|
||||||
|
|
||||||
|
Any backtest will populate the full dataframe including all time stamps at the beginning.
|
||||||
|
If the programmer is not careful or oblivious how things work internally
|
||||||
|
(which sometimes can be really hard to find out) then it will just look into the future making the strategy amazing
|
||||||
|
but not realistic.
|
||||||
|
|
||||||
|
This command is made to try to verify the validity in the form of the aforementioned look ahead bias.
|
||||||
|
|
||||||
|
### How does the command work?
|
||||||
|
|
||||||
|
It will start with a backtest of all pairs to generate a baseline for indicators and entries/exits.
|
||||||
|
After the backtest ran, it will look if the `minimum-trade-amount` is met
|
||||||
|
and if not cancel the lookahead-analysis for this strategy.
|
||||||
|
|
||||||
|
After setting the baseline it will then do additional runs for every entry and exit separately.
|
||||||
|
When a verification-backtest is done, it will compare the indicators as the signal (either entry or exit) and report the bias.
|
||||||
|
After all signals have been verified or falsified a result-table will be generated for the user to see.
|
||||||
|
|
||||||
|
### Caveats
|
||||||
|
|
||||||
|
- `lookahead-analysis` can only verify / falsify the trades it calculated and verified.
|
||||||
|
If the strategy has many different signals / signal types, it's up to you to select appropriate parameters to ensure that all signals have triggered at least once. Not triggered signals will not have been verified.
|
||||||
|
This could lead to a false-negative (the strategy will then be reported as non-biased).
|
||||||
|
- `lookahead-analysis` has access to everything that backtesting has too.
|
||||||
|
Please don't provoke any configs like enabling position stacking.
|
||||||
|
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` amount and neither leftover money in your wallet.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.3.7
|
markdown==3.4.4
|
||||||
mkdocs==1.4.3
|
mkdocs==1.5.2
|
||||||
mkdocs-material==9.1.14
|
mkdocs-material==9.2.1
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.0.1
|
pymdown-extensions==10.1
|
||||||
jinja2==3.1.2
|
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!
|
# Hope you have a deep wallet!
|
||||||
try:
|
try:
|
||||||
# This returns first order stake size
|
# 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
|
# This then calculates current safety order size
|
||||||
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
|
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
|
||||||
return stake_amount
|
return stake_amount
|
||||||
|
|||||||
@@ -342,16 +342,12 @@ The above configuration would therefore mean:
|
|||||||
|
|
||||||
The calculation does include fees.
|
The calculation does include fees.
|
||||||
|
|
||||||
To disable ROI completely, set it to an insanely high number:
|
To disable ROI completely, set it to an empty dictionary:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
minimal_roi = {
|
minimal_roi = {}
|
||||||
"0": 100
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
While technically not completely disabled, this would exit once the trade reaches 10000% Profit.
|
|
||||||
|
|
||||||
To use times based on candle duration (timeframe), the following snippet can be handy.
|
To use times based on candle duration (timeframe), the following snippet can be handy.
|
||||||
This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
|
This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
|
||||||
|
|
||||||
|
|||||||
@@ -728,3 +728,86 @@ Targets now get their own, dedicated method.
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### FreqAI - New data Pipeline
|
||||||
|
|
||||||
|
If you have created your own custom `IFreqaiModel` with a custom `train()`/`predict()` function, *and* you still rely on `data_cleaning_train/predict()`, then you will need to migrate to the new pipeline. If your model does *not* rely on `data_cleaning_train/predict()`, then you do not need to worry about this migration. That means that this migration guide is relevant for a very small percentage of power-users. If you stumbled upon this guide by mistake, feel free to inquire in depth about your problem in the Freqtrade discord server.
|
||||||
|
|
||||||
|
The conversion involves first removing `data_cleaning_train/predict()` and replacing them with a `define_data_pipeline()` and `define_label_pipeline()` function to your `IFreqaiModel` class:
|
||||||
|
|
||||||
|
```python linenums="1" hl_lines="11-14 47-49 55-57"
|
||||||
|
class MyCoolFreqaiModel(BaseRegressionModel):
|
||||||
|
"""
|
||||||
|
Some cool custom IFreqaiModel you made before Freqtrade version 2023.6
|
||||||
|
"""
|
||||||
|
def train(
|
||||||
|
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Any:
|
||||||
|
|
||||||
|
# ... your custom stuff
|
||||||
|
|
||||||
|
# Remove these lines
|
||||||
|
# data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
||||||
|
# self.data_cleaning_train(dk)
|
||||||
|
# data_dictionary = dk.normalize_data(data_dictionary)
|
||||||
|
# (1)
|
||||||
|
|
||||||
|
# Add these lines. Now we control the pipeline fit/transform ourselves
|
||||||
|
dd = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
||||||
|
dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
|
||||||
|
dk.label_pipeline = self.define_label_pipeline(threads=dk.thread_count)
|
||||||
|
|
||||||
|
(dd["train_features"],
|
||||||
|
dd["train_labels"],
|
||||||
|
dd["train_weights"]) = dk.feature_pipeline.fit_transform(dd["train_features"],
|
||||||
|
dd["train_labels"],
|
||||||
|
dd["train_weights"])
|
||||||
|
|
||||||
|
(dd["test_features"],
|
||||||
|
dd["test_labels"],
|
||||||
|
dd["test_weights"]) = dk.feature_pipeline.transform(dd["test_features"],
|
||||||
|
dd["test_labels"],
|
||||||
|
dd["test_weights"])
|
||||||
|
|
||||||
|
dd["train_labels"], _, _ = dk.label_pipeline.fit_transform(dd["train_labels"])
|
||||||
|
dd["test_labels"], _, _ = dk.label_pipeline.transform(dd["test_labels"])
|
||||||
|
|
||||||
|
# ... your custom code
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
def predict(
|
||||||
|
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||||
|
|
||||||
|
# ... your custom stuff
|
||||||
|
|
||||||
|
# Remove these lines:
|
||||||
|
# self.data_cleaning_predict(dk)
|
||||||
|
# (2)
|
||||||
|
|
||||||
|
# Add these lines:
|
||||||
|
dk.data_dictionary["prediction_features"], outliers, _ = dk.feature_pipeline.transform(
|
||||||
|
dk.data_dictionary["prediction_features"], outlier_check=True)
|
||||||
|
|
||||||
|
# Remove this line
|
||||||
|
# pred_df = dk.denormalize_labels_from_metadata(pred_df)
|
||||||
|
# (3)
|
||||||
|
|
||||||
|
# Replace with these lines
|
||||||
|
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
||||||
|
if self.freqai_info.get("DI_threshold", 0) > 0:
|
||||||
|
dk.DI_values = dk.feature_pipeline["di"].di_values
|
||||||
|
else:
|
||||||
|
dk.DI_values = np.zeros(outliers.shape[0])
|
||||||
|
dk.do_predict = outliers
|
||||||
|
|
||||||
|
# ... your custom code
|
||||||
|
return (pred_df, dk.do_predict)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
1. Data normalization and cleaning is now homogenized with the new pipeline definition. This is created in the new `define_data_pipeline()` and `define_label_pipeline()` functions. The `data_cleaning_train()` and `data_cleaning_predict()` functions are no longer used. You can override `define_data_pipeline()` to create your own custom pipeline if you wish.
|
||||||
|
2. Data normalization and cleaning is now homogenized with the new pipeline definition. This is created in the new `define_data_pipeline()` and `define_label_pipeline()` functions. The `data_cleaning_train()` and `data_cleaning_predict()` functions are no longer used. You can override `define_data_pipeline()` to create your own custom pipeline if you wish.
|
||||||
|
3. Data denormalization is done with the new pipeline. Replace this with the lines below.
|
||||||
|
|||||||
@@ -287,12 +287,17 @@ Return a summary of your profit/loss and performance.
|
|||||||
> **Best Performing:** `PAY/BTC: 50.23%`
|
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||||
> **Trading volume:** `0.5 BTC`
|
> **Trading volume:** `0.5 BTC`
|
||||||
> **Profit factor:** `1.04`
|
> **Profit factor:** `1.04`
|
||||||
|
> **Win / Loss:** `102 / 36`
|
||||||
|
> **Winrate:** `73.91%`
|
||||||
|
> **Expectancy (Ratio):** `4.87 (1.66)`
|
||||||
> **Max Drawdown:** `9.23% (0.01255 BTC)`
|
> **Max Drawdown:** `9.23% (0.01255 BTC)`
|
||||||
|
|
||||||
The relative profit of `1.2%` is the average profit per trade.
|
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`.
|
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.
|
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.
|
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)`.
|
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.
|
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
|
`amount` | float | Amount in base currency
|
||||||
`filled` | float | Filled amount (in base currency)
|
`filled` | float | Filled amount (in base currency)
|
||||||
`remaining` | float | Remaining amount
|
`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` | datetime | Order creation date **use `order_date_utc` instead**
|
||||||
`order_date_utc` | datetime | Order creation date (in UTC)
|
`order_date_utc` | datetime | Order creation date (in UTC)
|
||||||
`order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead**
|
`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
|
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.
|
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.
|
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.
|
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
|
```json
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "https://<YOURHOOKURL>",
|
"url": "https://<YOURHOOKURL>",
|
||||||
|
"timeout": 10,
|
||||||
"retries": 3,
|
"retries": 3,
|
||||||
"retry_delay": 0.2,
|
"retry_delay": 0.2,
|
||||||
"status": {
|
"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.
|
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
|
### Entry
|
||||||
|
|
||||||
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
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
|
``` powershell
|
||||||
cd \path\freqtrade
|
cd \path\freqtrade
|
||||||
python -m venv .env
|
python -m venv .venv
|
||||||
.env\Scripts\activate.ps1
|
.venv\Scripts\activate.ps1
|
||||||
# optionally install ta-lib from wheel
|
# optionally install ta-lib from wheel
|
||||||
# Eventually adjust the below filename to match the downloaded wheel
|
# Eventually adjust the below filename to match the downloaded wheel
|
||||||
pip install --find-links build_helpers\ TA-Lib -U
|
pip install --find-links build_helpers\ TA-Lib -U
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2023.5'
|
__version__ = '2023.8'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ from freqtrade.commands.list_commands import (start_list_exchanges, start_list_f
|
|||||||
start_list_markets, start_list_strategies,
|
start_list_markets, start_list_strategies,
|
||||||
start_list_timeframes, start_show_trades)
|
start_list_timeframes, start_show_trades)
|
||||||
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show,
|
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show,
|
||||||
start_edge, start_hyperopt)
|
start_edge, start_hyperopt,
|
||||||
|
start_lookahead_analysis)
|
||||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||||
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
||||||
|
|||||||
Regular → Executable
+21
-6
@@ -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 = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
||||||
|
|
||||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode",
|
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
||||||
"candle_types"]
|
|
||||||
|
|
||||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||||
|
|
||||||
@@ -117,7 +116,11 @@ NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list
|
|||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
|
|
||||||
ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||||
|
|
||||||
|
ARGS_LOOKAHEAD_ANALYSIS = [
|
||||||
|
a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", 'cache')
|
||||||
|
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
||||||
|
|
||||||
|
|
||||||
class Arguments:
|
class Arguments:
|
||||||
@@ -201,8 +204,9 @@ class Arguments:
|
|||||||
start_install_ui, start_list_data, start_list_exchanges,
|
start_install_ui, start_list_data, start_list_exchanges,
|
||||||
start_list_freqAI_models, start_list_markets,
|
start_list_freqAI_models, start_list_markets,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
start_lookahead_analysis, start_new_config,
|
||||||
start_plot_profit, start_show_trades, start_strategy_update,
|
start_new_strategy, start_plot_dataframe, start_plot_profit,
|
||||||
|
start_show_trades, start_strategy_update,
|
||||||
start_test_pairlist, start_trading, start_webserver)
|
start_test_pairlist, start_trading, start_webserver)
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='command',
|
subparsers = self.parser.add_subparsers(dest='command',
|
||||||
@@ -451,4 +455,15 @@ class Arguments:
|
|||||||
'files to the current version',
|
'files to the current version',
|
||||||
parents=[_common_parser])
|
parents=[_common_parser])
|
||||||
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
||||||
self._build_args(optionlist=ARGS_STRATEGY_UTILS, parser=strategy_updater_cmd)
|
self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd)
|
||||||
|
|
||||||
|
# Add lookahead_analysis subcommand
|
||||||
|
lookahead_analayis_cmd = subparsers.add_parser(
|
||||||
|
'lookahead-analysis',
|
||||||
|
help="Check for potential look ahead bias.",
|
||||||
|
parents=[_common_parser, _strategy_parser])
|
||||||
|
|
||||||
|
lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis)
|
||||||
|
|
||||||
|
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS,
|
||||||
|
parser=lookahead_analayis_cmd)
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ from typing import Any, Dict, List
|
|||||||
|
|
||||||
from questionary import Separator, prompt
|
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.configuration.directory_operations import chown_user_directory
|
||||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -104,7 +105,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"type": "select",
|
"type": "select",
|
||||||
"name": "exchange_name",
|
"name": "exchange_name",
|
||||||
"message": "Select exchange",
|
"message": "Select exchange",
|
||||||
"choices": lambda x: [
|
"choices": [
|
||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
"bittrex",
|
||||||
@@ -179,7 +180,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"name": "api_server_listen_addr",
|
"name": "api_server_listen_addr",
|
||||||
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
|
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||||
"otherwise best left untouched)"),
|
"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']
|
"when": lambda x: x['api_server']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Regular → Executable
+22
-7
@@ -381,7 +381,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
),
|
),
|
||||||
"candle_types": Arg(
|
"candle_types": Arg(
|
||||||
'--candle-types',
|
'--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],
|
choices=[c.value for c in CandleType],
|
||||||
nargs='+',
|
nargs='+',
|
||||||
),
|
),
|
||||||
@@ -435,13 +435,13 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
),
|
),
|
||||||
"dataformat_ohlcv": Arg(
|
"dataformat_ohlcv": Arg(
|
||||||
'--data-format-ohlcv',
|
'--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,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
),
|
),
|
||||||
"dataformat_trades": Arg(
|
"dataformat_trades": Arg(
|
||||||
'--data-format-trades',
|
'--data-format-trades',
|
||||||
help='Storage format for downloaded trades data. (default: `jsongz`).',
|
help='Storage format for downloaded trades data. (default: `feather`).',
|
||||||
choices=constants.AVAILABLE_DATAHANDLERS_TRADES,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
),
|
),
|
||||||
"show_timerange": Arg(
|
"show_timerange": Arg(
|
||||||
'--show-timerange',
|
'--show-timerange',
|
||||||
@@ -450,14 +450,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
),
|
),
|
||||||
"exchange": Arg(
|
"exchange": Arg(
|
||||||
'--exchange',
|
'--exchange',
|
||||||
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
|
help='Exchange name. Only valid if no config is provided.',
|
||||||
f'Only valid if no config is provided.',
|
|
||||||
),
|
),
|
||||||
"timeframes": Arg(
|
"timeframes": Arg(
|
||||||
'-t', '--timeframes',
|
'-t', '--timeframes',
|
||||||
help='Specify which tickers to download. Space-separated list. '
|
help='Specify which tickers to download. Space-separated list. '
|
||||||
'Default: `1m 5m`.',
|
'Default: `1m 5m`.',
|
||||||
default=['1m', '5m'],
|
|
||||||
nargs='+',
|
nargs='+',
|
||||||
),
|
),
|
||||||
"prepend_data": Arg(
|
"prepend_data": Arg(
|
||||||
@@ -690,4 +688,21 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help='Run backtest with ready models.',
|
help='Run backtest with ready models.',
|
||||||
action='store_true'
|
action='store_true'
|
||||||
),
|
),
|
||||||
|
"minimum_trade_amount": Arg(
|
||||||
|
'--minimum-trade-amount',
|
||||||
|
help='Minimum trade amount for lookahead-analysis',
|
||||||
|
type=check_int_positive,
|
||||||
|
metavar='INT',
|
||||||
|
),
|
||||||
|
"targeted_trade_amount": Arg(
|
||||||
|
'--targeted-trade-amount',
|
||||||
|
help='Targeted trade amount for lookahead analysis',
|
||||||
|
type=check_int_positive,
|
||||||
|
metavar='INT',
|
||||||
|
),
|
||||||
|
"lookahead_analysis_exportfilename": Arg(
|
||||||
|
'--lookahead-analysis-exportfilename',
|
||||||
|
help="Use this csv-filename to store lookahead-analysis-results",
|
||||||
|
type=str
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from typing import Any, Dict
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
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.converter import convert_ohlcv_format, convert_trades_format
|
||||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
from freqtrade.data.history import convert_trades_to_ohlcv, download_data_main
|
||||||
refresh_backtest_trades_data)
|
from freqtrade.enums import RunMode, TradingMode
|
||||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import market_is_active, timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||||
|
|
||||||
@@ -20,7 +18,7 @@ from freqtrade.util.binance_mig import migrate_binance_futures_data
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _data_download_sanity(config: Config) -> None:
|
def _check_data_config_download_sanity(config: Config) -> None:
|
||||||
if 'days' in config and 'timerange' in config:
|
if 'days' in config and 'timerange' in config:
|
||||||
raise OperationalException("--days and --timerange are mutually exclusive. "
|
raise OperationalException("--days and --timerange are mutually exclusive. "
|
||||||
"You can only specify one or the other.")
|
"You can only specify one or the other.")
|
||||||
@@ -37,78 +35,14 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
_data_download_sanity(config)
|
_check_data_config_download_sanity(config)
|
||||||
timerange = TimeRange()
|
|
||||||
if 'days' in config:
|
|
||||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
|
||||||
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
|
||||||
|
|
||||||
if 'timerange' in config:
|
|
||||||
timerange = timerange.parse_timerange(config['timerange'])
|
|
||||||
|
|
||||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
|
||||||
config['stake_currency'] = ''
|
|
||||||
|
|
||||||
pairs_not_available: List[str] = []
|
|
||||||
|
|
||||||
# Init exchange
|
|
||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
|
||||||
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
|
|
||||||
or config.get('include_inactive')]
|
|
||||||
|
|
||||||
expanded_pairs = dynamic_expand_pairlist(config, markets)
|
|
||||||
|
|
||||||
# Manual validations of relevant settings
|
|
||||||
if not config['exchange'].get('skip_pair_validation', False):
|
|
||||||
exchange.validate_pairs(expanded_pairs)
|
|
||||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
|
||||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
|
||||||
|
|
||||||
for timeframe in config['timeframes']:
|
|
||||||
exchange.validate_timeframes(timeframe)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
download_data_main(config)
|
||||||
if config.get('download_trades'):
|
|
||||||
if config.get('trading_mode') == 'futures':
|
|
||||||
raise OperationalException("Trade download not supported for futures.")
|
|
||||||
pairs_not_available = refresh_backtest_trades_data(
|
|
||||||
exchange, pairs=expanded_pairs, datadir=config['datadir'],
|
|
||||||
timerange=timerange, new_pairs_days=config['new_pairs_days'],
|
|
||||||
erase=bool(config.get('erase')), data_format=config['dataformat_trades'])
|
|
||||||
|
|
||||||
# Convert downloaded trade data to different timeframes
|
|
||||||
convert_trades_to_ohlcv(
|
|
||||||
pairs=expanded_pairs, timeframes=config['timeframes'],
|
|
||||||
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
|
||||||
data_format_ohlcv=config['dataformat_ohlcv'],
|
|
||||||
data_format_trades=config['dataformat_trades'],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if not exchange.get_option('ohlcv_has_history', True):
|
|
||||||
raise OperationalException(
|
|
||||||
f"Historic klines not available for {exchange.name}. "
|
|
||||||
"Please use `--dl-trades` instead for this exchange "
|
|
||||||
"(will unfortunately take a long time)."
|
|
||||||
)
|
|
||||||
migrate_binance_futures_data(config)
|
|
||||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
|
||||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
|
||||||
datadir=config['datadir'], timerange=timerange,
|
|
||||||
new_pairs_days=config['new_pairs_days'],
|
|
||||||
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'],
|
|
||||||
trading_mode=config.get('trading_mode', 'spot'),
|
|
||||||
prepend=config.get('prepend_data', False)
|
|
||||||
)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit("SIGINT received, aborting ...")
|
sys.exit("SIGINT received, aborting ...")
|
||||||
|
|
||||||
finally:
|
|
||||||
if pairs_not_available:
|
|
||||||
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
|
|
||||||
f"on exchange {exchange.name}.")
|
|
||||||
|
|
||||||
|
|
||||||
def start_convert_trades(args: Dict[str, Any]) -> None:
|
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
@@ -123,6 +57,8 @@ def start_convert_trades(args: Dict[str, Any]) -> None:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Downloading data requires a list of pairs. "
|
"Downloading data requires a list of pairs. "
|
||||||
"Please check the documentation on how to configure this.")
|
"Please check the documentation on how to configure this.")
|
||||||
|
if 'timeframes' not in config:
|
||||||
|
config['timeframes'] = DL_DATA_TIMEFRAMES
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
@@ -152,11 +88,10 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
if ohlcv:
|
if ohlcv:
|
||||||
migrate_binance_futures_data(config)
|
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_ohlcv_format(config,
|
||||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
convert_from=args['format_from'],
|
||||||
erase=args['erase'], candle_type=candle_type)
|
convert_to=args['format_to'],
|
||||||
|
erase=args['erase'])
|
||||||
else:
|
else:
|
||||||
convert_trades_format(config,
|
convert_trades_format(config,
|
||||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
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.constants import USERPATH_STRATEGIES
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
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__)
|
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
|
Deploy new strategy from template to strategy_path
|
||||||
"""
|
"""
|
||||||
fallback = 'full'
|
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(
|
indicators = render_template_with_fallback(
|
||||||
templatefile=f"strategy_subtemplates/indicators_{subtemplate}.j2",
|
templatefile=f"strategy_subtemplates/indicators_{subtemplate}.j2",
|
||||||
templatefallbackfile=f"strategy_subtemplates/indicators_{fallback}.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',
|
strategy_text = render_template(templatefile='base_strategy.py.j2',
|
||||||
arguments={"strategy": strategy_name,
|
arguments={"strategy": strategy_name,
|
||||||
|
"attributes": attributes,
|
||||||
"indicators": indicators,
|
"indicators": indicators,
|
||||||
"buy_trend": buy_trend,
|
"buy_trend": buy_trend,
|
||||||
"sell_trend": sell_trend,
|
"sell_trend": sell_trend,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
import rapidjson
|
import rapidjson
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
@@ -11,9 +11,10 @@ from tabulate import tabulate
|
|||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import market_is_active, validate_exchanges
|
from freqtrade.exchange import list_available_exchanges, market_is_active
|
||||||
from freqtrade.misc import parse_db_uri_for_logging, plural
|
from freqtrade.misc import parse_db_uri_for_logging, plural
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
|
from freqtrade.types import ValidExchangesType
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -25,18 +26,42 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
|||||||
:param args: Cli args from Arguments()
|
:param args: Cli args from Arguments()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
exchanges = validate_exchanges(args['list_exchanges_all'])
|
exchanges = list_available_exchanges(args['list_exchanges_all'])
|
||||||
|
|
||||||
if args['print_one_column']:
|
if args['print_one_column']:
|
||||||
print('\n'.join([e[0] for e in exchanges]))
|
print('\n'.join([e['name'] for e in exchanges]))
|
||||||
else:
|
else:
|
||||||
|
headers = {
|
||||||
|
'name': 'Exchange name',
|
||||||
|
'supported': 'Supported',
|
||||||
|
'trade_modes': 'Markets',
|
||||||
|
'comment': 'Reason',
|
||||||
|
}
|
||||||
|
headers.update({'valid': 'Valid'} if args['list_exchanges_all'] else {})
|
||||||
|
|
||||||
|
def build_entry(exchange: ValidExchangesType, valid: bool):
|
||||||
|
valid_entry = {'valid': exchange['valid']} if valid else {}
|
||||||
|
result: Dict[str, Union[str, bool]] = {
|
||||||
|
'name': exchange['name'],
|
||||||
|
**valid_entry,
|
||||||
|
'supported': 'Official' if exchange['supported'] else '',
|
||||||
|
'trade_modes': ', '.join(
|
||||||
|
(f"{a['margin_mode']} " if a['margin_mode'] else '') + a['trading_mode']
|
||||||
|
for a in exchange['trade_modes']
|
||||||
|
),
|
||||||
|
'comment': exchange['comment'],
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
if args['list_exchanges_all']:
|
if args['list_exchanges_all']:
|
||||||
print("All exchanges supported by the ccxt library:")
|
print("All exchanges supported by the ccxt library:")
|
||||||
|
exchanges = [build_entry(e, True) for e in exchanges]
|
||||||
else:
|
else:
|
||||||
print("Exchanges available for Freqtrade:")
|
print("Exchanges available for Freqtrade:")
|
||||||
exchanges = [e for e in exchanges if e[1] is not False]
|
exchanges = [build_entry(e, False) for e in exchanges if e['valid'] is not False]
|
||||||
|
|
||||||
print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason']))
|
print(tabulate(exchanges, headers=headers, ))
|
||||||
|
|
||||||
|
|
||||||
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
||||||
|
|||||||
@@ -132,3 +132,15 @@ def start_edge(args: Dict[str, Any]) -> None:
|
|||||||
# Initialize Edge object
|
# Initialize Edge object
|
||||||
edge_cli = EdgeCli(config)
|
edge_cli = EdgeCli(config)
|
||||||
edge_cli.start()
|
edge_cli.start()
|
||||||
|
|
||||||
|
|
||||||
|
def start_lookahead_analysis(args: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Start the backtest bias tester script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
from freqtrade.optimize.lookahead_analysis_helpers import LookaheadAnalysisSubFunctions
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
LookaheadAnalysisSubFunctions.start(config)
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ def start_webserver(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
Main entry point for webserver mode
|
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
|
from freqtrade.rpc.api_server import ApiServer
|
||||||
|
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
config = Configuration(args, RunMode.WEBSERVER).get_config()
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.WEBSERVER)
|
||||||
ApiServer(config, standalone=True)
|
ApiServer(config, standalone=True)
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||||
from freqtrade.configuration.configuration import Configuration
|
from freqtrade.configuration.configuration import Configuration
|
||||||
|
from freqtrade.configuration.detect_environment import running_in_docker
|
||||||
from freqtrade.configuration.timerange import TimeRange
|
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
|
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED
|
||||||
else:
|
else:
|
||||||
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
|
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
|
||||||
|
elif conf.get('runmode', RunMode.OTHER) == RunMode.WEBSERVER:
|
||||||
|
conf_schema['required'] = constants.SCHEMA_MINIMAL_WEBSERVER
|
||||||
else:
|
else:
|
||||||
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -300,6 +300,9 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='hyperoptexportfilename',
|
self._args_to_config(config, argname='hyperoptexportfilename',
|
||||||
logstring='Using hyperopt file: {}')
|
logstring='Using hyperopt file: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='lookahead_analysis_exportfilename',
|
||||||
|
logstring='Saving lookahead analysis results into {} ...')
|
||||||
|
|
||||||
self._args_to_config(config, argname='epochs',
|
self._args_to_config(config, argname='epochs',
|
||||||
logstring='Parameter --epochs detected ... '
|
logstring='Parameter --epochs detected ... '
|
||||||
'Will run Hyperopt with for {} epochs ...'
|
'Will run Hyperopt with for {} epochs ...'
|
||||||
@@ -474,6 +477,19 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='analysis_csv_path',
|
self._args_to_config(config, argname='analysis_csv_path',
|
||||||
logstring='Path to store analysis CSVs: {}')
|
logstring='Path to store analysis CSVs: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='analysis_csv_path',
|
||||||
|
logstring='Path to store analysis CSVs: {}')
|
||||||
|
|
||||||
|
# Lookahead analysis results
|
||||||
|
self._args_to_config(config, argname='targeted_trade_amount',
|
||||||
|
logstring='Targeted Trade amount: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='minimum_trade_amount',
|
||||||
|
logstring='Minimum Trade amount: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='lookahead_analysis_exportfilename',
|
||||||
|
logstring='Path to store lookahead-analysis-results: {}')
|
||||||
|
|
||||||
def _process_runmode(self, config: Config) -> None:
|
def _process_runmode(self, config: Config) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='dry_run',
|
self._args_to_config(config, argname='dry_run',
|
||||||
@@ -552,6 +568,7 @@ class Configuration:
|
|||||||
# Fall back to /dl_path/pairs.json
|
# Fall back to /dl_path/pairs.json
|
||||||
pairs_file = config['datadir'] / 'pairs.json'
|
pairs_file = config['datadir'] / 'pairs.json'
|
||||||
if pairs_file.exists():
|
if pairs_file.exists():
|
||||||
|
logger.info(f'Reading pairs file "{pairs_file}".')
|
||||||
config['pairs'] = load_file(pairs_file)
|
config['pairs'] = load_file(pairs_file)
|
||||||
if 'pairs' in config and isinstance(config['pairs'], list):
|
if 'pairs' in config and isinstance(config['pairs'], list):
|
||||||
config['pairs'].sort()
|
config['pairs'].sort()
|
||||||
|
|||||||
@@ -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 pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from freqtrade.configuration.detect_environment import running_in_docker
|
||||||
from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS,
|
from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS,
|
||||||
USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config)
|
USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config)
|
||||||
from freqtrade.exceptions import OperationalException
|
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
|
Use Sudo to change permissions of the home-directory if necessary
|
||||||
Only applies when running in docker!
|
Only applies when running in docker!
|
||||||
"""
|
"""
|
||||||
import os
|
if running_in_docker():
|
||||||
if os.environ.get('FT_APP_ENV') == 'docker':
|
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
subprocess.check_output(
|
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, '')
|
key = env_var.replace(prefix, '')
|
||||||
for k in reversed(key.split('__')):
|
for k in reversed(key.split('__')):
|
||||||
val = {k.lower(): get_var_typed(val)
|
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)
|
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||||
return relevant_vars
|
return relevant_vars
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import re
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
@@ -107,15 +109,15 @@ class TimeRange:
|
|||||||
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
|
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
|
||||||
self.starttype = 'date'
|
self.starttype = 'date'
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def parse_timerange(text: Optional[str]) -> 'TimeRange':
|
def parse_timerange(cls, text: Optional[str]) -> Self:
|
||||||
"""
|
"""
|
||||||
Parse the value of the argument --timerange to determine what is the range desired
|
Parse the value of the argument --timerange to determine what is the range desired
|
||||||
:param text: value from --timerange
|
:param text: value from --timerange
|
||||||
:return: Start and End range period
|
:return: Start and End range period
|
||||||
"""
|
"""
|
||||||
if not text:
|
if not text:
|
||||||
return TimeRange(None, None, 0, 0)
|
return cls(None, None, 0, 0)
|
||||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||||
(r'^(\d{8})-$', ('date', None)),
|
(r'^(\d{8})-$', ('date', None)),
|
||||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||||
@@ -156,5 +158,5 @@ class TimeRange:
|
|||||||
if start > stop > 0:
|
if start > stop > 0:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Start date is after stop date for timerange "{text}"')
|
f'Start date is after stop date for timerange "{text}"')
|
||||||
return TimeRange(stype[0], stype[1], start, stop)
|
return cls(stype[0], stype[1], start, stop)
|
||||||
raise OperationalException(f'Incorrect syntax for timerange "{text}"')
|
raise OperationalException(f'Incorrect syntax for timerange "{text}"')
|
||||||
|
|||||||
+26
-10
@@ -8,8 +8,8 @@ from typing import Any, Dict, List, Literal, Tuple
|
|||||||
from freqtrade.enums import CandleType, PriceType, RPCMessageType
|
from freqtrade.enums import CandleType, PriceType, RPCMessageType
|
||||||
|
|
||||||
|
|
||||||
|
DOCS_LINK = "https://www.freqtrade.io/en/stable"
|
||||||
DEFAULT_CONFIG = 'config.json'
|
DEFAULT_CONFIG = 'config.json'
|
||||||
DEFAULT_EXCHANGE = 'bittrex'
|
|
||||||
PROCESS_THROTTLE_SECS = 5 # sec
|
PROCESS_THROTTLE_SECS = 5 # sec
|
||||||
HYPEROPT_EPOCH = 100 # epochs
|
HYPEROPT_EPOCH = 100 # epochs
|
||||||
RETRY_TIMEOUT = 30 # sec
|
RETRY_TIMEOUT = 30 # sec
|
||||||
@@ -38,8 +38,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
|
|||||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
||||||
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
|
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5', 'feather', 'parquet']
|
||||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
|
|
||||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||||
BACKTEST_CACHE_DEFAULT = 'day'
|
BACKTEST_CACHE_DEFAULT = 'day'
|
||||||
@@ -50,6 +49,15 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
|
|||||||
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
||||||
# it has wide consequences for stored trades files
|
# it has wide consequences for stored trades files
|
||||||
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
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']
|
TRADING_MODES = ['spot', 'margin', 'futures']
|
||||||
MARGIN_MODES = ['cross', 'isolated', '']
|
MARGIN_MODES = ['cross', 'isolated', '']
|
||||||
|
|
||||||
@@ -65,6 +73,7 @@ TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
|||||||
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||||
FULL_DATAFRAME_THRESHOLD = 100
|
FULL_DATAFRAME_THRESHOLD = 100
|
||||||
CUSTOM_TAG_MAX_LENGTH = 255
|
CUSTOM_TAG_MAX_LENGTH = 255
|
||||||
|
DL_DATA_TIMEFRAMES = ['1m', '5m']
|
||||||
|
|
||||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||||
|
|
||||||
@@ -111,6 +120,8 @@ MINIMAL_CONFIG = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__MESSAGE_TYPE_DICT: Dict[str, Dict[str, str]] = {x: {'type': 'object'} for x in RPCMessageType}
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
CONF_SCHEMA = {
|
CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@@ -148,10 +159,9 @@ CONF_SCHEMA = {
|
|||||||
'patternProperties': {
|
'patternProperties': {
|
||||||
'^[0-9.]+$': {'type': 'number'}
|
'^[0-9.]+$': {'type': 'number'}
|
||||||
},
|
},
|
||||||
'minProperties': 1
|
|
||||||
},
|
},
|
||||||
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
|
'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': {'type': 'boolean'},
|
||||||
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
@@ -164,6 +174,9 @@ CONF_SCHEMA = {
|
|||||||
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
||||||
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
|
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
|
||||||
'reduce_df_footprint': {'type': 'boolean', 'default': False},
|
'reduce_df_footprint': {'type': 'boolean', 'default': False},
|
||||||
|
'minimum_trade_amount': {'type': 'number', 'default': 10},
|
||||||
|
'targeted_trade_amount': {'type': 'number', 'default': 20},
|
||||||
|
'lookahead_analysis_exportfilename': {'type': 'string'},
|
||||||
'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99},
|
'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99},
|
||||||
'backtest_breakdown': {
|
'backtest_breakdown': {
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
@@ -351,7 +364,8 @@ CONF_SCHEMA = {
|
|||||||
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||||
'retries': {'type': 'integer', 'minimum': 0},
|
'retries': {'type': 'integer', 'minimum': 0},
|
||||||
'retry_delay': {'type': 'number', 'minimum': 0},
|
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||||
**dict([(x, {'type': 'object'}) for x in RPCMessageType]),
|
**__MESSAGE_TYPE_DICT,
|
||||||
|
# **{x: {'type': 'object'} for x in RPCMessageType},
|
||||||
# Below -> Deprecated
|
# Below -> Deprecated
|
||||||
'webhookentry': {'type': 'object'},
|
'webhookentry': {'type': 'object'},
|
||||||
'webhookentrycancel': {'type': 'object'},
|
'webhookentrycancel': {'type': 'object'},
|
||||||
@@ -440,12 +454,12 @@ CONF_SCHEMA = {
|
|||||||
'dataformat_ohlcv': {
|
'dataformat_ohlcv': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': AVAILABLE_DATAHANDLERS,
|
'enum': AVAILABLE_DATAHANDLERS,
|
||||||
'default': 'json'
|
'default': 'feather'
|
||||||
},
|
},
|
||||||
'dataformat_trades': {
|
'dataformat_trades': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': AVAILABLE_DATAHANDLERS_TRADES,
|
'enum': AVAILABLE_DATAHANDLERS,
|
||||||
'default': 'jsongz'
|
'default': 'feather'
|
||||||
},
|
},
|
||||||
'position_adjustment_enable': {'type': 'boolean'},
|
'position_adjustment_enable': {'type': 'boolean'},
|
||||||
'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1},
|
'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1},
|
||||||
@@ -455,7 +469,6 @@ CONF_SCHEMA = {
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'name': {'type': 'string'},
|
'name': {'type': 'string'},
|
||||||
'sandbox': {'type': 'boolean', 'default': False},
|
|
||||||
'key': {'type': 'string', 'default': ''},
|
'key': {'type': 'string', 'default': ''},
|
||||||
'secret': {'type': 'string', 'default': ''},
|
'secret': {'type': 'string', 'default': ''},
|
||||||
'password': {'type': 'string', 'default': ''},
|
'password': {'type': 'string', 'default': ''},
|
||||||
@@ -662,6 +675,9 @@ SCHEMA_MINIMAL_REQUIRED = [
|
|||||||
'dataformat_ohlcv',
|
'dataformat_ohlcv',
|
||||||
'dataformat_trades',
|
'dataformat_trades',
|
||||||
]
|
]
|
||||||
|
SCHEMA_MINIMAL_WEBSERVER = SCHEMA_MINIMAL_REQUIRED + [
|
||||||
|
'api_server',
|
||||||
|
]
|
||||||
|
|
||||||
CANCEL_REASON = {
|
CANCEL_REASON = {
|
||||||
"TIMEOUT": "cancelled due to timeout",
|
"TIMEOUT": "cancelled due to timeout",
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import logging
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
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 numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
||||||
from freqtrade.exceptions import OperationalException
|
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.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||||
|
from freqtrade.types import BacktestHistoryEntryType, BacktestResultType
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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
|
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.
|
Load backtest statistics file.
|
||||||
:param filename: pathlib.Path object, or string pointing to the 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.
|
# Legacy list format does not contain metadata.
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
data['metadata'] = load_backtest_metadata(filename)
|
data['metadata'] = load_backtest_metadata(filename)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
|
def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
Load one strategy from multi-strategy result
|
Load one strategy from multi-strategy result and merge it with results
|
||||||
and merge it with results
|
|
||||||
:param strategy_name: Name of the strategy contained in the result
|
:param strategy_name: Name of the strategy contained in the result
|
||||||
:param filename: Backtest-result-filename to load
|
:param filename: Backtest-result-filename to load
|
||||||
:param results: dict to merge the result to.
|
:param results: dict to merge the result to.
|
||||||
"""
|
"""
|
||||||
bt_data = load_backtest_stats(filename)
|
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[k][strategy_name] = bt_data[k][strategy_name]
|
||||||
|
results['metadata'][strategy_name]['filename'] = filename.stem
|
||||||
comparison = bt_data['strategy_comparison']
|
comparison = bt_data['strategy_comparison']
|
||||||
for i in range(len(comparison)):
|
for i in range(len(comparison)):
|
||||||
if comparison[i]['key'] == strategy_name:
|
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]:
|
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'))))
|
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
|
Get list of backtest results read from metadata files
|
||||||
"""
|
"""
|
||||||
results = []
|
return [
|
||||||
for filename in _get_backtest_files(dirname):
|
{
|
||||||
metadata = load_backtest_metadata(filename)
|
'filename': filename.stem,
|
||||||
if not metadata:
|
|
||||||
continue
|
|
||||||
for s, v in metadata.items():
|
|
||||||
results.append({
|
|
||||||
'filename': filename.name,
|
|
||||||
'strategy': s,
|
'strategy': s,
|
||||||
'run_id': v['run_id'],
|
'run_id': v['run_id'],
|
||||||
|
'notes': v.get('notes', ''),
|
||||||
'backtest_start_time': v['backtest_start_time'],
|
'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],
|
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': [],
|
'strategy_comparison': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Weird glob expression here avoids including .meta.json files.
|
|
||||||
for filename in _get_backtest_files(dirname):
|
for filename in _get_backtest_files(dirname):
|
||||||
metadata = load_backtest_metadata(filename)
|
metadata = load_backtest_metadata(filename)
|
||||||
if not metadata:
|
if not metadata:
|
||||||
|
|||||||
+66
-34
@@ -1,17 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Functions to convert data from one format to another
|
Functions to convert data from one format to another
|
||||||
"""
|
"""
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
from operator import itemgetter
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, Config, TradeList
|
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES,
|
||||||
from freqtrade.enums import CandleType
|
Config, TradeList)
|
||||||
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -96,8 +95,14 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
|||||||
'volume': 'sum'
|
'volume': 'sum'
|
||||||
}
|
}
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
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
|
# 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
|
# Forwardfill close for missing columns
|
||||||
df['close'] = df['close'].fillna(method='ffill')
|
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
|
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:
|
startup_candles: int = 0) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Trim dataframe based on given timerange
|
Trim dataframe based on given timerange
|
||||||
@@ -189,15 +194,14 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
|||||||
return frame
|
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.
|
Removes duplicates from the trades DataFrame.
|
||||||
Uses itertools.groupby to avoid converting to pandas.
|
Uses pandas.DataFrame.drop_duplicates to remove duplicates based on the 'timestamp' column.
|
||||||
Tests show it as being pretty efficient on lists of 4M Lists.
|
:param trades: DataFrame with the columns constants.DEFAULT_TRADES_COLUMNS
|
||||||
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
|
:return: DataFrame with duplicates removed based on the 'timestamp' column
|
||||||
:return: same format as above, but with duplicates removed
|
|
||||||
"""
|
"""
|
||||||
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:
|
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]
|
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
|
Converts trades list to OHLCV list
|
||||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
: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
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||||
if not trades:
|
if trades.empty:
|
||||||
raise ValueError('Trade-list empty.')
|
raise ValueError('Trade-list empty.')
|
||||||
df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
|
df = trades.set_index('date', drop=True)
|
||||||
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms',
|
|
||||||
utc=True,)
|
|
||||||
df = df.set_index('timestamp')
|
|
||||||
|
|
||||||
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
||||||
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
||||||
@@ -264,7 +290,6 @@ def convert_ohlcv_format(
|
|||||||
convert_from: str,
|
convert_from: str,
|
||||||
convert_to: str,
|
convert_to: str,
|
||||||
erase: bool,
|
erase: bool,
|
||||||
candle_type: CandleType
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Convert OHLCV from one format to another
|
Convert OHLCV from one format to another
|
||||||
@@ -272,7 +297,6 @@ def convert_ohlcv_format(
|
|||||||
:param convert_from: Source format
|
:param convert_from: Source format
|
||||||
:param convert_to: Target format
|
:param convert_to: Target format
|
||||||
:param erase: Erase source data (does not apply if source and target format are identical)
|
: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
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
@@ -280,20 +304,28 @@ def convert_ohlcv_format(
|
|||||||
timeframes = config.get('timeframes', [config.get('timeframe')])
|
timeframes = config.get('timeframes', [config.get('timeframe')])
|
||||||
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
|
||||||
|
|
||||||
if 'pairs' not in config:
|
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', [
|
||||||
config['pairs'] = []
|
c.value for c in CandleType])]
|
||||||
# Check timeframes or fall back to timeframe.
|
logger.info(candle_types)
|
||||||
for timeframe in timeframes:
|
paircombs = src.ohlcv_get_available_data(config['datadir'], TradingMode.SPOT)
|
||||||
config['pairs'].extend(src.ohlcv_get_pairs(
|
paircombs.extend(src.ohlcv_get_available_data(config['datadir'], TradingMode.FUTURES))
|
||||||
config['datadir'],
|
|
||||||
timeframe,
|
|
||||||
candle_type=candle_type
|
|
||||||
))
|
|
||||||
config['pairs'] = sorted(set(config['pairs']))
|
|
||||||
logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
|
|
||||||
|
|
||||||
for timeframe in timeframes:
|
if 'pairs' in config:
|
||||||
for pair in config['pairs']:
|
# 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,
|
data = src.ohlcv_load(pair=pair, timeframe=timeframe,
|
||||||
timerange=None,
|
timerange=None,
|
||||||
fill_missing=False,
|
fill_missing=False,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWith
|
|||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
||||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
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.exchange.types import OrderBook
|
||||||
from freqtrade.misc import append_candles_to_dataframe
|
from freqtrade.misc import append_candles_to_dataframe
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
@@ -46,6 +46,8 @@ class DataProvider:
|
|||||||
self.__rpc = rpc
|
self.__rpc = rpc
|
||||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||||
self.__slice_index: Optional[int] = None
|
self.__slice_index: Optional[int] = None
|
||||||
|
self.__slice_date: Optional[datetime] = None
|
||||||
|
|
||||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
self.__producer_pairs_df: Dict[str,
|
self.__producer_pairs_df: Dict[str,
|
||||||
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {}
|
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {}
|
||||||
@@ -64,10 +66,19 @@ class DataProvider:
|
|||||||
def _set_dataframe_max_index(self, limit_index: int):
|
def _set_dataframe_max_index(self, limit_index: int):
|
||||||
"""
|
"""
|
||||||
Limit analyzed dataframe to max specified index.
|
Limit analyzed dataframe to max specified index.
|
||||||
|
Only relevant in backtesting.
|
||||||
:param limit_index: dataframe index.
|
:param limit_index: dataframe index.
|
||||||
"""
|
"""
|
||||||
self.__slice_index = limit_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(
|
def _set_cached_df(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@@ -284,7 +295,7 @@ class DataProvider:
|
|||||||
def historic_ohlcv(
|
def historic_ohlcv(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
timeframe: Optional[str] = None,
|
timeframe: str,
|
||||||
candle_type: str = ''
|
candle_type: str = ''
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@@ -307,10 +318,10 @@ class DataProvider:
|
|||||||
timerange.subtract_start(tf_seconds * startup_candles)
|
timerange.subtract_start(tf_seconds * startup_candles)
|
||||||
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timeframe=timeframe or self._config['timeframe'],
|
timeframe=timeframe,
|
||||||
datadir=self._config['datadir'],
|
datadir=self._config['datadir'],
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
data_format=self._config.get('dataformat_ohlcv', 'json'),
|
data_format=self._config['dataformat_ohlcv'],
|
||||||
candle_type=_candle_type,
|
candle_type=_candle_type,
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -354,7 +365,13 @@ class DataProvider:
|
|||||||
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
else:
|
else:
|
||||||
# Get historical OHLCV data (cached on disk).
|
# 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)
|
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:
|
if len(data) == 0:
|
||||||
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Includes:
|
|||||||
* download data from exchange and store to disk
|
* download data from exchange and store to disk
|
||||||
"""
|
"""
|
||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
from .history_utils import (convert_trades_to_ohlcv, get_timerange, load_data, load_pair_history,
|
from .history_utils import (convert_trades_to_ohlcv, download_data_main, get_timerange, load_data,
|
||||||
refresh_backtest_ohlcv_data, refresh_backtest_trades_data, refresh_data,
|
load_pair_history, refresh_backtest_ohlcv_data,
|
||||||
validate_backtest_data)
|
refresh_backtest_trades_data, refresh_data, validate_backtest_data)
|
||||||
from .idatahandler import get_datahandler
|
from .idatahandler import get_datahandler
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from pandas import DataFrame, read_feather, to_datetime
|
from pandas import DataFrame, read_feather, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
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 freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -82,43 +82,41 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
: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
|
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)
|
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)
|
def trades_append(self, pair: str, data: DataFrame):
|
||||||
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
|
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
|
||||||
"""
|
"""
|
||||||
Append data to existing files
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
: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
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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
|
Load a pair from file, either .json.gz or .json
|
||||||
# TODO: respect timerange ...
|
# TODO: respect timerange ...
|
||||||
:param pair: Load trades for this pair
|
:param pair: Load trades for this pair
|
||||||
:param timerange: Timerange to load trades for - currently not implemented
|
: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)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return []
|
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
|
||||||
tradesdata = read_feather(filename)
|
tradesdata = read_feather(filename)
|
||||||
|
|
||||||
return tradesdata.values.tolist()
|
return tradesdata
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
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 freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -100,42 +100,42 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
: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
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
key = self._pair_trades_key(pair)
|
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,
|
self._pair_trades_filename(self._datadir, pair), key,
|
||||||
mode='a', complevel=9, complib='blosc',
|
mode='a', complevel=9, complib='blosc',
|
||||||
format='table', data_columns=['timestamp']
|
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
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
: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
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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.
|
Load a pair from h5 file.
|
||||||
:param pair: Load trades for this pair
|
:param pair: Load trades for this pair
|
||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: Dataframe containing trades
|
||||||
"""
|
"""
|
||||||
key = self._pair_trades_key(pair)
|
key = self._pair_trades_key(pair)
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return []
|
return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
where = []
|
where = []
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
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: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||||
return trades.values.tolist()
|
return trades
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
@@ -7,14 +7,19 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
from pandas import DataFrame, concat
|
from pandas import DataFrame, concat
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
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,
|
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.data.history.idatahandler import IDataHandler, get_datahandler
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import Exchange
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -66,7 +71,7 @@ def load_data(datadir: Path,
|
|||||||
fill_up_missing: bool = True,
|
fill_up_missing: bool = True,
|
||||||
startup_candles: int = 0,
|
startup_candles: int = 0,
|
||||||
fail_without_data: bool = False,
|
fail_without_data: bool = False,
|
||||||
data_format: str = 'json',
|
data_format: str = 'feather',
|
||||||
candle_type: CandleType = CandleType.SPOT,
|
candle_type: CandleType = CandleType.SPOT,
|
||||||
user_futures_funding_rate: Optional[int] = None,
|
user_futures_funding_rate: Optional[int] = None,
|
||||||
) -> Dict[str, DataFrame]:
|
) -> Dict[str, DataFrame]:
|
||||||
@@ -227,9 +232,11 @@ def _download_pair_history(pair: str, *,
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Current Start: %s",
|
logger.debug("Current Start: %s",
|
||||||
f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||||
|
if not data.empty else 'None')
|
||||||
logger.debug("Current End: %s",
|
logger.debug("Current End: %s",
|
||||||
f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||||
|
if not data.empty else 'None')
|
||||||
|
|
||||||
# Default since_ms to 30 days if nothing is given
|
# Default since_ms to 30 days if nothing is given
|
||||||
new_data = exchange.get_historic_ohlcv(pair=pair,
|
new_data = exchange.get_historic_ohlcv(pair=pair,
|
||||||
@@ -253,9 +260,11 @@ def _download_pair_history(pair: str, *,
|
|||||||
fill_missing=False, drop_incomplete=False)
|
fill_missing=False, drop_incomplete=False)
|
||||||
|
|
||||||
logger.debug("New Start: %s",
|
logger.debug("New Start: %s",
|
||||||
f"{data.iloc[0]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
f"{data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||||
|
if not data.empty else 'None')
|
||||||
logger.debug("New End: %s",
|
logger.debug("New End: %s",
|
||||||
f"{data.iloc[-1]['date']:DATETIME_PRINT_FORMAT}" if not data.empty else 'None')
|
f"{data.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}"
|
||||||
|
if not data.empty else 'None')
|
||||||
|
|
||||||
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
|
||||||
return True
|
return True
|
||||||
@@ -290,7 +299,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
continue
|
continue
|
||||||
for timeframe in timeframes:
|
for timeframe in timeframes:
|
||||||
|
|
||||||
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
|
logger.debug(f'Downloading pair {pair}, {candle_type}, interval {timeframe}.')
|
||||||
process = f'{idx}/{len(pairs)}'
|
process = f'{idx}/{len(pairs)}'
|
||||||
_download_pair_history(pair=pair, process=process,
|
_download_pair_history(pair=pair, process=process,
|
||||||
datadir=datadir, exchange=exchange,
|
datadir=datadir, exchange=exchange,
|
||||||
@@ -342,24 +351,27 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
# 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
|
# since is before the first trade
|
||||||
logger.info(f"Start earlier than available data. Redownloading trades for {pair}...")
|
logger.info(f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than "
|
||||||
trades = []
|
f"available data. Redownloading trades for {pair}...")
|
||||||
|
trades = trades_list_to_df([])
|
||||||
|
|
||||||
if not since:
|
from_id = trades.iloc[-1]['id'] if not trades.empty else None
|
||||||
since = int((datetime.now() - timedelta(days=-new_pairs_days)).timestamp()) * 1000
|
if not trades.empty and since < trades.iloc[-1]['timestamp']:
|
||||||
|
|
||||||
from_id = trades[-1][1] if trades else None
|
|
||||||
if trades and since < trades[-1][0]:
|
|
||||||
# Reset since to the last available point
|
# Reset since to the last available point
|
||||||
# - 5 seconds (to ensure we're getting all trades)
|
# - 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} "
|
logger.info(f"Using last trade date -5s - Downloading trades for {pair} "
|
||||||
f"since: {format_ms_time(since)}.")
|
f"since: {format_ms_time(since)}.")
|
||||||
|
|
||||||
logger.debug(f"Current Start: {format_ms_time(trades[0][0]) if trades else 'None'}")
|
if not since:
|
||||||
logger.debug(f"Current End: {format_ms_time(trades[-1][0]) if trades else 'None'}")
|
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)}")
|
logger.info(f"Current Amount of trades: {len(trades)}")
|
||||||
|
|
||||||
# Default since_ms to 30 days if nothing is given
|
# Default since_ms to 30 days if nothing is given
|
||||||
@@ -368,13 +380,16 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
until=until,
|
until=until,
|
||||||
from_id=from_id,
|
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
|
# 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)
|
data_handler.trades_store(pair, data=trades)
|
||||||
|
|
||||||
logger.debug(f"New Start: {format_ms_time(trades[0][0])}")
|
logger.debug("New Start: %s", 'None' if trades.empty else
|
||||||
logger.debug(f"New End: {format_ms_time(trades[-1][0])}")
|
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)}")
|
logger.info(f"New Amount of trades: {len(trades)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -387,7 +402,7 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
|
|
||||||
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
||||||
timerange: TimeRange, new_pairs_days: int = 30,
|
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.
|
Refresh stored trades data for backtesting and hyperopt operations.
|
||||||
Used by freqtrade download-data subcommand.
|
Used by freqtrade download-data subcommand.
|
||||||
@@ -420,8 +435,8 @@ def convert_trades_to_ohlcv(
|
|||||||
datadir: Path,
|
datadir: Path,
|
||||||
timerange: TimeRange,
|
timerange: TimeRange,
|
||||||
erase: bool = False,
|
erase: bool = False,
|
||||||
data_format_ohlcv: str = 'json',
|
data_format_ohlcv: str = 'feather',
|
||||||
data_format_trades: str = 'jsongz',
|
data_format_trades: str = 'feather',
|
||||||
candle_type: CandleType = CandleType.SPOT
|
candle_type: CandleType = CandleType.SPOT
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -479,3 +494,79 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
|
|||||||
logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values",
|
logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values",
|
||||||
pair, expected_frames, dflen, expected_frames - dflen)
|
pair, expected_frames, dflen, expected_frames - dflen)
|
||||||
return found_missing
|
return found_missing
|
||||||
|
|
||||||
|
|
||||||
|
def download_data_main(config: Config) -> None:
|
||||||
|
|
||||||
|
timerange = TimeRange()
|
||||||
|
if 'days' in config:
|
||||||
|
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||||
|
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
||||||
|
|
||||||
|
if 'timerange' in config:
|
||||||
|
timerange = timerange.parse_timerange(config['timerange'])
|
||||||
|
|
||||||
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
|
config['stake_currency'] = ''
|
||||||
|
|
||||||
|
pairs_not_available: List[str] = []
|
||||||
|
|
||||||
|
# Init exchange
|
||||||
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
available_pairs = [
|
||||||
|
p for p in exchange.get_markets(
|
||||||
|
tradable_only=True, active_only=not config.get('include_inactive')
|
||||||
|
).keys()
|
||||||
|
]
|
||||||
|
|
||||||
|
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):
|
||||||
|
exchange.validate_pairs(expanded_pairs)
|
||||||
|
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||||
|
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||||
|
|
||||||
|
for timeframe in config['timeframes']:
|
||||||
|
exchange.validate_timeframes(timeframe)
|
||||||
|
|
||||||
|
# Start downloading
|
||||||
|
try:
|
||||||
|
if config.get('download_trades'):
|
||||||
|
if config.get('trading_mode') == 'futures':
|
||||||
|
raise OperationalException("Trade download not supported for futures.")
|
||||||
|
pairs_not_available = refresh_backtest_trades_data(
|
||||||
|
exchange, pairs=expanded_pairs, datadir=config['datadir'],
|
||||||
|
timerange=timerange, new_pairs_days=config['new_pairs_days'],
|
||||||
|
erase=bool(config.get('erase')), data_format=config['dataformat_trades'])
|
||||||
|
|
||||||
|
# Convert downloaded trade data to different timeframes
|
||||||
|
convert_trades_to_ohlcv(
|
||||||
|
pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
|
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
||||||
|
data_format_ohlcv=config['dataformat_ohlcv'],
|
||||||
|
data_format_trades=config['dataformat_trades'],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not exchange.get_option('ohlcv_has_history', True):
|
||||||
|
raise OperationalException(
|
||||||
|
f"Historic klines not available for {exchange.name}. "
|
||||||
|
"Please use `--dl-trades` instead for this exchange "
|
||||||
|
"(will unfortunately take a long time)."
|
||||||
|
)
|
||||||
|
migrate_binance_futures_data(config)
|
||||||
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||||
|
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
|
datadir=config['datadir'], timerange=timerange,
|
||||||
|
new_pairs_days=config['new_pairs_days'],
|
||||||
|
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'],
|
||||||
|
trading_mode=config.get('trading_mode', 'spot'),
|
||||||
|
prepend=config.get('prepend_data', False)
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if pairs_not_available:
|
||||||
|
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||||
|
f"on exchange {exchange.name}.")
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
from freqtrade.constants import DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
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.enums import CandleType, TradingMode
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
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]
|
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
: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
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: DataFrame):
|
||||||
"""
|
"""
|
||||||
Append data to existing files
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
: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
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
Load a pair from file, either .json.gz or .json
|
||||||
:param pair: Load trades for this pair
|
:param pair: Load trades for this pair
|
||||||
:param timerange: Timerange to load trades for - currently not implemented
|
: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:
|
def trades_purge(self, pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Remove data for this pair
|
Remove data for this pair
|
||||||
@@ -208,7 +219,7 @@ class IDataHandler(ABC):
|
|||||||
return True
|
return True
|
||||||
return False
|
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
|
Load a pair from file, either .json.gz or .json
|
||||||
Removes duplicates in the process.
|
Removes duplicates in the process.
|
||||||
@@ -216,7 +227,10 @@ class IDataHandler(ABC):
|
|||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
: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
|
@classmethod
|
||||||
def create_dir_if_needed(cls, datadir: Path):
|
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:
|
if not data_handler:
|
||||||
HandlerClass = get_datahandlerclass(data_format or 'json')
|
HandlerClass = get_datahandlerclass(data_format or 'feather')
|
||||||
data_handler = HandlerClass(datadir)
|
data_handler = HandlerClass(datadir)
|
||||||
return data_handler
|
return data_handler
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from pandas import DataFrame, read_json, to_datetime
|
|||||||
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
||||||
from freqtrade.data.converter import trades_dict_to_list
|
from freqtrade.data.converter import trades_dict_to_list, trades_list_to_df
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -94,45 +94,46 @@ class JsonDataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
: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
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
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
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
: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
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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
|
Load a pair from file, either .json.gz or .json
|
||||||
# TODO: respect timerange ...
|
# TODO: respect timerange ...
|
||||||
:param pair: Load trades for this pair
|
:param pair: Load trades for this pair
|
||||||
:param timerange: Timerange to load trades for - currently not implemented
|
: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)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
tradesdata = misc.file_load_json(filename)
|
tradesdata = misc.file_load_json(filename)
|
||||||
|
|
||||||
if not tradesdata:
|
if not tradesdata:
|
||||||
return []
|
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
|
||||||
if isinstance(tradesdata[0], dict):
|
if isinstance(tradesdata[0], dict):
|
||||||
# Convert trades dict to list
|
# Convert trades dict to list
|
||||||
logger.info("Old trades format detected - converting")
|
logger.info("Old trades format detected - converting")
|
||||||
tradesdata = trades_dict_to_list(tradesdata)
|
tradesdata = trades_dict_to_list(tradesdata)
|
||||||
pass
|
pass
|
||||||
return tradesdata
|
return trades_list_to_df(tradesdata, convert=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from pandas import DataFrame, read_parquet, to_datetime
|
from pandas import DataFrame, read_parquet, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
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 freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -81,25 +81,22 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
: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
|
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()
|
def trades_append(self, pair: str, data: DataFrame):
|
||||||
# array = pa.array(data)
|
|
||||||
# array
|
|
||||||
# feather.write_feather(data, filename)
|
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
|
||||||
"""
|
"""
|
||||||
Append data to existing files
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
: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
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -112,14 +109,13 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
if not filename.exists():
|
||||||
# tradesdata = misc.file_load_json(filename)
|
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
|
||||||
# if not tradesdata:
|
tradesdata = read_parquet(filename)
|
||||||
# return []
|
|
||||||
|
|
||||||
# return tradesdata
|
return tradesdata
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
+22
-19
@@ -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
|
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
|
Calculate expectancy
|
||||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
: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
|
|
||||||
|
|
||||||
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 (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
|
expectancy = 0
|
||||||
|
expectancy_ratio = 100
|
||||||
|
|
||||||
return expectancy
|
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)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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,
|
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class Edge:
|
|||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
timeframe=self.strategy.timeframe,
|
timeframe=self.strategy.timeframe,
|
||||||
timerange=timerange_startup,
|
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),
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||||
)
|
)
|
||||||
# Download informative pairs too
|
# Download informative pairs too
|
||||||
@@ -132,7 +132,7 @@ class Edge:
|
|||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
timerange=timerange_startup,
|
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),
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ class Edge:
|
|||||||
timeframe=self.strategy.timeframe,
|
timeframe=self.strategy.timeframe,
|
||||||
timerange=self._timerange,
|
timerange=self._timerange,
|
||||||
startup_candles=self.strategy.startup_candle_count,
|
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),
|
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.sort_values(by=['date'])
|
||||||
pair_data = pair_data.reset_index(drop=True)
|
pair_data = pair_data.reset_index(drop=True)
|
||||||
|
|
||||||
df_analyzed = self.strategy.advise_exit(
|
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})[headers].copy()
|
||||||
dataframe=self.strategy.advise_entry(
|
|
||||||
dataframe=pair_data,
|
|
||||||
metadata={'pair': pair}
|
|
||||||
),
|
|
||||||
metadata={'pair': pair}
|
|
||||||
)[headers].copy()
|
|
||||||
|
|
||||||
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class MarginMode(Enum):
|
class MarginMode(str, Enum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between
|
Enum to distinguish between
|
||||||
cross margin/futures margin_mode and
|
cross margin/futures margin_mode and
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_c
|
|||||||
amount_to_contracts, amount_to_precision,
|
amount_to_contracts, amount_to_precision,
|
||||||
available_exchanges, ccxt_exchanges,
|
available_exchanges, ccxt_exchanges,
|
||||||
contracts_to_amount, date_minus_candles,
|
contracts_to_amount, date_minus_candles,
|
||||||
is_exchange_known_ccxt, market_is_active,
|
is_exchange_known_ccxt, list_available_exchanges,
|
||||||
price_to_precision, timeframe_to_minutes,
|
market_is_active, price_to_precision,
|
||||||
timeframe_to_msecs, timeframe_to_next_date,
|
timeframe_to_minutes, timeframe_to_msecs,
|
||||||
timeframe_to_prev_date, timeframe_to_seconds,
|
timeframe_to_next_date, timeframe_to_prev_date,
|
||||||
validate_exchange, validate_exchanges)
|
timeframe_to_seconds, validate_exchange)
|
||||||
from freqtrade.exchange.gate import Gate
|
from freqtrade.exchange.gate import Gate
|
||||||
from freqtrade.exchange.hitbtc import Hitbtc
|
from freqtrade.exchange.hitbtc import Hitbtc
|
||||||
from freqtrade.exchange.huobi import Huobi
|
from freqtrade.exchange.huobi import Huobi
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class Binance(Exchange):
|
|||||||
"tickers_have_price": False,
|
"tickers_have_price": False,
|
||||||
"floor_leverage": True,
|
"floor_leverage": True,
|
||||||
"stop_price_type_field": "workingType",
|
"stop_price_type_field": "workingType",
|
||||||
|
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||||
"stop_price_type_value_mapping": {
|
"stop_price_type_value_mapping": {
|
||||||
PriceType.LAST: "CONTRACT_PRICE",
|
PriceType.LAST: "CONTRACT_PRICE",
|
||||||
PriceType.MARK: "MARK_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.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
|
from freqtrade.enums.candletype import CandleType
|
||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -27,8 +27,8 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 200,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": True,
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
@@ -91,28 +91,13 @@ class Bybit(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
async def _fetch_funding_rate_history(
|
def ohlcv_candle_limit(
|
||||||
self,
|
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
||||||
pair: str,
|
|
||||||
timeframe: str,
|
if candle_type in (CandleType.FUNDING_RATE):
|
||||||
limit: int,
|
return 200
|
||||||
since_ms: Optional[int] = None,
|
|
||||||
) -> List[List]:
|
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
|
||||||
"""
|
|
||||||
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 _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||||
if self.trading_mode != TradingMode.SPOT:
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Cryptocurrency Exchanges support
|
|||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
import signal
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import floor
|
from math import floor
|
||||||
@@ -80,9 +81,8 @@ class Exchange:
|
|||||||
"mark_ohlcv_price": "mark",
|
"mark_ohlcv_price": "mark",
|
||||||
"mark_ohlcv_timeframe": "8h",
|
"mark_ohlcv_timeframe": "8h",
|
||||||
"ccxt_futures_name": "swap",
|
"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
|
"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
|
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
|
||||||
"marketOrderRequiresPrice": False,
|
"marketOrderRequiresPrice": False,
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ class Exchange:
|
|||||||
|
|
||||||
# Converts the interval provided in minutes in config to seconds
|
# Converts the interval provided in minutes in config to seconds
|
||||||
self.markets_refresh_interval: int = exchange_conf.get(
|
self.markets_refresh_interval: int = exchange_conf.get(
|
||||||
"markets_refresh_interval", 60) * 60
|
"markets_refresh_interval", 60) * 60 * 1000
|
||||||
|
|
||||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||||
self.fill_leverage_tiers()
|
self.fill_leverage_tiers()
|
||||||
@@ -264,8 +264,6 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
|
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
|
||||||
|
|
||||||
self.set_sandbox(api, exchange_config, name)
|
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -301,7 +299,7 @@ class Exchange:
|
|||||||
return list((self._api.timeframes or {}).keys())
|
return list((self._api.timeframes or {}).keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def markets(self) -> Dict:
|
def markets(self) -> Dict[str, Any]:
|
||||||
"""exchange ccxt markets"""
|
"""exchange ccxt markets"""
|
||||||
if not self._markets:
|
if not self._markets:
|
||||||
logger.info("Markets were not loaded. Loading them now..")
|
logger.info("Markets were not loaded. Loading them now..")
|
||||||
@@ -466,16 +464,6 @@ class Exchange:
|
|||||||
return amount_to_contract_precision(amount, self.get_precision_amount(pair),
|
return amount_to_contract_precision(amount, self.get_precision_amount(pair),
|
||||||
self.precisionMode, contract_size)
|
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:
|
def _load_async_markets(self, reload: bool = False) -> None:
|
||||||
try:
|
try:
|
||||||
if self._api_async:
|
if self._api_async:
|
||||||
@@ -581,7 +569,7 @@ class Exchange:
|
|||||||
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
||||||
if pair in self.markets and self.markets[pair].get('active'):
|
if pair in self.markets and self.markets[pair].get('active'):
|
||||||
return pair
|
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:
|
def validate_timeframes(self, timeframe: Optional[str]) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -1148,8 +1136,8 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
limit_rate = stop_price * (2 - limit_price_pct)
|
limit_rate = stop_price * (2 - limit_price_pct)
|
||||||
|
|
||||||
bad_stop_price = ((stop_price <= limit_rate) if side ==
|
bad_stop_price = ((stop_price < limit_rate) if side ==
|
||||||
"sell" else (stop_price >= limit_rate))
|
"sell" else (stop_price > limit_rate))
|
||||||
# Ensure rate is less than stop price
|
# Ensure rate is less than stop price
|
||||||
if bad_stop_price:
|
if bad_stop_price:
|
||||||
# This can for example happen if the stop / liquidation price is set to 0
|
# This can for example happen if the stop / liquidation price is set to 0
|
||||||
@@ -1662,13 +1650,48 @@ class Exchange:
|
|||||||
|
|
||||||
price_side = self._get_price_side(side, is_short, conf_strategy)
|
price_side = self._get_price_side(side, is_short, conf_strategy)
|
||||||
|
|
||||||
price_side_word = price_side.capitalize()
|
|
||||||
|
|
||||||
if conf_strategy.get('use_order_book', False):
|
if conf_strategy.get('use_order_book', False):
|
||||||
|
|
||||||
order_book_top = conf_strategy.get('order_book_top', 1)
|
order_book_top = conf_strategy.get('order_book_top', 1)
|
||||||
if order_book is None:
|
if order_book is None:
|
||||||
order_book = self.fetch_l2_order_book(pair, order_book_top)
|
order_book = self.fetch_l2_order_book(pair, order_book_top)
|
||||||
|
rate = self._get_rate_from_ob(pair, side, order_book, name, price_side,
|
||||||
|
order_book_top)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Using Last {price_side.capitalize()} / Last Price")
|
||||||
|
if ticker is None:
|
||||||
|
ticker = self.fetch_ticker(pair)
|
||||||
|
rate = self._get_rate_from_ticker(side, ticker, conf_strategy, price_side)
|
||||||
|
|
||||||
|
if rate is None:
|
||||||
|
raise PricingError(f"{name}-Rate for {pair} was empty.")
|
||||||
|
with self._cache_lock:
|
||||||
|
cache_rate[pair] = rate
|
||||||
|
|
||||||
|
return rate
|
||||||
|
|
||||||
|
def _get_rate_from_ticker(self, side: EntryExit, ticker: Ticker, conf_strategy: Dict[str, Any],
|
||||||
|
price_side: BidAsk) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Get rate from ticker.
|
||||||
|
"""
|
||||||
|
ticker_rate = ticker[price_side]
|
||||||
|
if ticker['last'] and ticker_rate:
|
||||||
|
if side == 'entry' and ticker_rate > ticker['last']:
|
||||||
|
balance = conf_strategy.get('price_last_balance', 0.0)
|
||||||
|
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||||
|
elif side == 'exit' and ticker_rate < ticker['last']:
|
||||||
|
balance = conf_strategy.get('price_last_balance', 0.0)
|
||||||
|
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
|
||||||
|
rate = ticker_rate
|
||||||
|
return rate
|
||||||
|
|
||||||
|
def _get_rate_from_ob(self, pair: str, side: EntryExit, order_book: OrderBook, name: str,
|
||||||
|
price_side: BidAsk, order_book_top: int) -> float:
|
||||||
|
"""
|
||||||
|
Get rate from orderbook
|
||||||
|
:raises: PricingError if rate could not be determined.
|
||||||
|
"""
|
||||||
logger.debug('order_book %s', order_book)
|
logger.debug('order_book %s', order_book)
|
||||||
# top 1 = index 0
|
# top 1 = index 0
|
||||||
try:
|
try:
|
||||||
@@ -1680,27 +1703,8 @@ class Exchange:
|
|||||||
f"could not be determined. Orderbook: {order_book}"
|
f"could not be determined. Orderbook: {order_book}"
|
||||||
)
|
)
|
||||||
raise PricingError from e
|
raise PricingError from e
|
||||||
logger.debug(f"{pair} - {name} price from orderbook {price_side_word}"
|
logger.debug(f"{pair} - {name} price from orderbook {price_side.capitalize()}"
|
||||||
f"side - top {order_book_top} order book {side} rate {rate:.8f}")
|
f"side - top {order_book_top} order book {side} rate {rate:.8f}")
|
||||||
else:
|
|
||||||
logger.debug(f"Using Last {price_side_word} / Last Price")
|
|
||||||
if ticker is None:
|
|
||||||
ticker = self.fetch_ticker(pair)
|
|
||||||
ticker_rate = ticker[price_side]
|
|
||||||
if ticker['last'] and ticker_rate:
|
|
||||||
if side == 'entry' and ticker_rate > ticker['last']:
|
|
||||||
balance = conf_strategy.get('price_last_balance', 0.0)
|
|
||||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
|
||||||
elif side == 'exit' and ticker_rate < ticker['last']:
|
|
||||||
balance = conf_strategy.get('price_last_balance', 0.0)
|
|
||||||
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
|
|
||||||
rate = ticker_rate
|
|
||||||
|
|
||||||
if rate is None:
|
|
||||||
raise PricingError(f"{name}-Rate for {pair} was empty.")
|
|
||||||
with self._cache_lock:
|
|
||||||
cache_rate[pair] = rate
|
|
||||||
|
|
||||||
return rate
|
return rate
|
||||||
|
|
||||||
def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
|
def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
|
||||||
@@ -1843,9 +1847,6 @@ class Exchange:
|
|||||||
if fee_curr is None:
|
if fee_curr is None:
|
||||||
return None
|
return None
|
||||||
fee_cost = float(fee['cost'])
|
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
|
# Calculate fee based on order details
|
||||||
if fee_curr == self.get_pair_base_currency(symbol):
|
if fee_curr == self.get_pair_base_currency(symbol):
|
||||||
@@ -1864,7 +1865,7 @@ class Exchange:
|
|||||||
tick = self.fetch_ticker(comb)
|
tick = self.fetch_ticker(comb)
|
||||||
|
|
||||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
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)
|
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
|
||||||
if not fee_to_quote_rate:
|
if not fee_to_quote_rate:
|
||||||
return None
|
return None
|
||||||
@@ -2151,7 +2152,7 @@ class Exchange:
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
logger.exception("Error loading %s. Result was %s.", pair, data)
|
logger.exception("Error loading %s. Result was %s.", pair, data)
|
||||||
return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
|
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
|
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
|
||||||
|
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
@@ -2253,6 +2254,7 @@ class Exchange:
|
|||||||
from_id = t[-1][1]
|
from_id = t[-1][1]
|
||||||
trades.extend(t[:-1])
|
trades.extend(t[:-1])
|
||||||
while True:
|
while True:
|
||||||
|
try:
|
||||||
t = await self._async_fetch_trades(pair,
|
t = await self._async_fetch_trades(pair,
|
||||||
params={self._trades_pagination_arg: from_id})
|
params={self._trades_pagination_arg: from_id})
|
||||||
if t:
|
if t:
|
||||||
@@ -2268,6 +2270,9 @@ class Exchange:
|
|||||||
from_id = t[-1][1]
|
from_id = t[-1][1]
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||||
|
break
|
||||||
|
|
||||||
return (pair, trades)
|
return (pair, trades)
|
||||||
|
|
||||||
@@ -2286,6 +2291,7 @@ class Exchange:
|
|||||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||||
while True:
|
while True:
|
||||||
|
try:
|
||||||
t = await self._async_fetch_trades(pair, since=since)
|
t = await self._async_fetch_trades(pair, since=since)
|
||||||
if t:
|
if t:
|
||||||
since = t[-1][0]
|
since = t[-1][0]
|
||||||
@@ -2297,6 +2303,9 @@ class Exchange:
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||||
|
break
|
||||||
|
|
||||||
return (pair, trades)
|
return (pair, trades)
|
||||||
|
|
||||||
@@ -2344,9 +2353,16 @@ class Exchange:
|
|||||||
raise OperationalException("This exchange does not support downloading Trades.")
|
raise OperationalException("This exchange does not support downloading Trades.")
|
||||||
|
|
||||||
with self._loop_lock:
|
with self._loop_lock:
|
||||||
return self.loop.run_until_complete(
|
task = asyncio.ensure_future(self._async_get_trade_history(
|
||||||
self._async_get_trade_history(pair=pair, since=since,
|
pair=pair, since=since, until=until, from_id=from_id))
|
||||||
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
|
@retrier
|
||||||
def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
|
def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import ccxt
|
|||||||
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
|
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
|
||||||
TRUNCATE, decimal_to_precision)
|
TRUNCATE, decimal_to_precision)
|
||||||
|
|
||||||
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
from freqtrade.exchange.common import (BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||||
|
SUPPORTED_EXCHANGES)
|
||||||
|
from freqtrade.types import ValidExchangesType
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts
|
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts
|
||||||
|
|
||||||
@@ -55,14 +57,41 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]:
|
|||||||
return True, ''
|
return True, ''
|
||||||
|
|
||||||
|
|
||||||
def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]:
|
def _build_exchange_list_entry(
|
||||||
|
exchange_name: str, exchangeClasses: Dict[str, Any]) -> ValidExchangesType:
|
||||||
|
valid, comment = validate_exchange(exchange_name)
|
||||||
|
result: ValidExchangesType = {
|
||||||
|
'name': exchange_name,
|
||||||
|
'valid': valid,
|
||||||
|
'supported': exchange_name.lower() in SUPPORTED_EXCHANGES,
|
||||||
|
'comment': comment,
|
||||||
|
'trade_modes': [{'trading_mode': 'spot', 'margin_mode': ''}],
|
||||||
|
}
|
||||||
|
if resolved := exchangeClasses.get(exchange_name.lower()):
|
||||||
|
supported_modes = [{'trading_mode': 'spot', 'margin_mode': ''}] + [
|
||||||
|
{'trading_mode': tm.value, 'margin_mode': mm.value}
|
||||||
|
for tm, mm in resolved['class']._supported_trading_mode_margin_pairs
|
||||||
|
]
|
||||||
|
result.update({
|
||||||
|
'trade_modes': supported_modes,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def list_available_exchanges(all_exchanges: bool) -> List[ValidExchangesType]:
|
||||||
"""
|
"""
|
||||||
:return: List of tuples with exchangename, valid, reason.
|
:return: List of tuples with exchangename, valid, reason.
|
||||||
"""
|
"""
|
||||||
exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
|
exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
|
||||||
exchanges_valid = [
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
(e, *validate_exchange(e)) for e in exchanges
|
|
||||||
|
subclassed = {e['name'].lower(): e for e in ExchangeResolver.search_all_objects({}, False)}
|
||||||
|
|
||||||
|
exchanges_valid: List[ValidExchangesType] = [
|
||||||
|
_build_exchange_list_entry(e, subclassed) for e in exchanges
|
||||||
]
|
]
|
||||||
|
|
||||||
return exchanges_valid
|
return exchanges_valid
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ class Gate(Exchange):
|
|||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"needs_trading_fees": True,
|
"needs_trading_fees": True,
|
||||||
"marketOrderRequiresPrice": False,
|
"marketOrderRequiresPrice": False,
|
||||||
"tickers_have_bid_ask": False,
|
|
||||||
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
|
||||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
|
||||||
"stop_price_type_field": "price_type",
|
"stop_price_type_field": "price_type",
|
||||||
"stop_price_type_value_mapping": {
|
"stop_price_type_value_mapping": {
|
||||||
PriceType.LAST: 0,
|
PriceType.LAST: 0,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class Okx(Exchange):
|
|||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"tickers_have_quoteVolume": False,
|
"tickers_have_quoteVolume": False,
|
||||||
"fee_cost_in_contracts": True,
|
|
||||||
"stop_price_type_field": "slTriggerPxType",
|
"stop_price_type_field": "slTriggerPxType",
|
||||||
"stop_price_type_value_mapping": {
|
"stop_price_type_value_mapping": {
|
||||||
PriceType.LAST: "last",
|
PriceType.LAST: "last",
|
||||||
@@ -125,6 +124,20 @@ class Okx(Exchange):
|
|||||||
params['posSide'] = self._get_posSide(side, reduceOnly)
|
params['posSide'] = self._get_posSide(side, reduceOnly)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def __fetch_leverage_already_set(self, pair: str, leverage: float, side: BuySell) -> bool:
|
||||||
|
try:
|
||||||
|
res_lev = self._api.fetch_leverage(symbol=pair, params={
|
||||||
|
"mgnMode": self.margin_mode.value,
|
||||||
|
"posSide": self._get_posSide(side, False),
|
||||||
|
})
|
||||||
|
self._log_exchange_response('get_leverage', res_lev)
|
||||||
|
already_set = all(float(x['lever']) == leverage for x in res_lev['data'])
|
||||||
|
return already_set
|
||||||
|
|
||||||
|
except ccxt.BaseError:
|
||||||
|
# Assume all errors as "not set yet"
|
||||||
|
return False
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||||
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
||||||
@@ -141,8 +154,11 @@ class Okx(Exchange):
|
|||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
already_set = self.__fetch_leverage_already_set(pair, leverage, side)
|
||||||
|
if not already_set:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}'
|
||||||
|
) from e
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@@ -182,6 +198,7 @@ class Okx(Exchange):
|
|||||||
order_reg['type'] = 'stoploss'
|
order_reg['type'] = 'stoploss'
|
||||||
order_reg['status_stop'] = 'triggered'
|
order_reg['status_stop'] = 'triggered'
|
||||||
return order_reg
|
return order_reg
|
||||||
|
order = self._order_contracts_to_amount(order)
|
||||||
order['type'] = 'stoploss'
|
order['type'] = 'stoploss'
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Type, Union
|
from typing import List, Optional, Type, Union
|
||||||
|
|
||||||
import gymnasium as gym
|
import gymnasium as gym
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -11,6 +11,8 @@ from gymnasium import spaces
|
|||||||
from gymnasium.utils import seeding
|
from gymnasium.utils import seeding
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -80,8 +82,9 @@ class BaseEnvironment(gym.Env):
|
|||||||
self.can_short: bool = can_short
|
self.can_short: bool = can_short
|
||||||
self.live: bool = live
|
self.live: bool = live
|
||||||
if not self.live and self.add_state_info:
|
if not self.live and self.add_state_info:
|
||||||
self.add_state_info = False
|
raise OperationalException("`add_state_info` is not available in backtesting. Change "
|
||||||
logger.warning("add_state_info is not available in backtesting. Deactivating.")
|
"parameter to false in your rl_config. See `add_state_info` "
|
||||||
|
"docs for more info.")
|
||||||
self.seed(seed)
|
self.seed(seed)
|
||||||
self.reset_env(df, prices, window_size, reward_kwargs, starting_point)
|
self.reset_env(df, prices, window_size, reward_kwargs, starting_point)
|
||||||
|
|
||||||
@@ -141,6 +144,9 @@ class BaseEnvironment(gym.Env):
|
|||||||
Unique to the environment action count. Must be inherited.
|
Unique to the environment action count. Must be inherited.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def action_masks(self) -> List[bool]:
|
||||||
|
return [self._is_valid(action.value) for action in self.actions]
|
||||||
|
|
||||||
def seed(self, seed: int = 1):
|
def seed(self, seed: int = 1):
|
||||||
self.np_random, seed = seeding.np_random(seed)
|
self.np_random, seed = seeding.np_random(seed)
|
||||||
return [seed]
|
return [seed]
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import pandas as pd
|
|||||||
import torch as th
|
import torch as th
|
||||||
import torch.multiprocessing
|
import torch.multiprocessing
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from stable_baselines3.common.callbacks import EvalCallback
|
from sb3_contrib.common.maskable.callbacks import MaskableEvalCallback
|
||||||
|
from sb3_contrib.common.maskable.utils import is_masking_supported
|
||||||
from stable_baselines3.common.monitor import Monitor
|
from stable_baselines3.common.monitor import Monitor
|
||||||
from stable_baselines3.common.utils import set_random_seed
|
from stable_baselines3.common.utils import set_random_seed
|
||||||
from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor
|
from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor
|
||||||
@@ -48,7 +49,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
self.reward_params = self.freqai_info['rl_config']['model_reward_parameters']
|
self.reward_params = self.freqai_info['rl_config']['model_reward_parameters']
|
||||||
self.train_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env()
|
self.train_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env()
|
||||||
self.eval_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env()
|
self.eval_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env()
|
||||||
self.eval_callback: Optional[EvalCallback] = None
|
self.eval_callback: Optional[MaskableEvalCallback] = None
|
||||||
self.model_type = self.freqai_info['rl_config']['model_type']
|
self.model_type = self.freqai_info['rl_config']['model_type']
|
||||||
self.rl_config = self.freqai_info['rl_config']
|
self.rl_config = self.freqai_info['rl_config']
|
||||||
self.df_raw: DataFrame = DataFrame()
|
self.df_raw: DataFrame = DataFrame()
|
||||||
@@ -82,6 +83,9 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
if self.ft_params.get('use_DBSCAN_to_remove_outliers', False):
|
if self.ft_params.get('use_DBSCAN_to_remove_outliers', False):
|
||||||
self.ft_params.update({'use_DBSCAN_to_remove_outliers': False})
|
self.ft_params.update({'use_DBSCAN_to_remove_outliers': False})
|
||||||
logger.warning('User tried to use DBSCAN with RL. Deactivating DBSCAN.')
|
logger.warning('User tried to use DBSCAN with RL. Deactivating DBSCAN.')
|
||||||
|
if self.ft_params.get('DI_threshold', False):
|
||||||
|
self.ft_params.update({'DI_threshold': False})
|
||||||
|
logger.warning('User tried to use DI_threshold with RL. Deactivating DI_threshold.')
|
||||||
if self.freqai_info['data_split_parameters'].get('shuffle', False):
|
if self.freqai_info['data_split_parameters'].get('shuffle', False):
|
||||||
self.freqai_info['data_split_parameters'].update({'shuffle': False})
|
self.freqai_info['data_split_parameters'].update({'shuffle': False})
|
||||||
logger.warning('User tried to shuffle training data. Setting shuffle to False')
|
logger.warning('User tried to shuffle training data. Setting shuffle to False')
|
||||||
@@ -107,27 +111,37 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
training_filter=True,
|
training_filter=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
data_dictionary: Dict[str, Any] = dk.make_train_test_datasets(
|
dd: Dict[str, Any] = dk.make_train_test_datasets(
|
||||||
features_filtered, labels_filtered)
|
features_filtered, labels_filtered)
|
||||||
self.df_raw = copy.deepcopy(data_dictionary["train_features"])
|
self.df_raw = copy.deepcopy(dd["train_features"])
|
||||||
dk.fit_labels() # FIXME useless for now, but just satiating append methods
|
dk.fit_labels() # FIXME useless for now, but just satiating append methods
|
||||||
|
|
||||||
# normalize all data based on train_dataset only
|
# normalize all data based on train_dataset only
|
||||||
prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk)
|
prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk)
|
||||||
|
|
||||||
data_dictionary = dk.normalize_data(data_dictionary)
|
dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
|
||||||
|
|
||||||
# data cleaning/analysis
|
(dd["train_features"],
|
||||||
self.data_cleaning_train(dk)
|
dd["train_labels"],
|
||||||
|
dd["train_weights"]) = dk.feature_pipeline.fit_transform(dd["train_features"],
|
||||||
|
dd["train_labels"],
|
||||||
|
dd["train_weights"])
|
||||||
|
|
||||||
|
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0:
|
||||||
|
(dd["test_features"],
|
||||||
|
dd["test_labels"],
|
||||||
|
dd["test_weights"]) = dk.feature_pipeline.transform(dd["test_features"],
|
||||||
|
dd["test_labels"],
|
||||||
|
dd["test_weights"])
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Training model on {len(dk.data_dictionary["train_features"].columns)}'
|
f'Training model on {len(dk.data_dictionary["train_features"].columns)}'
|
||||||
f' features and {len(data_dictionary["train_features"])} data points'
|
f' features and {len(dd["train_features"])} data points'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.set_train_and_eval_environments(data_dictionary, prices_train, prices_test, dk)
|
self.set_train_and_eval_environments(dd, prices_train, prices_test, dk)
|
||||||
|
|
||||||
model = self.fit(data_dictionary, dk)
|
model = self.fit(dd, dk)
|
||||||
|
|
||||||
logger.info(f"--------------------done training {pair}--------------------")
|
logger.info(f"--------------------done training {pair}--------------------")
|
||||||
|
|
||||||
@@ -151,9 +165,11 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
|
|
||||||
self.train_env = self.MyRLEnv(df=train_df, prices=prices_train, **env_info)
|
self.train_env = self.MyRLEnv(df=train_df, prices=prices_train, **env_info)
|
||||||
self.eval_env = Monitor(self.MyRLEnv(df=test_df, prices=prices_test, **env_info))
|
self.eval_env = Monitor(self.MyRLEnv(df=test_df, prices=prices_test, **env_info))
|
||||||
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
|
self.eval_callback = MaskableEvalCallback(self.eval_env, deterministic=True,
|
||||||
render=False, eval_freq=len(train_df),
|
render=False, eval_freq=len(train_df),
|
||||||
best_model_save_path=str(dk.data_path))
|
best_model_save_path=str(dk.data_path),
|
||||||
|
use_masking=(self.model_type == 'MaskablePPO' and
|
||||||
|
is_masking_supported(self.eval_env)))
|
||||||
|
|
||||||
actions = self.train_env.get_actions()
|
actions = self.train_env.get_actions()
|
||||||
self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions)
|
self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions)
|
||||||
@@ -236,13 +252,10 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
unfiltered_df, dk.training_features_list, training_filter=False
|
unfiltered_df, dk.training_features_list, training_filter=False
|
||||||
)
|
)
|
||||||
|
|
||||||
filtered_dataframe = self.drop_ohlc_from_df(filtered_dataframe, dk)
|
dk.data_dictionary["prediction_features"] = self.drop_ohlc_from_df(filtered_dataframe, dk)
|
||||||
|
|
||||||
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
|
dk.data_dictionary["prediction_features"], _, _ = dk.feature_pipeline.transform(
|
||||||
dk.data_dictionary["prediction_features"] = filtered_dataframe
|
dk.data_dictionary["prediction_features"], outlier_check=True)
|
||||||
|
|
||||||
# optional additional data cleaning/analysis
|
|
||||||
self.data_cleaning_predict(dk)
|
|
||||||
|
|
||||||
pred_df = self.rl_model_predict(
|
pred_df = self.rl_model_predict(
|
||||||
dk.data_dictionary["prediction_features"], dk, self.model)
|
dk.data_dictionary["prediction_features"], dk, self.model)
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ logger = logging.getLogger(__name__)
|
|||||||
class BaseClassifierModel(IFreqaiModel):
|
class BaseClassifierModel(IFreqaiModel):
|
||||||
"""
|
"""
|
||||||
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
|
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
|
||||||
User *must* inherit from this class and set fit() and predict(). See example scripts
|
User *must* inherit from this class and set fit(). See example scripts
|
||||||
such as prediction_models/CatboostPredictionModel.py for guidance.
|
such as prediction_models/CatboostClassifier.py for guidance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def train(
|
def train(
|
||||||
@@ -50,21 +50,30 @@ class BaseClassifierModel(IFreqaiModel):
|
|||||||
logger.info(f"-------------------- Training on data from {start_date} to "
|
logger.info(f"-------------------- Training on data from {start_date} to "
|
||||||
f"{end_date} --------------------")
|
f"{end_date} --------------------")
|
||||||
# split data into train/test data.
|
# split data into train/test data.
|
||||||
data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
dd = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
||||||
if not self.freqai_info.get("fit_live_predictions_candles", 0) or not self.live:
|
if not self.freqai_info.get("fit_live_predictions_candles", 0) or not self.live:
|
||||||
dk.fit_labels()
|
dk.fit_labels()
|
||||||
# normalize all data based on train_dataset only
|
dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
|
||||||
data_dictionary = dk.normalize_data(data_dictionary)
|
|
||||||
|
|
||||||
# optional additional data cleaning/analysis
|
(dd["train_features"],
|
||||||
self.data_cleaning_train(dk)
|
dd["train_labels"],
|
||||||
|
dd["train_weights"]) = dk.feature_pipeline.fit_transform(dd["train_features"],
|
||||||
|
dd["train_labels"],
|
||||||
|
dd["train_weights"])
|
||||||
|
|
||||||
|
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0:
|
||||||
|
(dd["test_features"],
|
||||||
|
dd["test_labels"],
|
||||||
|
dd["test_weights"]) = dk.feature_pipeline.transform(dd["test_features"],
|
||||||
|
dd["test_labels"],
|
||||||
|
dd["test_weights"])
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Training model on {len(dk.data_dictionary['train_features'].columns)} features"
|
f"Training model on {len(dk.data_dictionary['train_features'].columns)} features"
|
||||||
)
|
)
|
||||||
logger.info(f"Training model on {len(data_dictionary['train_features'])} data points")
|
logger.info(f"Training model on {len(dd['train_features'])} data points")
|
||||||
|
|
||||||
model = self.fit(data_dictionary, dk)
|
model = self.fit(dd, dk)
|
||||||
|
|
||||||
end_time = time()
|
end_time = time()
|
||||||
|
|
||||||
@@ -89,10 +98,11 @@ class BaseClassifierModel(IFreqaiModel):
|
|||||||
filtered_df, _ = dk.filter_features(
|
filtered_df, _ = dk.filter_features(
|
||||||
unfiltered_df, dk.training_features_list, training_filter=False
|
unfiltered_df, dk.training_features_list, training_filter=False
|
||||||
)
|
)
|
||||||
filtered_df = dk.normalize_data_from_metadata(filtered_df)
|
|
||||||
dk.data_dictionary["prediction_features"] = filtered_df
|
dk.data_dictionary["prediction_features"] = filtered_df
|
||||||
|
|
||||||
self.data_cleaning_predict(dk)
|
dk.data_dictionary["prediction_features"], outliers, _ = dk.feature_pipeline.transform(
|
||||||
|
dk.data_dictionary["prediction_features"], outlier_check=True)
|
||||||
|
|
||||||
predictions = self.model.predict(dk.data_dictionary["prediction_features"])
|
predictions = self.model.predict(dk.data_dictionary["prediction_features"])
|
||||||
if self.CONV_WIDTH == 1:
|
if self.CONV_WIDTH == 1:
|
||||||
@@ -107,4 +117,10 @@ class BaseClassifierModel(IFreqaiModel):
|
|||||||
|
|
||||||
pred_df = pd.concat([pred_df, pred_df_prob], axis=1)
|
pred_df = pd.concat([pred_df, pred_df_prob], axis=1)
|
||||||
|
|
||||||
|
if dk.feature_pipeline["di"]:
|
||||||
|
dk.DI_values = dk.feature_pipeline["di"].di_values
|
||||||
|
else:
|
||||||
|
dk.DI_values = np.zeros(outliers.shape[0])
|
||||||
|
dk.do_predict = outliers
|
||||||
|
|
||||||
return (pred_df, dk.do_predict)
|
return (pred_df, dk.do_predict)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Tuple
|
from time import time
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
@@ -35,6 +36,7 @@ class BasePyTorchClassifier(BasePyTorchModel):
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.class_name_to_index = None
|
self.class_name_to_index = None
|
||||||
@@ -68,9 +70,12 @@ class BasePyTorchClassifier(BasePyTorchModel):
|
|||||||
filtered_df, _ = dk.filter_features(
|
filtered_df, _ = dk.filter_features(
|
||||||
unfiltered_df, dk.training_features_list, training_filter=False
|
unfiltered_df, dk.training_features_list, training_filter=False
|
||||||
)
|
)
|
||||||
filtered_df = dk.normalize_data_from_metadata(filtered_df)
|
|
||||||
dk.data_dictionary["prediction_features"] = filtered_df
|
dk.data_dictionary["prediction_features"] = filtered_df
|
||||||
self.data_cleaning_predict(dk)
|
|
||||||
|
dk.data_dictionary["prediction_features"], outliers, _ = dk.feature_pipeline.transform(
|
||||||
|
dk.data_dictionary["prediction_features"], outlier_check=True)
|
||||||
|
|
||||||
x = self.data_convertor.convert_x(
|
x = self.data_convertor.convert_x(
|
||||||
dk.data_dictionary["prediction_features"],
|
dk.data_dictionary["prediction_features"],
|
||||||
device=self.device
|
device=self.device
|
||||||
@@ -85,6 +90,13 @@ class BasePyTorchClassifier(BasePyTorchModel):
|
|||||||
pred_df_prob = DataFrame(probs.detach().tolist(), columns=class_names)
|
pred_df_prob = DataFrame(probs.detach().tolist(), columns=class_names)
|
||||||
pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]])
|
pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]])
|
||||||
pred_df = pd.concat([pred_df, pred_df_prob], axis=1)
|
pred_df = pd.concat([pred_df, pred_df_prob], axis=1)
|
||||||
|
|
||||||
|
if dk.feature_pipeline["di"]:
|
||||||
|
dk.DI_values = dk.feature_pipeline["di"].di_values
|
||||||
|
else:
|
||||||
|
dk.DI_values = np.zeros(outliers.shape[0])
|
||||||
|
dk.do_predict = outliers
|
||||||
|
|
||||||
return (pred_df, dk.do_predict)
|
return (pred_df, dk.do_predict)
|
||||||
|
|
||||||
def encode_class_names(
|
def encode_class_names(
|
||||||
@@ -149,3 +161,58 @@ class BasePyTorchClassifier(BasePyTorchModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return self.class_names
|
return self.class_names
|
||||||
|
|
||||||
|
def train(
|
||||||
|
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Filter the training data and train a model to it. Train makes heavy use of the datakitchen
|
||||||
|
for storing, saving, loading, and analyzing the data.
|
||||||
|
:param unfiltered_df: Full dataframe for the current training period
|
||||||
|
:return:
|
||||||
|
:model: Trained model which can be used to inference (self.predict)
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"-------------------- Starting training {pair} --------------------")
|
||||||
|
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
features_filtered, labels_filtered = dk.filter_features(
|
||||||
|
unfiltered_df,
|
||||||
|
dk.training_features_list,
|
||||||
|
dk.label_list,
|
||||||
|
training_filter=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# split data into train/test data.
|
||||||
|
dd = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
||||||
|
if not self.freqai_info.get("fit_live_predictions_candles", 0) or not self.live:
|
||||||
|
dk.fit_labels()
|
||||||
|
|
||||||
|
dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
|
||||||
|
|
||||||
|
(dd["train_features"],
|
||||||
|
dd["train_labels"],
|
||||||
|
dd["train_weights"]) = dk.feature_pipeline.fit_transform(dd["train_features"],
|
||||||
|
dd["train_labels"],
|
||||||
|
dd["train_weights"])
|
||||||
|
|
||||||
|
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) != 0:
|
||||||
|
(dd["test_features"],
|
||||||
|
dd["test_labels"],
|
||||||
|
dd["test_weights"]) = dk.feature_pipeline.transform(dd["test_features"],
|
||||||
|
dd["test_labels"],
|
||||||
|
dd["test_weights"])
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Training model on {len(dk.data_dictionary['train_features'].columns)} features"
|
||||||
|
)
|
||||||
|
logger.info(f"Training model on {len(dd['train_features'])} data points")
|
||||||
|
|
||||||
|
model = self.fit(dd, dk)
|
||||||
|
end_time = time()
|
||||||
|
|
||||||
|
logger.info(f"-------------------- Done training {pair} "
|
||||||
|
f"({end_time - start_time:.2f} secs) --------------------")
|
||||||
|
|
||||||
|
return model
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from time import time
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
|
||||||
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
||||||
from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor
|
from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor
|
||||||
|
|
||||||
@@ -29,51 +25,6 @@ class BasePyTorchModel(IFreqaiModel, ABC):
|
|||||||
self.splits = ["train", "test"] if test_size != 0 else ["train"]
|
self.splits = ["train", "test"] if test_size != 0 else ["train"]
|
||||||
self.window_size = self.freqai_info.get("conv_width", 1)
|
self.window_size = self.freqai_info.get("conv_width", 1)
|
||||||
|
|
||||||
def train(
|
|
||||||
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
Filter the training data and train a model to it. Train makes heavy use of the datakitchen
|
|
||||||
for storing, saving, loading, and analyzing the data.
|
|
||||||
:param unfiltered_df: Full dataframe for the current training period
|
|
||||||
:return:
|
|
||||||
:model: Trained model which can be used to inference (self.predict)
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger.info(f"-------------------- Starting training {pair} --------------------")
|
|
||||||
|
|
||||||
start_time = time()
|
|
||||||
|
|
||||||
features_filtered, labels_filtered = dk.filter_features(
|
|
||||||
unfiltered_df,
|
|
||||||
dk.training_features_list,
|
|
||||||
dk.label_list,
|
|
||||||
training_filter=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# split data into train/test data.
|
|
||||||
data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
|
||||||
if not self.freqai_info.get("fit_live_predictions", 0) or not self.live:
|
|
||||||
dk.fit_labels()
|
|
||||||
# normalize all data based on train_dataset only
|
|
||||||
data_dictionary = dk.normalize_data(data_dictionary)
|
|
||||||
|
|
||||||
# optional additional data cleaning/analysis
|
|
||||||
self.data_cleaning_train(dk)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Training model on {len(dk.data_dictionary['train_features'].columns)} features"
|
|
||||||
)
|
|
||||||
logger.info(f"Training model on {len(data_dictionary['train_features'])} data points")
|
|
||||||
|
|
||||||
model = self.fit(data_dictionary, dk)
|
|
||||||
end_time = time()
|
|
||||||
|
|
||||||
logger.info(f"-------------------- Done training {pair} "
|
|
||||||
f"({end_time - start_time:.2f} secs) --------------------")
|
|
||||||
|
|
||||||
return model
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def data_convertor(self) -> PyTorchDataConvertor:
|
def data_convertor(self) -> PyTorchDataConvertor:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user