Compare commits
995 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f142abfb76 | |||
| c98c6c38dc | |||
| 9a73f7a7b8 | |||
| 7813d1acd6 | |||
| 2c20464983 | |||
| e97d9013d5 | |||
| 1e18b35f22 | |||
| bb78285661 | |||
| b47ba8cfd3 | |||
| 1dccdf0594 | |||
| 5da48d8ffc | |||
| 4f4b7c5625 | |||
| 9dcf236387 | |||
| 97bc5804fd | |||
| beab966851 | |||
| 00a7e8f9a2 | |||
| 59dc526683 | |||
| aef7f9dc03 | |||
| abc6a08397 | |||
| eb4da8a6f6 | |||
| 494fd734bc | |||
| 9174f60b1d | |||
| c2905a4522 | |||
| adf03e2ee3 | |||
| fe5f085ec1 | |||
| 748ac50d83 | |||
| d2069c1729 | |||
| 38cf50830a | |||
| bf96a43efb | |||
| 22b4555926 | |||
| 7fb3632e7b | |||
| e5f0fe288a | |||
| 58aaaffe2b | |||
| c62aefbfde | |||
| 16dfde22fc | |||
| bbbc8a760c | |||
| 9b45bb2e24 | |||
| e69c21c9f5 | |||
| 72933859cf | |||
| 0a5cee6a73 | |||
| a80c979482 | |||
| 4a4b695b94 | |||
| a8c246ce0d | |||
| c6e979ddda | |||
| 1a78346cf1 | |||
| e6d0d53e47 | |||
| 68680bb2c6 | |||
| 7de415a6a4 | |||
| f46fa440f0 | |||
| 56177d7e92 | |||
| 41584317e4 | |||
| 3264e2c31d | |||
| fadc7a2051 | |||
| cebc55687b | |||
| 5940279f7b | |||
| 35f6333b67 | |||
| 262b044969 | |||
| a571df784c | |||
| 078aa8327b | |||
| 10945edbe6 | |||
| 5d9b69cbfe | |||
| 89117dd53f | |||
| 4af9654696 | |||
| 1e0ce7d47a | |||
| 27cd54b42c | |||
| ec64a182f9 | |||
| dd30df7057 | |||
| 4c6e33aa82 | |||
| 8cb934849a | |||
| c627aee096 | |||
| 9d552d45c2 | |||
| 1e1b8dbe53 | |||
| 2d9d8dc976 | |||
| ba674fc796 | |||
| 58550515ad | |||
| 27bae60b68 | |||
| 646dd63faf | |||
| 8d2b389e27 | |||
| fa174a392a | |||
| 042e35e8d3 | |||
| b76513ce33 | |||
| 368bfcf476 | |||
| fee3c598d0 | |||
| 69264cc164 | |||
| e81929bc55 | |||
| 0843b19b6c | |||
| 813b472c6c | |||
| 2f079711ec | |||
| 97e9d2dc42 | |||
| 7344f20803 | |||
| bfe04464b4 | |||
| 2225f5661b | |||
| 9e77c93a54 | |||
| b65fa98cee | |||
| b57821b273 | |||
| db7f9598b0 | |||
| e547da10de | |||
| 5a0c15f377 | |||
| 0f5b69b4f2 | |||
| 86d4497aaf | |||
| 4b80961c65 | |||
| 785cc9df85 | |||
| 1bfecb0313 | |||
| b88a561eb5 | |||
| 4bf026605e | |||
| db6933856f | |||
| 01107ffa59 | |||
| 74f678a26f | |||
| bf93648198 | |||
| 51dccaeb4c | |||
| ed4252af53 | |||
| 2fde902c82 | |||
| 4ea75af2d9 | |||
| 1bba0cf7fc | |||
| b1fd0c73c7 | |||
| 29eb6d938b | |||
| 19620470bd | |||
| 7a69b01b9b | |||
| bc531cf846 | |||
| 14908e52e2 | |||
| a86bf8d6d7 | |||
| 3662ca3ca6 | |||
| 5fae03f7be | |||
| f7f0f3bebb | |||
| ccbab6ee6f | |||
| ef12dafea8 | |||
| 08a65b50aa | |||
| d28d6cbdb4 | |||
| 41a6709ece | |||
| 756f5d5408 | |||
| 422b9c8fbc | |||
| 40ec2c4921 | |||
| 5cf7ad85a0 | |||
| f02c9f0435 | |||
| c59b5e7781 | |||
| e525c6694b | |||
| 897c14e2e5 | |||
| 6c00bf423b | |||
| 094984eb2f | |||
| 081411fba4 | |||
| 178c0972ff | |||
| 910e317a45 | |||
| 1ef5adbb0e | |||
| f5db856a75 | |||
| b2ad40253f | |||
| 1e0d622d80 | |||
| 1d941249ed | |||
| 5bfd3fdc33 | |||
| 753b807a45 | |||
| 04409602a6 | |||
| b2796b850b | |||
| 8c633aab19 | |||
| f14b4133c3 | |||
| 8f2e4aa4fd | |||
| 3c2e043e82 | |||
| 609ed2d7b3 | |||
| 183166b3fb | |||
| 12dc91719e | |||
| cd071c3bdc | |||
| 0618ecce47 | |||
| 13438ad8de | |||
| 60c2b1d6ca | |||
| eaf4ffcbaf | |||
| 79e59bdda0 | |||
| b409ab5ead | |||
| 790c7036a1 | |||
| 73902f507a | |||
| 1a80f6bc4e | |||
| 5feb7fe654 | |||
| 980960bc6a | |||
| 52c614733f | |||
| e913d12c71 | |||
| 5dc24232ca | |||
| ef2115d667 | |||
| 74476eae43 | |||
| 84335d58b3 | |||
| f175fc6e9b | |||
| 9edd7e6bc8 | |||
| 7478c56ca5 | |||
| 1f36f4802a | |||
| 4d920d7866 | |||
| 19fb3f9b54 | |||
| fafc102573 | |||
| 3868f9c698 | |||
| 0204f666b3 | |||
| f32a13bfb0 | |||
| 4809c9f07a | |||
| 19af144b33 | |||
| 4db7962048 | |||
| 7e6f2cba53 | |||
| c6199e5ca6 | |||
| e73f215a67 | |||
| f7ff1dc610 | |||
| 96c5db3e38 | |||
| c7f248fe3b | |||
| e27636b798 | |||
| 2d56b1bc8c | |||
| 659cbd987a | |||
| aff5372a8f | |||
| 2e7b5e2be9 | |||
| 7971cb29bb | |||
| 154149ff03 | |||
| 92d7f27983 | |||
| 20ea679b2b | |||
| 39ede449a0 | |||
| 37550d3bdb | |||
| b688526623 | |||
| d52d30cfbe | |||
| 9cf08e0434 | |||
| 56a85690b4 | |||
| 0f2612c231 | |||
| f9edc72f35 | |||
| 90475e52db | |||
| 504f51fabb | |||
| a905d1bd67 | |||
| a27baf1a51 | |||
| f174c2e01e | |||
| 3f60b2c140 | |||
| 927d1d2686 | |||
| d04a4db270 | |||
| 8ad6eb9896 | |||
| 74709461e3 | |||
| 9e6cc5ebbd | |||
| a2d8f92e05 | |||
| a7d90e2a25 | |||
| 83b37e2f78 | |||
| 488629096b | |||
| a3f167f6df | |||
| 1e8814b43e | |||
| 2e430519e3 | |||
| 2164b02c66 | |||
| ed45dcdf8a | |||
| e518e741af | |||
| 56b9c250ba | |||
| a009816ac4 | |||
| c29d2ac05b | |||
| 551d1b0962 | |||
| 5e696f4ea0 | |||
| d8423272da | |||
| b605d4d71f | |||
| c39290d7da | |||
| 4849b23fd6 | |||
| c1beb0ff33 | |||
| d08a015b45 | |||
| 4f7487e931 | |||
| d032aaaba0 | |||
| 073cb0a5a9 | |||
| ce6c44eda0 | |||
| 0099381dd0 | |||
| 7bcb3d5580 | |||
| c15c13a367 | |||
| 53a7273065 | |||
| da9a8b616e | |||
| e6277222ca | |||
| 8002b476ff | |||
| d9b5d05e2a | |||
| f5d7a4417d | |||
| 04b3b1d4d9 | |||
| 38a3ce6164 | |||
| 123e51774e | |||
| 2322d0f3f3 | |||
| 8a5287581f | |||
| 41cdd28f5f | |||
| b387c315da | |||
| 3c647a3794 | |||
| aac9ee0d55 | |||
| 966247baaf | |||
| 589b9858f4 | |||
| c0b2b0b96d | |||
| 6cfc1836a2 | |||
| 35800f3ada | |||
| 9fa365e766 | |||
| a4a3b19566 | |||
| 8452399c12 | |||
| ff8a5ad2e5 | |||
| dda5a59019 | |||
| 30e8466cae | |||
| 3881c51892 | |||
| 4959d124ad | |||
| f133ee3cca | |||
| ef6afaa2cb | |||
| 0e406c4d7d | |||
| b21775142d | |||
| 8ccc66cd97 | |||
| b4e732617e | |||
| 450219c83b | |||
| eb7df30061 | |||
| 9385400000 | |||
| 961f50e335 | |||
| 478e6f1e64 | |||
| a0e115ebd9 | |||
| ce4f1b0709 | |||
| b0c639bac8 | |||
| aba576f79f | |||
| d92a6d7b73 | |||
| 89d47ffd8f | |||
| b9e9f82503 | |||
| 08b94a2077 | |||
| 9f445cb053 | |||
| 1d23ba6e30 | |||
| 28e43a4867 | |||
| 1b28521875 | |||
| 4ba3363bc6 | |||
| f1ed9ed048 | |||
| d465fcffd5 | |||
| 32b0098ec1 | |||
| e77b9de89e | |||
| aec67cda66 | |||
| f3fb801b33 | |||
| fe33088245 | |||
| 4bca8b97f3 | |||
| f62f4f7711 | |||
| 896e2b6285 | |||
| ddb0ae10b4 | |||
| 37fa186c55 | |||
| 979e485f24 | |||
| 1555667da7 | |||
| 8a52a7b50d | |||
| a74b6df14e | |||
| 8600ae0387 | |||
| 02306884f0 | |||
| 82ce5af454 | |||
| 830f2219c8 | |||
| d21f0f4081 | |||
| 8f883f2310 | |||
| 2f63b0199d | |||
| 5a96c775fd | |||
| cfdd9d6be9 | |||
| c749aae19f | |||
| fd4877759c | |||
| 2e7acb4482 | |||
| b43b60b26d | |||
| 5ca8076072 | |||
| 8d3c2470e3 | |||
| 3f85f3cce6 | |||
| 2801bccdc7 | |||
| 22651a8629 | |||
| c7106a6802 | |||
| 6e1f457fb3 | |||
| 533a16658c | |||
| 9b35dae465 | |||
| e6d01b04ea | |||
| d185b2020a | |||
| 74ed0e0b11 | |||
| 75f89ec12f | |||
| f6fce2162c | |||
| 158bf09774 | |||
| 1f1abfe798 | |||
| 29a5e049b9 | |||
| 2bc0c4ecd5 | |||
| 224213840d | |||
| d81fdeb3ed | |||
| 14c5f435aa | |||
| 5745722a37 | |||
| a7cd9d77f2 | |||
| 5b857aeaf0 | |||
| afb1b787c8 | |||
| 659168d341 | |||
| 3f4715ba49 | |||
| 1d0f1bd1ee | |||
| 220bc3c23e | |||
| c90be601f5 | |||
| 5d8e0573f6 | |||
| 3d858f6599 | |||
| 6d2d5f93d0 | |||
| d01bc0fb9f | |||
| 394d758d32 | |||
| 91c710408a | |||
| 3a7f390510 | |||
| a7d6efdcd6 | |||
| fed24c1308 | |||
| ae4021da14 | |||
| 8378a0234d | |||
| d26869ea0a | |||
| cf96ad1d1b | |||
| 3919bf3740 | |||
| 9814cf5360 | |||
| c19fe95d39 | |||
| 383bdb7d56 | |||
| 41765b14dc | |||
| 33bf7f9f30 | |||
| 454c2343a8 | |||
| 6d8bf75572 | |||
| 310c9f6034 | |||
| c0a600858f | |||
| 5b07385414 | |||
| 844ab4aef5 | |||
| 44ca6f1c46 | |||
| 552f947248 | |||
| 7655bf6ea7 | |||
| 25ae25248c | |||
| dd01ae880f | |||
| e0a06ca454 | |||
| a93592c467 | |||
| 08dffc95d8 | |||
| 57800e78c7 | |||
| bd4d7efbc9 | |||
| 5019fb5bf3 | |||
| a0e0d7fe27 | |||
| 6377fd2689 | |||
| bd9ea9bd8c | |||
| cfeefa8754 | |||
| 475d8486bb | |||
| 628963c207 | |||
| 4d1810c2b6 | |||
| 05f0dccb8e | |||
| 6360e7fb15 | |||
| 40695a39d5 | |||
| 008f621211 | |||
| a52cf42218 | |||
| a866b0a35e | |||
| af1054fa70 | |||
| 5608bbde9c | |||
| ee9d2c637a | |||
| f0819d9df1 | |||
| 96e5615d1b | |||
| 4798dea6e1 | |||
| 23a4ff9f25 | |||
| 5ddfbea611 | |||
| e8e67d1c85 | |||
| dc9c31252b | |||
| 82baa3f93f | |||
| 78ca11155b | |||
| 45c90c2013 | |||
| 3a1057e1ff | |||
| e45d5d3594 | |||
| 14ee5a2076 | |||
| da83243eb3 | |||
| 46ec7d5ea6 | |||
| 29fbac9f96 | |||
| 421a6c02a0 | |||
| 2ee152c5a6 | |||
| bef27467b6 | |||
| 52f971cbb7 | |||
| 563e68e894 | |||
| 49b4ab6d3b | |||
| 7455e56a29 | |||
| 245a67e37e | |||
| e76d4241a0 | |||
| 01ff054a0b | |||
| 9e1f7dc6f7 | |||
| 067c9219b6 | |||
| f4e5ce892b | |||
| 830fc7580c | |||
| 21859f79db | |||
| 376b5fce54 | |||
| cb5cd5cd86 | |||
| f8b97b6aa7 | |||
| 7b6e2eac49 | |||
| f13e134e9b | |||
| 97a37198b9 | |||
| 43bb4114d0 | |||
| 8c95207ca4 | |||
| ffaa121bc7 | |||
| 227b194a88 | |||
| 95d8c45481 | |||
| bae4abace2 | |||
| 44461bd1a1 | |||
| 2c095c07f2 | |||
| 9c3656e24e | |||
| 97275f5a46 | |||
| 7ccff18437 | |||
| c5f26e72e1 | |||
| f09c0a5bba | |||
| aba9450098 | |||
| 4ef1647132 | |||
| 6e93bff374 | |||
| 4d20e37f4d | |||
| 20cca01d10 | |||
| 8cbb5d2a93 | |||
| fbae2142d1 | |||
| feea20fb0a | |||
| 2ed52a77e5 | |||
| e854667eb8 | |||
| 7bf20d9060 | |||
| 7c5a11623e | |||
| 783a2d945e | |||
| 2073c71811 | |||
| d8122962db | |||
| 1ea626fb5b | |||
| f66719fc1f | |||
| 4b2813ff37 | |||
| 191c85bccc | |||
| 31816cdb2a | |||
| 574744348a | |||
| efb9975e3e | |||
| cd8743b80c | |||
| 237f3c58c8 | |||
| be7b0e4247 | |||
| 5954e7796c | |||
| f44eef3855 | |||
| 0232eeca54 | |||
| f5f0506a29 | |||
| 39b6a00224 | |||
| 206557d4b7 | |||
| 07e07bd66b | |||
| fac8e0fde5 | |||
| ffb1cf52b1 | |||
| 399f144c27 | |||
| 459b9d80d4 | |||
| 28e685ee2b | |||
| c58a1649cb | |||
| a4512ac791 | |||
| ac6cdb76ce | |||
| 8ac3e8ad5f | |||
| e0f52696a5 | |||
| f75f0ccf2c | |||
| 440f6ab8e1 | |||
| e7daf3177d | |||
| 8ffc0ad9fc | |||
| df83bdea8d | |||
| 420f6fb9a9 | |||
| cea3f7d3fa | |||
| a9f63c6a99 | |||
| 0ada2d9390 | |||
| e1b6b9b5a6 | |||
| 3fea2a35a2 | |||
| 821a598ff4 | |||
| feab5f82c1 | |||
| 607c604a45 | |||
| b77f926cdd | |||
| d2c0e9e438 | |||
| a4077e96ba | |||
| a4fc7ce0c4 | |||
| 00cef56a57 | |||
| d1984945d5 | |||
| 3c20ab683a | |||
| f838bc760f | |||
| 25f5dbfcbd | |||
| be044fbacf | |||
| 7a4276f6c7 | |||
| 925e18368a | |||
| 250e00e6c7 | |||
| 81672da57b | |||
| d68c22b21d | |||
| e806e4a796 | |||
| 6f86e30c7e | |||
| a87404b5a8 | |||
| 0357d373a9 | |||
| 28c62724df | |||
| 1eb90b115b | |||
| d13357bb0c | |||
| 65fe1a671c | |||
| 6429282f05 | |||
| b5fa013600 | |||
| 02bd052e45 | |||
| 4ed46ef6b3 | |||
| 65c7607e36 | |||
| bfc2c70b44 | |||
| 3a6c00dbaf | |||
| 133660ff4e | |||
| 994b2a0f28 | |||
| d4c042c523 | |||
| b48fe4ce51 | |||
| 78f356c0df | |||
| 7263d321f8 | |||
| 1ccbe87f90 | |||
| e7d2a48766 | |||
| c894843d0f | |||
| 23aa8dcd51 | |||
| f3187ddcbf | |||
| 5eb446f1ce | |||
| 5efa40215b | |||
| 1a8b793c0a | |||
| e0e0a25811 | |||
| 99b01a39d1 | |||
| dbf53ce952 | |||
| ae08a832c8 | |||
| 2bae4c98b1 | |||
| f717928ae0 | |||
| b66db8e89c | |||
| 3643ad0059 | |||
| 2bdf4c54bd | |||
| 66be4fd159 | |||
| 078a4ca8b6 | |||
| 07ab9bba69 | |||
| a964f56b07 | |||
| 46b8708ffa | |||
| 9136c13cd2 | |||
| 0df9436d06 | |||
| d32aabecd3 | |||
| f2a3dd6414 | |||
| fc23b47c4d | |||
| f6b6b5976d | |||
| f256701679 | |||
| c1df94b507 | |||
| 0c15eb4ace | |||
| 3388bc5012 | |||
| 981cf1f6ee | |||
| f19f3ed4eb | |||
| 2f97b00d31 | |||
| 911e238494 | |||
| 2d66f3c022 | |||
| 04122abd17 | |||
| 5659ca2ecd | |||
| 0181abc629 | |||
| acda2ff909 | |||
| 43c73c75c5 | |||
| 95daff182d | |||
| b82b77d03f | |||
| 193dcf578d | |||
| 4c0a6611c8 | |||
| 3e986e24fa | |||
| e5a88fdeda | |||
| 9c4aca2b90 | |||
| 67e3ce308b | |||
| a740b9458f | |||
| ffdb5fb790 | |||
| 0cc7039232 | |||
| 0f73e38f98 | |||
| cfe1187cd9 | |||
| a36e131838 | |||
| 94864a6ab3 | |||
| e1e90112ba | |||
| c303d47f26 | |||
| 5cf6f0b491 | |||
| 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 | |||
| 5ed5907809 | |||
| 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 | |||
| a78d704998 | |||
| cb85a53042 | |||
| 7f1a81eeed | |||
| 106dffe2c5 | |||
| 070a1990e8 | |||
| ddf79088fb | |||
| 3ed682a9c6 | |||
| 6e32f172be | |||
| bef5e191a4 | |||
| fc60c0df19 | |||
| 62d83b8dbd | |||
| e1eeaa24d2 | |||
| 6249392526 | |||
| 4da8c91161 | |||
| ec8ba821ed | |||
| 6b9547a9ad | |||
| ae9f730624 | |||
| 147cc4f0b6 | |||
| e2274e813a | |||
| d091931279 | |||
| 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 | |||
| 88925d6c1d | |||
| 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 | |||
| 2893f0544a | |||
| 3e77bc3ba2 | |||
| 365766c957 | |||
| 81cd241954 | |||
| 6d6111864e | |||
| 23a2b95994 | |||
| 36b84241b1 | |||
| 51879ffd2c | |||
| 0d71a74d8a | |||
| 78972604d0 | |||
| 2f95c44777 | |||
| 3d3dcc68e0 | |||
| f397d973f3 | |||
| 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 | |||
| 71737d8792 | |||
| 6659d26131 | |||
| 15dd8bec1c | |||
| 5b8800ee18 | |||
| 5bb74e448e | |||
| e4b488cb84 | |||
| 70fa175f57 | |||
| ad428aa9b0 | |||
| 1ab357dc32 | |||
| a33be8a349 | |||
| a5f5293bc8 | |||
| d61f512e20 | |||
| 77f1584713 | |||
| ffcba45b1b | |||
| 9fb0ce664c | |||
| 5734358d91 | |||
| 7d28dad209 | |||
| 588ffeedc1 | |||
| 49a7de4ebd | |||
| 0c9aa86885 | |||
| 9cb45a3810 | |||
| b593205ad9 | |||
| 5399253786 | |||
| 2babb36fc2 | |||
| ec2960a167 | |||
| 1c5d20e9a3 | |||
| a1efd6b783 | |||
| 1d77497f64 | |||
| 586692b73f | |||
| 4d3740d4ce | |||
| 15aa1fd876 | |||
| 578110488c | |||
| 08fdb3a47d | |||
| cca8c4e5b8 | |||
| f224f743da | |||
| be062c5fbe | |||
| 5c0d89feb5 | |||
| 4e6068a923 | |||
| 07c629922a | |||
| 3f506bb474 | |||
| 1c4c2272f5 | |||
| 1ed6f1875b | |||
| ca4ef22d07 | |||
| db5383927c | |||
| 2b88137612 | |||
| 9fddc1499e | |||
| c7f4dc1651 | |||
| 29d77a17e5 | |||
| e34bfa9767 | |||
| d08bad7288 | |||
| ac4e3028d2 | |||
| f1bed95153 | |||
| f3f5b63b7f | |||
| a98e8ef201 | |||
| b031470979 | |||
| 171c4f182d | |||
| 32c919cfad | |||
| bf60f38a23 | |||
| 93994756e8 | |||
| 7c55a2c6e2 | |||
| ee43792566 | |||
| f14b42f202 | |||
| 6e8c765ece | |||
| 5f70406880 | |||
| 069759c7c5 | |||
| ebd5fac91d | |||
| 156c202889 | |||
| 8e0faf4aaa | |||
| 20a2b27498 | |||
| 73d1201ed8 | |||
| fcbacae6f1 | |||
| 60a50a2ea8 | |||
| defa6f45b2 | |||
| 9cdff0b0a5 | |||
| 450fc5763f | |||
| 2495661554 | |||
| ae92557dd7 | |||
| 057f852e06 | |||
| 4874d10455 |
@@ -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"
|
||||||
|
|||||||
+18
-18
@@ -25,10 +25,10 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
python-version: ["3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --format=github .
|
ruff check --output-format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -127,10 +127,10 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest ]
|
os: [ macos-latest ]
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
python-version: ["3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -217,7 +217,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --format=github .
|
ruff check --output-format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -237,10 +237,10 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest ]
|
os: [ windows-latest ]
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
python-version: ["3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -287,7 +287,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --format=github .
|
ruff check --output-format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -304,7 +304,7 @@ jobs:
|
|||||||
mypy_version_check:
|
mypy_version_check:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -319,7 +319,7 @@ jobs:
|
|||||||
pre-commit:
|
pre-commit:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
@@ -329,7 +329,7 @@ jobs:
|
|||||||
docs_check:
|
docs_check:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Documentation syntax
|
- name: Documentation syntax
|
||||||
run: |
|
run: |
|
||||||
@@ -359,7 +359,7 @@ jobs:
|
|||||||
# Run pytest with "live" checks
|
# Run pytest with "live" checks
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -443,12 +443,12 @@ jobs:
|
|||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Extract branch name
|
- name: Extract branch name
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -461,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.8
|
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__
|
||||||
@@ -469,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.8
|
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__
|
||||||
@@ -515,7 +515,7 @@ jobs:
|
|||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Extract branch name
|
- name: Extract branch name
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Docker Hub Description
|
- name: Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@v3
|
uses: peter-evans/dockerhub-description@v3
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ instance/
|
|||||||
# Scrapy stuff:
|
# Scrapy stuff:
|
||||||
.scrapy
|
.scrapy
|
||||||
|
|
||||||
|
# memray
|
||||||
|
memray-*
|
||||||
|
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/_build/
|
docs/_build/
|
||||||
# Mkdocs documentation
|
# Mkdocs documentation
|
||||||
|
|||||||
@@ -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.3.0"
|
rev: "v1.5.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.6
|
- types-cachetools==5.3.0.6
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.31.0.2
|
- types-requests==2.31.0.10
|
||||||
- types-tabulate==0.9.0.3
|
- types-tabulate==0.9.0.3
|
||||||
- types-python-dateutil==2.8.19.14
|
- types-python-dateutil==2.8.19.14
|
||||||
- SQLAlchemy==2.0.19
|
- SQLAlchemy==2.0.22
|
||||||
# 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.270'
|
rev: 'v0.1.1'
|
||||||
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.11.4-slim-bullseye as base
|
FROM python:3.11.5-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] **Based on Python 3.8+**: For botting on any operating system - Windows, macOS and Linux.
|
- [x] **Based on Python 3.9+**: For botting on any operating system - Windows, macOS and Linux.
|
||||||
- [x] **Persistence**: Persistence is achieved through sqlite.
|
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||||
- [x] **Dry-run**: Run the bot without paying money.
|
- [x] **Dry-run**: Run the bot without paying money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
@@ -207,7 +207,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
|||||||
|
|
||||||
### Software requirements
|
### Software requirements
|
||||||
|
|
||||||
- [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/)
|
- [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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}')"
|
||||||
|
|
||||||
|
|
||||||
pip install --find-links=build_helpers\ TA-Lib
|
pip install --find-links=build_helpers\ --prefer-binary TA-Lib
|
||||||
|
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -32,11 +32,8 @@
|
|||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"ccxt_config": {"enableRateLimit": true},
|
"ccxt_config": {},
|
||||||
"ccxt_async_config": {
|
"ccxt_async_config": {},
|
||||||
"enableRateLimit": true,
|
|
||||||
"rateLimit": 500
|
|
||||||
},
|
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ETH/BTC",
|
"ETH/BTC",
|
||||||
"LTC/BTC",
|
"LTC/BTC",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
},
|
},
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"},
|
{"method": "StaticPairList"},
|
||||||
|
{"method": "FullTradesFilter"},
|
||||||
{
|
{
|
||||||
"method": "VolumePairList",
|
"method": "VolumePairList",
|
||||||
"number_assets": 20,
|
"number_assets": 20,
|
||||||
@@ -89,7 +90,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 +206,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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ WORKDIR /freqtrade
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
FROM base as python-deps
|
FROM base as python-deps
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
&& apt-get -y install build-essential libssl-dev libffi-dev libopenblas-dev libgfortran5 pkg-config cmake gcc \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& pip install --upgrade pip \
|
&& pip install --upgrade pip \
|
||||||
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
||||||
@@ -36,8 +36,9 @@ ENV LD_LIBRARY_PATH /usr/local/lib
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||||
USER ftuser
|
USER ftuser
|
||||||
RUN pip install --user --no-cache-dir numpy \
|
RUN pip install --user --no-cache-dir numpy==1.25.2 \
|
||||||
&& pip install --user /tmp/pyarrow-*.whl \
|
&& pip install --user /tmp/pyarrow-*.whl \
|
||||||
|
&& pip install --user --no-build-isolation TA-Lib==0.4.28 \
|
||||||
&& pip install --user --no-cache-dir -r requirements.txt
|
&& pip install --user --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy dependencies to runtime-image
|
# Copy dependencies to runtime-image
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
+2
-2
@@ -31,9 +31,9 @@ optional arguments:
|
|||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--data-format-ohlcv {json,jsongz,hdf5}
|
--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`).
|
||||||
--max-open-trades INT
|
--max-open-trades INT
|
||||||
Override the value of the `max_open_trades`
|
Override the value of the `max_open_trades`
|
||||||
configuration setting.
|
configuration setting.
|
||||||
|
|||||||
+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.
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||||
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
||||||
| | **TODO**
|
| | **TODO**
|
||||||
| `use_exit_signal` | Use exit signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `use_exit_signal` | Use exit signals produced by the strategy in addition to the `minimal_roi`. <br>Setting this to false disables the usage of `"exit_long"` and `"exit_short"` columns. Has no influence on other exit methods (Stoploss, ROI, callbacks). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `exit_profit_only` | Wait until the bot reaches `exit_profit_offset` before taking an exit decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `exit_profit_only` | Wait until the bot reaches `exit_profit_offset` before taking an exit decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `exit_profit_offset` | Exit-signal is only active above this value. Only active in combination with `exit_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
| `exit_profit_offset` | Exit-signal is only active above this value. Only active in combination with `exit_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `ignore_roi_if_entry_signal` | Do not exit if the entry signal is still active. This setting takes preference over `minimal_roi` and `use_exit_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `ignore_roi_if_entry_signal` | Do not exit if the entry signal is still active. This setting takes preference over `minimal_roi` and `use_exit_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
@@ -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
|
||||||
@@ -614,6 +613,7 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
|
|||||||
* Orders are simulated, and will not be posted to the exchange.
|
* Orders are simulated, and will not be posted to the exchange.
|
||||||
* Market orders fill based on orderbook volume the moment the order is placed.
|
* Market orders fill based on orderbook volume the moment the order is placed.
|
||||||
* Limit orders fill once the price reaches the defined level - or time out based on `unfilledtimeout` settings.
|
* Limit orders fill once the price reaches the defined level - or time out based on `unfilledtimeout` settings.
|
||||||
|
* Limit orders will be converted to market orders if they cross the price by more than 1%.
|
||||||
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
|
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
|
||||||
* Open orders (not trades, which are stored in the database) are kept open after bot restarts, with the assumption that they were not filled while being offline.
|
* Open orders (not trades, which are stored in the database) are kept open after bot restarts, with the assumption that they were not filled while being offline.
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ You can run this server using the following command: `docker compose -f docker/d
|
|||||||
This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`.
|
This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`.
|
||||||
Please use the link that's printed in the console after startup for simplified login.
|
Please use the link that's printed in the console after startup for simplified login.
|
||||||
|
|
||||||
For more information, Please visit the [Data analysis with Docker](docker_quickstart.md#data-analayis-using-docker-compose) section.
|
For more information, Please visit the [Data analysis with Docker](docker_quickstart.md#data-analysis-using-docker-compose) section.
|
||||||
|
|
||||||
### Pro tips
|
### Pro tips
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
+36
-36
@@ -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
|
||||||
@@ -154,13 +154,13 @@ freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT --
|
|||||||
|
|
||||||
Freqtrade currently supports the following data-formats:
|
Freqtrade currently supports the following data-formats:
|
||||||
|
|
||||||
|
* `feather` - a dataformat based on Apache Arrow
|
||||||
* `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)
|
|
||||||
* `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, both OHLCV data and trades data are stored in the `feather` format.
|
||||||
|
|
||||||
This can be changed via the `--data-format-ohlcv` and `--data-format-trades` command line arguments respectively.
|
This can be changed via the `--data-format-ohlcv` and `--data-format-trades` command line arguments respectively.
|
||||||
To persist this change, you should also add the following snippet to your configuration, so you don't have to insert the above arguments each time:
|
To persist this change, you should also add the following snippet to your configuration, so you don't have to insert the above arguments each time:
|
||||||
@@ -203,15 +203,15 @@ time freqtrade list-data --show-timerange --data-format-ohlcv <dataformat>
|
|||||||
|
|
||||||
| Format | Size | timing |
|
| Format | Size | timing |
|
||||||
|------------|-------------|-------------|
|
|------------|-------------|-------------|
|
||||||
|
| `feather` | 72Mb | 3.5s |
|
||||||
| `json` | 149Mb | 25.6s |
|
| `json` | 149Mb | 25.6s |
|
||||||
| `jsongz` | 39Mb | 27s |
|
| `jsongz` | 39Mb | 27s |
|
||||||
| `hdf5` | 145Mb | 3.9s |
|
| `hdf5` | 145Mb | 3.9s |
|
||||||
| `feather` | 72Mb | 3.5s |
|
|
||||||
| `parquet` | 83Mb | 3.8s |
|
| `parquet` | 83Mb | 3.8s |
|
||||||
|
|
||||||
Size has been taken from the BTC/USDT 1m spot combination for the timerange specified above.
|
Size has been taken from the BTC/USDT 1m spot combination for the timerange specified above.
|
||||||
|
|
||||||
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 using the default feather format, or parquet.
|
||||||
|
|
||||||
### Pairs file
|
### Pairs file
|
||||||
|
|
||||||
@@ -255,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-
|
||||||
@@ -266,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
|
||||||
@@ -291,7 +292,6 @@ 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
|
||||||
@@ -314,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-
|
||||||
@@ -325,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
|
||||||
@@ -367,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-
|
||||||
@@ -377,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
|
||||||
@@ -422,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.
|
||||||
@@ -439,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
|
||||||
@@ -474,7 +474,7 @@ ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
|||||||
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
|
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.
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -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).
|
||||||
|
|
||||||
|
|||||||
+19
-12
@@ -14,6 +14,9 @@ Start by downloading and installing Docker / Docker Desktop for your platform:
|
|||||||
Freqtrade documentation assumes the use of Docker desktop (or the docker compose plugin).
|
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
|
||||||
@@ -203,16 +206,20 @@ 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.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
taskkill /IM "Docker Desktop.exe" /F
|
taskkill /IM "Docker Desktop.exe" /F
|
||||||
wsl --shutdown
|
wsl --shutdown
|
||||||
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.
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
||||||
|
|
||||||
|
!!! Danger "Deprecated functionality"
|
||||||
|
`Edge positioning` (or short Edge) is currently in maintenance mode only (we keep existing functionality alive) and should be considered as deprecated.
|
||||||
|
It will currently not receive new features until either someone stepped forward to take up ownership of that module - or we'll decide to remove edge from freqtrade.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
||||||
|
|
||||||
|
|||||||
+35
-7
@@ -55,7 +55,7 @@ This configuration enables kraken, as well as rate-limiting to avoid bans from t
|
|||||||
## Binance
|
## Binance
|
||||||
|
|
||||||
!!! Warning "Server location and geo-ip restrictions"
|
!!! Warning "Server location and geo-ip restrictions"
|
||||||
Please be aware that binance restrict api access regarding the server country. The currents and non exhaustive countries blocked are United States, Malaysia (Singapour), Ontario (Canada). Please go to [binance terms > b. Eligibility](https://www.binance.com/en/terms) to find up to date list.
|
Please be aware that Binance restricts API access regarding the server country. The current and non-exhaustive countries blocked are Canada, Malaysia, Netherlands and United States. Please go to [binance terms > b. Eligibility](https://www.binance.com/en/terms) to find up to date list.
|
||||||
|
|
||||||
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
@@ -136,13 +136,41 @@ Freqtrade will not attempt to change these settings.
|
|||||||
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
||||||
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
||||||
|
|
||||||
Due to the heavy rate-limiting applied by Kraken, the following configuration section should be used to download data:
|
To speed up downloading, you can download the [trades zip files](https://support.kraken.com/hc/en-us/articles/360047543791-Downloadable-historical-market-data-time-and-sales-) kraken provides.
|
||||||
|
These are usually updated once per quarter. Freqtrade expects these files to be placed in `user_data/data/kraken/trades_csv`.
|
||||||
|
|
||||||
``` json
|
A structure as follows can make sense if using incremental files, with the "full" history in one directory, and incremental files in different directories.
|
||||||
"ccxt_async_config": {
|
The assumption for this mode is that the data is downloaded and unzipped keeping filenames as they are.
|
||||||
"enableRateLimit": true,
|
Duplicate content will be ignored (based on timestamp) - though the assumption is that there is no gap in the data.
|
||||||
"rateLimit": 3100
|
|
||||||
},
|
This means, if your "full" history ends in Q4 2022 - then both incremental updates Q1 2023 and Q2 2023 are available.
|
||||||
|
Not having this will lead to incomplete data, and therefore invalid results while using the data.
|
||||||
|
|
||||||
|
```
|
||||||
|
└── trades_csv
|
||||||
|
├── Kraken_full_history
|
||||||
|
│ ├── BCHEUR.csv
|
||||||
|
│ └── XBTEUR.csv
|
||||||
|
├── Kraken_Trading_History_Q1_2023
|
||||||
|
│ ├── BCHEUR.csv
|
||||||
|
│ └── XBTEUR.csv
|
||||||
|
└── Kraken_Trading_History_Q2_2023
|
||||||
|
├── BCHEUR.csv
|
||||||
|
└── XBTEUR.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
You can convert these files into freqtrade files:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade convert-trade-data --exchange kraken --format-from kraken_csv --format-to feather
|
||||||
|
# Convert trade data to different ohlcv timeframes
|
||||||
|
freqtrade trades-to-ohlcv -p BTC/EUR BCH/EUR --exchange kraken -t 1m 5m 15m 1h
|
||||||
|
```
|
||||||
|
|
||||||
|
The converted data also makes downloading data possible, and will start the download after the latest loaded trade.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade download-data --exchange kraken --dl-trades -p BTC/EUR BCH/EUR
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning "Downloading data from kraken"
|
!!! Warning "Downloading data from kraken"
|
||||||
|
|||||||
+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.
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ You can ask for each of the defined features to be included also for informative
|
|||||||
|
|
||||||
`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells FreqAI to include the past 2 candles for each of the features in the feature set.
|
`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells FreqAI to include the past 2 candles for each of the features in the feature set.
|
||||||
|
|
||||||
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 strategy 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"
|
!!! note "Learn more about creative feature engineering"
|
||||||
|
|||||||
@@ -100,12 +100,12 @@ 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.
|
||||||
|
|
||||||
@@ -237,11 +237,10 @@ class MyCoolRLModel(ReinforcementLearner):
|
|||||||
Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd freqtrade
|
|
||||||
tensorboard --logdir user_data/models/unique-id
|
tensorboard --logdir user_data/models/unique-id
|
||||||
```
|
```
|
||||||
|
|
||||||
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell to view the output in their browser at 127.0.0.1:6006 (6006 is the default port used by Tensorboard).
|
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell to view the output in the browser at 127.0.0.1:6006 (6006 is the default port used by Tensorboard).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -254,7 +253,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
-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
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
|||||||
* [`ProducerPairList`](#producerpairlist)
|
* [`ProducerPairList`](#producerpairlist)
|
||||||
* [`RemotePairList`](#remotepairlist)
|
* [`RemotePairList`](#remotepairlist)
|
||||||
* [`AgeFilter`](#agefilter)
|
* [`AgeFilter`](#agefilter)
|
||||||
|
* [`FullTradesFilter`](#fulltradesfilter)
|
||||||
* [`OffsetFilter`](#offsetfilter)
|
* [`OffsetFilter`](#offsetfilter)
|
||||||
* [`PerformanceFilter`](#performancefilter)
|
* [`PerformanceFilter`](#performancefilter)
|
||||||
* [`PrecisionFilter`](#precisionfilter)
|
* [`PrecisionFilter`](#precisionfilter)
|
||||||
@@ -236,6 +237,17 @@ be caught out buying before the pair has finished dropping in price.
|
|||||||
|
|
||||||
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days and listed before `max_days_listed`.
|
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days and listed before `max_days_listed`.
|
||||||
|
|
||||||
|
#### FullTradesFilter
|
||||||
|
|
||||||
|
Shrink whitelist to consist only in-trade pairs when the trade slots are full (when `max_open_trades` isn't being set to `-1` in the config).
|
||||||
|
|
||||||
|
When the trade slots are full, there is no need to calculate indicators of the rest of the pairs (except informative pairs) since no new trade can be opened. By shrinking the whitelist to just the in-trade pairs, you can improve calculation speeds and reduce CPU usage. When a trade slot is free (either a trade is closed or `max_open_trades` value in config is increased), then the whitelist will return to normal state.
|
||||||
|
|
||||||
|
When multiple pairlist filters are being used, it's recommended to put this filter at second position directly below the primary pairlist, so when the trade slots are full, the bot doesn't have to download data for the rest of the filters.
|
||||||
|
|
||||||
|
!!! Warning "Backtesting"
|
||||||
|
`FullTradesFilter` does not support backtesting mode.
|
||||||
|
|
||||||
#### OffsetFilter
|
#### OffsetFilter
|
||||||
|
|
||||||
Offsets an incoming pairlist by a given `offset` value.
|
Offsets an incoming pairlist by a given `offset` value.
|
||||||
@@ -376,7 +388,7 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from
|
|||||||
"lookback_days": 10,
|
"lookback_days": 10,
|
||||||
"min_rate_of_change": 0.01,
|
"min_rate_of_change": 0.01,
|
||||||
"max_rate_of_change": 0.99,
|
"max_rate_of_change": 0.99,
|
||||||
"refresh_period": 1440
|
"refresh_period": 86400
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -431,7 +443,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
|
|||||||
"method": "RangeStabilityFilter",
|
"method": "RangeStabilityFilter",
|
||||||
"lookback_days": 10,
|
"lookback_days": 10,
|
||||||
"min_rate_of_change": 0.01,
|
"min_rate_of_change": 0.01,
|
||||||
"refresh_period": 1440
|
"refresh_period": 86400
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "VolatilityFilter",
|
"method": "VolatilityFilter",
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
|||||||
|
|
||||||
Alternatively
|
Alternatively
|
||||||
|
|
||||||
- Python 3.8+
|
- Python 3.9+
|
||||||
- pip (pip3)
|
- pip (pip3)
|
||||||
- git
|
- git
|
||||||
- TA-Lib
|
- TA-Lib
|
||||||
|
|||||||
+16
-16
@@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
|||||||
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.8 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
Python3.9 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
||||||
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
||||||
|
|
||||||
!!! Warning "Up-to-date clock"
|
!!! Warning "Up-to-date clock"
|
||||||
@@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
|||||||
|
|
||||||
### Install guide
|
### Install guide
|
||||||
|
|
||||||
* [Python >= 3.8.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
* [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||||
@@ -54,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
|
|||||||
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
|
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python3.8 or higher and the corresponding pip are assumed to be available.
|
Python3.9 or higher and the corresponding pip are assumed to be available.
|
||||||
|
|
||||||
=== "Debian/Ubuntu"
|
=== "Debian/Ubuntu"
|
||||||
#### Install necessary dependencies
|
#### Install necessary dependencies
|
||||||
@@ -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
|
||||||
@@ -169,10 +169,10 @@ You can as well update, configure and reset the codebase of your bot with `./scr
|
|||||||
** --install **
|
** --install **
|
||||||
|
|
||||||
With this option, the script will install the bot and most dependencies:
|
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.9+ 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
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ It also supports the lookahead-analysis of freqai strategies.
|
|||||||
|
|
||||||
- `--cache` is forced to "none".
|
- `--cache` is forced to "none".
|
||||||
- `--max-open-trades` is forced to be at least equal to the number of pairs.
|
- `--max-open-trades` is forced to be at least equal to the number of pairs.
|
||||||
- `--dry-run-wallet` is forced to be basically infinite.
|
- `--dry-run-wallet` is forced to be basically infinite (1 billion).
|
||||||
|
- `--stake-amount` is forced to be a static 10000 (10k).
|
||||||
|
|
||||||
|
Those are set to avoid users accidentally generating false positives.
|
||||||
|
|
||||||
## Lookahead-analysis command reference
|
## Lookahead-analysis command reference
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Recursive analysis
|
||||||
|
|
||||||
|
This page explains how to validate your strategy for inaccuracies due to recursive issues with certain indicators.
|
||||||
|
|
||||||
|
A recursive formula defines any term of a sequence relative to its preceding term(s). An example of a recursive formula is a<sub>n</sub> = a<sub>n-1</sub> + b.
|
||||||
|
|
||||||
|
Why does this matter for Freqtrade? In backtesting, the bot will get full data of the pairs according to the timerange specified. But in a dry/live run, the bot will be limited by the amount of data each exchanges gives.
|
||||||
|
|
||||||
|
For example, to calculate a very basic indicator called `steps`, the first row's value is always 0, while the following rows' values are equal to the value of the previous row plus 1. If I were to calculate it using the latest 1000 candles, then the `steps` value of the first row is 0, and the `steps` value at the last closed candle is 999.
|
||||||
|
|
||||||
|
What happens if the calculation is using only the latest 500 candles? Then instead of 999, the `steps` value at last closed candle is 499. The difference of the value means your backtest result can differ from your dry/live run result.
|
||||||
|
|
||||||
|
The `recursive-analysis` command 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 preparing different lengths of data and calculates indicators based on them.
|
||||||
|
This does not backtest the strategy itself, but rather only calculates the indicators. After calculating the indicators of different startup candle values (`startup_candle_count`) are done, the values of last rows across all specified `startup_candle_count` are compared to see how much variance they show compared to the base calculation.
|
||||||
|
|
||||||
|
Command settings:
|
||||||
|
|
||||||
|
- Use the `-p` option to set your desired pair to analyze. Since we are only looking at indicator values, using more than one pair is redundant. Preferably use a pair with a relatively high price and at least moderate volatility, such as BTC or ETH, to avoid rounding issues that can make the results inaccurate. If no pair is set on the command, the pair used for this analysis is the first pair in the whitelist.
|
||||||
|
- It is recommended to set a long timerange (at least 5000 candles) so that the initial indicators' calculation that is going to be used as a benchmark has very small or no recursive issues itself. For example, for a 5m timeframe, a timerange of 5000 candles would be equal to 18 days.
|
||||||
|
- `--cache` is forced to "none" to avoid loading previous indicators calculation automatically.
|
||||||
|
|
||||||
|
In addition to the recursive formula check, this command also carries out a simple lookahead bias check on the indicator values only. For a full lookahead check, use [Lookahead-analysis](lookahead-analysis.md).
|
||||||
|
|
||||||
|
## Recursive-analysis command reference
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade recursive-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}]
|
||||||
|
[-p PAIR]
|
||||||
|
[--freqai-backtest-live-models]
|
||||||
|
[--startup-candle STARTUP_CANDLES [STARTUP_CANDLES ...]]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
|
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||||
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
|
(default: `feather`).
|
||||||
|
-p PAIR, --pairs PAIR
|
||||||
|
Limit command to this pair.
|
||||||
|
--startup-candle STARTUP_CANDLE [STARTUP_CANDLE ...]
|
||||||
|
Provide a space-separated list of startup_candle_count to
|
||||||
|
be checked. Default : `199 399 499 999 1999`.
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
Specify configuration file (default:
|
||||||
|
`userdir/config.json` or `config.json` whichever
|
||||||
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
Strategy arguments:
|
||||||
|
-s NAME, --strategy NAME
|
||||||
|
Specify strategy class name which will be used by the
|
||||||
|
bot.
|
||||||
|
--strategy-path PATH Specify additional strategy lookup path.
|
||||||
|
--timerange TIMERANGE
|
||||||
|
Specify what timerange of data to use.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why are odd-numbered default startup candles used?
|
||||||
|
|
||||||
|
The default value for startup candles are odd numbers. When the bot fetches candle data from the exchange's API, the last candle is the one being checked by the bot and the rest of the data are the "startup candles".
|
||||||
|
|
||||||
|
For example, Binance allows 1000 candles per API call. When the bot receives 1000 candles, the last candle is the "current candle", and the preceding 999 candles are the "startup candles". By setting the startup candle count as 1000 instead of 999, the bot will try to fetch 1001 candles instead. The exchange API will then send candle data in a paginated form, i.e. in case of the Binance API, this will be two groups- one of length 1000 and another of length 1. This results in the bot thinking the strategy needs 1001 candles of data, and so it will download 2000 candles worth of data instead, which means there will be 1 "current candle" and 1999 "startup candles".
|
||||||
|
|
||||||
|
Furthermore, exchanges limit the number of consecutive bulk API calls, e.g. Binance allows 5 calls. In this case, only 5000 candles can be downloaded from Binance API without hitting the API rate limit, which means the max `startup_candle_count` you can have is 4999.
|
||||||
|
|
||||||
|
Please note that this candle limit may be changed in the future by the exchanges without any prior notice.
|
||||||
|
|
||||||
|
### How does the command work?
|
||||||
|
|
||||||
|
- Firstly an initial indicator calculation is carried out using the supplied timerange to generate a benchmark for indicator values.
|
||||||
|
- After setting the benchmark it will then carry out additional runs for each of the different startup candle count values.
|
||||||
|
- The command will then compare the indicator values at the last candle rows and report the differences in a table.
|
||||||
|
|
||||||
|
## Understanding the recursive-analysis output
|
||||||
|
|
||||||
|
This is an example of an output results table where at least one indicator has a recursive formula issue:
|
||||||
|
|
||||||
|
```
|
||||||
|
| indicators | 20 | 40 | 80 | 100 | 150 | 300 | 999 |
|
||||||
|
|--------------+---------+---------+--------+--------+---------+---------+--------|
|
||||||
|
| rsi_30 | nan% | -6.025% | 0.612% | 0.828% | -0.140% | 0.000% | 0.000% |
|
||||||
|
| rsi_14 | 24.141% | -0.876% | 0.070% | 0.007% | -0.000% | -0.000% | - |
|
||||||
|
```
|
||||||
|
|
||||||
|
The column headers indicate the different `startup_candle_count` used in the analysis. The values in the table indicate the variance of the calculated indicators compared to the benchmark value.
|
||||||
|
|
||||||
|
`nan%` means the value of that indicator cannot be calculated due to lack of data. In this example, you cannot calculate RSI with length 30 with just 21 candles (1 current candle + 20 startup candles).
|
||||||
|
|
||||||
|
Users should assess the table per indicator to decide if the specified `startup_candle_count` results in a sufficiently small variance so that the indicator does not have any effect on entries and/or exits.
|
||||||
|
|
||||||
|
As such, aiming for absolute zero variance (shown by `-` value) might not be the best option, because some indicators might require you to use such a long `startup_candle_count` to have zero variance.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- `recursive-analysis` will only calculate and compare the indicator values at the last row. The output table reports the percentage differences between the different startup candle count calculations and the original benchmark calculation. Whether it has any actual impact on your entries and exits is not included.
|
||||||
|
- The ideal scenario is that indicators will have no variance (or at least very close to 0%) despite the startup candle being varied. In reality, indicators such as EMA are using a recursive formula to calculate indicator values, so the goal is not necessarily to have zero percentage variance, but to have the variance low enough (and therefore `startup_candle_count` high enough) that the recursion inherent in the indicator will not have any real impact on trading decisions.
|
||||||
|
- `recursive-analysis` will only run calculations on `populate_indicators` and `@informative` decorator(s). If you put any indicator calculation on `populate_entry_trend` or `populate_exit_trend`, it won't be calculated.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.3.7
|
markdown==3.5
|
||||||
mkdocs==1.4.3
|
mkdocs==1.5.3
|
||||||
mkdocs-material==9.1.19
|
mkdocs-material==9.4.6
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.1
|
pymdown-extensions==10.3.1
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||||||
| `performance` | Show performance of each finished trade grouped by pair.
|
| `performance` | Show performance of each finished trade grouped by pair.
|
||||||
| `balance` | Show account balance per currency.
|
| `balance` | Show account balance per currency.
|
||||||
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
|
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
|
||||||
|
| `weekly <n>` | Shows profit or loss per week, over the last n days (n defaults to 4).
|
||||||
|
| `monthly <n>` | Shows profit or loss per month, over the last n days (n defaults to 3).
|
||||||
| `stats` | Display a summary of profit / loss reasons as well as average holding times.
|
| `stats` | Display a summary of profit / loss reasons as well as average holding times.
|
||||||
| `whitelist` | Show the current whitelist.
|
| `whitelist` | Show the current whitelist.
|
||||||
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
|
|||||||
@@ -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.
|
|
||||||
+160
-31
@@ -164,6 +164,31 @@ E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoplo
|
|||||||
During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades).
|
During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades).
|
||||||
|
|
||||||
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
|
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
|
||||||
|
Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
|
||||||
|
|
||||||
|
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
|
||||||
|
|
||||||
|
!!! Note "Use of dates"
|
||||||
|
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
||||||
|
|
||||||
|
!!! Tip "Trailing stoploss"
|
||||||
|
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
||||||
|
|
||||||
|
### Adjust stoploss after position adjustments
|
||||||
|
|
||||||
|
Depending on your strategy, you may encounter the need to adjust the stoploss in both directions after a [position adjustment](#adjust-trade-position).
|
||||||
|
For this, freqtrade will make an additional call with `after_fill=True` after an order fills, which will allow the strategy to move the stoploss in any direction (also widening the gap between stoploss and current price, which is otherwise forbidden).
|
||||||
|
|
||||||
|
!!! Note "backwards compatibility"
|
||||||
|
This call will only be made if the `after_fill` parameter is part of the function definition of your `custom_stoploss` function.
|
||||||
|
As such, this will not impact (and with that, surprise) existing, running strategies.
|
||||||
|
|
||||||
|
### Custom stoploss examples
|
||||||
|
|
||||||
|
The next section will show some examples on what's possible with the custom stoploss function.
|
||||||
|
Of course, many more things are possible, and all examples can be combined at will.
|
||||||
|
|
||||||
|
#### Trailing stop via custom stoploss
|
||||||
|
|
||||||
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
||||||
|
|
||||||
@@ -179,7 +204,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
@@ -187,7 +213,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns the initial stoploss value
|
When not implemented by a strategy, returns the initial stoploss value.
|
||||||
Only called when use_custom_stoploss is set to True.
|
Only called when use_custom_stoploss is set to True.
|
||||||
|
|
||||||
:param pair: Pair that's currently analyzed
|
:param pair: Pair that's currently analyzed
|
||||||
@@ -195,25 +221,13 @@ class AwesomeStrategy(IStrategy):
|
|||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param after_fill: True if the stoploss is called after the order was filled.
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return float: New stoploss value, relative to the current rate
|
:return float: New stoploss value, relative to the current_rate
|
||||||
"""
|
"""
|
||||||
return -0.04
|
return -0.04
|
||||||
```
|
```
|
||||||
|
|
||||||
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
|
|
||||||
|
|
||||||
!!! Note "Use of dates"
|
|
||||||
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
|
||||||
|
|
||||||
!!! Tip "Trailing stoploss"
|
|
||||||
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
|
||||||
|
|
||||||
### Custom stoploss examples
|
|
||||||
|
|
||||||
The next section will show some examples on what's possible with the custom stoploss function.
|
|
||||||
Of course, many more things are possible, and all examples can be combined at will.
|
|
||||||
|
|
||||||
#### Time based trailing stop
|
#### Time based trailing stop
|
||||||
|
|
||||||
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
||||||
@@ -229,14 +243,45 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
|
||||||
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||||
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||||
return -0.05
|
return -0.05
|
||||||
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
||||||
return -0.10
|
return -0.10
|
||||||
return 1
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Time based trailing stop with after-fill adjustments
|
||||||
|
|
||||||
|
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
||||||
|
If an additional order fills, set stoploss to -10% below the new `open_rate` ([Averaged across all entries](#position-adjust-calculations)).
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
|
||||||
|
if after_fill:
|
||||||
|
# After an additional order, start with a stoploss of 10% below the new open rate
|
||||||
|
return stoploss_from_open(0.10, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
|
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||||
|
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||||
|
return -0.05
|
||||||
|
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
||||||
|
return -0.10
|
||||||
|
return None
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Different stoploss per pair
|
#### Different stoploss per pair
|
||||||
@@ -255,7 +300,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
|
||||||
if pair in ('ETH/BTC', 'XRP/BTC'):
|
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||||
return -0.10
|
return -0.10
|
||||||
@@ -281,7 +327,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
|
||||||
if current_profit < 0.04:
|
if current_profit < 0.04:
|
||||||
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
||||||
@@ -314,7 +361,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
|
||||||
# evaluate highest to lowest, so that highest possible stop is used
|
# evaluate highest to lowest, so that highest possible stop is used
|
||||||
if current_profit > 0.40:
|
if current_profit > 0.40:
|
||||||
@@ -325,7 +373,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
return 1
|
return None
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Custom stoploss using an indicator from dataframe example
|
#### Custom stoploss using an indicator from dataframe example
|
||||||
@@ -342,7 +390,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
last_candle = dataframe.iloc[-1].squeeze()
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
@@ -355,7 +404,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)
|
return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
return 1
|
return None
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||||
@@ -364,15 +413,89 @@ See [Dataframe access](strategy-advanced.md#dataframe-access) for more informati
|
|||||||
|
|
||||||
#### Stoploss relative to open price
|
#### Stoploss relative to open price
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
Stoploss values returned from `custom_stoploss()` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the _entry_ price instead.
|
||||||
|
`stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point.
|
||||||
|
|
||||||
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
|
||||||
|
|
||||||
|
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
|
||||||
|
|
||||||
|
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
|
||||||
|
|
||||||
|
This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%).
|
||||||
|
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, stoploss_from_open
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
|
||||||
|
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||||
|
if current_profit > 0.10:
|
||||||
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
|
||||||
|
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
|
||||||
|
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `exit_reason` in
|
||||||
|
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
|
||||||
|
`current_profit < open_relative_stop`.
|
||||||
|
|
||||||
#### Stoploss percentage from absolute price
|
#### Stoploss percentage from absolute price
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||||
|
|
||||||
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
The helper function `stoploss_from_absolute()` can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
|
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
||||||
|
|
||||||
|
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
|
||||||
|
For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, stoploss_from_absolute, timeframe_to_prev_date
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
|
||||||
|
candle = dataframe.iloc[-1].squeeze()
|
||||||
|
sign = 1 if trade.is_short else -1
|
||||||
|
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
|
||||||
|
current_rate, is_short=trade.is_short,
|
||||||
|
leverage=trade.leverage)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -387,6 +510,9 @@ Each of these methods are called right before placing an order on the exchange.
|
|||||||
!!! Note
|
!!! Note
|
||||||
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Using custom_entry_price, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
|
||||||
|
|
||||||
### Custom order entry and exit price example
|
### Custom order entry and exit price example
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
@@ -397,7 +523,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
def custom_entry_price(self, pair: str, trade: Optional['Trade'], current_time: datetime, proposed_rate: float,
|
||||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
|
||||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
@@ -700,7 +826,7 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
"""
|
"""
|
||||||
Custom trade adjustment logic, returning the stake amount that a trade should be
|
Custom trade adjustment logic, returning the stake amount that a trade should be
|
||||||
increased or decreased.
|
increased or decreased.
|
||||||
This means extra buy or sell orders with additional fees.
|
This means extra entry or exit orders with additional fees.
|
||||||
Only called when `position_adjustment_enable` is set to True.
|
Only called when `position_adjustment_enable` is set to True.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
@@ -709,8 +835,9 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
|
|
||||||
:param trade: trade object.
|
:param trade: trade object.
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param current_rate: Current buy rate.
|
:param current_rate: Current entry rate (same as current_entry_profit)
|
||||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
:param current_profit: Current profit (as ratio), calculated based on current_rate
|
||||||
|
(same as current_entry_profit).
|
||||||
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
|
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
|
||||||
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
|
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
|
||||||
:param current_entry_rate: Current rate using entry pricing.
|
:param current_entry_rate: Current rate using entry pricing.
|
||||||
@@ -793,6 +920,8 @@ Returning any other price will cancel the existing order, and replace it with a
|
|||||||
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
|
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
|
||||||
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
|
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
|
||||||
|
|
||||||
|
If the cancellation of the original order fails, then the order will not be replaced - though the order will most likely have been canceled on exchange. Having this happen on initial entries will result in the deletion of the order, while on position adjustment orders, it'll result in the trade size remaining as is.
|
||||||
|
|
||||||
!!! Warning "Regular timeout"
|
!!! Warning "Regular timeout"
|
||||||
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`) takes precedence over this.
|
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`) takes precedence over this.
|
||||||
Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
||||||
|
|||||||
+72
-145
@@ -168,10 +168,12 @@ Most indicators have an instable startup period, in which they are either not av
|
|||||||
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
||||||
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators. In the case where a user includes higher timeframes with informative pairs, the `startup_candle_count` does not necessarily change. The value is the maximum period (in candles) that any of the informatives timeframes need to compute stable indicators.
|
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators. In the case where a user includes higher timeframes with informative pairs, the `startup_candle_count` does not necessarily change. The value is the maximum period (in candles) that any of the informatives timeframes need to compute stable indicators.
|
||||||
|
|
||||||
In this example strategy, this should be set to 100 (`startup_candle_count = 100`), since the longest needed history is 100 candles.
|
You can use [recursive-analysis](recursive-analysis.md) to check and find the correct `startup_candle_count` to be used.
|
||||||
|
|
||||||
|
In this example strategy, this should be set to 400 (`startup_candle_count = 400`), since the minimum needed history for ema100 calculation to make sure the value is correct is 400 candles.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=400)
|
||||||
```
|
```
|
||||||
|
|
||||||
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
||||||
@@ -193,11 +195,11 @@ Let's try to backtest 1 month (January 2019) of 5m candles using an example stra
|
|||||||
freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m
|
freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m
|
||||||
```
|
```
|
||||||
|
|
||||||
Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2018-12-31 15:30:00.
|
Assuming `startup_candle_count` is set to 400, backtesting knows it needs 400 candles to generate valid buy signals. It will load data from `20190101 - (400 * 5m)` - which is ~2018-12-30 11:40:00.
|
||||||
If this data is available, indicators will be calculated with this extended timerange. The instable startup period (up to 2019-01-01 00:00:00) will then be removed before starting backtesting.
|
If this data is available, indicators will be calculated with this extended timerange. The instable startup period (up to 2019-01-01 00:00:00) will then be removed before starting backtesting.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00.
|
If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-02 09:20:00.
|
||||||
|
|
||||||
### Entry signal rules
|
### Entry signal rules
|
||||||
|
|
||||||
@@ -264,7 +266,7 @@ def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFram
|
|||||||
### Exit signal rules
|
### Exit signal rules
|
||||||
|
|
||||||
Edit the method `populate_exit_trend()` into your strategy file to update your exit strategy.
|
Edit the method `populate_exit_trend()` into your strategy file to update your exit strategy.
|
||||||
The exit-signal is only used for exits if `use_exit_signal` is set to true in the configuration.
|
The exit-signal can be suppressed by setting `use_exit_signal` to false in the configuration or strategy.
|
||||||
`use_exit_signal` will not influence [signal collision rules](#colliding-signals) - which will still apply and can prevent entries.
|
`use_exit_signal` will not influence [signal collision rules](#colliding-signals) - which will still apply and can prevent entries.
|
||||||
|
|
||||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||||
@@ -586,6 +588,67 @@ for more information.
|
|||||||
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
||||||
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||||
|
|
||||||
|
### *merge_informative_pair()*
|
||||||
|
|
||||||
|
This method helps you merge an informative pair to a regular dataframe without lookahead bias.
|
||||||
|
It's there to help you merge the dataframe in a safe and consistent way.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
- Rename the columns for you to create unique columns
|
||||||
|
- Merge the dataframe without lookahead bias
|
||||||
|
- Forward-fill (optional)
|
||||||
|
|
||||||
|
For a full sample, please refer to the [complete data provider example](#complete-data-provider-sample) below.
|
||||||
|
|
||||||
|
All columns of the informative dataframe will be available on the returning dataframe in a renamed fashion:
|
||||||
|
|
||||||
|
!!! Example "Column renaming"
|
||||||
|
Assuming `inf_tf = '1d'` the resulting columns will be:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
|
||||||
|
'date_1d', 'open_1d', 'high_1d', 'low_1d', 'close_1d', 'rsi_1d' # from the informative dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
??? Example "Column renaming - 1h"
|
||||||
|
Assuming `inf_tf = '1h'` the resulting columns will be:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
|
||||||
|
'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
??? Example "Custom implementation"
|
||||||
|
A custom implementation for this is possible, and can be done as follows:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
# Shift date by 1 candle
|
||||||
|
# This is necessary since the data is always the "open date"
|
||||||
|
# and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00
|
||||||
|
minutes = timeframe_to_minutes(inf_tf)
|
||||||
|
# Only do this if the timeframes are different:
|
||||||
|
informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm')
|
||||||
|
|
||||||
|
# Rename columns to be unique
|
||||||
|
informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
|
||||||
|
# Assuming inf_tf = '1d' - then the columns will now be:
|
||||||
|
# date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d
|
||||||
|
|
||||||
|
# Combine the 2 dataframes
|
||||||
|
# all indicators on the informative sample MUST be calculated before this point
|
||||||
|
dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left')
|
||||||
|
# FFill to have the 1d value available in every row throughout the day.
|
||||||
|
# Without this, comparisons would only work once per day.
|
||||||
|
dataframe = dataframe.ffill()
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning "Informative timeframe < timeframe"
|
||||||
|
Using informative timeframes smaller than the dataframe timeframe is not recommended with this method, as it will not use any of the additional information this would provide.
|
||||||
|
To use the more detailed information properly, more advanced methods should be applied (which are out of scope for freqtrade documentation, as it'll depend on the respective need).
|
||||||
|
|
||||||
## Additional data (DataProvider)
|
## Additional data (DataProvider)
|
||||||
|
|
||||||
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
||||||
@@ -810,146 +873,6 @@ class SampleStrategy(IStrategy):
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## Helper functions
|
|
||||||
|
|
||||||
### *merge_informative_pair()*
|
|
||||||
|
|
||||||
This method helps you merge an informative pair to a regular dataframe without lookahead bias.
|
|
||||||
It's there to help you merge the dataframe in a safe and consistent way.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
- Rename the columns for you to create unique columns
|
|
||||||
- Merge the dataframe without lookahead bias
|
|
||||||
- Forward-fill (optional)
|
|
||||||
|
|
||||||
For a full sample, please refer to the [complete data provider example](#complete-data-provider-sample) below.
|
|
||||||
|
|
||||||
All columns of the informative dataframe will be available on the returning dataframe in a renamed fashion:
|
|
||||||
|
|
||||||
!!! Example "Column renaming"
|
|
||||||
Assuming `inf_tf = '1d'` the resulting columns will be:
|
|
||||||
|
|
||||||
``` python
|
|
||||||
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
|
|
||||||
'date_1d', 'open_1d', 'high_1d', 'low_1d', 'close_1d', 'rsi_1d' # from the informative dataframe
|
|
||||||
```
|
|
||||||
|
|
||||||
??? Example "Column renaming - 1h"
|
|
||||||
Assuming `inf_tf = '1h'` the resulting columns will be:
|
|
||||||
|
|
||||||
``` python
|
|
||||||
'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe
|
|
||||||
'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe
|
|
||||||
```
|
|
||||||
|
|
||||||
??? Example "Custom implementation"
|
|
||||||
A custom implementation for this is possible, and can be done as follows:
|
|
||||||
|
|
||||||
``` python
|
|
||||||
|
|
||||||
# Shift date by 1 candle
|
|
||||||
# This is necessary since the data is always the "open date"
|
|
||||||
# and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00
|
|
||||||
minutes = timeframe_to_minutes(inf_tf)
|
|
||||||
# Only do this if the timeframes are different:
|
|
||||||
informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm')
|
|
||||||
|
|
||||||
# Rename columns to be unique
|
|
||||||
informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
|
|
||||||
# Assuming inf_tf = '1d' - then the columns will now be:
|
|
||||||
# date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d
|
|
||||||
|
|
||||||
# Combine the 2 dataframes
|
|
||||||
# all indicators on the informative sample MUST be calculated before this point
|
|
||||||
dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left')
|
|
||||||
# FFill to have the 1d value available in every row throughout the day.
|
|
||||||
# Without this, comparisons would only work once per day.
|
|
||||||
dataframe = dataframe.ffill()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Warning "Informative timeframe < timeframe"
|
|
||||||
Using informative timeframes smaller than the dataframe timeframe is not recommended with this method, as it will not use any of the additional information this would provide.
|
|
||||||
To use the more detailed information properly, more advanced methods should be applied (which are out of scope for freqtrade documentation, as it'll depend on the respective need).
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### *stoploss_from_open()*
|
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the entry point instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point.
|
|
||||||
|
|
||||||
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
|
|
||||||
|
|
||||||
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
|
|
||||||
|
|
||||||
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
|
|
||||||
|
|
||||||
This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%).
|
|
||||||
|
|
||||||
|
|
||||||
``` python
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.strategy import IStrategy, stoploss_from_open
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
# ... populate_* methods
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
|
|
||||||
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
|
||||||
if current_profit > 0.10:
|
|
||||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
|
||||||
|
|
||||||
return 1
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
|
|
||||||
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
|
|
||||||
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `exit_reason` in
|
|
||||||
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
|
|
||||||
`current_profit < open_relative_stop`.
|
|
||||||
|
|
||||||
### *stoploss_from_absolute()*
|
|
||||||
|
|
||||||
In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price.
|
|
||||||
|
|
||||||
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
|
||||||
|
|
||||||
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)`.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.strategy import IStrategy, stoploss_from_absolute
|
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
use_custom_stoploss = True
|
|
||||||
|
|
||||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
|
||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
|
||||||
candle = dataframe.iloc[-1].squeeze()
|
|
||||||
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional data (Wallets)
|
## Additional data (Wallets)
|
||||||
|
|
||||||
The strategy provides access to the `wallets` object. This contains the current balances on the exchange.
|
The strategy provides access to the `wallets` object. This contains the current balances on the exchange.
|
||||||
@@ -1085,6 +1008,10 @@ The following lists some common patterns which should be avoided to prevent frus
|
|||||||
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
||||||
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||||
|
|
||||||
|
!!! Tip "Identifying problems"
|
||||||
|
You may also want to check the 2 helper commands [lookahead-analysis](lookahead-analysis.md) and [recursive-analysis](recursive-analysis.md), which can each help you figure out problems with your strategy in different ways.
|
||||||
|
Please treat them as what they are - helpers to identify most common problems. A negative result of each does not guarantee that there's none of the above errors included.
|
||||||
|
|
||||||
### Colliding signals
|
### Colliding signals
|
||||||
|
|
||||||
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
|
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ trades.groupby("pair")["exit_reason"].value_counts()
|
|||||||
# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)
|
# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)
|
||||||
|
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
|
from freqtrade.data.btanalysis import load_backtest_stats
|
||||||
import plotly.express as px
|
import plotly.express as px
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@@ -178,20 +178,8 @@ import pandas as pd
|
|||||||
stats = load_backtest_stats(backtest_dir)
|
stats = load_backtest_stats(backtest_dir)
|
||||||
strategy_stats = stats['strategy'][strategy]
|
strategy_stats = stats['strategy'][strategy]
|
||||||
|
|
||||||
dates = []
|
df = pd.DataFrame(columns=['dates','equity'], data=strategy_stats['daily_profit'])
|
||||||
profits = []
|
df['equity_daily'] = df['equity'].cumsum()
|
||||||
for date_profit in strategy_stats['daily_profit']:
|
|
||||||
dates.append(date_profit[0])
|
|
||||||
profits.append(date_profit[1])
|
|
||||||
|
|
||||||
equity = 0
|
|
||||||
equity_daily = []
|
|
||||||
for daily_profit in profits:
|
|
||||||
equity_daily.append(equity)
|
|
||||||
equity += float(daily_profit)
|
|
||||||
|
|
||||||
|
|
||||||
df = pd.DataFrame({'dates': dates,'equity_daily': equity_daily})
|
|
||||||
|
|
||||||
fig = px.line(df, x="dates", y="equity_daily")
|
fig = px.line(df, x="dates", y="equity_daily")
|
||||||
fig.show()
|
fig.show()
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ After:
|
|||||||
|
|
||||||
``` python hl_lines="3"
|
``` python hl_lines="3"
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
def custom_entry_price(self, pair: str, trade: Optional[Trade], current_time: datetime, proposed_rate: float,
|
||||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
return proposed_rate
|
return proposed_rate
|
||||||
```
|
```
|
||||||
@@ -311,12 +311,13 @@ After:
|
|||||||
|
|
||||||
``` python hl_lines="5 7"
|
``` python hl_lines="5 7"
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, after_fill: bool,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||||
if current_profit > 0.10:
|
if current_profit > 0.10:
|
||||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
||||||
|
|
||||||
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
|
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+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.
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ git clone https://github.com/freqtrade/freqtrade.git
|
|||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows.
|
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10 and 3.11) and for 64bit Windows.
|
||||||
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
||||||
|
|
||||||
Other versions must be downloaded from the above link.
|
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.7'
|
__version__ = '2023.10'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
__main__.py for Freqtrade
|
__main__.py for Freqtrade
|
||||||
To launch Freqtrade as a module
|
To launch Freqtrade as a module
|
||||||
|
|
||||||
> python -m freqtrade (with Python >= 3.8)
|
> python -m freqtrade (with Python >= 3.9)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from freqtrade import main
|
from freqtrade import main
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ from freqtrade.commands.list_commands import (start_list_exchanges, start_list_f
|
|||||||
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)
|
start_lookahead_analysis,
|
||||||
|
start_recursive_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
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ ARGS_BUILD_CONFIG = ["config"]
|
|||||||
|
|
||||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||||
|
|
||||||
|
ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", "exchange"]
|
||||||
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", "candle_types"]
|
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
||||||
|
|
||||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||||
@@ -122,6 +122,8 @@ ARGS_LOOKAHEAD_ANALYSIS = [
|
|||||||
a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", 'cache')
|
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"]
|
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
||||||
|
|
||||||
|
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
|
||||||
|
|
||||||
|
|
||||||
class Arguments:
|
class Arguments:
|
||||||
"""
|
"""
|
||||||
@@ -206,8 +208,9 @@ class Arguments:
|
|||||||
start_list_strategies, start_list_timeframes,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_lookahead_analysis, start_new_config,
|
start_lookahead_analysis, start_new_config,
|
||||||
start_new_strategy, start_plot_dataframe, start_plot_profit,
|
start_new_strategy, start_plot_dataframe, start_plot_profit,
|
||||||
start_show_trades, start_strategy_update,
|
start_recursive_analysis, start_show_trades,
|
||||||
start_test_pairlist, start_trading, start_webserver)
|
start_strategy_update, start_test_pairlist, start_trading,
|
||||||
|
start_webserver)
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='command',
|
subparsers = self.parser.add_subparsers(dest='command',
|
||||||
# Use custom message when no subhandler is added
|
# Use custom message when no subhandler is added
|
||||||
@@ -265,7 +268,7 @@ class Arguments:
|
|||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
||||||
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
self._build_args(optionlist=ARGS_CONVERT_DATA_TRADES, parser=convert_trade_data_cmd)
|
||||||
|
|
||||||
# Add trades-to-ohlcv subcommand
|
# Add trades-to-ohlcv subcommand
|
||||||
convert_trade_data_cmd = subparsers.add_parser(
|
convert_trade_data_cmd = subparsers.add_parser(
|
||||||
@@ -467,3 +470,14 @@ class Arguments:
|
|||||||
|
|
||||||
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS,
|
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS,
|
||||||
parser=lookahead_analayis_cmd)
|
parser=lookahead_analayis_cmd)
|
||||||
|
|
||||||
|
# Add recursive_analysis subcommand
|
||||||
|
recursive_analayis_cmd = subparsers.add_parser(
|
||||||
|
'recursive-analysis',
|
||||||
|
help="Check for potential recursive formula issue.",
|
||||||
|
parents=[_common_parser, _strategy_parser])
|
||||||
|
|
||||||
|
recursive_analayis_cmd.set_defaults(func=start_recursive_analysis)
|
||||||
|
|
||||||
|
self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS,
|
||||||
|
parser=recursive_analayis_cmd)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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__)
|
||||||
@@ -105,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",
|
||||||
|
|||||||
@@ -421,6 +421,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
'desired timeframe as specified as --timeframes/-t.',
|
'desired timeframe as specified as --timeframes/-t.',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
),
|
),
|
||||||
|
"format_from_trades": Arg(
|
||||||
|
'--format-from',
|
||||||
|
help='Source format for data conversion.',
|
||||||
|
choices=constants.AVAILABLE_DATAHANDLERS + ['kraken_csv'],
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
"format_from": Arg(
|
"format_from": Arg(
|
||||||
'--format-from',
|
'--format-from',
|
||||||
help='Source format for data conversion.',
|
help='Source format for data conversion.',
|
||||||
@@ -435,13 +441,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',
|
||||||
@@ -705,4 +711,9 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help="Use this csv-filename to store lookahead-analysis-results",
|
help="Use this csv-filename to store lookahead-analysis-results",
|
||||||
type=str
|
type=str
|
||||||
),
|
),
|
||||||
|
"startup_candle": Arg(
|
||||||
|
'--startup-candle',
|
||||||
|
help='Specify startup candles to be checked (`199`, `499`, `999`, `1999`).',
|
||||||
|
nargs='+',
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, 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, download_data_main
|
convert_trades_to_ohlcv)
|
||||||
|
from freqtrade.data.history import download_data_main
|
||||||
from freqtrade.enums import RunMode, TradingMode
|
from freqtrade.enums import RunMode, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
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
|
||||||
|
|
||||||
@@ -53,28 +53,19 @@ def start_convert_trades(args: Dict[str, Any]) -> None:
|
|||||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
config['stake_currency'] = ''
|
config['stake_currency'] = ''
|
||||||
|
|
||||||
if 'pairs' not in config:
|
|
||||||
raise OperationalException(
|
|
||||||
"Downloading data requires a list of pairs. "
|
|
||||||
"Please check the documentation on how to configure this.")
|
|
||||||
if 'timeframes' not in config:
|
if 'timeframes' not in config:
|
||||||
config['timeframes'] = DL_DATA_TIMEFRAMES
|
config['timeframes'] = DL_DATA_TIMEFRAMES
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
# Manual validations of relevant settings
|
# Manual validations of relevant settings
|
||||||
if not config['exchange'].get('skip_pair_validation', False):
|
|
||||||
exchange.validate_pairs(config['pairs'])
|
|
||||||
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
|
||||||
|
|
||||||
logger.info(f"About to Convert pairs: {expanded_pairs}, "
|
|
||||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
|
||||||
|
|
||||||
for timeframe in config['timeframes']:
|
for timeframe in config['timeframes']:
|
||||||
exchange.validate_timeframes(timeframe)
|
exchange.validate_timeframes(timeframe)
|
||||||
|
|
||||||
# Convert downloaded trade data to different timeframes
|
# Convert downloaded trade data to different timeframes
|
||||||
convert_trades_to_ohlcv(
|
convert_trades_to_ohlcv(
|
||||||
pairs=expanded_pairs, timeframes=config['timeframes'],
|
pairs=config.get('pairs', []), timeframes=config['timeframes'],
|
||||||
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
||||||
data_format_ohlcv=config['dataformat_ohlcv'],
|
data_format_ohlcv=config['dataformat_ohlcv'],
|
||||||
data_format_trades=config['dataformat_trades'],
|
data_format_trades=config['dataformat_trades'],
|
||||||
@@ -94,7 +85,7 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||||||
erase=args['erase'])
|
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_trades'], convert_to=args['format_to'],
|
||||||
erase=args['erase'])
|
erase=args['erase'])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -140,7 +140,19 @@ def start_lookahead_analysis(args: Dict[str, Any]) -> None:
|
|||||||
:param args: Cli args from Arguments()
|
:param args: Cli args from Arguments()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
from freqtrade.optimize.lookahead_analysis_helpers import LookaheadAnalysisSubFunctions
|
from freqtrade.optimize.analysis.lookahead_helpers import LookaheadAnalysisSubFunctions
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
LookaheadAnalysisSubFunctions.start(config)
|
LookaheadAnalysisSubFunctions.start(config)
|
||||||
|
|
||||||
|
|
||||||
|
def start_recursive_analysis(args: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Start the backtest recursive tester script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
from freqtrade.optimize.analysis.recursive_helpers import RecursiveAnalysisSubFunctions
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
RecursiveAnalysisSubFunctions.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:
|
||||||
|
|||||||
@@ -490,6 +490,9 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='lookahead_analysis_exportfilename',
|
self._args_to_config(config, argname='lookahead_analysis_exportfilename',
|
||||||
logstring='Path to store lookahead-analysis-results: {}')
|
logstring='Path to store lookahead-analysis-results: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='startup_candle',
|
||||||
|
logstring='Startup candle to be used on recursive analysis: {}')
|
||||||
|
|
||||||
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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+29
-9
@@ -33,13 +33,12 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
|||||||
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
|
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
|
||||||
'ProfitDrawDownHyperOptLoss']
|
'ProfitDrawDownHyperOptLoss']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'RemotePairList',
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'RemotePairList',
|
||||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
'AgeFilter', "FullTradesFilter", 'OffsetFilter', 'PerformanceFilter',
|
||||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||||
'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', '']
|
||||||
|
|
||||||
@@ -69,7 +77,8 @@ DL_DATA_TIMEFRAMES = ['1m', '5m']
|
|||||||
|
|
||||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||||
|
|
||||||
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
CANCELED_EXCHANGE_STATES = ('cancelled', 'canceled', 'expired')
|
||||||
|
NON_OPEN_EXCHANGE_STATES = CANCELED_EXCHANGE_STATES + ('closed',)
|
||||||
|
|
||||||
# Define decimals per coin for outputs
|
# Define decimals per coin for outputs
|
||||||
# Only used for outputs.
|
# Only used for outputs.
|
||||||
@@ -153,7 +162,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'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},
|
||||||
@@ -169,6 +178,11 @@ CONF_SCHEMA = {
|
|||||||
'minimum_trade_amount': {'type': 'number', 'default': 10},
|
'minimum_trade_amount': {'type': 'number', 'default': 10},
|
||||||
'targeted_trade_amount': {'type': 'number', 'default': 20},
|
'targeted_trade_amount': {'type': 'number', 'default': 20},
|
||||||
'lookahead_analysis_exportfilename': {'type': 'string'},
|
'lookahead_analysis_exportfilename': {'type': 'string'},
|
||||||
|
'startup_candle': {
|
||||||
|
'type': 'array',
|
||||||
|
'uniqueItems': True,
|
||||||
|
'default': [199, 399, 499, 999, 1999],
|
||||||
|
},
|
||||||
'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',
|
||||||
@@ -446,12 +460,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},
|
||||||
@@ -461,7 +475,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': ''},
|
||||||
@@ -668,6 +681,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",
|
||||||
@@ -678,6 +694,7 @@ CANCEL_REASON = {
|
|||||||
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
|
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
|
||||||
"FORCE_EXIT": "forcesold",
|
"FORCE_EXIT": "forcesold",
|
||||||
"REPLACE": "cancelled to be replaced by new limit order",
|
"REPLACE": "cancelled to be replaced by new limit order",
|
||||||
|
"REPLACE_FAILED": "failed to replace order, deleting Trade",
|
||||||
"USER_CANCEL": "user requested order cancel"
|
"USER_CANCEL": "user requested order cancel"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,3 +716,6 @@ Config = Dict[str, Any]
|
|||||||
# Exchange part of the configuration.
|
# Exchange part of the configuration.
|
||||||
ExchangeConfig = Dict[str, Any]
|
ExchangeConfig = Dict[str, Any]
|
||||||
IntOrInf = float
|
IntOrInf = float
|
||||||
|
|
||||||
|
|
||||||
|
EntryExecuteMode = Literal['initial', 'pos_adjust', 'replace']
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -174,24 +175,37 @@ def _get_backtest_files(dirname: Path) -> List[Path]:
|
|||||||
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:
|
'strategy': s,
|
||||||
continue
|
'run_id': v['run_id'],
|
||||||
for s, v in metadata.items():
|
'notes': v.get('notes', ''),
|
||||||
results.append({
|
'backtest_start_time': v['backtest_start_time'],
|
||||||
'filename': filename.stem,
|
}
|
||||||
'strategy': s,
|
for filename in _get_backtest_files(dirname)
|
||||||
'run_id': v['run_id'],
|
for s, v in load_backtest_metadata(filename).items()
|
||||||
'backtest_start_time': v['backtest_start_time'],
|
if v
|
||||||
|
]
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def delete_backtest_result(file_abs: Path):
|
def delete_backtest_result(file_abs: Path):
|
||||||
@@ -205,6 +219,21 @@ def delete_backtest_result(file_abs: Path):
|
|||||||
file_abs_meta.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],
|
||||||
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
|
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from freqtrade.data.converter.converter import (clean_ohlcv_dataframe, convert_ohlcv_format,
|
||||||
|
ohlcv_fill_up_missing_data, ohlcv_to_dataframe,
|
||||||
|
order_book_to_dataframe, reduce_dataframe_footprint,
|
||||||
|
trim_dataframe, trim_dataframes)
|
||||||
|
from freqtrade.data.converter.trade_converter import (convert_trades_format,
|
||||||
|
convert_trades_to_ohlcv, trades_convert_types,
|
||||||
|
trades_df_remove_duplicates,
|
||||||
|
trades_dict_to_list, trades_list_to_df,
|
||||||
|
trades_to_ohlcv)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'clean_ohlcv_dataframe',
|
||||||
|
'convert_ohlcv_format',
|
||||||
|
'ohlcv_fill_up_missing_data',
|
||||||
|
'ohlcv_to_dataframe',
|
||||||
|
'order_book_to_dataframe',
|
||||||
|
'reduce_dataframe_footprint',
|
||||||
|
'trim_dataframe',
|
||||||
|
'trim_dataframes',
|
||||||
|
'convert_trades_format',
|
||||||
|
'convert_trades_to_ohlcv',
|
||||||
|
'trades_convert_types',
|
||||||
|
'trades_df_remove_duplicates',
|
||||||
|
'trades_dict_to_list',
|
||||||
|
'trades_list_to_df',
|
||||||
|
'trades_to_ohlcv',
|
||||||
|
]
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
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
|
||||||
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, Config
|
||||||
from freqtrade.enums import CandleType, TradingMode
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
|
|
||||||
|
|
||||||
@@ -106,7 +104,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
|||||||
df = dataframe.resample(resample_interval, 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'].ffill()
|
||||||
# Use close for "open, high, low"
|
# Use close for "open, high, low"
|
||||||
df.loc[:, ['open', 'high', 'low']] = df[['open', 'high', 'low']].fillna(
|
df.loc[:, ['open', 'high', 'low']] = df[['open', 'high', 'low']].fillna(
|
||||||
value={'open': df['close'],
|
value={'open': df['close'],
|
||||||
@@ -195,76 +193,6 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
|||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
def trades_remove_duplicates(trades: List[List]) -> List[List]:
|
|
||||||
"""
|
|
||||||
Removes duplicates from the trades list.
|
|
||||||
Uses itertools.groupby to avoid converting to pandas.
|
|
||||||
Tests show it as being pretty efficient on lists of 4M Lists.
|
|
||||||
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
|
|
||||||
:return: same format as above, but with duplicates removed
|
|
||||||
"""
|
|
||||||
return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))]
|
|
||||||
|
|
||||||
|
|
||||||
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
|
||||||
"""
|
|
||||||
Convert fetch_trades result into a List (to be more memory efficient).
|
|
||||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
|
||||||
:return: List of Lists, with constants.DEFAULT_TRADES_COLUMNS as columns
|
|
||||||
"""
|
|
||||||
return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades]
|
|
||||||
|
|
||||||
|
|
||||||
def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Converts trades list to OHLCV list
|
|
||||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
|
||||||
:param timeframe: Timeframe to resample data to
|
|
||||||
:return: OHLCV Dataframe.
|
|
||||||
:raises: ValueError if no trades are provided
|
|
||||||
"""
|
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
|
||||||
if not trades:
|
|
||||||
raise ValueError('Trade-list empty.')
|
|
||||||
df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
|
|
||||||
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms',
|
|
||||||
utc=True,)
|
|
||||||
df = df.set_index('timestamp')
|
|
||||||
|
|
||||||
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
|
||||||
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
|
||||||
df_new['date'] = df_new.index
|
|
||||||
# Drop 0 volume rows
|
|
||||||
df_new = df_new.dropna()
|
|
||||||
return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS]
|
|
||||||
|
|
||||||
|
|
||||||
def convert_trades_format(config: Config, convert_from: str, convert_to: str, erase: bool):
|
|
||||||
"""
|
|
||||||
Convert trades from one format to another format.
|
|
||||||
:param config: Config dictionary
|
|
||||||
:param convert_from: Source format
|
|
||||||
:param convert_to: Target format
|
|
||||||
:param erase: Erase source data (does not apply if source and target format are identical)
|
|
||||||
"""
|
|
||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
|
||||||
trg = get_datahandler(config['datadir'], convert_to)
|
|
||||||
|
|
||||||
if 'pairs' not in config:
|
|
||||||
config['pairs'] = src.trades_get_pairs(config['datadir'])
|
|
||||||
logger.info(f"Converting trades for {config['pairs']}")
|
|
||||||
|
|
||||||
for pair in config['pairs']:
|
|
||||||
data = src.trades_load(pair=pair)
|
|
||||||
logger.info(f"Converting {len(data)} trades for {pair}")
|
|
||||||
trg.trades_store(pair, data)
|
|
||||||
if erase and convert_from != convert_to:
|
|
||||||
logger.info(f"Deleting source Trade data for {pair}.")
|
|
||||||
src.trades_purge(pair=pair)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_ohlcv_format(
|
def convert_ohlcv_format(
|
||||||
config: Config,
|
config: Config,
|
||||||
convert_from: str,
|
convert_from: str,
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"""
|
||||||
|
Functions to convert data from one format to another
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
|
from freqtrade.configuration import TimeRange
|
||||||
|
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES,
|
||||||
|
Config, TradeList)
|
||||||
|
from freqtrade.enums import CandleType
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def trades_df_remove_duplicates(trades: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Removes duplicates from the trades DataFrame.
|
||||||
|
Uses pandas.DataFrame.drop_duplicates to remove duplicates based on the 'timestamp' column.
|
||||||
|
:param trades: DataFrame with the columns constants.DEFAULT_TRADES_COLUMNS
|
||||||
|
:return: DataFrame with duplicates removed based on the 'timestamp' column
|
||||||
|
"""
|
||||||
|
return trades.drop_duplicates(subset=['timestamp', 'id'])
|
||||||
|
|
||||||
|
|
||||||
|
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
||||||
|
"""
|
||||||
|
Convert fetch_trades result into a List (to be more memory efficient).
|
||||||
|
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||||
|
:return: List of Lists, with constants.DEFAULT_TRADES_COLUMNS as columns
|
||||||
|
"""
|
||||||
|
return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades]
|
||||||
|
|
||||||
|
|
||||||
|
def trades_convert_types(trades: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Convert Trades dtypes and add 'date' column
|
||||||
|
"""
|
||||||
|
trades = trades.astype(TRADES_DTYPES)
|
||||||
|
trades['date'] = to_datetime(trades['timestamp'], unit='ms', utc=True)
|
||||||
|
return trades
|
||||||
|
|
||||||
|
|
||||||
|
def trades_list_to_df(trades: TradeList, convert: bool = True):
|
||||||
|
"""
|
||||||
|
convert trades list to dataframe
|
||||||
|
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
|
||||||
|
"""
|
||||||
|
if not trades:
|
||||||
|
df = DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
else:
|
||||||
|
df = DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
|
||||||
|
if convert:
|
||||||
|
df = trades_convert_types(df)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def trades_to_ohlcv(trades: DataFrame, timeframe: str) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Converts trades list to OHLCV list
|
||||||
|
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||||
|
:param timeframe: Timeframe to resample data to
|
||||||
|
:return: OHLCV Dataframe.
|
||||||
|
:raises: ValueError if no trades are provided
|
||||||
|
"""
|
||||||
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
|
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||||
|
if trades.empty:
|
||||||
|
raise ValueError('Trade-list empty.')
|
||||||
|
df = trades.set_index('date', drop=True)
|
||||||
|
|
||||||
|
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
||||||
|
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
||||||
|
df_new['date'] = df_new.index
|
||||||
|
# Drop 0 volume rows
|
||||||
|
df_new = df_new.dropna()
|
||||||
|
return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS]
|
||||||
|
|
||||||
|
|
||||||
|
def convert_trades_to_ohlcv(
|
||||||
|
pairs: List[str],
|
||||||
|
timeframes: List[str],
|
||||||
|
datadir: Path,
|
||||||
|
timerange: TimeRange,
|
||||||
|
erase: bool = False,
|
||||||
|
data_format_ohlcv: str = 'feather',
|
||||||
|
data_format_trades: str = 'feather',
|
||||||
|
candle_type: CandleType = CandleType.SPOT
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Convert stored trades data to ohlcv data
|
||||||
|
"""
|
||||||
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
|
data_handler_trades = get_datahandler(datadir, data_format=data_format_trades)
|
||||||
|
data_handler_ohlcv = get_datahandler(datadir, data_format=data_format_ohlcv)
|
||||||
|
if not pairs:
|
||||||
|
pairs = data_handler_trades.trades_get_pairs(datadir)
|
||||||
|
|
||||||
|
logger.info(f"About to convert pairs: '{', '.join(pairs)}', "
|
||||||
|
f"intervals: '{', '.join(timeframes)}' to {datadir}")
|
||||||
|
|
||||||
|
for pair in pairs:
|
||||||
|
trades = data_handler_trades.trades_load(pair)
|
||||||
|
for timeframe in timeframes:
|
||||||
|
if erase:
|
||||||
|
if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
||||||
|
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
||||||
|
try:
|
||||||
|
ohlcv = trades_to_ohlcv(trades, timeframe)
|
||||||
|
# Store ohlcv
|
||||||
|
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type)
|
||||||
|
except ValueError:
|
||||||
|
logger.exception(f'Could not convert {pair} to OHLCV.')
|
||||||
|
|
||||||
|
|
||||||
|
def convert_trades_format(config: Config, convert_from: str, convert_to: str, erase: bool):
|
||||||
|
"""
|
||||||
|
Convert trades from one format to another format.
|
||||||
|
:param config: Config dictionary
|
||||||
|
:param convert_from: Source format
|
||||||
|
:param convert_to: Target format
|
||||||
|
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||||
|
"""
|
||||||
|
if convert_from == 'kraken_csv':
|
||||||
|
if config['exchange']['name'] != 'kraken':
|
||||||
|
raise OperationalException(
|
||||||
|
'Converting from csv is only supported for kraken.'
|
||||||
|
'Please refer to the documentation for details about this special mode.'
|
||||||
|
)
|
||||||
|
from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv
|
||||||
|
import_kraken_trades_from_csv(config, convert_to)
|
||||||
|
return
|
||||||
|
|
||||||
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
|
trg = get_datahandler(config['datadir'], convert_to)
|
||||||
|
|
||||||
|
if 'pairs' not in config:
|
||||||
|
config['pairs'] = src.trades_get_pairs(config['datadir'])
|
||||||
|
logger.info(f"Converting trades for {config['pairs']}")
|
||||||
|
|
||||||
|
for pair in config['pairs']:
|
||||||
|
data = src.trades_load(pair=pair)
|
||||||
|
logger.info(f"Converting {len(data)} trades for {pair}")
|
||||||
|
trg.trades_store(pair, data)
|
||||||
|
if erase and convert_from != convert_to:
|
||||||
|
logger.info(f"Deleting source Trade data for {pair}.")
|
||||||
|
src.trades_purge(pair=pair)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, Config
|
||||||
|
from freqtrade.data.converter.trade_converter import (trades_convert_types,
|
||||||
|
trades_df_remove_duplicates)
|
||||||
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
KRAKEN_CSV_TRADE_COLUMNS = ['timestamp', 'price', 'amount']
|
||||||
|
|
||||||
|
|
||||||
|
def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
||||||
|
"""
|
||||||
|
Import kraken trades from csv
|
||||||
|
"""
|
||||||
|
if config['exchange']['name'] != 'kraken':
|
||||||
|
raise OperationalException('This function is only for the kraken exchange.')
|
||||||
|
|
||||||
|
datadir: Path = config['datadir']
|
||||||
|
data_handler = get_datahandler(datadir, data_format=convert_to)
|
||||||
|
|
||||||
|
tradesdir: Path = config['datadir'] / 'trades_csv'
|
||||||
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
# iterate through directories in this directory
|
||||||
|
data_symbols = {p.stem for p in tradesdir.rglob('*.csv')}
|
||||||
|
|
||||||
|
# create pair/filename mapping
|
||||||
|
markets = {
|
||||||
|
(m['symbol'], m['altname']) for m in exchange.markets.values()
|
||||||
|
if m.get('altname') in data_symbols
|
||||||
|
}
|
||||||
|
logger.info(f"Found csv files for {', '.join(data_symbols)}.")
|
||||||
|
|
||||||
|
for pair, name in markets:
|
||||||
|
dfs = []
|
||||||
|
# Load and combine all csv files for this pair
|
||||||
|
for f in tradesdir.rglob(f"{name}.csv"):
|
||||||
|
df = pd.read_csv(f, names=KRAKEN_CSV_TRADE_COLUMNS)
|
||||||
|
dfs.append(df)
|
||||||
|
|
||||||
|
# Load existing trades data
|
||||||
|
if not dfs:
|
||||||
|
# edgecase, can only happen if the file was deleted between the above glob and here
|
||||||
|
logger.info(f"No data found for pair {pair}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
trades = pd.concat(dfs, ignore_index=True)
|
||||||
|
|
||||||
|
trades.loc[:, 'timestamp'] = trades['timestamp'] * 1e3
|
||||||
|
trades.loc[:, 'cost'] = trades['price'] * trades['amount']
|
||||||
|
for col in DEFAULT_TRADES_COLUMNS:
|
||||||
|
if col not in trades.columns:
|
||||||
|
trades[col] = ''
|
||||||
|
|
||||||
|
trades = trades[DEFAULT_TRADES_COLUMNS]
|
||||||
|
trades = trades_convert_types(trades)
|
||||||
|
|
||||||
|
trades_df = trades_df_remove_duplicates(trades)
|
||||||
|
logger.info(f"{pair}: {len(trades_df)} trades, from "
|
||||||
|
f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to "
|
||||||
|
f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}")
|
||||||
|
|
||||||
|
data_handler.trades_store(pair, trades_df)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -119,8 +119,15 @@ def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ):
|
|||||||
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0)
|
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0)
|
||||||
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0)
|
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0)
|
||||||
|
|
||||||
new.columns = ['total_num_buys', 'wins', 'losses', 'profit_abs_wins', 'profit_abs_loss',
|
new['exp_ratio'] = (
|
||||||
'profit_tot', 'wl_ratio_pct', 'avg_win', 'avg_loss']
|
(
|
||||||
|
(1 + (new['avg_win'] / abs(new['avg_loss']))) * (new['wl_ratio_pct'] / 100)
|
||||||
|
) - 1).fillna(0)
|
||||||
|
|
||||||
|
new.columns = ['total_num_buys', 'wins', 'losses',
|
||||||
|
'profit_abs_wins', 'profit_abs_loss',
|
||||||
|
'profit_tot', 'wl_ratio_pct',
|
||||||
|
'avg_win', 'avg_loss', 'exp_ratio']
|
||||||
|
|
||||||
sortcols = ['total_num_buys']
|
sortcols = ['total_num_buys']
|
||||||
|
|
||||||
@@ -204,6 +211,7 @@ def prepare_results(analysed_trades, stratname,
|
|||||||
timerange=None):
|
timerange=None):
|
||||||
res_df = pd.DataFrame()
|
res_df = pd.DataFrame()
|
||||||
for pair, trades in analysed_trades[stratname].items():
|
for pair, trades in analysed_trades[stratname].items():
|
||||||
|
trades.dropna(subset=['close_date'], inplace=True)
|
||||||
res_df = pd.concat([res_df, trades], ignore_index=True)
|
res_df = pd.concat([res_df, trades], ignore_index=True)
|
||||||
|
|
||||||
res_df = _select_rows_within_dates(res_df, timerange)
|
res_df = _select_rows_within_dates(res_df, timerange)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ from pandas import DataFrame, concat
|
|||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
||||||
DL_DATA_TIMEFRAMES, Config)
|
DL_DATA_TIMEFRAMES, Config)
|
||||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_ohlcv,
|
||||||
trades_remove_duplicates, trades_to_ohlcv)
|
ohlcv_to_dataframe, trades_df_remove_duplicates,
|
||||||
|
trades_list_to_df)
|
||||||
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.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||||
from freqtrade.util import format_ms_time
|
from freqtrade.util import dt_ts, format_ms_time
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
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__)
|
||||||
@@ -69,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]:
|
||||||
@@ -349,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
|
||||||
@@ -375,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
|
||||||
|
|
||||||
@@ -394,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.
|
||||||
@@ -421,36 +429,6 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
|
|||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
|
|
||||||
|
|
||||||
def convert_trades_to_ohlcv(
|
|
||||||
pairs: List[str],
|
|
||||||
timeframes: List[str],
|
|
||||||
datadir: Path,
|
|
||||||
timerange: TimeRange,
|
|
||||||
erase: bool = False,
|
|
||||||
data_format_ohlcv: str = 'json',
|
|
||||||
data_format_trades: str = 'jsongz',
|
|
||||||
candle_type: CandleType = CandleType.SPOT
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Convert stored trades data to ohlcv data
|
|
||||||
"""
|
|
||||||
data_handler_trades = get_datahandler(datadir, data_format=data_format_trades)
|
|
||||||
data_handler_ohlcv = get_datahandler(datadir, data_format=data_format_ohlcv)
|
|
||||||
|
|
||||||
for pair in pairs:
|
|
||||||
trades = data_handler_trades.trades_load(pair)
|
|
||||||
for timeframe in timeframes:
|
|
||||||
if erase:
|
|
||||||
if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
|
||||||
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
|
||||||
try:
|
|
||||||
ohlcv = trades_to_ohlcv(trades, timeframe)
|
|
||||||
# Store ohlcv
|
|
||||||
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type)
|
|
||||||
except ValueError:
|
|
||||||
logger.exception(f'Could not convert {pair} to OHLCV.')
|
|
||||||
|
|
||||||
|
|
||||||
def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]:
|
def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]:
|
||||||
"""
|
"""
|
||||||
Get the maximum common timerange for the given backtest data.
|
Get the maximum common timerange for the given backtest data.
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
@@ -121,10 +123,14 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
def funding_fee_cutoff(self, open_date: datetime):
|
def funding_fee_cutoff(self, open_date: datetime):
|
||||||
"""
|
"""
|
||||||
|
Funding fees are only charged at full hours (usually every 4-8h).
|
||||||
|
Therefore a trade opening at 10:00:01 will not be charged a funding fee until the next hour.
|
||||||
|
On binance, this cutoff is 15s.
|
||||||
|
https://github.com/freqtrade/freqtrade/pull/5779#discussion_r740175931
|
||||||
:param open_date: The open date for a trade
|
:param open_date: The open date for a trade
|
||||||
:return: The cutoff open time for when a funding fee is charged
|
:return: True if the date falls on a full hour, False otherwise
|
||||||
"""
|
"""
|
||||||
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)
|
return open_date.minute == 0 and open_date.second < 15
|
||||||
|
|
||||||
def dry_run_liquidation_price(
|
def dry_run_liquidation_price(
|
||||||
self,
|
self,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+47
-29
@@ -1,16 +1,16 @@
|
|||||||
""" Bybit exchange subclass """
|
""" Bybit exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, ExchangeError, 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
|
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -27,7 +27,7 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 200,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
@@ -36,6 +36,8 @@ class Bybit(Exchange):
|
|||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
|
# bybit response parsing fails to populate stopLossPrice
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
"stop_price_type_field": "triggerBy",
|
"stop_price_type_field": "triggerBy",
|
||||||
"stop_price_type_value_mapping": {
|
"stop_price_type_value_mapping": {
|
||||||
PriceType.LAST: "LastPrice",
|
PriceType.LAST: "LastPrice",
|
||||||
@@ -91,28 +93,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:
|
||||||
@@ -215,6 +202,37 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
# Bybit does not provide "applied" funding fees per position.
|
# Bybit does not provide "applied" funding fees per position.
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
return self._fetch_and_calculate_funding_fees(
|
try:
|
||||||
pair, amount, is_short, open_date)
|
return self._fetch_and_calculate_funding_fees(
|
||||||
|
pair, amount, is_short, open_date)
|
||||||
|
except ExchangeError:
|
||||||
|
logger.warning(f"Could not update funding fees for {pair}.")
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Fetch all orders for a pair "since"
|
||||||
|
:param pair: Pair for the query
|
||||||
|
:param since: Starting time for the query
|
||||||
|
"""
|
||||||
|
# On bybit, the distance between since and "until" can't exceed 7 days.
|
||||||
|
# we therefore need to split the query into multiple queries.
|
||||||
|
orders = []
|
||||||
|
|
||||||
|
while since < dt_now():
|
||||||
|
until = since + timedelta(days=7, minutes=-1)
|
||||||
|
orders += super().fetch_orders(pair, since, params={'until': dt_ts(until)})
|
||||||
|
since = until
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
|
order = super().fetch_order(order_id, pair, params)
|
||||||
|
if (
|
||||||
|
order.get('status') == 'canceled'
|
||||||
|
and order.get('filled') == 0.0
|
||||||
|
and order.get('remaining') == 0.0
|
||||||
|
):
|
||||||
|
# Canceled orders will have "remaining=0" on bybit.
|
||||||
|
order['remaining'] = None
|
||||||
|
return order
|
||||||
|
|||||||
+113
-88
@@ -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
|
||||||
@@ -22,8 +23,7 @@ from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHAN
|
|||||||
BuySell, Config, EntryExit, ExchangeConfig,
|
BuySell, Config, EntryExit, ExchangeConfig,
|
||||||
ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe)
|
ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe)
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.enums.pricetype import PriceType
|
|
||||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, OperationalException, PricingError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
@@ -61,7 +61,8 @@ class Exchange:
|
|||||||
# or by specifying them in the configuration.
|
# or by specifying them in the configuration.
|
||||||
_ft_has_default: Dict = {
|
_ft_has_default: Dict = {
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
"stop_price_param": "stopPrice",
|
"stop_price_param": "stopLossPrice", # Used for stoploss_on_exchange request
|
||||||
|
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
||||||
"order_time_in_force": ["GTC"],
|
"order_time_in_force": ["GTC"],
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_candle_limit": 500,
|
"ohlcv_candle_limit": 500,
|
||||||
@@ -263,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
|
||||||
@@ -465,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:
|
||||||
@@ -580,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:
|
||||||
"""
|
"""
|
||||||
@@ -843,7 +832,7 @@ class Exchange:
|
|||||||
rate: float, leverage: float, params: Dict = {},
|
rate: float, leverage: float, params: Dict = {},
|
||||||
stop_loss: bool = False) -> Dict[str, Any]:
|
stop_loss: bool = False) -> Dict[str, Any]:
|
||||||
now = dt_now()
|
now = dt_now()
|
||||||
order_id = f'dry_run_{side}_{now.timestamp()}'
|
order_id = f'dry_run_{side}_{pair}_{now.timestamp()}'
|
||||||
# Rounding here must respect to contract sizes
|
# Rounding here must respect to contract sizes
|
||||||
_amount = self._contracts_to_amount(
|
_amount = self._contracts_to_amount(
|
||||||
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
|
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
|
||||||
@@ -867,15 +856,15 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
if stop_loss:
|
if stop_loss:
|
||||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||||
dry_order[self._ft_has['stop_price_param']] = dry_order["price"]
|
dry_order[self._ft_has['stop_price_prop']] = dry_order["price"]
|
||||||
# Workaround to avoid filling stoploss orders immediately
|
# Workaround to avoid filling stoploss orders immediately
|
||||||
dry_order["ft_order_type"] = "stoploss"
|
dry_order["ft_order_type"] = "stoploss"
|
||||||
orderbook: Optional[OrderBook] = None
|
orderbook: Optional[OrderBook] = None
|
||||||
if self.exchange_has('fetchL2OrderBook'):
|
if self.exchange_has('fetchL2OrderBook'):
|
||||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
if ordertype == "limit" and orderbook:
|
if ordertype == "limit" and orderbook:
|
||||||
# Allow a 3% price difference
|
# Allow a 1% price difference
|
||||||
allowed_diff = 0.03
|
allowed_diff = 0.01
|
||||||
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
|
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Converted order {pair} to market order due to price {rate} crossing spread "
|
f"Converted order {pair} to market order due to price {rate} crossing spread "
|
||||||
@@ -931,7 +920,7 @@ class Exchange:
|
|||||||
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
|
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
|
||||||
|
|
||||||
remaining_amount = amount
|
remaining_amount = amount
|
||||||
filled_amount = 0.0
|
filled_value = 0.0
|
||||||
book_entry_price = 0.0
|
book_entry_price = 0.0
|
||||||
for book_entry in orderbook[ob_type]:
|
for book_entry in orderbook[ob_type]:
|
||||||
book_entry_price = book_entry[0]
|
book_entry_price = book_entry[0]
|
||||||
@@ -939,17 +928,17 @@ class Exchange:
|
|||||||
if remaining_amount > 0:
|
if remaining_amount > 0:
|
||||||
if remaining_amount < book_entry_coin_volume:
|
if remaining_amount < book_entry_coin_volume:
|
||||||
# Orderbook at this slot bigger than remaining amount
|
# Orderbook at this slot bigger than remaining amount
|
||||||
filled_amount += remaining_amount * book_entry_price
|
filled_value += remaining_amount * book_entry_price
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
filled_amount += book_entry_coin_volume * book_entry_price
|
filled_value += book_entry_coin_volume * book_entry_price
|
||||||
remaining_amount -= book_entry_coin_volume
|
remaining_amount -= book_entry_coin_volume
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# If remaining_amount wasn't consumed completely (break was not called)
|
# If remaining_amount wasn't consumed completely (break was not called)
|
||||||
filled_amount += remaining_amount * book_entry_price
|
filled_value += remaining_amount * book_entry_price
|
||||||
forecast_avg_filled_price = max(filled_amount, 0) / amount
|
forecast_avg_filled_price = max(filled_value, 0) / amount
|
||||||
# Limit max. slippage to specified value
|
# Limit max. slippage to specified value
|
||||||
if side == 'buy':
|
if side == 'buy':
|
||||||
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
|
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
|
||||||
@@ -1019,7 +1008,7 @@ class Exchange:
|
|||||||
from freqtrade.persistence import Order
|
from freqtrade.persistence import Order
|
||||||
order = Order.order_by_id(order_id)
|
order = Order.order_by_id(order_id)
|
||||||
if order:
|
if order:
|
||||||
ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_param'])
|
ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_prop'])
|
||||||
self._dry_run_open_orders[order_id] = ccxt_order
|
self._dry_run_open_orders[order_id] = ccxt_order
|
||||||
return ccxt_order
|
return ccxt_order
|
||||||
# Gracefully handle errors with dry-run orders.
|
# Gracefully handle errors with dry-run orders.
|
||||||
@@ -1091,6 +1080,13 @@ class Exchange:
|
|||||||
rate_for_order,
|
rate_for_order,
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
|
if order.get('status') is None:
|
||||||
|
# Map empty status to open.
|
||||||
|
order['status'] = 'open'
|
||||||
|
|
||||||
|
if order.get('type') is None:
|
||||||
|
order['type'] = ordertype
|
||||||
|
|
||||||
self._log_exchange_response('create_order', order)
|
self._log_exchange_response('create_order', order)
|
||||||
order = self._order_contracts_to_amount(order)
|
order = self._order_contracts_to_amount(order)
|
||||||
return order
|
return order
|
||||||
@@ -1120,7 +1116,7 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
if not self._ft_has.get('stoploss_on_exchange'):
|
if not self._ft_has.get('stoploss_on_exchange'):
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
price_param = self._ft_has['stop_price_param']
|
price_param = self._ft_has['stop_price_prop']
|
||||||
return (
|
return (
|
||||||
order.get(price_param, None) is None
|
order.get(price_param, None) is None
|
||||||
or ((side == "sell" and stop_loss > float(order[price_param])) or
|
or ((side == "sell" and stop_loss > float(order[price_param])) or
|
||||||
@@ -1432,8 +1428,17 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
|
||||||
|
orders = []
|
||||||
|
if self.exchange_has('fetchClosedOrders'):
|
||||||
|
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
||||||
|
if self.exchange_has('fetchOpenOrders'):
|
||||||
|
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
||||||
|
orders.extend(orders_open)
|
||||||
|
return orders
|
||||||
|
|
||||||
@retrier(retries=0)
|
@retrier(retries=0)
|
||||||
def fetch_orders(self, pair: str, since: datetime) -> List[Dict]:
|
def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Fetch all orders for a pair "since"
|
Fetch all orders for a pair "since"
|
||||||
:param pair: Pair for the query
|
:param pair: Pair for the query
|
||||||
@@ -1442,26 +1447,20 @@ class Exchange:
|
|||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def fetch_orders_emulate() -> List[Dict]:
|
|
||||||
orders = []
|
|
||||||
if self.exchange_has('fetchClosedOrders'):
|
|
||||||
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
|
||||||
if self.exchange_has('fetchOpenOrders'):
|
|
||||||
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
|
||||||
orders.extend(orders_open)
|
|
||||||
return orders
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
since_ms = int((since.timestamp() - 10) * 1000)
|
since_ms = int((since.timestamp() - 10) * 1000)
|
||||||
|
|
||||||
if self.exchange_has('fetchOrders'):
|
if self.exchange_has('fetchOrders'):
|
||||||
|
if not params:
|
||||||
|
params = {}
|
||||||
try:
|
try:
|
||||||
orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms)
|
orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms, params=params)
|
||||||
except ccxt.NotSupported:
|
except ccxt.NotSupported:
|
||||||
# Some exchanges don't support fetchOrders
|
# Some exchanges don't support fetchOrders
|
||||||
# attempt to fetch open and closed orders separately
|
# attempt to fetch open and closed orders separately
|
||||||
orders = fetch_orders_emulate()
|
orders = self._fetch_orders_emulate(pair, since_ms)
|
||||||
else:
|
else:
|
||||||
orders = fetch_orders_emulate()
|
orders = self._fetch_orders_emulate(pair, since_ms)
|
||||||
self._log_exchange_response('fetch_orders', orders)
|
self._log_exchange_response('fetch_orders', orders)
|
||||||
orders = [self._order_contracts_to_amount(o) for o in orders]
|
orders = [self._order_contracts_to_amount(o) for o in orders]
|
||||||
return orders
|
return orders
|
||||||
@@ -1876,7 +1875,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
|
||||||
@@ -2163,7 +2162,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:
|
||||||
@@ -2265,20 +2264,25 @@ class Exchange:
|
|||||||
from_id = t[-1][1]
|
from_id = t[-1][1]
|
||||||
trades.extend(t[:-1])
|
trades.extend(t[:-1])
|
||||||
while True:
|
while True:
|
||||||
t = await self._async_fetch_trades(pair,
|
try:
|
||||||
params={self._trades_pagination_arg: from_id})
|
t = await self._async_fetch_trades(pair,
|
||||||
if t:
|
params={self._trades_pagination_arg: from_id})
|
||||||
# Skip last id since its the key for the next call
|
if t:
|
||||||
trades.extend(t[:-1])
|
# Skip last id since its the key for the next call
|
||||||
if from_id == t[-1][1] or t[-1][0] > until:
|
trades.extend(t[:-1])
|
||||||
logger.debug(f"Stopping because from_id did not change. "
|
if from_id == t[-1][1] or t[-1][0] > until:
|
||||||
f"Reached {t[-1][0]} > {until}")
|
logger.debug(f"Stopping because from_id did not change. "
|
||||||
# Reached the end of the defined-download period - add last trade as well.
|
f"Reached {t[-1][0]} > {until}")
|
||||||
trades.extend(t[-1:])
|
# Reached the end of the defined-download period - add last trade as well.
|
||||||
break
|
trades.extend(t[-1:])
|
||||||
|
break
|
||||||
|
|
||||||
from_id = t[-1][1]
|
from_id = t[-1][1]
|
||||||
else:
|
else:
|
||||||
|
logger.debug("Stopping as no more trades were returned.")
|
||||||
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||||
break
|
break
|
||||||
|
|
||||||
return (pair, trades)
|
return (pair, trades)
|
||||||
@@ -2298,16 +2302,26 @@ 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:
|
||||||
t = await self._async_fetch_trades(pair, since=since)
|
try:
|
||||||
if t:
|
t = await self._async_fetch_trades(pair, since=since)
|
||||||
since = t[-1][0]
|
if t:
|
||||||
trades.extend(t)
|
# No more trades to download available at the exchange,
|
||||||
# Reached the end of the defined-download period
|
# So we repeatedly get the same trade over and over again.
|
||||||
if until and t[-1][0] > until:
|
if since == t[-1][0] and len(t) == 1:
|
||||||
logger.debug(
|
logger.debug("Stopping because no more trades are available.")
|
||||||
f"Stopping because until was reached. {t[-1][0]} > {until}")
|
break
|
||||||
|
since = t[-1][0]
|
||||||
|
trades.extend(t)
|
||||||
|
# Reached the end of the defined-download period
|
||||||
|
if until and t[-1][0] > until:
|
||||||
|
logger.debug(
|
||||||
|
f"Stopping because until was reached. {t[-1][0]} > {until}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.debug("Stopping as no more trades were returned.")
|
||||||
break
|
break
|
||||||
else:
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||||
break
|
break
|
||||||
|
|
||||||
return (pair, trades)
|
return (pair, trades)
|
||||||
@@ -2356,9 +2370,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:
|
||||||
@@ -2639,12 +2660,14 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def funding_fee_cutoff(self, open_date: datetime):
|
def funding_fee_cutoff(self, open_date: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
|
Funding fees are only charged at full hours (usually every 4-8h).
|
||||||
|
Therefore a trade opening at 10:00:01 will not be charged a funding fee until the next hour.
|
||||||
:param open_date: The open date for a trade
|
:param open_date: The open date for a trade
|
||||||
:return: The cutoff open time for when a funding fee is charged
|
:return: True if the date falls on a full hour, False otherwise
|
||||||
"""
|
"""
|
||||||
return open_date.minute > 0 or open_date.second > 0
|
return open_date.minute == 0 and open_date.second == 0
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
|
def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
|
||||||
@@ -2692,15 +2715,16 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if self.funding_fee_cutoff(open_date):
|
if self.funding_fee_cutoff(open_date):
|
||||||
open_date += timedelta(hours=1)
|
# Shift back to 1h candle to avoid missing funding fees
|
||||||
|
# Only really relevant for trades very close to the full hour
|
||||||
|
open_date = timeframe_to_prev_date('1h', open_date)
|
||||||
timeframe = self._ft_has['mark_ohlcv_timeframe']
|
timeframe = self._ft_has['mark_ohlcv_timeframe']
|
||||||
timeframe_ff = self._ft_has.get('funding_fee_timeframe',
|
timeframe_ff = self._ft_has.get('funding_fee_timeframe',
|
||||||
self._ft_has['mark_ohlcv_timeframe'])
|
self._ft_has['mark_ohlcv_timeframe'])
|
||||||
|
|
||||||
if not close_date:
|
if not close_date:
|
||||||
close_date = datetime.now(timezone.utc)
|
close_date = datetime.now(timezone.utc)
|
||||||
open_timestamp = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
|
since_ms = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
|
||||||
# close_timestamp = int(close_date.timestamp()) * 1000
|
|
||||||
|
|
||||||
mark_comb: PairWithTimeframe = (
|
mark_comb: PairWithTimeframe = (
|
||||||
pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
|
pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
|
||||||
@@ -2708,7 +2732,7 @@ class Exchange:
|
|||||||
funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
||||||
candle_histories = self.refresh_latest_ohlcv(
|
candle_histories = self.refresh_latest_ohlcv(
|
||||||
[mark_comb, funding_comb],
|
[mark_comb, funding_comb],
|
||||||
since_ms=open_timestamp,
|
since_ms=since_ms,
|
||||||
cache=False,
|
cache=False,
|
||||||
drop_incomplete=False,
|
drop_incomplete=False,
|
||||||
)
|
)
|
||||||
@@ -2719,8 +2743,7 @@ class Exchange:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise ExchangeError("Could not find funding rates.") from None
|
raise ExchangeError("Could not find funding rates.") from None
|
||||||
|
|
||||||
funding_mark_rates = self.combine_funding_and_mark(
|
funding_mark_rates = self.combine_funding_and_mark(funding_rates, mark_rates)
|
||||||
funding_rates=funding_rates, mark_rates=mark_rates)
|
|
||||||
|
|
||||||
return self.calculate_funding_fees(
|
return self.calculate_funding_fees(
|
||||||
funding_mark_rates,
|
funding_mark_rates,
|
||||||
@@ -2767,7 +2790,7 @@ class Exchange:
|
|||||||
amount: float,
|
amount: float,
|
||||||
is_short: bool,
|
is_short: bool,
|
||||||
open_date: datetime,
|
open_date: datetime,
|
||||||
close_date: Optional[datetime] = None,
|
close_date: datetime,
|
||||||
time_in_ratio: Optional[float] = None
|
time_in_ratio: Optional[float] = None
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -2783,8 +2806,8 @@ class Exchange:
|
|||||||
fees: float = 0
|
fees: float = 0
|
||||||
|
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
|
df1 = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
|
||||||
fees = sum(df['open_fund'] * df['open_mark'] * amount)
|
fees = sum(df1['open_fund'] * df1['open_mark'] * amount)
|
||||||
|
|
||||||
# Negate fees for longs as funding_fees expects it this way based on live endpoints.
|
# Negate fees for longs as funding_fees expects it this way based on live endpoints.
|
||||||
return fees if is_short else -fees
|
return fees if is_short else -fees
|
||||||
@@ -2799,17 +2822,19 @@ class Exchange:
|
|||||||
:param amount: Trade amount
|
:param amount: Trade amount
|
||||||
:param open_date: Open date of the trade
|
:param open_date: Open date of the trade
|
||||||
:return: funding fee since open_date
|
:return: funding fee since open_date
|
||||||
:raises: ExchangeError if something goes wrong.
|
|
||||||
"""
|
"""
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
if self._config['dry_run']:
|
try:
|
||||||
funding_fees = self._fetch_and_calculate_funding_fees(
|
if self._config['dry_run']:
|
||||||
pair, amount, is_short, open_date)
|
funding_fees = self._fetch_and_calculate_funding_fees(
|
||||||
else:
|
pair, amount, is_short, open_date)
|
||||||
funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
|
else:
|
||||||
return funding_fees
|
funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
|
||||||
else:
|
return funding_fees
|
||||||
return 0.0
|
except ExchangeError:
|
||||||
|
logger.warning(f"Could not update funding fees for {pair}.")
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
def get_liquidation_price(
|
def get_liquidation_price(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -248,6 +248,39 @@ def amount_to_contract_precision(
|
|||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
|
def __price_to_precision_significant_digits(
|
||||||
|
price: float,
|
||||||
|
price_precision: float,
|
||||||
|
*,
|
||||||
|
rounding_mode: int = ROUND,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Implementation of ROUND_UP/Round_down for significant digits mode.
|
||||||
|
"""
|
||||||
|
from decimal import ROUND_DOWN as dec_ROUND_DOWN
|
||||||
|
from decimal import ROUND_UP as dec_ROUND_UP
|
||||||
|
from decimal import Decimal
|
||||||
|
dec = Decimal(str(price))
|
||||||
|
string = f'{dec:f}'
|
||||||
|
precision = round(price_precision)
|
||||||
|
|
||||||
|
q = precision - dec.adjusted() - 1
|
||||||
|
sigfig = Decimal('10') ** -q
|
||||||
|
if q < 0:
|
||||||
|
string_to_precision = string[:precision]
|
||||||
|
# string_to_precision is '' when we have zero precision
|
||||||
|
below = sigfig * Decimal(string_to_precision if string_to_precision else '0')
|
||||||
|
above = below + sigfig
|
||||||
|
res = above if rounding_mode == ROUND_UP else below
|
||||||
|
precise = f'{res:f}'
|
||||||
|
else:
|
||||||
|
precise = '{:f}'.format(dec.quantize(
|
||||||
|
sigfig,
|
||||||
|
rounding=dec_ROUND_DOWN if rounding_mode == ROUND_DOWN else dec_ROUND_UP)
|
||||||
|
)
|
||||||
|
return float(precise)
|
||||||
|
|
||||||
|
|
||||||
def price_to_precision(
|
def price_to_precision(
|
||||||
price: float,
|
price: float,
|
||||||
price_precision: Optional[float],
|
price_precision: Optional[float],
|
||||||
@@ -271,28 +304,39 @@ def price_to_precision(
|
|||||||
:return: price rounded up to the precision the Exchange accepts
|
:return: price rounded up to the precision the Exchange accepts
|
||||||
"""
|
"""
|
||||||
if price_precision is not None and precisionMode is not None:
|
if price_precision is not None and precisionMode is not None:
|
||||||
|
if rounding_mode not in (ROUND_UP, ROUND_DOWN):
|
||||||
|
# Use CCXT code where possible.
|
||||||
|
return float(decimal_to_precision(price, rounding_mode=rounding_mode,
|
||||||
|
precision=price_precision,
|
||||||
|
counting_mode=precisionMode
|
||||||
|
))
|
||||||
|
|
||||||
if precisionMode == TICK_SIZE:
|
if precisionMode == TICK_SIZE:
|
||||||
if rounding_mode == ROUND:
|
|
||||||
ticks = price / price_precision
|
|
||||||
rounded_ticks = round(ticks)
|
|
||||||
return rounded_ticks * price_precision
|
|
||||||
precision = FtPrecise(price_precision)
|
precision = FtPrecise(price_precision)
|
||||||
price_str = FtPrecise(price)
|
price_str = FtPrecise(price)
|
||||||
missing = price_str % precision
|
missing = price_str % precision
|
||||||
if not missing == FtPrecise("0"):
|
if not missing == FtPrecise("0"):
|
||||||
return round(float(str(price_str - missing + precision)), 14)
|
if rounding_mode == ROUND_UP:
|
||||||
|
res = price_str - missing + precision
|
||||||
|
elif rounding_mode == ROUND_DOWN:
|
||||||
|
res = price_str - missing
|
||||||
|
return round(float(str(res)), 14)
|
||||||
return price
|
return price
|
||||||
elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
|
elif precisionMode == DECIMAL_PLACES:
|
||||||
|
|
||||||
ndigits = round(price_precision)
|
ndigits = round(price_precision)
|
||||||
if rounding_mode == ROUND:
|
|
||||||
return round(price, ndigits)
|
|
||||||
ticks = price * (10**ndigits)
|
ticks = price * (10**ndigits)
|
||||||
if rounding_mode == ROUND_UP:
|
if rounding_mode == ROUND_UP:
|
||||||
return ceil(ticks) / (10**ndigits)
|
return ceil(ticks) / (10**ndigits)
|
||||||
if rounding_mode == TRUNCATE:
|
|
||||||
return int(ticks) / (10**ndigits)
|
|
||||||
if rounding_mode == ROUND_DOWN:
|
if rounding_mode == ROUND_DOWN:
|
||||||
return floor(ticks) / (10**ndigits)
|
return floor(ticks) / (10**ndigits)
|
||||||
|
|
||||||
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
|
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
|
||||||
|
elif precisionMode == SIGNIFICANT_DIGITS:
|
||||||
|
if rounding_mode in (ROUND_UP, ROUND_DOWN):
|
||||||
|
return __price_to_precision_significant_digits(
|
||||||
|
price, price_precision, rounding_mode=rounding_mode
|
||||||
|
)
|
||||||
|
|
||||||
raise ValueError(f"Unknown precisionMode {precisionMode}")
|
raise ValueError(f"Unknown precisionMode {precisionMode}")
|
||||||
return price
|
return price
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ class Gate(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"order_time_in_force": ['GTC', 'IOC'],
|
"order_time_in_force": ['GTC', 'IOC'],
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
"marketOrderRequiresPrice": True,
|
"marketOrderRequiresPrice": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class Huobi(Exchange):
|
|||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
"stoploss_order_types": {"limit": "stop-limit"},
|
"stoploss_order_types": {"limit": "stop-limit"},
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"l2_limit_range": [5, 10, 20],
|
"l2_limit_range": [5, 10, 20],
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class Kraken(Exchange):
|
|||||||
_params: Dict = {"trading_agreement": "agree"}
|
_params: Dict = {"trading_agreement": "agree"}
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
"ohlcv_candle_limit": 720,
|
"ohlcv_candle_limit": 720,
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
@@ -193,7 +195,7 @@ class Kraken(Exchange):
|
|||||||
amount: float,
|
amount: float,
|
||||||
is_short: bool,
|
is_short: bool,
|
||||||
open_date: datetime,
|
open_date: datetime,
|
||||||
close_date: Optional[datetime] = None,
|
close_date: datetime,
|
||||||
time_in_ratio: Optional[float] = None
|
time_in_ratio: Optional[float] = None
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class Kucoin(Exchange):
|
|||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
"l2_limit_range": [20, 100],
|
"l2_limit_range": [20, 100],
|
||||||
"l2_limit_range_required": False,
|
"l2_limit_range_required": False,
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.enums.pricetype import PriceType
|
|
||||||
from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
|
from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
|
||||||
TemporaryError)
|
TemporaryError)
|
||||||
from freqtrade.exchange import Exchange, date_minus_candles
|
from freqtrade.exchange import Exchange, date_minus_candles
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.misc import safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback2
|
||||||
|
from freqtrade.util import dt_now, dt_ts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -28,7 +29,6 @@ class Okx(Exchange):
|
|||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"stop_price_param": "stopLossPrice",
|
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"tickers_have_quoteVolume": False,
|
"tickers_have_quoteVolume": False,
|
||||||
@@ -187,7 +187,7 @@ class Okx(Exchange):
|
|||||||
|
|
||||||
def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict:
|
def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict:
|
||||||
if (
|
if (
|
||||||
order['status'] == 'closed'
|
order.get('status', 'open') == 'closed'
|
||||||
and (real_order_id := order.get('info', {}).get('ordId')) is not None
|
and (real_order_id := order.get('info', {}).get('ordId')) is not None
|
||||||
):
|
):
|
||||||
# Once a order triggered, we fetch the regular followup order.
|
# Once a order triggered, we fetch the regular followup order.
|
||||||
@@ -241,3 +241,18 @@ class Okx(Exchange):
|
|||||||
pair=pair,
|
pair=pair,
|
||||||
params=params1,
|
params=params1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
|
||||||
|
orders = []
|
||||||
|
|
||||||
|
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
||||||
|
if (since_ms < dt_ts(dt_now() - timedelta(days=6, hours=23))):
|
||||||
|
# Regular fetch_closed_orders only returns 7 days of data.
|
||||||
|
# Force usage of "archive" endpoint, which returns 3 months of data.
|
||||||
|
params = {'method': 'privateGetTradeOrdersHistoryArchive'}
|
||||||
|
orders_hist = self._api.fetch_closed_orders(pair, since=since_ms, params=params)
|
||||||
|
orders.extend(orders_hist)
|
||||||
|
|
||||||
|
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
||||||
|
orders.extend(orders_open)
|
||||||
|
return orders
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -156,7 +159,7 @@ class BaseEnvironment(gym.Env):
|
|||||||
function is designed for tracking incremented objects,
|
function is designed for tracking incremented objects,
|
||||||
events, actions inside the training environment.
|
events, actions inside the training environment.
|
||||||
For example, a user can call this to track the
|
For example, a user can call this to track the
|
||||||
frequency of occurence of an `is_valid` call in
|
frequency of occurrence of an `is_valid` call in
|
||||||
their `calculate_reward()`:
|
their `calculate_reward()`:
|
||||||
|
|
||||||
def calculate_reward(self, action: int) -> float:
|
def calculate_reward(self, action: int) -> float:
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
|
|||||||
torch.multiprocessing.set_sharing_strategy('file_system')
|
torch.multiprocessing.set_sharing_strategy('file_system')
|
||||||
|
|
||||||
SB3_MODELS = ['PPO', 'A2C', 'DQN']
|
SB3_MODELS = ['PPO', 'A2C', 'DQN']
|
||||||
SB3_CONTRIB_MODELS = ['TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO']
|
SB3_CONTRIB_MODELS = ['TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'QRDQN']
|
||||||
|
|
||||||
|
|
||||||
class BaseReinforcementLearningModel(IFreqaiModel):
|
class BaseReinforcementLearningModel(IFreqaiModel):
|
||||||
|
|||||||
@@ -263,23 +263,45 @@ class FreqaiDataDrawer:
|
|||||||
self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy()
|
self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy()
|
||||||
return
|
return
|
||||||
|
|
||||||
def set_initial_return_values(self, pair: str, pred_df: DataFrame) -> None:
|
def set_initial_return_values(self, pair: str,
|
||||||
|
pred_df: DataFrame,
|
||||||
|
dataframe: DataFrame
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Set the initial return values to the historical predictions dataframe. This avoids needing
|
Set the initial return values to the historical predictions dataframe. This avoids needing
|
||||||
to repredict on historical candles, and also stores historical predictions despite
|
to repredict on historical candles, and also stores historical predictions despite
|
||||||
retrainings (so stored predictions are true predictions, not just inferencing on trained
|
retrainings (so stored predictions are true predictions, not just inferencing on trained
|
||||||
data)
|
data).
|
||||||
|
|
||||||
|
We also aim to keep the date from historical predictions so that the FreqUI displays
|
||||||
|
zeros during any downtime (between FreqAI reloads).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
hist_df = self.historic_predictions
|
new_pred = pred_df.copy()
|
||||||
len_diff = len(hist_df[pair].index) - len(pred_df.index)
|
# set new_pred values to nans (we want to signal to user that there was nothing
|
||||||
if len_diff < 0:
|
# historically made during downtime. The newest pred will get appeneded later in
|
||||||
df_concat = pd.concat([pred_df.iloc[:abs(len_diff)], hist_df[pair]],
|
# append_model_predictions)
|
||||||
ignore_index=True, keys=hist_df[pair].keys())
|
new_pred.iloc[:, :] = np.nan
|
||||||
|
new_pred["date_pred"] = dataframe["date"]
|
||||||
|
hist_preds = self.historic_predictions[pair].copy()
|
||||||
|
|
||||||
|
# find the closest common date between new_pred and historic predictions
|
||||||
|
# and cut off the new_pred dataframe at that date
|
||||||
|
common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner")
|
||||||
|
if len(common_dates.index) > 0:
|
||||||
|
new_pred = new_pred.iloc[len(common_dates):]
|
||||||
else:
|
else:
|
||||||
df_concat = hist_df[pair].tail(len(pred_df.index)).reset_index(drop=True)
|
logger.warning("No common dates found between new predictions and historic "
|
||||||
|
"predictions. You likely left your FreqAI instance offline "
|
||||||
|
f"for more than {len(dataframe.index)} candles.")
|
||||||
|
|
||||||
|
df_concat = pd.concat([hist_preds, new_pred], ignore_index=True, keys=hist_preds.keys())
|
||||||
|
|
||||||
|
# any missing values will get zeroed out so users can see the exact
|
||||||
|
# downtime in FreqUI
|
||||||
df_concat = df_concat.fillna(0)
|
df_concat = df_concat.fillna(0)
|
||||||
self.model_return_values[pair] = df_concat
|
self.historic_predictions[pair] = df_concat
|
||||||
|
self.model_return_values[pair] = df_concat.tail(len(dataframe.index)).reset_index(drop=True)
|
||||||
|
|
||||||
def append_model_predictions(self, pair: str, predictions: DataFrame,
|
def append_model_predictions(self, pair: str, predictions: DataFrame,
|
||||||
do_preds: NDArray[np.int_],
|
do_preds: NDArray[np.int_],
|
||||||
@@ -375,7 +397,7 @@ class FreqaiDataDrawer:
|
|||||||
num_keep = self.freqai_info["purge_old_models"]
|
num_keep = self.freqai_info["purge_old_models"]
|
||||||
if not num_keep:
|
if not num_keep:
|
||||||
return
|
return
|
||||||
elif type(num_keep) == bool:
|
elif isinstance(num_keep, bool):
|
||||||
num_keep = 2
|
num_keep = 2
|
||||||
|
|
||||||
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
||||||
@@ -635,7 +657,7 @@ class FreqaiDataDrawer:
|
|||||||
timeframe=tf,
|
timeframe=tf,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
data_format=self.config.get("dataformat_ohlcv", "json"),
|
data_format=self.config.get("dataformat_ohlcv", "feather"),
|
||||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,14 @@ class FreqaiDataKitchen:
|
|||||||
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
||||||
f" due to NaNs in populated dataset {len(unfiltered_df)}."
|
f" due to NaNs in populated dataset {len(unfiltered_df)}."
|
||||||
)
|
)
|
||||||
|
if len(unfiltered_df) == 0 and not self.live:
|
||||||
|
raise OperationalException(
|
||||||
|
f"{self.pair}: all training data dropped due to NaNs. "
|
||||||
|
"You likely did not download enough training data prior "
|
||||||
|
"to your backtest timerange. Hint:\n"
|
||||||
|
f"{DOCS_LINK}/freqai-running/"
|
||||||
|
"#downloading-data-to-cover-the-full-backtest-period"
|
||||||
|
)
|
||||||
if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live:
|
if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live:
|
||||||
worst_indicator = str(unfiltered_df.count().idxmin())
|
worst_indicator = str(unfiltered_df.count().idxmin())
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ class IFreqaiModel(ABC):
|
|||||||
:param metadata: pair metadata coming from strategy.
|
:param metadata: pair metadata coming from strategy.
|
||||||
:param strategy: Strategy to train on
|
:param strategy: Strategy to train on
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE)
|
self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE)
|
||||||
self.dd.set_pair_dict_info(metadata)
|
self.dd.set_pair_dict_info(metadata)
|
||||||
self.data_provider = strategy.dp
|
self.data_provider = strategy.dp
|
||||||
@@ -394,6 +393,11 @@ class IFreqaiModel(ABC):
|
|||||||
dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
|
dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not strategy.process_only_new_candles:
|
||||||
|
raise OperationalException("You are trying to use a FreqAI strategy with "
|
||||||
|
"process_only_new_candles = False. This is not supported "
|
||||||
|
"by FreqAI, and it is therefore aborting.")
|
||||||
|
|
||||||
# get the model metadata associated with the current pair
|
# get the model metadata associated with the current pair
|
||||||
(_, trained_timestamp) = self.dd.get_pair_dict_info(metadata["pair"])
|
(_, trained_timestamp) = self.dd.get_pair_dict_info(metadata["pair"])
|
||||||
|
|
||||||
@@ -453,7 +457,7 @@ class IFreqaiModel(ABC):
|
|||||||
pred_df, do_preds = self.predict(dataframe, dk)
|
pred_df, do_preds = self.predict(dataframe, dk)
|
||||||
if pair not in self.dd.historic_predictions:
|
if pair not in self.dd.historic_predictions:
|
||||||
self.set_initial_historic_predictions(pred_df, dk, pair, dataframe)
|
self.set_initial_historic_predictions(pred_df, dk, pair, dataframe)
|
||||||
self.dd.set_initial_return_values(pair, pred_df)
|
self.dd.set_initial_return_values(pair, pred_df, dataframe)
|
||||||
|
|
||||||
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
|
dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe)
|
||||||
return
|
return
|
||||||
@@ -645,11 +649,11 @@ class IFreqaiModel(ABC):
|
|||||||
If the user reuses an identifier on a subsequent instance,
|
If the user reuses an identifier on a subsequent instance,
|
||||||
this function will not be called. In that case, "real" predictions
|
this function will not be called. In that case, "real" predictions
|
||||||
will be appended to the loaded set of historic predictions.
|
will be appended to the loaded set of historic predictions.
|
||||||
:param df: DataFrame = the dataframe containing the training feature data
|
:param pred_df: DataFrame = the dataframe containing the predictions coming
|
||||||
:param model: Any = A model which was `fit` using a common library such as
|
out of a model
|
||||||
catboost or lightgbm
|
|
||||||
:param dk: FreqaiDataKitchen = object containing methods for data analysis
|
:param dk: FreqaiDataKitchen = object containing methods for data analysis
|
||||||
:param pair: str = current pair
|
:param pair: str = current pair
|
||||||
|
:param strat_df: DataFrame = dataframe coming from strategy
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.dd.historic_predictions[pair] = pred_df
|
self.dd.historic_predictions[pair] = pred_df
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ class PyTorchMLPClassifier(BasePyTorchClassifier):
|
|||||||
"model_training_parameters" : {
|
"model_training_parameters" : {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
"max_iters": 5000,
|
"n_steps": 5000,
|
||||||
"batch_size": 64,
|
"batch_size": 64,
|
||||||
"max_n_eval_batches": null,
|
"n_epochs": null,
|
||||||
},
|
},
|
||||||
"model_kwargs": {
|
"model_kwargs": {
|
||||||
"hidden_dim": 512,
|
"hidden_dim": 512,
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ class PyTorchMLPRegressor(BasePyTorchRegressor):
|
|||||||
"model_training_parameters" : {
|
"model_training_parameters" : {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
"max_iters": 5000,
|
"n_steps": 5000,
|
||||||
"batch_size": 64,
|
"batch_size": 64,
|
||||||
"max_n_eval_batches": null,
|
"n_epochs": null,
|
||||||
},
|
},
|
||||||
"model_kwargs": {
|
"model_kwargs": {
|
||||||
"hidden_dim": 512,
|
"hidden_dim": 512,
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
|||||||
"model_training_parameters" : {
|
"model_training_parameters" : {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
"max_iters": 5000,
|
"n_steps": 5000,
|
||||||
"batch_size": 64,
|
"batch_size": 64,
|
||||||
"max_n_eval_batches": null
|
"n_epochs": null
|
||||||
},
|
},
|
||||||
"model_kwargs": {
|
"model_kwargs": {
|
||||||
"hidden_dim": 512,
|
"hidden_dim": 512,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Type
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
import torch as th
|
import torch as th
|
||||||
|
from stable_baselines3.common.callbacks import ProgressBarCallback
|
||||||
|
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||||
@@ -73,19 +74,27 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
|||||||
'trained agent.')
|
'trained agent.')
|
||||||
model = self.dd.model_dictionary[dk.pair]
|
model = self.dd.model_dictionary[dk.pair]
|
||||||
model.set_env(self.train_env)
|
model.set_env(self.train_env)
|
||||||
|
callbacks: List[Any] = [self.eval_callback, self.tensorboard_callback]
|
||||||
|
progressbar_callback: Optional[ProgressBarCallback] = None
|
||||||
|
if self.rl_config.get('progress_bar', False):
|
||||||
|
progressbar_callback = ProgressBarCallback()
|
||||||
|
callbacks.insert(0, progressbar_callback)
|
||||||
|
|
||||||
model.learn(
|
try:
|
||||||
total_timesteps=int(total_timesteps),
|
model.learn(
|
||||||
callback=[self.eval_callback, self.tensorboard_callback],
|
total_timesteps=int(total_timesteps),
|
||||||
progress_bar=self.rl_config.get('progress_bar', False)
|
callback=callbacks,
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
if progressbar_callback:
|
||||||
|
progressbar_callback.on_training_end()
|
||||||
|
|
||||||
if Path(dk.data_path / "best_model.zip").is_file():
|
if Path(dk.data_path / "best_model.zip").is_file():
|
||||||
logger.info('Callback found a best model.')
|
logger.info('Callback found a best model.')
|
||||||
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
||||||
return best_model
|
return best_model
|
||||||
|
|
||||||
logger.info('Couldnt find best model, using final model instead.')
|
logger.info("Couldn't find best model, using final model instead.")
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user