Compare commits
766 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c079ccdfd | |||
| 5407c2efec | |||
| cee8855dca | |||
| 1196237f82 | |||
| 71e1c91148 | |||
| e9718e064f | |||
| 3e8bb29c9f | |||
| 2fd703fcc5 | |||
| 2a84b00dad | |||
| 711ebfea28 | |||
| 8cd2ee95f9 | |||
| 539937c0eb | |||
| fefd698e96 | |||
| 3512d788dc | |||
| 54c4dd1ce6 | |||
| daaff66ff0 | |||
| 8610f9883d | |||
| e503d4a9c0 | |||
| 1cf1d9e3d7 | |||
| 6b250f9dfe | |||
| 3d24ef66d8 | |||
| ef0fc13235 | |||
| 394ff1bbf6 | |||
| 5cdd087465 | |||
| 93051f4f97 | |||
| 1c2eed1421 | |||
| 532f5ab080 | |||
| e3ffe56549 | |||
| 25d4cabe3b | |||
| 7539b6ffda | |||
| 8e623ee64c | |||
| 22ddd9a7d4 | |||
| 128ea45fee | |||
| 2495f68751 | |||
| 70ee652be5 | |||
| bb1d34c33d | |||
| 8f6d64f5e1 | |||
| b40848373d | |||
| 2824bcff41 | |||
| e8b967d634 | |||
| 12d617cf8d | |||
| 48ab5ed990 | |||
| 3363b1062e | |||
| d35dde6757 | |||
| 6016d4886e | |||
| f77eaa4083 | |||
| 460d88ced7 | |||
| 6bb559f1b0 | |||
| 7c91eb0091 | |||
| ae672e1110 | |||
| 1c4bf67e29 | |||
| c4e671fdc3 | |||
| c5c5198fb3 | |||
| 634ec3c85f | |||
| d15aa4b337 | |||
| e160739a28 | |||
| 030e07030c | |||
| a50a15ebc6 | |||
| 514452c4ec | |||
| f091db3b0e | |||
| caf1d6e7f2 | |||
| 81f6637803 | |||
| f764f1fc10 | |||
| 9a8ef17c73 | |||
| 8fe582a7a7 | |||
| 5cf3a2256a | |||
| caf4ffbc0b | |||
| d156f36f99 | |||
| 4390324d22 | |||
| 8d7ef1929e | |||
| 08fb8e42f7 | |||
| eaf6abecc8 | |||
| b4796cbf73 | |||
| e042b2e07b | |||
| 9f021cbee6 | |||
| bf615e415b | |||
| c72f5b79fe | |||
| 51c1674663 | |||
| e445452d35 | |||
| 9fff38d0a3 | |||
| bdd69dbfbc | |||
| ec4767acd5 | |||
| 78c8054954 | |||
| 24f5096ec6 | |||
| 030bc8641b | |||
| c9403eb946 | |||
| 87b0a6d3f3 | |||
| c0d404edb5 | |||
| a7a16d1e1e | |||
| 1f21d5e2c2 | |||
| a531f8644d | |||
| 765a0b52cb | |||
| 4919d6944e | |||
| ac20596f10 | |||
| 5e79a93235 | |||
| 2e8decbb6d | |||
| 196e599dc8 | |||
| 485cad2dc6 | |||
| 61cbe0c4b1 | |||
| 5510e138db | |||
| c9e9f9bd82 | |||
| 768ba1baaf | |||
| ad25a60149 | |||
| 3fdcd6e8f1 | |||
| be0c0761e3 | |||
| 00dd3f05ac | |||
| c6f30989c9 | |||
| 4ab6fba9e7 | |||
| 3ef082e2d9 | |||
| cccf44cd68 | |||
| 506d665a43 | |||
| 8514e35ad0 | |||
| af0a3f9a8d | |||
| 2df1a93dfc | |||
| 75e39d3154 | |||
| 5d755e3ca0 | |||
| fbeb2e4789 | |||
| 6b288d46af | |||
| 394cdfcc15 | |||
| 28df02da82 | |||
| d4ee725747 | |||
| 6617d3bb46 | |||
| 2453e344f4 | |||
| 7f0e886cfd | |||
| e34ea393d9 | |||
| a00a5e52c4 | |||
| 6abc0a3fb9 | |||
| 7e4b9d7481 | |||
| e3f241acb7 | |||
| eee233f894 | |||
| 615095c780 | |||
| b22bce3dd8 | |||
| 9f1d9add18 | |||
| 36f2bef6b8 | |||
| 07906d2576 | |||
| 5c977c212b | |||
| 09b084e85a | |||
| aefc72554a | |||
| 33c2cc806a | |||
| 74aa17d534 | |||
| 4de1501c68 | |||
| dc12b3e750 | |||
| dacd09db8b | |||
| c63d0e167c | |||
| 6ccd98d795 | |||
| 52b0e64950 | |||
| 5e5137edc1 | |||
| 5d4edb5ec0 | |||
| 6dccab9bfe | |||
| 653c160d76 | |||
| 3f65ed452e | |||
| 09cdc7ceca | |||
| e98e6d8aee | |||
| f10223bf13 | |||
| 38b3062653 | |||
| 6063e4accb | |||
| 06e249f569 | |||
| d064bd4f34 | |||
| 765d2c332c | |||
| 8857c77645 | |||
| 1ff4e40824 | |||
| 09d59c79a7 | |||
| dd6ac4b196 | |||
| 67fe3682b5 | |||
| a07e6183bb | |||
| ac99537c25 | |||
| 734a6f0442 | |||
| da52ef8729 | |||
| c934535dc8 | |||
| 3e3f092f0c | |||
| 834aafb8f2 | |||
| df19c77bcf | |||
| 6edcda1e42 | |||
| fe8c367bce | |||
| a37edf8c62 | |||
| 09ef2c9dea | |||
| 42b1d8ecf8 | |||
| 32437a95f3 | |||
| 2e29d3523b | |||
| 530300a951 | |||
| 612a09634c | |||
| fd6a2456d0 | |||
| 1ad0e7e365 | |||
| 055ee5c88f | |||
| ae676d8e03 | |||
| 0dc665e45b | |||
| bc6ea16db8 | |||
| a1d0528adb | |||
| ecfa1fd5b5 | |||
| eaf76047e3 | |||
| 761d3f216e | |||
| e7d9ac2247 | |||
| 842e651928 | |||
| 091d1c2d55 | |||
| 47bf2d1f26 | |||
| 758392c206 | |||
| 647011bf0c | |||
| 580413b5f8 | |||
| c8c9e5167e | |||
| 2e8707059d | |||
| e0d9d01493 | |||
| 45bf1ace7d | |||
| 39a5328f5a | |||
| 9f82738c96 | |||
| f69f6ec8be | |||
| 8b3fb11f14 | |||
| b0f4c57fa8 | |||
| d759deae8f | |||
| b695b5a83c | |||
| b8883b790e | |||
| 423197cb23 | |||
| 38107664b5 | |||
| 42d3f557bc | |||
| 1a3c913176 | |||
| 83de41ad0e | |||
| 8cc4664823 | |||
| aaecbc2206 | |||
| e36df7eff5 | |||
| 95f58b2b8c | |||
| 8df0ce3fb5 | |||
| 4177809f71 | |||
| 96aa214fc5 | |||
| a3c1c8c1b4 | |||
| 2bf3494cbc | |||
| e3dd3478bb | |||
| 457af15b35 | |||
| 1eef1675cc | |||
| 89ef31b381 | |||
| c00472c42d | |||
| c0143b04c8 | |||
| d1c901ae94 | |||
| dd0aeaf380 | |||
| 3e8e250a65 | |||
| a34b580444 | |||
| e7ea9d09c9 | |||
| e4e11be331 | |||
| 9774c51a97 | |||
| 87ff8dc7bc | |||
| 2434ae5476 | |||
| e474294d55 | |||
| b081716df7 | |||
| e1af43b133 | |||
| 1ea6a99c48 | |||
| 79fc80957f | |||
| 937eedd8ca | |||
| 93ae45dcbb | |||
| 1adbd525bc | |||
| ced7465c1f | |||
| d635cb8516 | |||
| 2b578633ab | |||
| 824f555d48 | |||
| 999fd15bee | |||
| be07879a1e | |||
| ab543c2680 | |||
| 79aa0bce5a | |||
| 05a76c0297 | |||
| 15eeac3c48 | |||
| d8a84a7d6b | |||
| 863a5b7b81 | |||
| 4ad049aec3 | |||
| 03c16d034d | |||
| c1f8dce109 | |||
| 887f513438 | |||
| 12fbae7204 | |||
| a05cb8f1c5 | |||
| cffee80b0e | |||
| 0c0f2e43e8 | |||
| 35456a1f32 | |||
| b6a6bca41b | |||
| 102cc87618 | |||
| 1ec45dcf19 | |||
| f2fb1b7919 | |||
| 78c5b18937 | |||
| 86de1dee29 | |||
| 7afd3cb730 | |||
| b887e2ad21 | |||
| ea2350f1ed | |||
| 786c858452 | |||
| a6b643a33f | |||
| 0e5235e6dc | |||
| c93c97c2de | |||
| 271ba462b1 | |||
| 690918f8d3 | |||
| de5dd66512 | |||
| c8593b2307 | |||
| fc21757e40 | |||
| b6d0cde29f | |||
| f0dfc23a5f | |||
| c9dda3480d | |||
| 45804e1bef | |||
| 6dd5dc5d2c | |||
| a5db75d361 | |||
| 4c4bc2be9b | |||
| e9cc9bb38a | |||
| 3b6395ddb8 | |||
| fdcbb3c0d0 | |||
| b07ffbf721 | |||
| a7b7e10c09 | |||
| f18472479a | |||
| 18c37b9b8f | |||
| d28abc18ea | |||
| 80b50141ee | |||
| b15d49f9ae | |||
| 668635892c | |||
| 670a29d420 | |||
| fd0d089f43 | |||
| b5fb6888f0 | |||
| edc23dcd2f | |||
| 1dc299f6f4 | |||
| 99d2423534 | |||
| b8b8da2eb2 | |||
| 4237d9fd11 | |||
| b98a91ba4c | |||
| e74db3e024 | |||
| ead3282229 | |||
| dfd9e7e777 | |||
| 71362c9140 | |||
| 68114691c6 | |||
| 37a3bdd507 | |||
| 50389cbbf4 | |||
| a7e5d3e427 | |||
| 48d1106546 | |||
| 6452521002 | |||
| 52374b39f2 | |||
| 300a866aac | |||
| 9873a862e0 | |||
| 1c8bbc14b8 | |||
| afa1311ff8 | |||
| 3ecc9c30cb | |||
| a85bb040f2 | |||
| 841e480ff5 | |||
| a0e9b77f75 | |||
| 47d59f8ff4 | |||
| 676c48411b | |||
| 954950c8cb | |||
| 2520b7cd51 | |||
| f9d7f05d38 | |||
| 46d4ff3d36 | |||
| 9347d823fc | |||
| 5e8bd359c8 | |||
| bae8a55f81 | |||
| 56c672b6d2 | |||
| de3956f2f9 | |||
| ddd7009fa8 | |||
| 31891b25af | |||
| 187b19b1d6 | |||
| e55d8512e7 | |||
| 697912f244 | |||
| 2f3ad09cf5 | |||
| 523dd305ba | |||
| 2d87d74829 | |||
| 820791e889 | |||
| a130122cc6 | |||
| 3a58297a8d | |||
| 65d22f9f1c | |||
| ec5df87318 | |||
| cf1533c478 | |||
| fc34f8cad2 | |||
| 3fbb38730c | |||
| 6a327440e0 | |||
| f1c8d8325b | |||
| d2557a70d7 | |||
| bf76347751 | |||
| a99a49e2af | |||
| b7fc924e82 | |||
| ef55bf6800 | |||
| 25ffc76655 | |||
| 15b1cff9c2 | |||
| c1ba2936ec | |||
| f08c0c9870 | |||
| 3b5b8bd007 | |||
| 74067d454b | |||
| 16359d617d | |||
| ae9f83ed30 | |||
| 49626ca31c | |||
| 884f543fb7 | |||
| 29df7499c5 | |||
| 5e64416dfa | |||
| 1118032c1f | |||
| 277828b1d4 | |||
| 47dcf7bc93 | |||
| 75081ab046 | |||
| 571497e58b | |||
| d710c85cda | |||
| 978f9c804b | |||
| 31522c681e | |||
| a5ac8a95a7 | |||
| 35dab9b566 | |||
| 2b05a49671 | |||
| b79b5b6c32 | |||
| 78124cd025 | |||
| 7c4c789711 | |||
| 30b32a0d2e | |||
| d33768ecee | |||
| ccf20b2fe1 | |||
| 23bbbddcdb | |||
| c6ed64746c | |||
| 8f4dfbf55d | |||
| 5296a3be23 | |||
| c92c64bac2 | |||
| aaca904455 | |||
| 19b57ad87e | |||
| 583738040c | |||
| 6e38b72601 | |||
| 8bf8ccfe50 | |||
| e721a741ca | |||
| 19e9effae4 | |||
| 84100ca7ae | |||
| a6f94f7d24 | |||
| 0a32be3fc3 | |||
| 065203c3db | |||
| 758ae42092 | |||
| 6fdc0f1b22 | |||
| 7c9fe22b9f | |||
| 53aecfa856 | |||
| bf511f12d4 | |||
| 0eed655e7c | |||
| d2d283a4b2 | |||
| 85492c1084 | |||
| f07134bf66 | |||
| 7678a59b1f | |||
| daced63d00 | |||
| 002d8c4022 | |||
| 8c85448ed7 | |||
| 2236b08fc2 | |||
| 24f904efc4 | |||
| 3eaa862caf | |||
| 469587e656 | |||
| b24064d706 | |||
| c4a29a0178 | |||
| aedbe0d1d2 | |||
| c5e3f7d0ba | |||
| bc019d6b6d | |||
| f889061b95 | |||
| 1f52ff3f94 | |||
| d0c3b30289 | |||
| e77feafee0 | |||
| 1889a315a3 | |||
| cc998afb44 | |||
| 97f30cf13d | |||
| ccbc48b590 | |||
| dde6001ad0 | |||
| aae3ab23ac | |||
| 604e08382e | |||
| 2ecadabd92 | |||
| 17c7826756 | |||
| fe92df7842 | |||
| e6dd932436 | |||
| 8acc05bb2d | |||
| c39f6f892b | |||
| f13b51cf91 | |||
| e90c60c940 | |||
| 60029c2a8b | |||
| 14429d449c | |||
| cba9d6078c | |||
| 2dfad98ed6 | |||
| 9de9cac23a | |||
| d1fd7c3f6e | |||
| 8fd5b93127 | |||
| dafa5d4bac | |||
| d29cd5b9d9 | |||
| 60094c6581 | |||
| dd32e228ce | |||
| 81639c61a8 | |||
| ae1fcbcffb | |||
| 263d035181 | |||
| 8ecd7011cb | |||
| f7d42ba60d | |||
| 701cf47c4b | |||
| 315009a7f0 | |||
| 6e98a369de | |||
| fa69b7017b | |||
| e98f3cfedf | |||
| 5f4184536a | |||
| ac904c60c8 | |||
| 16defdbfdf | |||
| d010a050d3 | |||
| 7b541ff958 | |||
| 78a47920ef | |||
| e5a2011a85 | |||
| 640cab2ca8 | |||
| deb8bde078 | |||
| 79f0271720 | |||
| 5b8b80cf59 | |||
| a7ee45a0ba | |||
| 37cc949d94 | |||
| 5f59e1435e | |||
| e1f26aeb4e | |||
| b49dafc412 | |||
| 7c84059b3a | |||
| 10a0bf386b | |||
| 2936cb425d | |||
| e5a9c65e66 | |||
| 81fdff1039 | |||
| 5101b1767b | |||
| e90aa6abda | |||
| 1dd56df5d8 | |||
| 9bd8b7acf5 | |||
| 2a5a422079 | |||
| e92afb74c6 | |||
| 4421e54cde | |||
| b5938985f2 | |||
| 0601f15bd0 | |||
| 66020d250c | |||
| 909286c0f7 | |||
| 5efad94d3d | |||
| 1e24653e79 | |||
| c9bc2b880a | |||
| 545cd3a994 | |||
| 51a09585db | |||
| b98816635d | |||
| f6ed609134 | |||
| aec2dc5bb0 | |||
| cc807d1cf9 | |||
| 15c836c566 | |||
| 5f907a4b1a | |||
| a464a2fdbc | |||
| fa3d95fd89 | |||
| ba8778b181 | |||
| 6421f767c6 | |||
| f18c7f0cad | |||
| e63017a2b9 | |||
| 049cc10169 | |||
| 595db54ac7 | |||
| bfa9c818e6 | |||
| 30afedd4f8 | |||
| 6049ac1561 | |||
| 25c24ddc9c | |||
| 03df35b49b | |||
| 3b441cd08f | |||
| 0f67d32b75 | |||
| 820a83c96f | |||
| fae30c6ff9 | |||
| 3e4ffb9c1d | |||
| f6d10dad6a | |||
| 79b530d98e | |||
| 3b8e521b81 | |||
| 7e228ba7ab | |||
| 6050d09ed8 | |||
| b7f158a79e | |||
| bc13d1636c | |||
| 021fdcab43 | |||
| d206b601b3 | |||
| 949c7318f4 | |||
| 3666362479 | |||
| f27540d221 | |||
| 31dc0fe327 | |||
| 1125c60f07 | |||
| 9f5fd574b4 | |||
| 74eb0d9f88 | |||
| 52c78966ef | |||
| 1641f56956 | |||
| d27981f8cf | |||
| 6031ccabac | |||
| c64b6b0d69 | |||
| 4203693428 | |||
| c76c26cc34 | |||
| 1c87748b99 | |||
| 4ae80423e0 | |||
| 4e6ae88557 | |||
| 102c33d68b | |||
| 12a72fa161 | |||
| 3a5b555f1e | |||
| 8f2ee28395 | |||
| e57d0e3bc4 | |||
| f5205da459 | |||
| bd773b9baf | |||
| 7b4a6a48ee | |||
| 24a3a29360 | |||
| 6a279043fa | |||
| 60764500db | |||
| ba5a64aa34 | |||
| f40576091a | |||
| de3eedb39e | |||
| fded8bf43e | |||
| 5a45b4a3e1 | |||
| 0b3ea4d0ed | |||
| 822dce09b7 | |||
| 2ebc5374f4 | |||
| 3465f35d75 | |||
| 2165c3e542 | |||
| 5e059660d4 | |||
| 5ca69687dc | |||
| d042085b24 | |||
| effd989796 | |||
| ca85deafb8 | |||
| d46cc552be | |||
| 1dc2bcb47e | |||
| d6bf20b7b4 | |||
| 2d842ea129 | |||
| 76d5423eee | |||
| 489b7e6d18 | |||
| 18dfb0fe39 | |||
| 63efed736c | |||
| 4c568e40c2 | |||
| b54d231b79 | |||
| 58f361f8b3 | |||
| f78d383448 | |||
| 1be9a79e96 | |||
| d9e98bb687 | |||
| 7fd7061304 | |||
| db50d2b85d | |||
| d2a1e52d79 | |||
| 70e78e6521 | |||
| 913148bf1d | |||
| 0ecb3e45f6 | |||
| 74f870c435 | |||
| 8f62462e15 | |||
| 38183fe952 | |||
| 215b111cff | |||
| 4ea9d70743 | |||
| 0893350285 | |||
| 27bd34c3be | |||
| ebf6ca1a5f | |||
| a91ed0b4d9 | |||
| 097b27208d | |||
| 136623b3f5 | |||
| 02648e29c4 | |||
| beb6d41360 | |||
| a1207eca56 | |||
| 1a247a1035 | |||
| 4e0203d4af | |||
| 332518c1a0 | |||
| 15353f44f0 | |||
| 093cdae39c | |||
| 5dd5b95d6a | |||
| 20e0b542e3 | |||
| 1d24f9047c | |||
| f2c37e2575 | |||
| 95fbe04225 | |||
| 43c5b8d0ed | |||
| 301d85ef4f | |||
| 48f3d612e9 | |||
| c53c70d597 | |||
| aa89ce80ab | |||
| 971dfef0ee | |||
| 5d8a3cdb31 | |||
| 5246eecaf6 | |||
| 18b6f374d6 | |||
| 95c6997209 | |||
| ca79b84e4a | |||
| 3670df8771 | |||
| ff06d58ace | |||
| 1b4592a2f3 | |||
| 5138e97b70 | |||
| 340cad3707 | |||
| ca32cb9e61 | |||
| c911b7fa70 | |||
| 9323b6b946 | |||
| 92ef1ef761 | |||
| e38504b9db | |||
| e437847916 | |||
| a3a30f9046 | |||
| df507e2335 | |||
| 2f1f60a8fc | |||
| 1c1a165ba4 | |||
| 00fe02c854 | |||
| 6ff26b075e | |||
| 03a46ec784 | |||
| ec31764aed | |||
| 1ba411cccf | |||
| 7957231ce4 | |||
| 6b88c49e0a | |||
| 4fe99e0333 | |||
| 9a38dfcc26 | |||
| 3ecbfeb1d5 | |||
| 02229c959f | |||
| 2a2f205fbe | |||
| c415dab846 | |||
| 5b47960567 | |||
| 3f9749b03b | |||
| 528b11af07 | |||
| 0a31ff3747 | |||
| e1ee566634 | |||
| 1cff4d13de | |||
| 22b1bc7210 | |||
| c741c168e8 | |||
| c997f02085 | |||
| ead6d89e35 | |||
| 985c15bdd9 | |||
| 7fb7c43e45 | |||
| 0e68de0c6d | |||
| ba5f771e65 | |||
| e0f147e076 | |||
| b874ceaf2f | |||
| 11f339f802 | |||
| 9d46fd21dc | |||
| 286f385f99 | |||
| 69cca13d15 | |||
| 37d03a5b44 | |||
| 0e1d3e279c | |||
| ae671c736e | |||
| 28064d013c | |||
| 5b9c03e7e2 | |||
| f2569e36e8 | |||
| bf2092977c | |||
| 613e31a9a5 | |||
| 326a4f17d2 | |||
| a8d617d747 | |||
| ba9f795df5 | |||
| dbccc69f05 | |||
| db80318946 | |||
| 5698793dcc | |||
| bf96be2b71 | |||
| 2af454333b | |||
| cb869a2e4d | |||
| afc5205bea | |||
| 842f3a2a36 | |||
| 582871b0d5 | |||
| c6c2934741 | |||
| d3b5be6cb6 | |||
| 473a15c4ef | |||
| 2c4452453a | |||
| da6affcd6a | |||
| 3ebbe2b56c | |||
| 8f13a5db29 | |||
| 3155dfc14c | |||
| cc30e71ab2 | |||
| f5cb486f5e | |||
| 42c8795141 | |||
| f878d051df | |||
| aefa7e401c | |||
| dc06510d4a | |||
| 7740b61f02 | |||
| 950f5a722e | |||
| 9b56245b1d | |||
| b05fb57fb7 | |||
| 1a4db77131 | |||
| 71920cefb1 | |||
| b5cbbd7c30 | |||
| f75a0817a6 | |||
| d12132ae7a | |||
| a81bec0da2 | |||
| cc41cb831f | |||
| 2b70ec896d | |||
| 470dbb6060 | |||
| afc61ca788 | |||
| ba62e5e007 | |||
| 9af401b091 | |||
| 1fdbace02d | |||
| cfeda4493b | |||
| 0de48d90f8 | |||
| c4eb26be9d | |||
| 04a8539f10 | |||
| d600461a84 | |||
| ce46cd7343 | |||
| d1cca4ac51 | |||
| eb5d5e0d3f | |||
| 77dcc5354c | |||
| 3ac4a872b1 | |||
| b8b94d58c4 | |||
| 33b5482065 | |||
| 43fea43363 | |||
| 168593d7af | |||
| 29ae71333a | |||
| 7a84d1ecba | |||
| bc78e2e1b9 | |||
| 3fb6bc888b | |||
| 0228ec796b | |||
| 69de7b4045 | |||
| 825c059c2e | |||
| 8e8f4dbdd3 | |||
| 865c253922 | |||
| cd6e06c86e | |||
| 422f225e8e | |||
| 9921c54882 |
@@ -1,5 +1,10 @@
|
||||
<!-- Thank you for sending your pull request. But first, have you included
|
||||
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
|
||||
Did you use AI to create your changes?
|
||||
If so, please state it clearly in the PR description (failing to do so may result in your PR being closed).
|
||||
|
||||
Also, please do a self review of the changes made before submitting the PR to make sure only relevant changes are included.
|
||||
-->
|
||||
## Summary
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ updates:
|
||||
mkdocs:
|
||||
patterns:
|
||||
- "mkdocs*"
|
||||
scipy:
|
||||
patterns:
|
||||
- "scipy"
|
||||
- "scipy-stubs"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
environment:
|
||||
name: develop
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
+44
-169
@@ -25,10 +25,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "ubuntu-22.04", "ubuntu-24.04" ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -47,24 +47,9 @@ jobs:
|
||||
cache-suffix: "${{ matrix.python-version }}"
|
||||
prune-cache: false
|
||||
|
||||
- name: Cache_dependencies
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: ~/dependencies/
|
||||
key: ${{ runner.os }}-dependencies
|
||||
|
||||
- name: TA binary *nix
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||
|
||||
- name: Installation - *nix
|
||||
run: |
|
||||
uv pip install --upgrade wheel
|
||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
uv pip install -r requirements-dev.txt
|
||||
uv pip install -e ft_client/
|
||||
uv pip install -e .
|
||||
@@ -90,6 +75,7 @@ jobs:
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
run: |
|
||||
# Allow failure for coveralls
|
||||
uv pip install coveralls
|
||||
coveralls || true
|
||||
|
||||
- name: Run json schema extract
|
||||
@@ -103,6 +89,8 @@ jobs:
|
||||
python build_helpers/create_command_partials.py
|
||||
|
||||
- name: Check for repository changes
|
||||
# TODO: python 3.13 slightly changed the output of argparse.
|
||||
if: (matrix.python-version != '3.13')
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Repository is dirty, changes detected:"
|
||||
@@ -145,7 +133,7 @@ jobs:
|
||||
mypy freqtrade scripts tests
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -156,11 +144,11 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "macos-13", "macos-14", "macos-15" ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
os: [ "macos-14", "macos-15" ]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -171,7 +159,7 @@ jobs:
|
||||
check-latest: true
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -180,18 +168,6 @@ jobs:
|
||||
cache-suffix: "${{ matrix.python-version }}"
|
||||
prune-cache: false
|
||||
|
||||
- name: Cache_dependencies
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: ~/dependencies/
|
||||
key: ${{ matrix.os }}-dependencies
|
||||
|
||||
- name: TA binary *nix
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||
|
||||
- name: Installation - macOS (Brew)
|
||||
run: |
|
||||
# brew update
|
||||
@@ -219,9 +195,6 @@ jobs:
|
||||
- name: Installation (python)
|
||||
run: |
|
||||
uv pip install wheel
|
||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
uv pip install -r requirements-dev.txt
|
||||
uv pip install -e ft_client/
|
||||
uv pip install -e .
|
||||
@@ -272,7 +245,7 @@ jobs:
|
||||
mypy freqtrade scripts
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: info
|
||||
@@ -284,11 +257,11 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
os: [ "windows-2022", "windows-2025" ]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -298,7 +271,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -312,7 +285,9 @@ jobs:
|
||||
function uvpipFunction { uv pip $args }
|
||||
Set-Alias -name pip -value uvpipFunction
|
||||
|
||||
./build_helpers/install_windows.ps1
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-dev.txt
|
||||
pip install -e .
|
||||
|
||||
- name: Tests
|
||||
run: |
|
||||
@@ -366,7 +341,7 @@ jobs:
|
||||
shell: powershell
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -376,7 +351,7 @@ jobs:
|
||||
mypy-version-check:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -393,7 +368,7 @@ jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -405,7 +380,7 @@ jobs:
|
||||
docs-check:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -424,7 +399,7 @@ jobs:
|
||||
mkdocs build
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: error
|
||||
@@ -436,7 +411,7 @@ jobs:
|
||||
# Run pytest with "live" checks
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -446,7 +421,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
with:
|
||||
activate-environment: true
|
||||
enable-cache: true
|
||||
@@ -455,25 +430,9 @@ jobs:
|
||||
cache-suffix: "3.12"
|
||||
prune-cache: false
|
||||
|
||||
- name: Cache_dependencies
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: ~/dependencies/
|
||||
key: ${{ runner.os }}-dependencies
|
||||
|
||||
|
||||
- name: TA binary *nix
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||
|
||||
- name: Installation - *nix
|
||||
run: |
|
||||
uv pip install --upgrade wheel
|
||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
uv pip install -r requirements-dev.txt
|
||||
uv pip install -e ft_client/
|
||||
uv pip install -e .
|
||||
@@ -505,15 +464,16 @@ jobs:
|
||||
|
||||
- name: Check user permission
|
||||
id: check
|
||||
uses: scherermichael-oss/action-has-permission@136e061bfe093832d87f090dd768e14e27a740d3 # 1.0.6
|
||||
continue-on-error: true
|
||||
uses: prince-chrismc/check-actor-permissions-action@d504e74ba31658f4cdf4fcfeb509d4c09736d88e # v3.0.2
|
||||
with:
|
||||
required-permission: write
|
||||
permission: "write"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: steps.check.outputs.permitted == 'true' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
severity: info
|
||||
details: Test Completed!
|
||||
@@ -525,7 +485,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -572,12 +532,12 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: freqtrade*-build
|
||||
path: dist
|
||||
@@ -601,12 +561,12 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download artifact 📦
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: freqtrade*-build
|
||||
path: dist
|
||||
@@ -616,100 +576,15 @@ jobs:
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
|
||||
|
||||
deploy-docker:
|
||||
docker-build:
|
||||
name: "Docker Build and Deploy"
|
||||
needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
run: |
|
||||
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||
echo "branch=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
# We need docker experimental to pull the ARM image.
|
||||
- name: Switch docker to experimental
|
||||
run: |
|
||||
docker version -f '{{.Server.Experimental}}'
|
||||
echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
docker version -f '{{.Server.Experimental}}'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 #v3.10.0
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${PLATFORMS}
|
||||
env:
|
||||
PLATFORMS: ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||
run: |
|
||||
build_helpers/publish_docker_multi.sh
|
||||
|
||||
deploy-arm:
|
||||
name: "Deploy Docker"
|
||||
uses: ./.github/workflows/docker-build.yml
|
||||
permissions:
|
||||
packages: write
|
||||
needs: [ deploy-docker ]
|
||||
# Only run on 64bit machines
|
||||
runs-on: [self-hosted, linux, ARM64]
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
run: |
|
||||
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||
echo "branch=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
build_helpers/publish_docker_arm64.sh
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@1399c1b2d57cc05894d506d2cfdc33c5f012b993 #v1.1.1
|
||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
||||
with:
|
||||
severity: info
|
||||
details: Deploy Succeeded!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
contents: read
|
||||
secrets:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
name: Deploy Docs through mike
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ jobs:
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
name: Docker Build and Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DISCORD_WEBHOOK:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch_name:
|
||||
description: 'Branch name to build Docker images for'
|
||||
required: false
|
||||
default: 'develop'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-docker:
|
||||
name: "Deploy Docker x64 and armv7l"
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
env:
|
||||
BRANCH_NAME_INPUT: ${{ github.event.inputs.branch_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
BRANCH_NAME="${BRANCH_NAME_INPUT}"
|
||||
else
|
||||
BRANCH_NAME="${GITHUB_REF##*/}"
|
||||
fi
|
||||
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||
echo "branch=${BRANCH_NAME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${PLATFORMS}
|
||||
env:
|
||||
PLATFORMS: ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||
run: |
|
||||
build_helpers/publish_docker_multi.sh
|
||||
|
||||
deploy-arm:
|
||||
name: "Deploy Docker ARM64"
|
||||
permissions:
|
||||
packages: write
|
||||
needs: [ deploy-docker ]
|
||||
# Only run on 64bit machines
|
||||
runs-on: [self-hosted, linux, ARM64]
|
||||
if: github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract-branch
|
||||
env:
|
||||
BRANCH_NAME_INPUT: ${{ github.event.inputs.branch_name }}
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
BRANCH_NAME="${BRANCH_NAME_INPUT}"
|
||||
else
|
||||
BRANCH_NAME="${GITHUB_REF##*/}"
|
||||
fi
|
||||
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||
echo "branch=${BRANCH_NAME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
build_helpers/publish_docker_arm64.sh
|
||||
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
||||
with:
|
||||
severity: info
|
||||
details: Deploy Succeeded!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
auto-update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- stable
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
# contents: read # only needed for private repos
|
||||
# actions: read # only needed for private repos
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@5ca5fc7a4779c5263a3ffa0e1f693009994446d1 # v0.1.2
|
||||
+11
-10
@@ -14,24 +14,25 @@ repos:
|
||||
additional_dependencies: ["python-rapidjson", "jsonschema"]
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: "7.2.0"
|
||||
rev: "7.3.0"
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [Flake8-pyproject]
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.15.0"
|
||||
rev: "v1.17.1"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==6.0.0.20250525
|
||||
- types-cachetools==6.1.0.20250717
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.0.20250515
|
||||
- types-requests==2.32.4.20250809
|
||||
- types-tabulate==0.9.0.20241207
|
||||
- types-python-dateutil==2.9.0.20250516
|
||||
- SQLAlchemy==2.0.41
|
||||
- types-python-dateutil==2.9.0.20250822
|
||||
- scipy-stubs==1.16.1.1
|
||||
- SQLAlchemy==2.0.43
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -43,13 +44,13 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.11.11'
|
||||
rev: 'v0.12.10'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
exclude: |
|
||||
@@ -69,7 +70,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/stefmolin/exif-stripper
|
||||
rev: 0.6.2
|
||||
rev: 1.1.0
|
||||
hooks:
|
||||
- id: strip-exif
|
||||
|
||||
@@ -82,6 +83,6 @@ repos:
|
||||
|
||||
# Ensure github actions remain safe
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.8.0
|
||||
rev: v1.12.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
+10
-15
@@ -1,10 +1,10 @@
|
||||
FROM python:3.12.10-slim-bookworm as base
|
||||
FROM python:3.13.7-slim-bookworm AS base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONFAULTHANDLER 1
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONFAULTHANDLER=1
|
||||
ENV PATH=/home/ftuser/.local/bin:$PATH
|
||||
ENV FT_APP_ENV="docker"
|
||||
|
||||
@@ -21,27 +21,22 @@ RUN mkdir /freqtrade \
|
||||
WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
FROM base as python-deps
|
||||
FROM base AS python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip wheel
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir "numpy<2.0" \
|
||||
RUN pip install --user --no-cache-dir "numpy<3.0" \
|
||||
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
||||
|
||||
# Copy dependencies to runtime-image
|
||||
FROM base as runtime-image
|
||||
FROM base AS runtime-image
|
||||
COPY --from=python-deps /usr/local/lib /usr/local/lib
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib
|
||||
|
||||
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
|
||||
|
||||
@@ -49,7 +44,7 @@ USER ftuser
|
||||
# Install and execute
|
||||
COPY --chown=ftuser:ftuser . /freqtrade/
|
||||
|
||||
RUN pip install -e . --user --no-cache-dir --no-build-isolation \
|
||||
RUN pip install -e . --user --no-cache-dir \
|
||||
&& mkdir /freqtrade/user_data/ \
|
||||
&& freqtrade install-ui
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://www.freqtrade.io)
|
||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||
|
||||
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||
|
||||
@@ -64,13 +63,12 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||
|
||||
## Features
|
||||
|
||||
- [x] **Based on Python 3.10+**: For botting on any operating system - Windows, macOS and Linux.
|
||||
- [x] **Based on Python 3.11+**: For botting on any operating system - Windows, macOS and Linux.
|
||||
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||
- [x] **Dry-run**: Run the bot without paying money.
|
||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||
- [X] **Adaptive prediction modeling**: Build a smart strategy with FreqAI that self-trains to the market via adaptive machine learning methods. [Learn more](https://www.freqtrade.io/en/stable/freqai/)
|
||||
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
|
||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||
- [x] **Builtin WebUI**: Builtin web UI to manage your bot.
|
||||
@@ -112,7 +110,6 @@ positional arguments:
|
||||
backtesting-show Show past Backtest results
|
||||
backtesting-analysis
|
||||
Backtest Analysis module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
hyperopt-list List Hyperopt results
|
||||
hyperopt-show Show details of Hyperopt results
|
||||
@@ -148,6 +145,8 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
|
||||
- `/stopentry`: Stop entering new trades.
|
||||
- `/status <trade_id>|[table]`: Lists all or specific open trades.
|
||||
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
|
||||
- `/profit_long [<n>]`: Lists cumulative profit from all finished long trades, over the last n days.
|
||||
- `/profit_short [<n>]`: Lists cumulative profit from all finished short trades, over the last n days.
|
||||
- `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||
- `/fx <trade_id>|all`: Alias to `/forceexit`
|
||||
- `/performance`: Show performance of each finished trade grouped by pair
|
||||
@@ -156,6 +155,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
|
||||
- `/help`: Show help message.
|
||||
- `/version`: Show version.
|
||||
|
||||
|
||||
## Development branches
|
||||
|
||||
The project is currently setup in two main branches:
|
||||
@@ -221,7 +221,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||
|
||||
### Software requirements
|
||||
|
||||
- [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [Python >= 3.11](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
if [ -z "$1" ]; then
|
||||
INSTALL_LOC=/usr/local
|
||||
else
|
||||
INSTALL_LOC=${1}
|
||||
fi
|
||||
echo "Installing to ${INSTALL_LOC}"
|
||||
if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||
cd ta-lib \
|
||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||
&& echo "Downloading gcc config.guess and config.sub" \
|
||||
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
|
||||
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
|
||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||
&& make
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed building ta-lib."
|
||||
cd .. && rm -rf ./ta-lib/
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$2" ]; then
|
||||
which sudo && sudo make install || make install
|
||||
if [ -x "$(command -v apt-get)" ]; then
|
||||
echo "Updating library path using ldconfig"
|
||||
sudo ldconfig
|
||||
fi
|
||||
else
|
||||
# Don't install with sudo
|
||||
make install
|
||||
fi
|
||||
|
||||
cd .. && rm -rf ./ta-lib/
|
||||
else
|
||||
echo "TA-lib already installed, skipping installation"
|
||||
fi
|
||||
@@ -1,10 +0,0 @@
|
||||
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
|
||||
|
||||
python -m pip install --upgrade pip
|
||||
python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
pip install -U wheel "numpy<2"
|
||||
pip install --only-binary ta-lib --find-links=build_helpers\ ta-lib
|
||||
|
||||
pip install -r requirements-dev.txt
|
||||
pip install -e .
|
||||
@@ -16,10 +16,12 @@ with require_dev.open("r") as rfile:
|
||||
with require.open("r") as rfile:
|
||||
requirements.extend(rfile.readlines())
|
||||
|
||||
# Extract types only
|
||||
type_reqs = [
|
||||
r.strip("\n") for r in requirements if r.startswith("types-") or r.startswith("SQLAlchemy")
|
||||
]
|
||||
# Extract relevant types only
|
||||
supported = ("types-", "SQLAlchemy", "scipy-stubs")
|
||||
|
||||
# Find relevant dependencies
|
||||
# Only keep the first part of the line up to the first space
|
||||
type_reqs = [r.strip("\n").split()[0] for r in requirements if r.startswith(supported)]
|
||||
|
||||
with pre_commit_file.open("r") as file:
|
||||
f = yaml.load(file, Loader=yaml.SafeLoader)
|
||||
|
||||
BIN
Binary file not shown.
+16
-51
@@ -180,6 +180,16 @@
|
||||
"description": "Offset for profit exit. \nUsually specified in the strategy and missing in the configuration.",
|
||||
"type": "number"
|
||||
},
|
||||
"recursive_strategy_search": {
|
||||
"description": "Enable recursive strategy search.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"user_data_dir": {
|
||||
"description": "Path to the user data directory."
|
||||
},
|
||||
"datadir": {
|
||||
"description": "Path to the data directory."
|
||||
},
|
||||
"fee": {
|
||||
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
|
||||
"type": "number",
|
||||
@@ -538,10 +548,6 @@
|
||||
"description": "Exchange configuration.",
|
||||
"$ref": "#/definitions/exchange"
|
||||
},
|
||||
"edge": {
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge"
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging"
|
||||
@@ -566,6 +572,7 @@
|
||||
"pairlists": {
|
||||
"description": "Configuration for pairlists.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1247,7 +1254,11 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ccxt_async_config": {
|
||||
"description": "CCXT asynchronous configuration settings.",
|
||||
"description": "CCXT asynchronous configuration settings.Usually ccxt_config should be used instead.",
|
||||
"type": "object"
|
||||
},
|
||||
"ccxt_sync_config": {
|
||||
"description": "CCXT synchronous configuration settings. Usually ccxt_config should be used instead.",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
@@ -1255,52 +1266,6 @@
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"edge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"process_throttle_secs": {
|
||||
"type": "integer",
|
||||
"minimum": 600
|
||||
},
|
||||
"calculate_since_number_of_days": {
|
||||
"type": "integer"
|
||||
},
|
||||
"allowed_risk": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_min": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_max": {
|
||||
"type": "number"
|
||||
},
|
||||
"stoploss_range_step": {
|
||||
"type": "number"
|
||||
},
|
||||
"minimum_winrate": {
|
||||
"type": "number"
|
||||
},
|
||||
"minimum_expectancy": {
|
||||
"type": "number"
|
||||
},
|
||||
"min_trade_number": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_trade_duration_minute": {
|
||||
"type": "integer"
|
||||
},
|
||||
"remove_pumps": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"process_throttle_secs",
|
||||
"allowed_risk"
|
||||
]
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": 0.05,
|
||||
"stake_amount": 30,
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"timeframe": "5m",
|
||||
|
||||
@@ -121,20 +121,6 @@
|
||||
"outdated_offset": 5,
|
||||
"markets_refresh_interval": 60
|
||||
},
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
"calculate_since_number_of_days": 7,
|
||||
"allowed_risk": 0.01,
|
||||
"stoploss_range_min": -0.01,
|
||||
"stoploss_range_max": -0.1,
|
||||
"stoploss_range_step": -0.01,
|
||||
"minimum_winrate": 0.60,
|
||||
"minimum_expectancy": 0.20,
|
||||
"min_trade_number": 10,
|
||||
"max_trade_duration_minute": 1440,
|
||||
"remove_pumps": false
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"token": "your_telegram_token",
|
||||
|
||||
+10
-10
@@ -1,10 +1,10 @@
|
||||
FROM python:3.11.12-slim-bookworm as base
|
||||
FROM python:3.11.13-slim-bookworm AS base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONFAULTHANDLER 1
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONFAULTHANDLER=1
|
||||
ENV PATH=/home/ftuser/.local/bin:$PATH
|
||||
ENV FT_APP_ENV="docker"
|
||||
|
||||
@@ -22,7 +22,7 @@ RUN mkdir /freqtrade \
|
||||
WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
FROM base as python-deps
|
||||
FROM base AS python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
@@ -34,14 +34,14 @@ COPY build_helpers/* /tmp/
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir "numpy<2" \
|
||||
RUN pip install --user --prefer-binary --no-cache-dir "numpy<3.0" build \
|
||||
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib \
|
||||
&& pip install --user --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy dependencies to runtime-image
|
||||
FROM base as runtime-image
|
||||
FROM base AS runtime-image
|
||||
COPY --from=python-deps /usr/local/lib /usr/local/lib
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib
|
||||
|
||||
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
|
||||
|
||||
@@ -49,7 +49,7 @@ USER ftuser
|
||||
# Install and execute
|
||||
COPY --chown=ftuser:ftuser . /freqtrade/
|
||||
|
||||
RUN pip install -e . --user --no-cache-dir --no-build-isolation\
|
||||
RUN pip install -e . --user --no-cache-dir \
|
||||
&& mkdir /freqtrade/user_data/ \
|
||||
&& freqtrade install-ui
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
FROM freqtradeorg/freqtrade:develop
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements-dev.txt /freqtrade/
|
||||
|
||||
RUN pip install numpy --user --no-cache-dir \
|
||||
&& pip install -r requirements-dev.txt --user --no-cache-dir
|
||||
|
||||
# Empty the ENTRYPOINT to allow all commands
|
||||
ENTRYPOINT []
|
||||
@@ -46,29 +46,32 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
|
||||
|
||||
More options are available by running with the `-h` option.
|
||||
|
||||
### Using export-filename
|
||||
### Using backtest-filename
|
||||
|
||||
Normally, `backtesting-analysis` uses the latest backtest results, but if you wanted to go
|
||||
back to a previous backtest output, you need to supply the `--export-filename` option.
|
||||
You can supply the same parameter to `backtest-analysis` with the name of the final backtest
|
||||
output file. This allows you to keep historical versions of backtest results and re-analyse
|
||||
them at a later date:
|
||||
By default, `backtesting-analysis` processes the most recent backtest results in the `user_data/backtest_results` directory.
|
||||
If you want to analyze results from an earlier backtest, use the `--backtest-filename` option to specify the desired file. This lets you revisit and re-analyze historical backtest outputs at any time by providing the filename of the relevant backtest result:
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange=<timerange> --export=signals --export-filename=/tmp/mystrat_backtest.json
|
||||
freqtrade backtesting-analysis -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||
```
|
||||
|
||||
You should see some output similar to below in the logs with the name of the timestamped
|
||||
filename that was exported:
|
||||
|
||||
```
|
||||
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "/tmp/mystrat_backtest-2022-06-14_16-28-32.json"
|
||||
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "mystrat_backtest-2022-06-14_16-28-32.json"
|
||||
```
|
||||
|
||||
You can then use that filename in `backtesting-analysis`:
|
||||
|
||||
```
|
||||
freqtrade backtesting-analysis -c <config.json> --export-filename=/tmp/mystrat_backtest-2022-06-14_16-28-32.json
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-filename=mystrat_backtest-2022-06-14_16-28-32.json
|
||||
```
|
||||
|
||||
To use a result from a different results directory, you can use `--backtest-directory` to specify the directory
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename mystrat_backtest-2022-06-14_16-28-32.json
|
||||
```
|
||||
|
||||
### Tuning the buy tags and sell tags to display
|
||||
|
||||
+243
-203
@@ -5,6 +5,8 @@ This page explains how to validate your strategy performance by using Backtestin
|
||||
Backtesting 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.
|
||||
|
||||
Backtesting is also available in [webserver mode](freq-ui.md#backtesting), which allows you to run backtests via the web interface.
|
||||
|
||||
## Backtesting command reference
|
||||
|
||||
--8<-- "commands/backtesting.md"
|
||||
@@ -103,12 +105,14 @@ Only use this if you're sure you'll not want to plot or analyze your results fur
|
||||
|
||||
---
|
||||
|
||||
Exporting trades to file specifying a custom filename
|
||||
Exporting trades to file specifying a custom directory
|
||||
|
||||
```bash
|
||||
freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json
|
||||
freqtrade backtesting --strategy backtesting --export trades --backtest-directory=user_data/custom-backtest-results
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
|
||||
|
||||
---
|
||||
@@ -156,117 +160,136 @@ The most important in the backtesting is to understand the result.
|
||||
A backtesting result will look like that:
|
||||
|
||||
```
|
||||
================================================ BACKTESTING REPORT =================================================
|
||||
| Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|
||||
|----------+--------+----------------+------------------+----------------+--------------+--------------------------|
|
||||
| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
|
||||
| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
|
||||
| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
|
||||
| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
|
||||
| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
|
||||
| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
|
||||
| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
|
||||
| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
|
||||
| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
|
||||
| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
|
||||
| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
|
||||
| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
|
||||
| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
|
||||
| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
|
||||
| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
|
||||
| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
|
||||
| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
||||
| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
||||
| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||
============================================= LEFT OPEN TRADES REPORT =============================================
|
||||
| Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
||||
|----------+---------+----------------+------------------+----------------+----------------+---------------------|
|
||||
| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||
| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||
| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||
==================== EXIT REASON STATS ====================
|
||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||
|--------------------+---------+-------+--------+---------|
|
||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||
| stop_loss | 166 | 0 | 0 | 166 |
|
||||
| exit_signal | 56 | 36 | 0 | 20 |
|
||||
| force_exit | 2 | 0 | 0 | 2 |
|
||||
BACKTESTING REPORT
|
||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ LTC/USDT:USDT │ 16 │ 1.0 │ 56.176 │ 5.62 │ 16:16:00 │ 16 0 0 100 │
|
||||
│ ETC/USDT:USDT │ 12 │ 0.72 │ 30.936 │ 3.09 │ 9:55:00 │ 11 0 1 91.7 │
|
||||
│ ETH/USDT:USDT │ 8 │ 0.66 │ 17.864 │ 1.79 │ 1 day, 13:55:00 │ 7 0 1 87.5 │
|
||||
│ XLM/USDT:USDT │ 10 │ 0.31 │ 11.054 │ 1.11 │ 12:08:00 │ 9 0 1 90.0 │
|
||||
│ BTC/USDT:USDT │ 8 │ 0.21 │ 7.289 │ 0.73 │ 3 days, 1:24:00 │ 6 0 2 75.0 │
|
||||
│ XRP/USDT:USDT │ 9 │ -0.14 │ -7.261 │ -0.73 │ 21:18:00 │ 8 0 1 88.9 │
|
||||
│ DOT/USDT:USDT │ 6 │ -0.4 │ -9.187 │ -0.92 │ 5:35:00 │ 4 0 2 66.7 │
|
||||
│ ADA/USDT:USDT │ 8 │ -1.76 │ -52.098 │ -5.21 │ 11:38:00 │ 6 0 2 75.0 │
|
||||
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
LEFT OPEN TRADES REPORT
|
||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ BTC/USDT:USDT │ 1 │ -4.14 │ -9.930 │ -0.99 │ 17 days, 8:00:00 │ 0 0 1 0 │
|
||||
│ ETC/USDT:USDT │ 1 │ -4.24 │ -15.365 │ -1.54 │ 10:40:00 │ 0 0 1 0 │
|
||||
│ DOT/USDT:USDT │ 1 │ -5.29 │ -19.125 │ -1.91 │ 11:30:00 │ 0 0 1 0 │
|
||||
│ TOTAL │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
|
||||
ENTER TAG STATS
|
||||
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Enter Tag ┃ Entries ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ OTHER │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────┴─────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┘
|
||||
EXIT REASON STATS
|
||||
┏━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Exit Reason ┃ Exits ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
|
||||
│ exit_signal │ 4 │ -2.23 │ -31.217 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
|
||||
│ force_exit │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
│ stop_loss │ 3 │ -10.14 │ -111.768 │ -11.18 │ 1 day, 3:05:00 │ 0 0 3 0 │
|
||||
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└─────────────┴───────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
MIXED TAG STATS
|
||||
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Enter Tag ┃ Exit Reason ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ │ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
|
||||
│ │ exit_signal │ 4 │ -2.23 │ -31.217 │ -3.12 │ 1 day, 8:38:00 │ 0 0 4 0 │
|
||||
│ │ force_exit │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||
│ │ stop_loss │ 3 │ -10.14 │ -111.768 │ -11.18 │ 1 day, 3:05:00 │ 0 0 3 0 │
|
||||
│ TOTAL │ │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
└───────────┴─────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
SUMMARY METRICS
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Metric ┃ Value ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||
│ Trading Mode │ Isolated Futures │
|
||||
│ Max open trades │ 3 │
|
||||
│ │ │
|
||||
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
||||
│ Starting balance │ 1000 USDT │
|
||||
│ Final balance │ 1054.774 USDT │
|
||||
│ Absolute profit │ 54.774 USDT │
|
||||
│ Total profit % │ 5.48% │
|
||||
│ CAGR % │ 87.36% │
|
||||
│ Sortino │ 2.48 │
|
||||
│ Sharpe │ 3.75 │
|
||||
│ Calmar │ 40.99 │
|
||||
│ SQN │ 0.69 │
|
||||
│ Profit factor │ 1.29 │
|
||||
│ Expectancy (Ratio) │ 0.71 (0.04) │
|
||||
│ Avg. daily profit │ 1.767 USDT │
|
||||
│ Avg. stake amount │ 345.016 USDT │
|
||||
│ Total trade volume │ 53316.954 USDT │
|
||||
│ │ │
|
||||
│ Long / Short trades │ 67 / 10 │
|
||||
│ Long / Short profit % │ 8.94% / -3.47% │
|
||||
│ Long / Short profit USDT │ 89.425 / -34.651 │
|
||||
│ │ │
|
||||
│ Best Pair │ LTC/USDT:USDT 5.62% │
|
||||
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
||||
│ Best trade │ ETC/USDT:USDT 2.00% │
|
||||
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||
│ Best day │ 26.91 USDT │
|
||||
│ Worst day │ -47.741 USDT │
|
||||
│ Days win/draw/lose │ 20 / 6 / 5 │
|
||||
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
||||
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
||||
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
||||
│ Rejected Entry signals │ 258 │
|
||||
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||
│ │ │
|
||||
│ Min balance │ 1003.168 USDT │
|
||||
│ Max balance │ 1149.421 USDT │
|
||||
│ Max % of account underwater │ 8.23% │
|
||||
│ Absolute drawdown │ 94.647 USDT (8.23%) │
|
||||
│ Drawdown duration │ 9 days 08:50:00 │
|
||||
│ Profit at drawdown start │ 149.421 USDT │
|
||||
│ Profit at drawdown end │ 54.774 USDT │
|
||||
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||
│ Market change │ 30.51% │
|
||||
└───────────────────────────────┴─────────────────────────────────┘
|
||||
|
||||
================== SUMMARY METRICS ==================
|
||||
| Metric | Value |
|
||||
|-----------------------------+---------------------|
|
||||
| Backtesting from | 2019-01-01 00:00:00 |
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Trading Mode | Spot |
|
||||
| Max open trades | 3 |
|
||||
| | |
|
||||
| Total/Daily Avg Trades | 429 / 3.575 |
|
||||
| Starting balance | 0.01000000 BTC |
|
||||
| Final balance | 0.01762792 BTC |
|
||||
| Absolute profit | 0.00762792 BTC |
|
||||
| Total profit % | 76.2% |
|
||||
| CAGR % | 460.87% |
|
||||
| Sortino | 1.88 |
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| SQN | 2.45 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
| Long / Short | 352 / 77 |
|
||||
| Total profit Long % | 1250.58% |
|
||||
| Total profit Short % | -15.02% |
|
||||
| Absolute profit Long | 0.00838792 BTC |
|
||||
| Absolute profit Short | -0.00076 BTC |
|
||||
| | |
|
||||
| Best Pair | LSK/BTC 26.26% |
|
||||
| Worst Pair | ZEC/BTC -10.18% |
|
||||
| Best Trade | LSK/BTC 4.25% |
|
||||
| Worst Trade | ZEC/BTC -10.25% |
|
||||
| Best day | 0.00076 BTC |
|
||||
| Worst day | -0.00036 BTC |
|
||||
| Days win/draw/lose | 12 / 82 / 25 |
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Max Consecutive Wins / Loss | 3 / 4 |
|
||||
| Rejected Entry signals | 3089 |
|
||||
| Entry/Exit Timeouts | 0 / 0 |
|
||||
| Canceled Trade Entries | 34 |
|
||||
| Canceled Entry Orders | 123 |
|
||||
| Replaced Entry Orders | 89 |
|
||||
| | |
|
||||
| Min balance | 0.00945123 BTC |
|
||||
| Max balance | 0.01846651 BTC |
|
||||
| Max % of account underwater | 25.19% |
|
||||
| Absolute Drawdown (Account) | 13.33% |
|
||||
| Drawdown | 0.0015 BTC |
|
||||
| Drawdown high | 0.0013 BTC |
|
||||
| Drawdown low | -0.0002 BTC |
|
||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||
| Drawdown End | 2019-04-11 18:15:00 |
|
||||
| Market change | -5.88% |
|
||||
=====================================================
|
||||
Backtested 2025-07-01 00:00:00 -> 2025-08-01 00:00:00 | Max open trades : 3
|
||||
STRATEGY SUMMARY
|
||||
┏━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Strategy ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃ Drawdown ┃
|
||||
┡━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ SampleStrategy │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │ 94.647 USDT 8.23% │
|
||||
└────────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┴────────────────────┘
|
||||
```
|
||||
|
||||
### Backtesting report table
|
||||
|
||||
The 1st table contains all trades the bot made, including "left open trades".
|
||||
The first table contains all trades the bot made, including "left open trades".
|
||||
|
||||
The last line will give you the overall performance of your strategy,
|
||||
here:
|
||||
|
||||
```
|
||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
||||
```
|
||||
|
||||
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
|
||||
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
|
||||
The bot has made `77` trades for an average duration of `22:12:00`, with a performance of `5.48%` (profit), that means it has earned a total of `54.774 USDT` starting with a capital of 1000 USDT.
|
||||
|
||||
The column `Avg Profit %` shows the average profit for all trades made.
|
||||
The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
|
||||
In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
|
||||
|
||||
In the above results, we have a starting balance of 1000 USDT and an absolute profit of 54.774 USDT - so the `Tot Profit %` will be `(54.774 / 1000) * 100 ~= 5.48%`.
|
||||
|
||||
Your strategy performance is influenced by your entry strategy, your exit strategy, and also by the `minimal_roi` and `stop_loss` you have set.
|
||||
|
||||
@@ -282,85 +305,83 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
|
||||
(55%), there is almost no chance that the bot will ever reach this profit.
|
||||
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
|
||||
|
||||
### Exit reasons table
|
||||
|
||||
The 2nd table contains a recap of exit reasons.
|
||||
This table can tell you which area needs some additional work (e.g. all or many of the `exit_signal` trades are losses, so you should work on improving the exit signal, or consider disabling it).
|
||||
|
||||
### Left open trades table
|
||||
|
||||
The 3rd table contains all trades the bot had to `force_exit` at the end of the backtesting period to present you the full picture.
|
||||
The second table contains all trades the bot had to `force_exit` at the end of the backtesting period to present you the full picture.
|
||||
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
|
||||
These trades are also included in the first table, but are also shown separately in this table for clarity.
|
||||
|
||||
### Enter tag stats table
|
||||
|
||||
The third table provides a breakdown of trades by their entry tags (e.g., `enter_long`, `enter_short`), showing the number of entries, average profit percentage, total profit in the stake currency, total profit percentage, average duration, and the number of wins, draws, and losses for each tag.
|
||||
|
||||
### Exit reason stats table
|
||||
|
||||
The fourth table contains a recap of exit reasons (e.g., `exit_signal`, `roi`, `stop_loss`, `force_exit`). This table can tell you which area needs additional work (e.g., if many `exit_signal` trades are losses, you should work on improving the exit signal or consider disabling it).
|
||||
|
||||
### Mixed tag stats table
|
||||
|
||||
The fifth table combines entry tags and exit reasons, providing a detailed view of how different entry tags performed with specific exit reasons. This can help identify which combinations of entry and exit strategies are most effective.
|
||||
|
||||
### Summary metrics
|
||||
|
||||
The last element of the backtest report is the summary metrics table.
|
||||
It contains some useful key metrics about performance of your strategy on backtesting data.
|
||||
It contains key metrics about the performance of your strategy on backtesting data.
|
||||
|
||||
```
|
||||
================== SUMMARY METRICS ==================
|
||||
| Metric | Value |
|
||||
|-----------------------------+---------------------|
|
||||
| Backtesting from | 2019-01-01 00:00:00 |
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Trading Mode | Spot |
|
||||
| Max open trades | 3 |
|
||||
| | |
|
||||
| Total/Daily Avg Trades | 429 / 3.575 |
|
||||
| Starting balance | 0.01000000 BTC |
|
||||
| Final balance | 0.01762792 BTC |
|
||||
| Absolute profit | 0.00762792 BTC |
|
||||
| Total profit % | 76.2% |
|
||||
| CAGR % | 460.87% |
|
||||
| Sortino | 1.88 |
|
||||
| Sharpe | 2.97 |
|
||||
| Calmar | 6.29 |
|
||||
| SQN | 2.45 |
|
||||
| Profit factor | 1.11 |
|
||||
| Expectancy (Ratio) | -0.15 (-0.05) |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
| Long / Short | 352 / 77 |
|
||||
| Total profit Long % | 1250.58% |
|
||||
| Total profit Short % | -15.02% |
|
||||
| Absolute profit Long | 0.00838792 BTC |
|
||||
| Absolute profit Short | -0.00076 BTC |
|
||||
| | |
|
||||
| Best Pair | LSK/BTC 26.26% |
|
||||
| Worst Pair | ZEC/BTC -10.18% |
|
||||
| Best Trade | LSK/BTC 4.25% |
|
||||
| Worst Trade | ZEC/BTC -10.25% |
|
||||
| Best day | 0.00076 BTC |
|
||||
| Worst day | -0.00036 BTC |
|
||||
| Days win/draw/lose | 12 / 82 / 25 |
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Max Consecutive Wins / Loss | 3 / 4 |
|
||||
| Rejected Entry signals | 3089 |
|
||||
| Entry/Exit Timeouts | 0 / 0 |
|
||||
| Canceled Trade Entries | 34 |
|
||||
| Canceled Entry Orders | 123 |
|
||||
| Replaced Entry Orders | 89 |
|
||||
| | |
|
||||
| Min balance | 0.00945123 BTC |
|
||||
| Max balance | 0.01846651 BTC |
|
||||
| Max % of account underwater | 25.19% |
|
||||
| Absolute Drawdown (Account) | 13.33% |
|
||||
| Drawdown | 0.0015 BTC |
|
||||
| Drawdown high | 0.0013 BTC |
|
||||
| Drawdown low | -0.0002 BTC |
|
||||
| Drawdown Start | 2019-02-15 14:10:00 |
|
||||
| Drawdown End | 2019-04-11 18:15:00 |
|
||||
| Market change | -5.88% |
|
||||
=====================================================
|
||||
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Metric ┃ Value ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||
│ Trading Mode │ Isolated Futures │
|
||||
│ Max open trades │ 3 │
|
||||
│ │ │
|
||||
│ Total/Daily Avg Trades │ 72 / 2.32 │
|
||||
│ Starting balance │ 1000 USDT │
|
||||
│ Final balance │ 1106.734 USDT │
|
||||
│ Absolute profit │ 106.734 USDT │
|
||||
│ Total profit % │ 10.67% │
|
||||
│ CAGR % │ 230.04% │
|
||||
│ Sortino │ 4.99 │
|
||||
│ Sharpe │ 8.00 │
|
||||
│ Calmar │ 77.76 │
|
||||
│ SQN │ 1.52 │
|
||||
│ Profit factor │ 1.79 │
|
||||
│ Expectancy (Ratio) │ 1.48 (0.07) │
|
||||
│ Avg. daily profit │ 3.443 USDT │
|
||||
│ Avg. stake amount │ 363.133 USDT │
|
||||
│ Total trade volume │ 52466.174 USDT │
|
||||
│ │ │
|
||||
│ Best Pair │ LTC/USDT:USDT 4.48% │
|
||||
│ Worst Pair │ ADA/USDT:USDT -1.78% │
|
||||
│ Best trade │ ETC/USDT:USDT 2.00% │
|
||||
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||
│ Best day │ 23.535 USDT │
|
||||
│ Worst day │ -49.813 USDT │
|
||||
│ Days win/draw/lose │ 21 / 6 / 4 │
|
||||
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:30 │
|
||||
│ Min/Max/Avg. Duration Losers │ 0d 12:00 / 17d 08:00 / 3d 23:28 │
|
||||
│ Max Consecutive Wins / Loss │ 58 / 4 │
|
||||
│ Rejected Entry signals │ 254 │
|
||||
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||
│ │ │
|
||||
│ Min balance │ 1003.168 USDT │
|
||||
│ Max balance │ 1209 USDT │
|
||||
│ Max % of account underwater │ 8.46% │
|
||||
│ Absolute drawdown │ 102.266 USDT (8.46%) │
|
||||
│ Drawdown duration │ 9 days 08:50:00 │
|
||||
│ Profit at drawdown start │ 209 USDT │
|
||||
│ Profit at drawdown end │ 106.734 USDT │
|
||||
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||
│ Market change │ 30.51% │
|
||||
└───────────────────────────────┴─────────────────────────────────┘
|
||||
```
|
||||
|
||||
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
||||
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
|
||||
- `Trading Mode`: Spot or Futures trading.
|
||||
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
|
||||
- `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
|
||||
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
|
||||
- `Final balance`: Final balance - starting balance + absolute profit.
|
||||
@@ -371,56 +392,71 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Sharpe`: Annualized Sharpe ratio.
|
||||
- `Calmar`: Annualized Calmar ratio.
|
||||
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
||||
- `Profit factor`: profit / loss.
|
||||
- `Profit factor`: Sum of the profits of all winning trades divided by the sum of the losses of all losing trades.
|
||||
- `Expectancy (Ratio)`: Expectancy ratio, which is the average profit or loss per trade. A negative expectancy ratio means that your strategy is not profitable.
|
||||
- `Avg. daily profit`: Average profit per day, calculated as `(Total Profit / Backtest Days)`.
|
||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Tot Profit %`.
|
||||
- `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade.
|
||||
- `Long / Short trades`: Split long/short trade counts (only shown when short trades were made).
|
||||
- `Long / Short profit %`: Profit percentage for long and short trades (only shown when short trades were made).
|
||||
- `Long / Short profit USDT`: Profit in stake currency for long and short trades (only shown when short trades were made).
|
||||
- `Best Pair` / `Worst Pair`: Best and worst performing pair (based on total profit percentage), and its corresponding `Tot Profit %`.
|
||||
- `Best trade` / `Worst trade`: Biggest single winning trade and biggest single losing trade.
|
||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trades).
|
||||
- `Min/Max/Avg. Duration Winners`: Minimum, maximum, and average durations for winning trades.
|
||||
- `Min/Max/Avg. Duration Losers`: Minimum, maximum, and average durations for losing trades.
|
||||
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
|
||||
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||
- `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`.
|
||||
- `Canceled Entry Orders`: Number of entry orders that have been canceled by user request via `adjust_entry_price`.
|
||||
- `Replaced Entry Orders`: Number of entry orders that have been replaced by user request via `adjust_entry_price`.
|
||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||
- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started.
|
||||
Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`.
|
||||
- `Absolute Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.
|
||||
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
|
||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
||||
- `Long / Short`: Split long/short values (Only shown when short trades were made).
|
||||
- `Total profit Long %` / `Absolute profit Long`: Profit long trades only (Only shown when short trades were made).
|
||||
- `Total profit Short %` / `Absolute profit Short`: Profit short trades only (Only shown when short trades were made).
|
||||
- `Max % of account underwater`: Maximum percentage your account has decreased from the top since the simulation started. Calculated as the maximum of `(Max Balance - Current Balance) / (Max Balance)`.
|
||||
- `Absolute drawdown`: Maximum absolute drawdown experienced, including percentage relative to the account calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`..
|
||||
- `Drawdown duration`: Duration of the largest drawdown period.
|
||||
- `Profit at drawdown start` / `Profit at drawdown end`: Profit at the beginning and end of the largest drawdown period.
|
||||
- `Drawdown start` / `Drawdown end`: Start and end datetime for the largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||
- `Market change`: Change of the market during the backtest period. Calculated as the average of all pairs' changes from the first to the last candle using the "close" column.
|
||||
|
||||
### Daily / Weekly / Monthly breakdown
|
||||
### Daily / Weekly / Monthly / Yearly breakdown
|
||||
|
||||
You can get an overview over daily / weekly or monthly results by using the `--breakdown <>` switch.
|
||||
You can get an overview over daily, weekly, monthly, or yearly results by using the `--breakdown <>` switch.
|
||||
|
||||
To visualize daily and weekly breakdowns, you can use the following:
|
||||
To visualize monthly and yearly breakdowns, you can use the following:
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown day week
|
||||
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown month year
|
||||
```
|
||||
|
||||
``` output
|
||||
======================== DAY BREAKDOWN =========================
|
||||
| Day | Tot Profit USDT | Wins | Draws | Losses |
|
||||
|------------+-------------------+--------+---------+----------|
|
||||
| 03/07/2021 | 200.0 | 2 | 0 | 0 |
|
||||
| 04/07/2021 | -50.31 | 0 | 0 | 2 |
|
||||
| 05/07/2021 | 220.611 | 3 | 2 | 0 |
|
||||
| 06/07/2021 | 150.974 | 3 | 0 | 2 |
|
||||
| 07/07/2021 | -70.193 | 1 | 0 | 2 |
|
||||
| 08/07/2021 | 212.413 | 2 | 0 | 3 |
|
||||
|
||||
MONTH BREAKDOWN
|
||||
┏━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Month ┃ Trades ┃ Tot Profit USDT ┃ Profit Factor ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ 31/01/2020 │ 12 │ 44.451 │ 7.28 │ 10 0 2 83.3 │
|
||||
│ 29/02/2020 │ 30 │ 45.41 │ 2.36 │ 17 0 13 56.7 │
|
||||
│ 31/03/2020 │ 35 │ 142.024 │ 2.42 │ 14 0 21 40.0 │
|
||||
│ 30/04/2020 │ 67 │ -23.692 │ 0.81 │ 24 0 43 35.8 │
|
||||
...
|
||||
...
|
||||
│ 30/04/2025 │ 203 │ -63.43 │ 0.81 │ 73 0 130 36.0 │
|
||||
│ 31/05/2025 │ 142 │ 104.675 │ 1.28 │ 59 0 83 41.5 │
|
||||
│ 30/06/2025 │ 177 │ -1.014 │ 1.0 │ 85 0 92 48.0 │
|
||||
│ 31/07/2025 │ 155 │ 232.762 │ 1.6 │ 63 0 92 40.6 │
|
||||
└────────────┴────────┴─────────────────┴───────────────┴────────────────────────┘
|
||||
YEAR BREAKDOWN
|
||||
┏━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Year ┃ Trades ┃ Tot Profit USDT ┃ Profit Factor ┃ Win Draw Loss Win% ┃
|
||||
┡━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ 31/12/2020 │ 896 │ 868.889 │ 1.46 │ 351 0 545 39.2 │
|
||||
│ 31/12/2021 │ 1778 │ 4487.163 │ 1.93 │ 745 0 1033 41.9 │
|
||||
│ 31/12/2022 │ 1736 │ 938.27 │ 1.27 │ 698 0 1038 40.2 │
|
||||
│ 31/12/2023 │ 1712 │ 1677.126 │ 1.68 │ 670 0 1042 39.1 │
|
||||
│ 31/12/2024 │ 1609 │ 3198.424 │ 2.22 │ 773 0 836 48.0 │
|
||||
│ 31/12/2025 │ 1042 │ 716.174 │ 1.33 │ 420 0 622 40.3 │
|
||||
└────────────┴────────┴─────────────────┴───────────────┴────────────────────────┘
|
||||
```
|
||||
|
||||
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day. Below that there will be a second table for the summarized values of weeks indicated by the date of the closing Sunday. The same would apply to a monthly breakdown indicated by the last day of the month.
|
||||
The output will display tables containing the realized absolute profit (in stake currency) for the selected period, along with additional statistics such as number of trades, profit factor, and distribution of wins, draws, and losses that materialized (closed) on this period.
|
||||
|
||||
### Backtest result caching
|
||||
|
||||
@@ -435,15 +471,19 @@ To save time, by default backtest will reuse a cached result from within the las
|
||||
To further analyze your backtest results, freqtrade will export the trades to file by default.
|
||||
You can then load the trades to perform further analysis as shown in the [data analysis](strategy_analysis_example.md#load-backtest-results-to-pandas-dataframe) backtesting section.
|
||||
|
||||
Also, you can use freqtrade in [webserver mode](freq-ui.md#backtesting) to visualize the backtest results in a web interface.
|
||||
This mode also allows you to load existing backtest results, so you can analyze them without running the backtest again.
|
||||
For this mode - `--notes "<notes>"` can be used to add notes to the backtest results, which will be shown in the web interface.
|
||||
|
||||
### Backtest output file
|
||||
|
||||
The output file freqtrade produces is a zip file containing the following files:
|
||||
|
||||
- The backtest report in json format
|
||||
- the market change data in feather format
|
||||
- a copy of the strategy file
|
||||
- a copy of the strategy parameters (if a parameter file was used)
|
||||
- a sanitized copy of the config file
|
||||
- The market change data in feather format
|
||||
- A copy of the strategy file
|
||||
- A copy of the strategy parameters (if a parameter file was used)
|
||||
- A sanitized copy of the config file
|
||||
|
||||
This will ensure results are reproducible - under the assumption that the same data is available.
|
||||
|
||||
@@ -461,7 +501,7 @@ Since backtesting lacks some detailed information about what happens within a ca
|
||||
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
|
||||
- ROI
|
||||
- Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
|
||||
- Exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit
|
||||
- Exits are never "below the candle", so a ROI of 2% may result in an exit at 2.4% if low was at 2.4% profit
|
||||
- ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate
|
||||
- Force-exits caused by `<N>=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles)
|
||||
- Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price
|
||||
@@ -527,7 +567,7 @@ freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-deta
|
||||
|
||||
This will load 1h data (the main timeframe) as well as 5m data (detail timeframe) for the selected timerange.
|
||||
The strategy will be analyzed with the 1h timeframe.
|
||||
Candles where activity may take place (there's an active signal, the pair is in a trade) are evaluated at the 5m timeframe.
|
||||
Candles where activity may take place (there's an active signal, the pair is in a trade) are evaluated at the 5m timeframe.
|
||||
This will allow for a more accurate simulation of intra-candle movements - and can lead to different results, especially on higher timeframes.
|
||||
|
||||
Entries will generally still happen at the main candle's open, however freed trade slots may be freed earlier (if the exit signal is triggered on the 5m candle), which can then be used for a new trade of a different pair.
|
||||
@@ -590,5 +630,5 @@ Detailed output for all strategies one after the other will be available, so mak
|
||||
|
||||
## Next step
|
||||
|
||||
Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy?
|
||||
Great, your strategy is profitable. What if the bot can give you the optimal parameters to use for your strategy?
|
||||
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE]
|
||||
[-V] [-c PATH] [-d PATH]
|
||||
[--userdir PATH]
|
||||
[--export-filename PATH]
|
||||
[--backtest-filename PATH]
|
||||
[--backtest-directory PATH]
|
||||
[--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]]
|
||||
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
|
||||
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
||||
@@ -14,10 +15,15 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--no-color] [--logfile FILE]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--export-filename PATH, --backtest-filename PATH
|
||||
Use this filename for backtest results.Requires
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--backtest-filename PATH, --export-filename PATH
|
||||
Use this filename for backtest results.Example:
|
||||
`--backtest-
|
||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||
Assumes either `user_data/backtest_results/` or
|
||||
`--export-directory` as base directory.
|
||||
--backtest-directory PATH, --export-directory PATH
|
||||
Directory to use for backtest results. Example:
|
||||
`--export-directory=user_data/backtest_results/`.
|
||||
--analysis-groups {0,1,2,3,4,5} [{0,1,2,3,4,5} ...]
|
||||
grouping output - 0: simple wins/losses by enter tag,
|
||||
1: by enter_tag, 2: by enter_tag and exit_tag, 3: by
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
```
|
||||
usage: freqtrade backtesting-show [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[-c PATH] [-d PATH] [--userdir PATH]
|
||||
[--export-filename PATH] [--show-pair-list]
|
||||
[--backtest-filename PATH]
|
||||
[--backtest-directory PATH]
|
||||
[--show-pair-list]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--export-filename PATH, --backtest-filename PATH
|
||||
Use this filename for backtest results.Requires
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--backtest-filename PATH, --export-filename PATH
|
||||
Use this filename for backtest results.Example:
|
||||
`--backtest-
|
||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||
Assumes either `user_data/backtest_results/` or
|
||||
`--export-directory` as base directory.
|
||||
--backtest-directory PATH, --export-directory PATH
|
||||
Directory to use for backtest results. Example:
|
||||
`--export-directory=user_data/backtest_results/`.
|
||||
--show-pair-list Show backtesting pairlist sorted by profit.
|
||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||
Show backtesting breakdown per [day, week, month,
|
||||
|
||||
@@ -14,10 +14,11 @@ usage: freqtrade backtesting [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export {none,trades,signals}]
|
||||
[--export-filename PATH]
|
||||
[--backtest-filename PATH]
|
||||
[--backtest-directory PATH]
|
||||
[--breakdown {day,week,month,year} [{day,week,month,year} ...]]
|
||||
[--cache {none,day,week,month}]
|
||||
[--freqai-backtest-live-models]
|
||||
[--freqai-backtest-live-models] [--notes TEXT]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -61,10 +62,15 @@ options:
|
||||
becomes `backtest-data-SampleStrategy.json`
|
||||
--export {none,trades,signals}
|
||||
Export backtest results (default: trades).
|
||||
--export-filename PATH, --backtest-filename PATH
|
||||
Use this filename for backtest results.Requires
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--backtest-filename PATH, --export-filename PATH
|
||||
Use this filename for backtest results.Example:
|
||||
`--backtest-
|
||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||
Assumes either `user_data/backtest_results/` or
|
||||
`--export-directory` as base directory.
|
||||
--backtest-directory PATH, --export-directory PATH
|
||||
Directory to use for backtest results. Example:
|
||||
`--export-directory=user_data/backtest_results/`.
|
||||
--breakdown {day,week,month,year} [{day,week,month,year} ...]
|
||||
Show backtesting breakdown per [day, week, month,
|
||||
year].
|
||||
@@ -73,6 +79,7 @@ options:
|
||||
age (default: day).
|
||||
--freqai-backtest-live-models
|
||||
Run backtest with ready models.
|
||||
--notes TEXT Add notes to the backtest results.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -7,7 +7,6 @@ usage: freqtrade edge [-h] [-v] [--no-color] [--logfile FILE] [-V] [-c PATH]
|
||||
[--data-format-ohlcv {json,jsongz,feather,parquet}]
|
||||
[--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
|
||||
[--fee FLOAT] [-p PAIRS [PAIRS ...]]
|
||||
[--stoplosses STOPLOSS_RANGE]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
@@ -29,11 +28,6 @@ options:
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
separated.
|
||||
--stoplosses STOPLOSS_RANGE
|
||||
Defines a range of stoploss values against which edge
|
||||
will assess the strategy. The format is "min,max,step"
|
||||
(without any space). Example:
|
||||
`--stoplosses=-0.01,-0.1,-0.001`
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
```
|
||||
usage: freqtrade list-exchanges [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[-c PATH] [-d PATH] [--userdir PATH] [-1] [-a]
|
||||
[--trading-mode {spot,margin,futures}]
|
||||
[--dex-exchanges]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-1, --one-column Print output in one column.
|
||||
-a, --all Print all exchanges known to the ccxt library.
|
||||
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||
Select Trading mode
|
||||
--dex-exchanges Print only DEX exchanges.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
||||
@@ -15,7 +15,8 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--no-color] [--logfile FILE]
|
||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export {none,trades,signals}]
|
||||
[--export-filename PATH]
|
||||
[--backtest-filename PATH]
|
||||
[--backtest-directory PATH]
|
||||
[--freqai-backtest-live-models]
|
||||
[--minimum-trade-amount INT]
|
||||
[--targeted-trade-amount INT]
|
||||
@@ -60,10 +61,15 @@ options:
|
||||
becomes `backtest-data-SampleStrategy.json`
|
||||
--export {none,trades,signals}
|
||||
Export backtest results (default: trades).
|
||||
--export-filename PATH, --backtest-filename PATH
|
||||
Use this filename for backtest results.Requires
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--backtest-filename PATH, --export-filename PATH
|
||||
Use this filename for backtest results.Example:
|
||||
`--backtest-
|
||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||
Assumes either `user_data/backtest_results/` or
|
||||
`--export-directory` as base directory.
|
||||
--backtest-directory PATH, --export-directory PATH
|
||||
Directory to use for backtest results. Example:
|
||||
`--export-directory=user_data/backtest_results/`.
|
||||
--freqai-backtest-live-models
|
||||
Run backtest with ready models.
|
||||
--minimum-trade-amount INT
|
||||
|
||||
@@ -22,7 +22,7 @@ positional arguments:
|
||||
backtesting-show Show past Backtest results
|
||||
backtesting-analysis
|
||||
Backtest Analysis module.
|
||||
edge Edge module.
|
||||
edge Edge module. No longer part of Freqtrade
|
||||
hyperopt Hyperopt module.
|
||||
hyperopt-list List Hyperopt results
|
||||
hyperopt-show Show details of Hyperopt results
|
||||
|
||||
@@ -10,7 +10,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--plot-limit INT] [--db-url PATH]
|
||||
[--trade-source {DB,file}]
|
||||
[--export {none,trades,signals}]
|
||||
[--export-filename PATH]
|
||||
[--backtest-filename PATH]
|
||||
[--timerange TIMERANGE] [-i TIMEFRAME]
|
||||
[--no-trades]
|
||||
|
||||
@@ -38,10 +38,12 @@ options:
|
||||
(backtest file)) Default: file
|
||||
--export {none,trades,signals}
|
||||
Export backtest results (default: trades).
|
||||
--export-filename PATH, --backtest-filename PATH
|
||||
Use this filename for backtest results.Requires
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--backtest-filename PATH, --export-filename PATH
|
||||
Use this filename for backtest results.Example:
|
||||
`--backtest-
|
||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||
Assumes either `user_data/backtest_results/` or
|
||||
`--export-directory` as base directory.
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
|
||||
@@ -6,7 +6,7 @@ usage: freqtrade plot-profit [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
||||
[--freqaimodel NAME] [--freqaimodel-path PATH]
|
||||
[-p PAIRS [PAIRS ...]] [--timerange TIMERANGE]
|
||||
[--export {none,trades,signals}]
|
||||
[--export-filename PATH] [--db-url PATH]
|
||||
[--backtest-filename PATH] [--db-url PATH]
|
||||
[--trade-source {DB,file}] [-i TIMEFRAME]
|
||||
[--auto-open]
|
||||
|
||||
@@ -19,10 +19,12 @@ options:
|
||||
Specify what timerange of data to use.
|
||||
--export {none,trades,signals}
|
||||
Export backtest results (default: trades).
|
||||
--export-filename PATH, --backtest-filename PATH
|
||||
Use this filename for backtest results.Requires
|
||||
`--export` to be set as well. Example: `--export-filen
|
||||
ame=user_data/backtest_results/backtest_today.json`
|
||||
--backtest-filename PATH, --export-filename PATH
|
||||
Use this filename for backtest results.Example:
|
||||
`--backtest-
|
||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
||||
Assumes either `user_data/backtest_results/` or
|
||||
`--export-directory` as base directory.
|
||||
--db-url PATH Override trades database URL, this is useful in custom
|
||||
deployments (default: `sqlite:///tradesv3.sqlite` for
|
||||
Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for
|
||||
|
||||
@@ -234,7 +234,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exchange.only_from_ccxt` | Prevent data-download from data.binance.vision. Leaving this as false can greatly speed up downloads, but may be problematic if the site is not available.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| | **Plugins**
|
||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
||||
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||
| | **Telegram**
|
||||
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
||||
@@ -567,8 +566,8 @@ Configuration:
|
||||
|
||||
### Understand order_time_in_force
|
||||
|
||||
The `order_time_in_force` configuration parameter defines the policy by which the order
|
||||
is executed on the exchange. Three commonly used time in force are:
|
||||
The `order_time_in_force` configuration parameter defines the policy by which the order is executed on the exchange.
|
||||
Commonly used time in force are:
|
||||
|
||||
**GTC (Good Till Canceled):**
|
||||
|
||||
@@ -590,11 +589,13 @@ is automatically cancelled by the exchange.
|
||||
Post only order. The order is either placed as a maker order, or it is canceled.
|
||||
This means the order must be placed on orderbook for at least time in an unfilled state.
|
||||
|
||||
Please check the [Exchange documentation](exchanges.md) for supported time in force values for your exchange.
|
||||
|
||||
#### time_in_force config
|
||||
|
||||
The `order_time_in_force` parameter contains a dict with entry and exit time in force policy values.
|
||||
This can be set in the configuration file or in the strategy.
|
||||
Values set in the configuration file overwrites values set in the strategy.
|
||||
Values set in the configuration file overwrite values from in the strategy, following the regular [precedence rules](#configuration-option-prevalence).
|
||||
|
||||
The possible values are: `GTC` (default), `FOK` or `IOC`.
|
||||
|
||||
@@ -606,9 +607,9 @@ The possible values are: `GTC` (default), `FOK` or `IOC`.
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
This is ongoing work. For now, it is supported only for binance, gate and kucoin.
|
||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
||||
|
||||
|
||||
### Fiat conversion
|
||||
|
||||
Freqtrade uses the Coingecko API to convert the coin value to it's corresponding fiat value for the Telegram reports.
|
||||
|
||||
@@ -93,3 +93,8 @@ Please use the [`convert-data` subcommand](data-download.md#sub-command-convert-
|
||||
|
||||
Configuring syslog and journald via `--logfile systemd` and `--logfile journald` respectively has been deprecated in 2025.3.
|
||||
Please use configuration based [log setup](advanced-setup.md#advanced-logging) instead.
|
||||
|
||||
## Removal of the edge module
|
||||
|
||||
The edge module has been deprecated in 2023.9 and removed in 2025.6.
|
||||
All functionalities of edge have been removed, and having edge configured will result in an error.
|
||||
|
||||
+23
-1
@@ -304,6 +304,13 @@ The `IProtection` parent class provides a helper method for this in `calculate_l
|
||||
|
||||
Most exchanges supported by CCXT should work out of the box.
|
||||
|
||||
If you need to implement a specific exchange class, these are found in the `freqtrade/exchange` source folder. You'll also need to add the import to `freqtrade/exchange/__init__.py` to make the loading logic aware of the new exchange.
|
||||
We recommend looking at existing exchange implementations to get an idea of what might be required.
|
||||
|
||||
!!! Warning
|
||||
Implementing and testing an exchange can be a lot of trial and error, so please bear this in mind.
|
||||
You should also have some development experience, as this is not a beginner task.
|
||||
|
||||
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `tests/exchange_online/conftest.py` and run these tests with `pytest --longrun tests/exchange_online/test_ccxt_compat.py`.
|
||||
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
||||
|
||||
@@ -401,6 +408,22 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace freqtrade/tem
|
||||
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade/templates/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md
|
||||
```
|
||||
|
||||
## Backtest documentation results
|
||||
|
||||
To generate backtest outputs, please use the following commands:
|
||||
|
||||
``` bash
|
||||
# Assume a dedicated user directory for this output
|
||||
freqtrade create-userdir --userdir user_data_bttest/
|
||||
# set can_short = True
|
||||
sed -i "s/can_short: bool = False/can_short: bool = True/" user_data_bttest/strategies/sample_strategy.py
|
||||
|
||||
freqtrade download-data --timerange 20250625-20250801 --config tests/testdata/config.tests.usdt.json --userdir user_data_bttest/ -t 5m
|
||||
|
||||
freqtrade backtesting --config tests/testdata/config.tests.usdt.json -s SampleStrategy --userdir user_data_bttest/ --cache none --timerange 20250701-20250801
|
||||
```
|
||||
|
||||
|
||||
## Continuous integration
|
||||
|
||||
This documents some decisions taken for the CI Pipeline.
|
||||
@@ -411,7 +434,6 @@ This documents some decisions taken for the CI Pipeline.
|
||||
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
|
||||
* Full docker image rebuilds are run once a week via schedule.
|
||||
* Deployments run on ubuntu.
|
||||
* ta-lib binaries are contained in the build_helpers directory to avoid fails related to external unavailability.
|
||||
* All tests must pass for a PR to be merged to `stable` or `develop`.
|
||||
|
||||
## Creating a release
|
||||
|
||||
-300
@@ -1,300 +0,0 @@
|
||||
# Edge positioning
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
!!! Note
|
||||
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
|
||||
`Edge Positioning` improves the performance of some trading strategies and *decreases* the performance of others.
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose.
|
||||
|
||||
To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money.
|
||||
|
||||
!!! tip "It doesn't matter how often, but how much!"
|
||||
A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit.
|
||||
|
||||
The Edge Positioning module seeks to improve a strategy's winning probability and the money that the strategy will make *on the long run*.
|
||||
|
||||
We raise the following question[^1]:
|
||||
|
||||
!!! Question "Which trade is a better option?"
|
||||
a) A trade with 80% of chance of losing 100\$ and 20% chance of winning 200\$<br/>
|
||||
b) A trade with 100% of chance of losing 30\$
|
||||
|
||||
???+ Info "Answer"
|
||||
The expected value of *a)* is smaller than the expected value of *b)*.<br/>
|
||||
Hence, *b*) represents a smaller loss in the long run.<br/>
|
||||
However, the answer is: *it depends*
|
||||
|
||||
Another way to look at it is to ask a similar question:
|
||||
|
||||
!!! Question "Which trade is a better option?"
|
||||
a) A trade with 80% of chance of winning 100\$ and 20% chance of losing 200\$<br/>
|
||||
b) A trade with 100% of chance of winning 30\$
|
||||
|
||||
Edge positioning tries to answer the hard questions about risk/reward and position size automatically, seeking to minimizes the chances of losing of a given strategy.
|
||||
|
||||
### Trading, winning and losing
|
||||
|
||||
Let's call $o$ the return of a single transaction $o$ where $o \in \mathbb{R}$. The collection $O = \{o_1, o_2, ..., o_N\}$ is the set of all returns of transactions made during a trading session. We say that $N$ is the cardinality of $O$, or, in lay terms, it is the number of transactions made in a trading session.
|
||||
|
||||
!!! Example
|
||||
In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o_3 = 15$.
|
||||
|
||||
A winning trade is a trade where a strategy *made* money. Making money means that the strategy closed the position in a value that returned a profit, after all deducted fees. Formally, a winning trade will have a return $o_i > 0$. Similarly, a losing trade will have a return $o_j \leq 0$. With that, we can discover the set of all winning trades, $T_{win}$, as follows:
|
||||
|
||||
$$ T_{win} = \{ o \in O | o > 0 \} $$
|
||||
|
||||
Similarly, we can discover the set of losing trades $T_{lose}$ as follows:
|
||||
|
||||
$$ T_{lose} = \{o \in O | o \leq 0\} $$
|
||||
|
||||
!!! Example
|
||||
In a section where a strategy made four transactions $O = \{3.5, -1, 15, 0\}$:<br>
|
||||
$T_{win} = \{3.5, 15\}$<br>
|
||||
$T_{lose} = \{-1, 0\}$<br>
|
||||
|
||||
### Win Rate and Lose Rate
|
||||
|
||||
The win rate $W$ is the proportion of winning trades with respect to all the trades made by a strategy. We use the following function to compute the win rate:
|
||||
|
||||
$$W = \frac{|T_{win}|}{N}$$
|
||||
|
||||
Where $W$ is the win rate, $N$ is the number of trades and, $T_{win}$ is the set of all trades where the strategy made money.
|
||||
|
||||
Similarly, we can compute the rate of losing trades:
|
||||
|
||||
$$
|
||||
L = \frac{|T_{lose}|}{N}
|
||||
$$
|
||||
|
||||
Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is the set of all trades where the strategy lost money. Note that the above formula is the same as calculating $L = 1 – W$ or $W = 1 – L$
|
||||
|
||||
### Risk Reward Ratio
|
||||
|
||||
Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally:
|
||||
|
||||
$$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$
|
||||
|
||||
???+ Example "Worked example of $R$ calculation"
|
||||
Let's say that you think that the price of *stonecoin* today is 10.0\$. You believe that, because they will start mining stonecoin, it will go up to 15.0\$ tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to 0\$ tomorrow. You are planning to invest 100\$, which will give you 10 shares (100 / 10).
|
||||
|
||||
Your potential profit is calculated as:
|
||||
|
||||
$\begin{aligned}
|
||||
\text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \frac{\text{investment}}{\text{entry_price}} \\
|
||||
&= (15 - 10) * (100 / 10) \\
|
||||
&= 50
|
||||
\end{aligned}$
|
||||
|
||||
Since the price might go to 0\$, the 100\$ dollars invested could turn into 0.
|
||||
|
||||
We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$\).
|
||||
|
||||
$\begin{aligned}
|
||||
\text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\
|
||||
&= (10 - 8.5) * (100 / 10)\\
|
||||
&= 15
|
||||
\end{aligned}$
|
||||
|
||||
We can compute the Risk Reward Ratio as follows:
|
||||
|
||||
$\begin{aligned}
|
||||
R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\
|
||||
&= \frac{50}{15}\\
|
||||
&= 3.33
|
||||
\end{aligned}$<br>
|
||||
What it effectively means is that the strategy have the potential to make 3.33\$ for each 1\$ invested.
|
||||
|
||||
On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows:
|
||||
|
||||
$$ \text{average_profit} = \mu_{win} = \frac{\text{sum_of_profits}}{\text{count_winning_trades}} = \frac{\sum^{o \in T_{win}} o}{|T_{win}|} $$
|
||||
|
||||
Similarly, we can calculate the average loss, $\mu_{lose}$, as follows:
|
||||
|
||||
$$ \text{average_loss} = \mu_{lose} = \frac{\text{sum_of_losses}}{\text{count_losing_trades}} = \frac{\sum^{o \in T_{lose}} o}{|T_{lose}|} $$
|
||||
|
||||
Finally, we can calculate the Risk Reward ratio, $R$, as follows:
|
||||
|
||||
$$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$
|
||||
|
||||
|
||||
???+ Example "Worked example of $R$ calculation using mean profit/loss"
|
||||
Let's say the strategy that we are using makes an average win $\mu_{win} = 2.06$ and an average loss $\mu_{loss} = 4.11$.<br>
|
||||
We calculate the risk reward ratio as follows:<br>
|
||||
$R = \frac{\mu_{win}}{\mu_{loss}} = \frac{2.06}{4.11} = 0.5012...$
|
||||
|
||||
|
||||
### Expectancy
|
||||
|
||||
By combining the Win Rate $W$ and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows:
|
||||
|
||||
$$E = R * W - L$$
|
||||
|
||||
!!! Example "Calculating $E$"
|
||||
Let's say that a strategy has a win rate $W = 0.28$ and a risk reward ratio $R = 5$. What this means is that the strategy is expected to make 5 times the investment around on 28% of the trades it makes. Working out the example:<br>
|
||||
$E = R * W - L = 5 * 0.28 - 0.72 = 0.68$
|
||||
<br>
|
||||
|
||||
The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes 1.68\$ for every 1\$ it loses, on average.
|
||||
|
||||
This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ.
|
||||
|
||||
It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future.
|
||||
|
||||
You can also use this value to evaluate the effectiveness of modifications to this system.
|
||||
|
||||
!!! Note
|
||||
It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Edge combines dynamic stoploss, dynamic positions, and whitelist generation into one isolated module which is then applied to the trading strategy. If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over *N* trades for each stoploss. Here is an example:
|
||||
|
||||
| Pair | Stoploss | Win Rate | Risk Reward Ratio | Expectancy |
|
||||
|----------|:-------------:|-------------:|------------------:|-----------:|
|
||||
| XZC/ETH | -0.01 | 0.50 |1.176384 | 0.088 |
|
||||
| XZC/ETH | -0.02 | 0.51 |1.115941 | 0.079 |
|
||||
| XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 |
|
||||
| XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 |
|
||||
|
||||
The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3%$ leads to the maximum expectancy according to historical data.
|
||||
|
||||
Edge module then forces stoploss value it evaluated to your strategy dynamically.
|
||||
|
||||
### Position size
|
||||
|
||||
Edge dictates the amount at stake for each trade to the bot according to the following factors:
|
||||
|
||||
- Allowed capital at risk
|
||||
- Stoploss
|
||||
|
||||
Allowed capital at risk is calculated as follows:
|
||||
|
||||
```
|
||||
Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade)
|
||||
```
|
||||
|
||||
Stoploss is calculated as described above with respect to historical data.
|
||||
|
||||
The position size is calculated as follows:
|
||||
|
||||
```
|
||||
Position size = (Allowed capital at risk) / Stoploss
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. The capital available percentage is $50%$ and the allowed risk per trade is $1\%$. Thus, the available capital for trading is $10 * 0.5 = 5$ **ETH** and the allowed capital at risk would be $5 * 0.01 = 0.05$ **ETH**.
|
||||
|
||||
- **Trade 1:** The strategy detects a new buy signal in the **XLM/ETH** market. `Edge Positioning` calculates a stoploss of $2\%$ and a position of $0.05 / 0.02 = 2.5$ **ETH**. The bot takes a position of $2.5$ **ETH** in the **XLM/ETH** market.
|
||||
|
||||
- **Trade 2:** The strategy detects a buy signal on the **BTC/ETH** market while **Trade 1** is still open. `Edge Positioning` calculates the stoploss of $4\%$ on this market. Thus, **Trade 2** position size is $0.05 / 0.04 = 1.25$ **ETH**.
|
||||
|
||||
!!! Tip "Available Capital $\neq$ Available in wallet"
|
||||
The available capital for trading didn't change in **Trade 2** even with **Trade 1** still open. The available capital **is not** the free amount in the wallet.
|
||||
|
||||
- **Trade 3:** The strategy detects a buy signal in the **ADA/ETH** market. `Edge Positioning` calculates a stoploss of $1\%$ and a position of $0.05 / 0.01 = 5$ **ETH**. Since **Trade 1** has $2.5$ **ETH** blocked and **Trade 2** has $1.25$ **ETH** blocked, there is only $5 - 1.25 - 2.5 = 1.25$ **ETH** available. Hence, the position size of **Trade 3** is $1.25$ **ETH**.
|
||||
|
||||
!!! Tip "Available Capital Updates"
|
||||
The available capital does not change before a position is sold. After a trade is closed the Available Capital goes up if the trade was profitable or goes down if the trade was a loss.
|
||||
|
||||
- The strategy detects a sell signal in the **XLM/ETH** market. The bot exits **Trade 1** for a profit of $1$ **ETH**. The total capital in the wallet becomes $11$ **ETH** and the available capital for trading becomes $5.5$ **ETH**.
|
||||
|
||||
- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2\%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**.
|
||||
|
||||
## Edge command reference
|
||||
|
||||
--8<-- "commands/edge.md"
|
||||
|
||||
## Configurations
|
||||
|
||||
Edge module has following configuration options:
|
||||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| `enabled` | If true, then Edge will run periodically. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `process_throttle_secs` | How often should Edge run in seconds. <br>*Defaults to `3600` (once per hour).* <br> **Datatype:** Integer
|
||||
| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy. <br> **Note** that it downloads historical data so increasing this number would lead to slowing down the bot. <br>*Defaults to `7`.* <br> **Datatype:** Integer
|
||||
| `allowed_risk` | Ratio of allowed risk per trade. <br>*Defaults to `0.01` (1%)).* <br> **Datatype:** Float
|
||||
| `stoploss_range_min` | Minimum stoploss. <br>*Defaults to `-0.01`.* <br> **Datatype:** Float
|
||||
| `stoploss_range_max` | Maximum stoploss. <br>*Defaults to `-0.10`.* <br> **Datatype:** Float
|
||||
| `stoploss_range_step` | As an example if this is set to -0.01 then Edge will test the strategy for `[-0.01, -0,02, -0,03 ..., -0.09, -0.10]` ranges. <br> **Note** than having a smaller step means having a bigger range which could lead to slow calculation. <br> If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10. <br>*Defaults to `-0.001`.* <br> **Datatype:** Float
|
||||
| `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate. <br>This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio. <br>*Defaults to `0.60`.* <br> **Datatype:** Float
|
||||
| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number. <br>Having an expectancy of 0.20 means if you put 10\$ on a trade you expect a 12\$ return. <br>*Defaults to `0.20`.* <br> **Datatype:** Float
|
||||
| `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. <br>Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br>*Defaults to `10` (it is highly recommended not to decrease this number).* <br> **Datatype:** Integer
|
||||
| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br>**NOTICE:** While configuring this value, you should take into consideration your timeframe. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).<br>*Defaults to `1440` (one day).* <br> **Datatype:** Integer
|
||||
| `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
|
||||
## Running Edge independently
|
||||
|
||||
You can run Edge independently in order to see in details the result. Here is an example:
|
||||
|
||||
``` bash
|
||||
freqtrade edge
|
||||
```
|
||||
|
||||
An example of its output:
|
||||
|
||||
| **pair** | **stoploss** | **win rate** | **risk reward ratio** | **required risk reward** | **expectancy** | **total number of trades** | **average duration (min)** |
|
||||
|:----------|-----------:|-----------:|--------------------:|-----------------------:|-------------:|-----------------:|---------------:|
|
||||
| **AGI/BTC** | -0.02 | 0.64 | 5.86 | 0.56 | 3.41 | 14 | 54 |
|
||||
| **NXS/BTC** | -0.03 | 0.64 | 2.99 | 0.57 | 1.54 | 11 | 26 |
|
||||
| **LEND/BTC** | -0.02 | 0.82 | 2.05 | 0.22 | 1.50 | 11 | 36 |
|
||||
| **VIA/BTC** | -0.01 | 0.55 | 3.01 | 0.83 | 1.19 | 11 | 48 |
|
||||
| **MTH/BTC** | -0.09 | 0.56 | 2.82 | 0.80 | 1.12 | 18 | 52 |
|
||||
| **ARDR/BTC** | -0.04 | 0.42 | 3.14 | 1.40 | 0.73 | 12 | 42 |
|
||||
| **BCPT/BTC** | -0.01 | 0.71 | 1.34 | 0.40 | 0.67 | 14 | 30 |
|
||||
| **WINGS/BTC** | -0.02 | 0.56 | 1.97 | 0.80 | 0.65 | 27 | 42 |
|
||||
| **VIBE/BTC** | -0.02 | 0.83 | 0.91 | 0.20 | 0.59 | 12 | 35 |
|
||||
| **MCO/BTC** | -0.02 | 0.79 | 0.97 | 0.27 | 0.55 | 14 | 31 |
|
||||
| **GNT/BTC** | -0.02 | 0.50 | 2.06 | 1.00 | 0.53 | 18 | 24 |
|
||||
| **HOT/BTC** | -0.01 | 0.17 | 7.72 | 4.81 | 0.50 | 209 | 7 |
|
||||
| **SNM/BTC** | -0.03 | 0.71 | 1.06 | 0.42 | 0.45 | 17 | 38 |
|
||||
| **APPC/BTC** | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 |
|
||||
| **NEBL/BTC** | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 |
|
||||
|
||||
Edge produced the above table by comparing `calculate_since_number_of_days` to `minimum_expectancy` to find `min_trade_number` historical information based on the config file. The timerange Edge uses for its comparisons can be further limited by using the `--timerange` switch.
|
||||
|
||||
In live and dry-run modes, after the `process_throttle_secs` has passed, Edge will again process `calculate_since_number_of_days` against `minimum_expectancy` to find `min_trade_number`. If no `min_trade_number` is found, the bot will return "whitelist empty". Depending on the trade strategy being deployed, "whitelist empty" may be return much of the time - or *all* of the time. The use of Edge may also cause trading to occur in bursts, though this is rare.
|
||||
|
||||
If you encounter "whitelist empty" a lot, condsider tuning `calculate_since_number_of_days`, `minimum_expectancy` and `min_trade_number` to align to the trading frequency of your strategy.
|
||||
|
||||
### Update cached pairs with the latest data
|
||||
|
||||
Edge requires historic data the same way as backtesting does.
|
||||
Please refer to the [Data Downloading](data-download.md) section of the documentation for details.
|
||||
|
||||
### Precising stoploss range
|
||||
|
||||
```bash
|
||||
freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
|
||||
```
|
||||
|
||||
### Advanced use of timerange
|
||||
|
||||
```bash
|
||||
freqtrade edge --timerange=20181110-20181113
|
||||
```
|
||||
|
||||
Doing `--timerange=-20190901` will get all available data until September 1st (excluding September 1st 2019).
|
||||
|
||||
The full timerange specification:
|
||||
|
||||
* Use tickframes till 2018/01/31: `--timerange=-20180131`
|
||||
* Use tickframes since 2018/01/31: `--timerange=20180131-`
|
||||
* Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
||||
* Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600`
|
||||
|
||||
|
||||
[^1]: Question extracted from MIT Opencourseware S096 - Mathematics with applications in Finance: https://ocw.mit.edu/courses/mathematics/18-s096-topics-in-mathematics-with-applications-in-finance-fall-2013/
|
||||
+55
-11
@@ -227,7 +227,7 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
||||
}
|
||||
```
|
||||
|
||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel) and "IOC" (immediate-or-cancel) settings.
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
@@ -271,7 +271,9 @@ Using the wrong exchange will result in the error "OKX Error 50119: API key does
|
||||
## Gate.io
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||
Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||
|
||||
Gate.io supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), and "IOC" (immediate-or-cancel) settings.
|
||||
|
||||
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
||||
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
||||
@@ -286,9 +288,15 @@ Without these permissions, the bot will not start correctly and show errors like
|
||||
|
||||
## Bybit
|
||||
|
||||
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||
|
||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors.
|
||||
Bybit supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
|
||||
|
||||
Futures trading on bybit is currently supported for isolated futures mode.
|
||||
|
||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that manual changes to this setting may result in exceptions and errors.
|
||||
|
||||
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
|
||||
|
||||
@@ -305,11 +313,6 @@ We do strongly recommend to limit all API keys to the IP you're going to use it
|
||||
We therefore recommend the usage of one subaccount per bot. This is especially important when using unified accounts.
|
||||
Other configurations (multiple bots on one account, manual non-bot trades on the bot account) are not supported and may lead to unexpected behavior.
|
||||
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||
|
||||
## Bitmart
|
||||
|
||||
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
||||
@@ -328,6 +331,26 @@ It's therefore required to pass the UID as well.
|
||||
!!! Warning "Necessary Verification"
|
||||
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
|
||||
|
||||
## Bitget
|
||||
|
||||
Bitget requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "bitget",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"password": "your_exchange_api_key_password",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Bitget supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "FOK" (full-or-cancel), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Bitget supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||
|
||||
## Hyperliquid
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
@@ -339,13 +362,13 @@ This needs to be configured like this:
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "your_eth_wallet_address",
|
||||
"walletAddress": "your_eth_wallet_address", // This should NOT be your API Wallet Address!
|
||||
"privateKey": "your_api_private_key",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your wallet address, not your API Wallet Address.
|
||||
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your main wallet address, not your API Wallet Address.
|
||||
* privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation.
|
||||
|
||||
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
|
||||
@@ -363,6 +386,27 @@ Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer
|
||||
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
|
||||
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
|
||||
|
||||
### Hyperliquid Vault / Subaccount
|
||||
|
||||
Hyperliquid allows you to create either a vault or a subaccount.
|
||||
To use these with Freqtrade, you will need to use the following configuration pattern:
|
||||
|
||||
``` json
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "your_vault_address", // Vault or subaccount address
|
||||
"privateKey": "your_api_private_key",
|
||||
"ccxt_config": {
|
||||
"options": {
|
||||
"vaultAddress": "your_vault_address" // Optional, only if you want to use a vault or subaccount
|
||||
}
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Your balance and trades will now be used from your vault / subaccount - and no longer from your main account.
|
||||
|
||||
### Historic Hyperliquid data
|
||||
|
||||
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
||||
|
||||
+8
-14
@@ -159,6 +159,14 @@ This warning can point to one of the below problems:
|
||||
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
||||
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
||||
|
||||
### I get the message "Couldn't reuse watch for xxx" in the log
|
||||
|
||||
This is an informational message that the bot tried to use candles from the websocket, but the exchange didn't provide the right information.
|
||||
This can happen if there was an interruption to the websocket connection - or if the pair didn't have any trades happen in the timeframe you are using.
|
||||
|
||||
Freqtrade will handle this gracefully by falling back to the REST api.
|
||||
While this makes the iteration slightly slower (due to the REST Api call) - it will not cause any problems to the bot's operation.
|
||||
|
||||
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
||||
|
||||
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io).
|
||||
@@ -276,20 +284,6 @@ Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we
|
||||
Example:
|
||||
`freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601`
|
||||
|
||||
## Edge module
|
||||
|
||||
### Edge implements interesting approach for controlling position size, is there any theory behind it?
|
||||
|
||||
The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members.
|
||||
|
||||
You can find further info on expectancy, win rate, risk management and position size in the following sources:
|
||||
|
||||
- https://www.tradeciety.com/ultimate-math-guide-for-traders/
|
||||
- https://samuraitradingacademy.com/trading-expectancy/
|
||||
- https://www.learningmarkets.com/determining-expectancy-in-your-trading/
|
||||
- https://www.lonestocktrader.com/make-money-trading-positive-expectancy/
|
||||
- https://www.babypips.com/trading/trade-expectancy-matter
|
||||
|
||||
## Official channels
|
||||
|
||||
Freqtrade is using exclusively the following official channels:
|
||||
|
||||
@@ -389,6 +389,8 @@ The `refresh_period` setting defines the interval (in seconds) at which the mark
|
||||
The `categories` setting specifies the [coingecko categories](https://www.coingecko.com/en/categories) from which to select coins from. The default is an empty list `[]`, meaning no category filtering is applied.
|
||||
If an incorrect category string is chosen, the plugin will print the available categories from CoinGecko and fail. The category should be the ID of the category, for example, for `https://www.coingecko.com/en/categories/layer-1`, the category ID would be `layer-1`. You can pass multiple categories such as `["layer-1", "meme-token"]` to select from several categories.
|
||||
|
||||
Coins like 1000PEPE/USDT or KPEPE/USDT:USDT are detected on a best effort basis, with the prefixes `1000` and `K` being used to identify them.
|
||||
|
||||
!!! Warning "Many categories"
|
||||
Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues.
|
||||
|
||||
|
||||
+5
-7
@@ -3,12 +3,11 @@
|
||||
[](https://github.com/freqtrade/freqtrade/actions/)
|
||||
[](https://doi.org/10.21105/joss.04864)
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||
|
||||
<!-- Place this tag where you want the button to render. -->
|
||||
<a class="github-button" href="https://github.com/freqtrade/freqtrade" data-icon="octicon-star" data-size="large" aria-label="Star freqtrade/freqtrade on GitHub">Star</a>
|
||||
<a class="github-button" href="https://github.com/freqtrade/freqtrade/fork" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork freqtrade/freqtrade on GitHub">Fork</a>
|
||||
<a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/stable.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a>
|
||||
<!-- GitHub action buttons -->
|
||||
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
||||
[:octicons-repo-forked-16: Fork](https://github.com/freqtrade/freqtrade/fork){ .md-button .md-button--sm }
|
||||
[:octicons-download-16: Download](https://github.com/freqtrade/freqtrade/archive/stable.zip){ .md-button .md-button--sm }
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -31,7 +30,6 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
||||
- Optimize: Find the best parameters for your strategy using hyperoptimization which employs machine learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy.
|
||||
- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade.
|
||||
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
|
||||
- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital.
|
||||
- Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
|
||||
- Analyze: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
|
||||
|
||||
@@ -88,7 +86,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
||||
|
||||
Alternatively
|
||||
|
||||
- Python 3.10+
|
||||
- Python 3.11+
|
||||
- pip (pip3)
|
||||
- git
|
||||
- TA-Lib
|
||||
|
||||
+4
-44
@@ -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).
|
||||
|
||||
!!! Note
|
||||
Python3.10 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.11 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.
|
||||
|
||||
!!! Warning "Up-to-date clock"
|
||||
@@ -42,11 +42,10 @@ These requirements apply to both [Script Installation](#script-installation) and
|
||||
|
||||
### Install guide
|
||||
|
||||
* [Python >= 3.10](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [Python >= 3.11](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||
* [TA-Lib](https://ta-lib.github.io/ta-lib-python/) (install instructions [below](#install-ta-lib))
|
||||
|
||||
### Install code
|
||||
|
||||
@@ -54,7 +53,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
|
||||
OS Specific steps are listed first, the common section below is necessary for all systems.
|
||||
|
||||
!!! Note
|
||||
Python3.10 or higher and the corresponding pip are assumed to be available.
|
||||
Python3.11 or higher and the corresponding pip are assumed to be available.
|
||||
|
||||
=== "Debian/Ubuntu"
|
||||
#### Install necessary dependencies
|
||||
@@ -179,7 +178,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
|
||||
** --install **
|
||||
|
||||
With this option, the script will install the bot and most dependencies:
|
||||
You will need to have git and python3.10+ installed beforehand for this to work.
|
||||
You will need to have git and python3.11+ installed beforehand for this to work.
|
||||
|
||||
* Mandatory software as: `ta-lib`
|
||||
* Setup your virtualenv under `.venv/`
|
||||
@@ -201,35 +200,6 @@ This option will hard reset your branch (only if you are on either `stable` or `
|
||||
|
||||
Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository).
|
||||
|
||||
### Install TA-Lib
|
||||
|
||||
#### TA-Lib script installation
|
||||
|
||||
```bash
|
||||
sudo ./build_helpers/install_ta-lib.sh
|
||||
```
|
||||
|
||||
!!! Note
|
||||
This will use the ta-lib tar.gz included in this repository.
|
||||
|
||||
##### TA-Lib manual installation
|
||||
|
||||
[Official installation guide](https://ta-lib.github.io/ta-lib-python/install.html)
|
||||
|
||||
```bash
|
||||
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||
tar xvzf ta-lib-0.4.0-src.tar.gz
|
||||
cd ta-lib
|
||||
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||
./configure --prefix=/usr/local
|
||||
make
|
||||
sudo make install
|
||||
# On debian based systems (debian, ubuntu, ...) - updating ldconfig might be necessary.
|
||||
sudo ldconfig
|
||||
cd ..
|
||||
rm -rf ./ta-lib*
|
||||
```
|
||||
|
||||
### Setup Python virtual environment (virtualenv)
|
||||
|
||||
You will run freqtrade in separated `virtual environment`
|
||||
@@ -332,16 +302,6 @@ python3 -m pip install -r requirements.txt
|
||||
python3 -m pip install -e .
|
||||
```
|
||||
|
||||
Patch conda libta-lib (Linux only)
|
||||
|
||||
```bash
|
||||
# Ensure that the environment is active!
|
||||
conda activate freqtrade
|
||||
|
||||
cd build_helpers
|
||||
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
|
||||
```
|
||||
|
||||
[You are now ready](#you-are-ready) to run the bot.
|
||||
|
||||
### Important shortcuts
|
||||
|
||||
@@ -38,7 +38,7 @@ Many strategies, without the programmer knowing, have fallen prey to lookahead b
|
||||
This typically makes the strategy backtest look profitable, sometimes to extremes, but this is not realistic as the strategy is "cheating" by looking at data it would not have in dry or live modes.
|
||||
|
||||
The reason why strategies can "cheat" is because the freqtrade backtesting process populates the full dataframe including all candle timestamps at the outset.
|
||||
If the programmer is not careful or oblivious how things work internally
|
||||
If the programmer is not careful or oblivious how things work internally
|
||||
(which sometimes can be really hard to find out) then the strategy will look into the future.
|
||||
|
||||
This command is made to try to verify the validity in the form of the aforementioned lookahead bias.
|
||||
@@ -50,8 +50,7 @@ After this initial backtest runs, it will look if the `minimum-trade-amount` is
|
||||
If this happens, use a wider timerange to get more trades for the analysis, or use a timerange where more trades occur.
|
||||
|
||||
After setting the baseline it will then do additional backtest runs for every entry and exit separately.
|
||||
When these verification backtests complete, it will compare the indicators at the signal candles (both entry or exit)
|
||||
and report the bias.
|
||||
When these verification backtests complete, it will compare both dataframes (baseline and sliced) for any difference in columns' value and report the bias.
|
||||
After all signals have been verified or falsified a result table will be generated for the user to see.
|
||||
|
||||
### How to find and remove bias? How can I salvage a biased strategy?
|
||||
@@ -98,8 +97,8 @@ If the strategy has many different signals / signal types, it's up to you to sel
|
||||
This would lead to a false-negative, i.e. the strategy will be reported as non-biased.
|
||||
- `lookahead-analysis` has access to the same backtesting options and this can introduce problems.
|
||||
Please don't use any options like enabling position stacking as this will distort the number of checked signals.
|
||||
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
|
||||
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
|
||||
and that you have enough capital in the backtest wallet configuration.
|
||||
- In the results table, the `biased_indicators` column
|
||||
- In the results table, the `biased_indicators` column
|
||||
will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased.
|
||||
**These are not biased and can safely be ignored.**
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
{{ super() }}
|
||||
|
||||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
||||
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
markdown==3.8
|
||||
markdown==3.8.2
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.6.14
|
||||
mkdocs-material==9.6.18
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.15
|
||||
pymdown-extensions==10.16.1
|
||||
jinja2==3.1.6
|
||||
mike==2.1.3
|
||||
|
||||
@@ -190,9 +190,6 @@ delete_trade
|
||||
|
||||
:param trade_id: Deletes the trade with this ID from the database.
|
||||
|
||||
edge
|
||||
Return information about edge.
|
||||
|
||||
forcebuy
|
||||
Buy an asset.
|
||||
|
||||
@@ -368,7 +365,6 @@ All endpoints in the below table need to be prefixed with the base URL of the AP
|
||||
| `/blacklist` | GET | Show the current blacklist.
|
||||
| `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `pair` (`str`)
|
||||
| `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`)
|
||||
| `/edge` | GET | Show validated pairs by Edge if it is enabled.
|
||||
| `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
|
||||
| `/pair_candles` | POST | Returns dataframe for a pair / timeframe combination while the bot is running, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
|
||||
| `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
|
||||
|
||||
+2
-1
@@ -31,6 +31,7 @@ The Order-type will be ignored if only one mode is available.
|
||||
| Binance | limit |
|
||||
| Binance Futures | market, limit |
|
||||
| Bingx | market, limit |
|
||||
| Bitget | market, limit |
|
||||
| HTX | limit |
|
||||
| kraken | market, limit |
|
||||
| Gate | limit |
|
||||
@@ -256,4 +257,4 @@ The new stoploss value will be applied to open trades (and corresponding log-mes
|
||||
|
||||
### Limitations
|
||||
|
||||
Stoploss values cannot be changed if `trailing_stop` is enabled and the stoploss has already been adjusted, or if [Edge](edge.md) is enabled (since Edge would recalculate stoploss based on the current market situation).
|
||||
Stoploss values cannot be changed if `trailing_stop` is enabled and the stoploss has already been adjusted.
|
||||
|
||||
+37
-14
@@ -174,17 +174,27 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
## Enter Tag
|
||||
|
||||
When your strategy has multiple buy signals, you can name the signal that triggered.
|
||||
Then you can access your buy signal on `custom_exit`
|
||||
When your strategy has multiple entry signals, you can name the signal that triggered.
|
||||
Then you can access your entry signal on `custom_exit`
|
||||
|
||||
```python
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe["enter_tag"] = ""
|
||||
signal_rsi = (qtpylib.crossed_above(dataframe["rsi"], 35))
|
||||
signal_bblower = (dataframe["bb_lowerband"] < dataframe["close"])
|
||||
# Additional conditions
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['rsi'] < 35) &
|
||||
(dataframe['volume'] > 0)
|
||||
),
|
||||
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
|
||||
signal_rsi
|
||||
| signal_bblower
|
||||
# ... additional signals to enter a long position
|
||||
)
|
||||
& (dataframe["volume"] > 0)
|
||||
, "enter_long"
|
||||
] = 1
|
||||
# Concatenate the tags so all signals are kept
|
||||
dataframe.loc[signal_rsi, "enter_tag"] += "long_signal_rsi "
|
||||
dataframe.loc[signal_bblower, "enter_tag"] += "long_signal_bblower "
|
||||
|
||||
return dataframe
|
||||
|
||||
@@ -192,14 +202,17 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r
|
||||
current_profit: float, **kwargs):
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
|
||||
return 'sell_signal_rsi'
|
||||
if "long_signal_rsi" in trade.enter_tag and last_candle["rsi"] > 80:
|
||||
return "exit_signal_rsi"
|
||||
if "long_signal_bblower" in trade.enter_tag and last_candle["high"] > last_candle["bb_upperband"]:
|
||||
return "exit_signal_bblower"
|
||||
# ...
|
||||
return None
|
||||
|
||||
```
|
||||
|
||||
!!! Note
|
||||
`enter_tag` is limited to 100 characters, remaining data will be truncated.
|
||||
`enter_tag` is limited to 255 characters, remaining data will be truncated.
|
||||
|
||||
!!! Warning
|
||||
There is only one `enter_tag` column, which is used for both long and short trades.
|
||||
@@ -213,17 +226,27 @@ Similar to [Entry Tagging](#enter-tag), you can also specify an exit tag.
|
||||
|
||||
``` python
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe["exit_tag"] = ""
|
||||
rsi_exit_signal = (dataframe["rsi"] > 70)
|
||||
ema_exit_signal = (dataframe["ema20"] < dataframe["ema50"])
|
||||
# Additional conditions
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['rsi'] > 70) &
|
||||
(dataframe['volume'] > 0)
|
||||
),
|
||||
['exit_long', 'exit_tag']] = (1, 'exit_rsi')
|
||||
rsi_exit_signal
|
||||
| ema_exit_signal
|
||||
# ... additional signals to exit a long position
|
||||
) &
|
||||
(dataframe["volume"] > 0)
|
||||
,
|
||||
"exit_long"] = 1
|
||||
# Concatenate the tags so all signals are kept
|
||||
dataframe.loc[rsi_exit_signal, "exit_tag"] += "exit_signal_rsi "
|
||||
dataframe.loc[rsi_exit_signal2, "exit_tag"] += "exit_signal_rsi "
|
||||
|
||||
return dataframe
|
||||
```
|
||||
|
||||
The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
|
||||
The provided exit-tag is then used as exit-reason - and shown as such in backtest results.
|
||||
|
||||
!!! Note
|
||||
`exit_reason` is limited to 100 characters, remaining data will be truncated.
|
||||
|
||||
@@ -1068,7 +1068,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||
``` python
|
||||
from freqtrade.persistence import Trade
|
||||
from datetime import timedelta, datetime, timezone
|
||||
# Put the above lines a the top of the strategy file, next to all the other imports
|
||||
# Put the above lines at the top of the strategy file, next to all the other imports
|
||||
# --------
|
||||
|
||||
# Within populate indicators (or populate_entry_trend):
|
||||
|
||||
@@ -19,3 +19,36 @@
|
||||
#available-endpoints ~ .md-typeset__scrollwrap .md-typeset__table th:first-of-type {
|
||||
width: 35% !important;
|
||||
}
|
||||
|
||||
|
||||
.md-typeset .md-button--sm {
|
||||
padding: 0.2em 1em;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
color: #24292f;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 0.25em;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.md-typeset .md-button--sm:hover {
|
||||
background-color: #e5eaee;
|
||||
border-color: #d1d9e0;
|
||||
text-decoration: none;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.md-typeset .md-button--sm:active {
|
||||
background-color: #ebecf0;
|
||||
border-color: #afb8c1;
|
||||
box-shadow: inset 0 1px 0 rgba(175, 184, 193, 0.2);
|
||||
}
|
||||
|
||||
.md-grid {
|
||||
/* default is max-width: 61rem; */
|
||||
max-width: 75rem;
|
||||
}
|
||||
|
||||
+4
-17
@@ -188,7 +188,7 @@ You can create your own keyboard in `config.json`:
|
||||
!!! Note "Supported Commands"
|
||||
Only the following commands are allowed. Command arguments are not supported!
|
||||
|
||||
`/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||
`/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/help`, `/version`, `/marketdir`
|
||||
|
||||
## Telegram commands
|
||||
|
||||
@@ -229,6 +229,7 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
|
||||
| **Metrics** |
|
||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||
| `/profit_[long|short] [<n>]` | Display a summary of your profit/loss from close trades in one direction and some stats about your performance, over the last n days (all trades by default)
|
||||
| `/performance` | Show performance of each finished trade grouped by pair
|
||||
| `/balance` | Show bot managed balance per currency
|
||||
| `/balance full` | Show account balance per currency
|
||||
@@ -240,7 +241,6 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/entries` | Shows Wins / losses by Exit reason as well as Avg. holding durations for buys and sells
|
||||
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing.
|
||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||
|
||||
## Telegram commands in action
|
||||
|
||||
@@ -310,6 +310,8 @@ current max
|
||||
|
||||
### /profit
|
||||
|
||||
Also available as `/profit_long` and `/profit_short` to show profit for long or short trades only.
|
||||
|
||||
Return a summary of your profit/loss and performance.
|
||||
|
||||
> **ROI:** Close trades
|
||||
@@ -451,21 +453,6 @@ Use `/reload_config` to reset the blacklist.
|
||||
> Using blacklist `StaticPairList` with 2 pairs
|
||||
>`DODGE/BTC`, `HOT/BTC`.
|
||||
|
||||
### /edge
|
||||
|
||||
Shows pairs validated by Edge along with their corresponding win-rate, expectancy and stoploss values.
|
||||
|
||||
> **Edge only validated following pairs:**
|
||||
```
|
||||
Pair Winrate Expectancy Stoploss
|
||||
-------- --------- ------------ ----------
|
||||
DOCK/ETH 0.522727 0.881821 -0.03
|
||||
PHX/ETH 0.677419 0.560488 -0.03
|
||||
HOT/ETH 0.733333 0.490492 -0.03
|
||||
HC/ETH 0.588235 0.280988 -0.02
|
||||
ARDR/ETH 0.366667 0.143059 -0.01
|
||||
```
|
||||
|
||||
### /version
|
||||
|
||||
> **Version:** `0.14.3`
|
||||
|
||||
@@ -42,7 +42,3 @@ freqtrade install-ui
|
||||
|
||||
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib).
|
||||
Please refer to the corresponding installation sections (common problems linked below)
|
||||
|
||||
Common problems and their solutions:
|
||||
|
||||
* [ta-lib update on windows](windows_installation.md#install-ta-lib)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Utility Subcommands
|
||||
|
||||
Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
|
||||
Besides the Live-Trade and Dry-Run run modes, the `backtesting` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
|
||||
|
||||
## Create userdir
|
||||
|
||||
|
||||
+9
-52
@@ -117,9 +117,9 @@ Different payloads can be configured for different events. Not all fields are ne
|
||||
|
||||
## Webhook Message types
|
||||
|
||||
### Entry
|
||||
### Entry / Entry fill
|
||||
|
||||
The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format.
|
||||
The fields in `webhook.entry` and `webhook.entry_fill` are filled when the bot places a long/short Order to increase a position, or when that order fills respectively. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
@@ -162,31 +162,9 @@ Possible parameters are:
|
||||
* `current_rate`
|
||||
* `enter_tag`
|
||||
|
||||
### Entry fill
|
||||
### Exit / Exit fill
|
||||
|
||||
The fields in `webhook.entry_fill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `open_rate`
|
||||
* `amount`
|
||||
* `open_date`
|
||||
* `stake_amount`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `quote_currency`
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
* `enter_tag`
|
||||
|
||||
### Exit
|
||||
|
||||
The fields in `webhook.exit` are filled when the bot exits a trade. Parameters are filled using string.format.
|
||||
The fields in `webhook.exit` and `webhook.exit_fill` are filled when the bot places an exit order, or when that exit order fills respectively. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
@@ -195,34 +173,9 @@ Possible parameters are:
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `gain`
|
||||
* `limit`
|
||||
* `amount`
|
||||
* `open_rate`
|
||||
* `profit_amount`
|
||||
* `profit_ratio`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `quote_currency`
|
||||
* `fiat_currency`
|
||||
* `exit_reason`
|
||||
* `order_type`
|
||||
* `open_date`
|
||||
* `close_date`
|
||||
|
||||
### Exit fill
|
||||
|
||||
The fields in `webhook.exit_fill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `gain`
|
||||
* `close_rate`
|
||||
* `amount`
|
||||
* `open_rate`
|
||||
* `current_rate`
|
||||
* `profit_amount`
|
||||
* `profit_ratio`
|
||||
@@ -230,10 +183,14 @@ Possible parameters are:
|
||||
* `base_currency`
|
||||
* `quote_currency`
|
||||
* `fiat_currency`
|
||||
* `enter_tag`
|
||||
* `exit_reason`
|
||||
* `order_type`
|
||||
* `open_date`
|
||||
* `close_date`
|
||||
* `sub_trade`
|
||||
* `is_final_exit`
|
||||
|
||||
|
||||
### Exit cancel
|
||||
|
||||
@@ -246,7 +203,7 @@ Possible parameters are:
|
||||
* `direction`
|
||||
* `leverage`
|
||||
* `gain`
|
||||
* `limit`
|
||||
* `order_rate`
|
||||
* `amount`
|
||||
* `open_rate`
|
||||
* `current_rate`
|
||||
|
||||
@@ -5,7 +5,7 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
|
||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||
Otherwise, please follow the instructions below.
|
||||
|
||||
All instructions assume that python 3.10+ is installed and available.
|
||||
All instructions assume that python 3.11+ is installed and available.
|
||||
|
||||
## Clone the git repository
|
||||
|
||||
@@ -38,30 +38,6 @@ cd freqtrade
|
||||
!!! Hint
|
||||
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
|
||||
|
||||
### Install ta-lib
|
||||
|
||||
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#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.10, 3.11 and 3.12) and for 64bit Windows.
|
||||
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.
|
||||
|
||||
``` powershell
|
||||
cd \path\freqtrade
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate.ps1
|
||||
# optionally install ta-lib from wheel
|
||||
# Eventually adjust the below filename to match the downloaded wheel
|
||||
pip install --find-links build_helpers\ TA-Lib -U
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
freqtrade
|
||||
```
|
||||
|
||||
!!! Note "Use Powershell"
|
||||
The above installation script assumes you're using powershell on a 64bit windows.
|
||||
Commands for the legacy CMD windows console may differ.
|
||||
|
||||
### Error during installation on Windows
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2025.5"
|
||||
__version__ = "2025.8"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
__main__.py for Freqtrade
|
||||
To launch Freqtrade as a module
|
||||
|
||||
> python -m freqtrade (with Python >= 3.10)
|
||||
> python -m freqtrade (with Python >= 3.11)
|
||||
"""
|
||||
|
||||
from freqtrade import main
|
||||
|
||||
@@ -17,7 +17,7 @@ def start_analysis_entries_exits(args: dict[str, Any]) -> None:
|
||||
from freqtrade.data.entryexitanalysis import process_entry_exit_reasons
|
||||
|
||||
# Initialize configuration
|
||||
config = setup_utils_configuration(args, RunMode.BACKTEST)
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
logger.info("Starting freqtrade in analysis mode")
|
||||
|
||||
|
||||
@@ -54,9 +54,11 @@ ARGS_BACKTEST = [
|
||||
"strategy_list",
|
||||
"export",
|
||||
"exportfilename",
|
||||
"exportdirectory",
|
||||
"backtest_breakdown",
|
||||
"backtest_cache",
|
||||
"freqai_backtest_live_models",
|
||||
"backtest_notes",
|
||||
]
|
||||
|
||||
ARGS_HYPEROPT = [
|
||||
@@ -81,7 +83,7 @@ ARGS_HYPEROPT = [
|
||||
"early_stop",
|
||||
]
|
||||
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE, "stoploss_range"]
|
||||
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE]
|
||||
|
||||
ARGS_LIST_STRATEGIES = [
|
||||
"strategy_path",
|
||||
@@ -93,9 +95,14 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column"]
|
||||
|
||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
|
||||
|
||||
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"]
|
||||
ARGS_BACKTEST_SHOW = [
|
||||
"exportfilename",
|
||||
"exportdirectory",
|
||||
"backtest_show_pair_list",
|
||||
"backtest_breakdown",
|
||||
]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
|
||||
|
||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||
|
||||
@@ -232,6 +239,7 @@ ARGS_HYPEROPT_SHOW = [
|
||||
|
||||
ARGS_ANALYZE_ENTRIES_EXITS = [
|
||||
"exportfilename",
|
||||
"exportdirectory",
|
||||
"analysis_groups",
|
||||
"enter_reason_list",
|
||||
"exit_reason_list",
|
||||
@@ -250,31 +258,33 @@ ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_s
|
||||
ARGS_LOOKAHEAD_ANALYSIS = [
|
||||
a
|
||||
for a in ARGS_BACKTEST
|
||||
if a not in ("position_stacking", "backtest_cache", "backtest_breakdown")
|
||||
if a not in ("position_stacking", "backtest_cache", "backtest_breakdown", "backtest_notes")
|
||||
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
||||
|
||||
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
|
||||
|
||||
# Command level configs - keep at the bottom of the above definitions
|
||||
NO_CONF_REQURIED = [
|
||||
"backtest-filter",
|
||||
"backtesting-show",
|
||||
"convert-data",
|
||||
"convert-trade-data",
|
||||
"download-data",
|
||||
"list-timeframes",
|
||||
"hyperopt-list",
|
||||
"hyperopt-show",
|
||||
"list-data",
|
||||
"list-freqaimodels",
|
||||
"list-hyperoptloss",
|
||||
"list-markets",
|
||||
"list-pairs",
|
||||
"list-strategies",
|
||||
"list-freqaimodels",
|
||||
"list-hyperoptloss",
|
||||
"list-data",
|
||||
"hyperopt-list",
|
||||
"hyperopt-show",
|
||||
"backtest-filter",
|
||||
"list-timeframes",
|
||||
"plot-dataframe",
|
||||
"plot-profit",
|
||||
"show-trades",
|
||||
"trades-to-ohlcv",
|
||||
"install-ui",
|
||||
"strategy-updater",
|
||||
"trades-to-ohlcv",
|
||||
]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
@@ -310,8 +320,6 @@ class Arguments:
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
if "config" in parsed_arg and parsed_arg.config is None:
|
||||
conf_required = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
|
||||
|
||||
if "user_data_dir" in parsed_arg and parsed_arg.user_data_dir is not None:
|
||||
user_dir = parsed_arg.user_data_dir
|
||||
else:
|
||||
@@ -324,7 +332,9 @@ class Arguments:
|
||||
else:
|
||||
# Else use "config.json".
|
||||
cfgfile = Path.cwd() / DEFAULT_CONFIG
|
||||
if cfgfile.is_file() or not conf_required:
|
||||
conf_optional = "command" in parsed_arg and parsed_arg.command in NO_CONF_REQURIED
|
||||
if cfgfile.is_file() or not conf_optional:
|
||||
# Only inject config if the file exists, or if the config is required
|
||||
parsed_arg.config = [DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
@@ -505,7 +515,9 @@ class Arguments:
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser(
|
||||
"edge", help="Edge module.", parents=[_common_parser, _strategy_parser]
|
||||
"edge",
|
||||
help="Edge module. No longer part of Freqtrade",
|
||||
parents=[_common_parser, _strategy_parser],
|
||||
)
|
||||
edge_cmd.set_defaults(func=start_edge)
|
||||
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
||||
|
||||
@@ -199,17 +199,29 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
|
||||
nargs="+",
|
||||
),
|
||||
"backtest_notes": Arg(
|
||||
"--notes",
|
||||
help="Add notes to the backtest results.",
|
||||
metavar="TEXT",
|
||||
),
|
||||
"export": Arg(
|
||||
"--export",
|
||||
help="Export backtest results (default: trades).",
|
||||
choices=constants.EXPORT_OPTIONS,
|
||||
),
|
||||
"exportdirectory": Arg(
|
||||
"--backtest-directory",
|
||||
"--export-directory",
|
||||
help="Directory to use for backtest results. "
|
||||
"Example: `--export-directory=user_data/backtest_results/`. ",
|
||||
metavar="PATH",
|
||||
),
|
||||
"exportfilename": Arg(
|
||||
"--export-filename",
|
||||
"--backtest-filename",
|
||||
"--export-filename",
|
||||
help="Use this filename for backtest results."
|
||||
"Requires `--export` to be set as well. "
|
||||
"Example: `--export-filename=user_data/backtest_results/backtest_today.json`",
|
||||
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
|
||||
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
|
||||
metavar="PATH",
|
||||
),
|
||||
"disableparamexport": Arg(
|
||||
@@ -235,13 +247,6 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
default=constants.BACKTEST_CACHE_DEFAULT,
|
||||
choices=constants.BACKTEST_CACHE_AGE,
|
||||
),
|
||||
# Edge
|
||||
"stoploss_range": Arg(
|
||||
"--stoplosses",
|
||||
help="Defines a range of stoploss values against which edge will assess the strategy. "
|
||||
'The format is "min,max,step" (without any space). '
|
||||
"Example: `--stoplosses=-0.01,-0.1,-0.001`",
|
||||
),
|
||||
# Hyperopt
|
||||
"hyperopt": Arg(
|
||||
"--hyperopt",
|
||||
@@ -371,6 +376,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help="Print all exchanges known to the ccxt library.",
|
||||
action="store_true",
|
||||
),
|
||||
"dex_exchanges": Arg(
|
||||
"--dex-exchanges",
|
||||
help="Print only DEX exchanges.",
|
||||
action="store_true",
|
||||
),
|
||||
# List pairs / markets
|
||||
"list_pairs_all": Arg(
|
||||
"-a",
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
|
||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||
from freqtrade.exceptions import ConfigurationError
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -134,7 +134,8 @@ def start_list_data(args: dict[str, Any]) -> None:
|
||||
config["datadir"], config.get("trading_mode", TradingMode.SPOT)
|
||||
)
|
||||
if args["pairs"]:
|
||||
paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]]
|
||||
pl = expand_pairlist(args["pairs"], [p[0] for p in paircombs], keep_invalid=True)
|
||||
paircombs = [comb for comb in paircombs if comb[0] in pl]
|
||||
title = f"Found {len(paircombs)} pair / timeframe combinations."
|
||||
if not config.get("show_timerange"):
|
||||
groupedpair = defaultdict(list)
|
||||
@@ -197,7 +198,8 @@ def start_list_trades_data(args: dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
if args["pairs"]:
|
||||
paircombs = [comb for comb in paircombs if comb in args["pairs"]]
|
||||
pl = expand_pairlist(args["pairs"], [p for p in paircombs], keep_invalid=True)
|
||||
paircombs = [comb for comb in paircombs if comb in pl]
|
||||
|
||||
title = f"Found trades data for {len(paircombs)} {plural(len(paircombs), 'pair')}."
|
||||
if not config.get("show_timerange"):
|
||||
|
||||
@@ -46,7 +46,18 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
table.add_column("Markets")
|
||||
table.add_column("Reason")
|
||||
|
||||
trading_mode = args.get("trading_mode", None)
|
||||
dex_only = args.get("dex_exchanges", False)
|
||||
|
||||
for exchange in available_exchanges:
|
||||
if trading_mode and not any(
|
||||
a["trading_mode"] == trading_mode for a in exchange["trade_modes"]
|
||||
):
|
||||
# If trading_mode is specified, only show exchanges that support it
|
||||
continue
|
||||
if dex_only and not exchange.get("dex", False):
|
||||
# If dex_only is specified, only show DEX exchanges
|
||||
continue
|
||||
name = Text(exchange["name"])
|
||||
if exchange["supported"]:
|
||||
name.append(" (Supported)", style="italic")
|
||||
@@ -135,6 +146,9 @@ def start_list_strategies(args: dict[str, Any]) -> None:
|
||||
strategy_objs = StrategyResolver.search_all_objects(
|
||||
config, not args["print_one_column"], config.get("recursive_strategy_search", False)
|
||||
)
|
||||
if not strategy_objs:
|
||||
logger.warning("No strategies found.")
|
||||
return
|
||||
# Sort alphabetically
|
||||
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
||||
for obj in strategy_objs:
|
||||
|
||||
@@ -72,7 +72,7 @@ def start_backtesting_show(args: dict[str, Any]) -> None:
|
||||
from freqtrade.data.btanalysis import load_backtest_stats
|
||||
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
||||
|
||||
results = load_backtest_stats(config["exportfilename"])
|
||||
results = load_backtest_stats(config["exportdirectory"], config["exportfilename"])
|
||||
|
||||
show_backtest_results(config, results)
|
||||
show_sorted_pairlist(config, results)
|
||||
@@ -129,15 +129,10 @@ def start_edge(args: dict[str, Any]) -> None:
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize.edge_cli import EdgeCli
|
||||
|
||||
# Initialize configuration
|
||||
config = setup_optimize_configuration(args, RunMode.EDGE)
|
||||
logger.info("Starting freqtrade in Edge mode")
|
||||
|
||||
# Initialize Edge object
|
||||
edge_cli = EdgeCli(config)
|
||||
edge_cli.start()
|
||||
raise ConfigurationError(
|
||||
"The Edge module has been deprecated in 2023.9 and removed in 2025.6. "
|
||||
"All functionalities of edge have been removed."
|
||||
)
|
||||
|
||||
|
||||
def start_lookahead_analysis(args: dict[str, Any]) -> None:
|
||||
|
||||
@@ -157,6 +157,16 @@ CONF_SCHEMA = {
|
||||
"description": f"Offset for profit exit. {__IN_STRATEGY}",
|
||||
"type": "number",
|
||||
},
|
||||
"recursive_strategy_search": {
|
||||
"description": "Enable recursive strategy search.",
|
||||
"type": "boolean",
|
||||
},
|
||||
"user_data_dir": {
|
||||
"description": "Path to the user data directory.",
|
||||
},
|
||||
"datadir": {
|
||||
"description": "Path to the data directory.",
|
||||
},
|
||||
"fee": {
|
||||
"description": "Trading fee percentage. Can help to simulate slippage in backtesting",
|
||||
"type": "number",
|
||||
@@ -423,10 +433,6 @@ CONF_SCHEMA = {
|
||||
"description": "Exchange configuration.",
|
||||
"$ref": "#/definitions/exchange",
|
||||
},
|
||||
"edge": {
|
||||
"description": "Edge configuration.",
|
||||
"$ref": "#/definitions/edge",
|
||||
},
|
||||
"log_config": {
|
||||
"description": "Logging configuration.",
|
||||
"$ref": "#/definitions/logging",
|
||||
@@ -447,6 +453,7 @@ CONF_SCHEMA = {
|
||||
"pairlists": {
|
||||
"description": "Configuration for pairlists.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -913,30 +920,22 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"ccxt_config": {"description": "CCXT configuration settings.", "type": "object"},
|
||||
"ccxt_async_config": {
|
||||
"description": "CCXT asynchronous configuration settings.",
|
||||
"description": (
|
||||
"CCXT asynchronous configuration settings."
|
||||
"Usually ccxt_config should be used instead."
|
||||
),
|
||||
"type": "object",
|
||||
},
|
||||
"ccxt_sync_config": {
|
||||
"description": (
|
||||
"CCXT synchronous configuration settings. "
|
||||
"Usually ccxt_config should be used instead."
|
||||
),
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"edge": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"process_throttle_secs": {"type": "integer", "minimum": 600},
|
||||
"calculate_since_number_of_days": {"type": "integer"},
|
||||
"allowed_risk": {"type": "number"},
|
||||
"stoploss_range_min": {"type": "number"},
|
||||
"stoploss_range_max": {"type": "number"},
|
||||
"stoploss_range_step": {"type": "number"},
|
||||
"minimum_winrate": {"type": "number"},
|
||||
"minimum_expectancy": {"type": "number"},
|
||||
"min_trade_number": {"type": "number"},
|
||||
"max_trade_duration_minute": {"type": "integer"},
|
||||
"remove_pumps": {"type": "boolean"},
|
||||
},
|
||||
"required": ["process_throttle_secs", "allowed_risk"],
|
||||
},
|
||||
"logging": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1383,6 +1382,7 @@ SCHEMA_TRADE_REQUIRED = [
|
||||
"entry_pricing",
|
||||
"stoploss",
|
||||
"minimal_roi",
|
||||
"pairlists",
|
||||
"internals",
|
||||
"dataformat_ohlcv",
|
||||
"dataformat_trades",
|
||||
@@ -1392,6 +1392,7 @@ SCHEMA_BACKTEST_REQUIRED = [
|
||||
"exchange",
|
||||
"stake_currency",
|
||||
"stake_amount",
|
||||
"pairlists",
|
||||
"dry_run_wallet",
|
||||
"dataformat_ohlcv",
|
||||
"dataformat_trades",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.configuration.config_secrets import sanitize_config
|
||||
from freqtrade.configuration.config_secrets import remove_exchange_credentials, sanitize_config
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.configuration.configuration import Configuration
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.constants import Config, ExchangeConfig
|
||||
|
||||
|
||||
_SENSITIVE_KEYS = [
|
||||
"exchange.key",
|
||||
"exchange.api_key",
|
||||
"exchange.apiKey",
|
||||
"exchange.secret",
|
||||
"exchange.password",
|
||||
"exchange.uid",
|
||||
"exchange.account_id",
|
||||
"exchange.accountId",
|
||||
"exchange.wallet_address",
|
||||
"exchange.walletAddress",
|
||||
"exchange.private_key",
|
||||
"exchange.privateKey",
|
||||
"telegram.token",
|
||||
"telegram.chat_id",
|
||||
"discord.webhook_url",
|
||||
"api_server.password",
|
||||
"webhook.url",
|
||||
]
|
||||
|
||||
|
||||
def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
||||
@@ -12,27 +33,8 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
||||
"""
|
||||
if show_sensitive:
|
||||
return config
|
||||
keys_to_remove = [
|
||||
"exchange.key",
|
||||
"exchange.api_key",
|
||||
"exchange.apiKey",
|
||||
"exchange.secret",
|
||||
"exchange.password",
|
||||
"exchange.uid",
|
||||
"exchange.account_id",
|
||||
"exchange.accountId",
|
||||
"exchange.wallet_address",
|
||||
"exchange.walletAddress",
|
||||
"exchange.private_key",
|
||||
"exchange.privateKey",
|
||||
"telegram.token",
|
||||
"telegram.chat_id",
|
||||
"discord.webhook_url",
|
||||
"api_server.password",
|
||||
"webhook.url",
|
||||
]
|
||||
config = deepcopy(config)
|
||||
for key in keys_to_remove:
|
||||
for key in _SENSITIVE_KEYS:
|
||||
if "." in key:
|
||||
nested_keys = key.split(".")
|
||||
nested_config = config
|
||||
@@ -45,3 +47,21 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
||||
config[key] = "REDACTED"
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt and utils.
|
||||
Modifies the input dict!
|
||||
:param exchange_config: Exchange configuration
|
||||
:param dry_run: If True, remove sensitive keys from the exchange configuration
|
||||
"""
|
||||
if not dry_run:
|
||||
return
|
||||
|
||||
for key in [k for k in _SENSITIVE_KEYS if k.startswith("exchange.")]:
|
||||
if "." in key:
|
||||
key1 = key.removeprefix("exchange.")
|
||||
if key1 in exchange_config:
|
||||
exchange_config[key1] = ""
|
||||
|
||||
@@ -66,7 +66,8 @@ def validate_config_schema(conf: dict[str, Any], preliminary: bool = False) -> d
|
||||
return conf
|
||||
except ValidationError as e:
|
||||
logger.critical(f"Invalid configuration. Reason: {e}")
|
||||
raise ValidationError(best_match(Draft4Validator(conf_schema).iter_errors(conf)).message)
|
||||
result = best_match(FreqtradeValidator(conf_schema).iter_errors(conf))
|
||||
raise ConfigurationError(result.message)
|
||||
|
||||
|
||||
def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = False) -> None:
|
||||
@@ -99,14 +100,12 @@ def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = Fal
|
||||
|
||||
def _validate_unlimited_amount(conf: dict[str, Any]) -> None:
|
||||
"""
|
||||
If edge is disabled, either max_open_trades or stake_amount need to be set.
|
||||
Either max_open_trades or stake_amount need to be set.
|
||||
:raise: ConfigurationError if config validation failed
|
||||
"""
|
||||
if (
|
||||
not conf.get("edge", {}).get("enabled")
|
||||
and (conf.get("max_open_trades") == float("inf") or conf.get("max_open_trades") == -1)
|
||||
and conf.get("stake_amount") == UNLIMITED_STAKE_AMOUNT
|
||||
):
|
||||
conf.get("max_open_trades") == float("inf") or conf.get("max_open_trades") == -1
|
||||
) and conf.get("stake_amount") == UNLIMITED_STAKE_AMOUNT:
|
||||
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||
|
||||
|
||||
@@ -164,12 +163,9 @@ def _validate_edge(conf: dict[str, Any]) -> None:
|
||||
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
|
||||
"""
|
||||
|
||||
if not conf.get("edge", {}).get("enabled"):
|
||||
return
|
||||
|
||||
if not conf.get("use_exit_signal", True):
|
||||
if conf.get("edge", {}).get("enabled"):
|
||||
raise ConfigurationError(
|
||||
"Edge requires `use_exit_signal` to be True, otherwise no sells will happen."
|
||||
"Edge is no longer supported and has been removed from Freqtrade with 2025.6."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
This module contains the configuration class
|
||||
"""
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
@@ -19,10 +18,7 @@ from freqtrade.constants import Config
|
||||
from freqtrade.enums import (
|
||||
NON_UTIL_MODES,
|
||||
TRADE_MODES,
|
||||
CandleType,
|
||||
MarginMode,
|
||||
RunMode,
|
||||
TradingMode,
|
||||
)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import setup_logging
|
||||
@@ -88,9 +84,6 @@ class Configuration:
|
||||
if "internals" not in config:
|
||||
config["internals"] = {}
|
||||
|
||||
if "pairlists" not in config:
|
||||
config["pairlists"] = []
|
||||
|
||||
# Keep a copy of the original configuration file
|
||||
config["original_config"] = deepcopy(config)
|
||||
|
||||
@@ -216,13 +209,28 @@ class Configuration:
|
||||
config.update({"datadir": create_datadir(config, self.args.get("datadir"))})
|
||||
logger.info("Using data directory: %s ...", config.get("datadir"))
|
||||
|
||||
self._args_to_config(
|
||||
config, argname="exportdirectory", logstring="Using {} as backtest directory ..."
|
||||
)
|
||||
|
||||
if self.args.get("exportfilename"):
|
||||
self._args_to_config(
|
||||
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
||||
)
|
||||
config["exportfilename"] = Path(config["exportfilename"])
|
||||
else:
|
||||
config["exportfilename"] = config["user_data_dir"] / "backtest_results"
|
||||
if config.get("exportdirectory") and Path(config["exportdirectory"]).is_dir():
|
||||
logger.warning(
|
||||
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
|
||||
"use `--backtest-directory` instead."
|
||||
)
|
||||
if config.get("exportdirectory") is None:
|
||||
# Fallback - assign export-directory directly.
|
||||
config["exportdirectory"] = config["exportfilename"]
|
||||
if not config.get("exportdirectory"):
|
||||
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
|
||||
if not config.get("exportfilename"):
|
||||
config["exportfilename"] = None
|
||||
config["exportdirectory"] = Path(config["exportdirectory"])
|
||||
|
||||
if self.args.get("show_sensitive"):
|
||||
logger.warning(
|
||||
@@ -310,17 +318,10 @@ class Configuration:
|
||||
("backtest_cache", "Parameter --cache={} detected ..."),
|
||||
("disableparamexport", "Parameter --disableparamexport detected: {} ..."),
|
||||
("freqai_backtest_live_models", "Parameter --freqai-backtest-live-models detected ..."),
|
||||
("backtest_notes", "Parameter --notes detected: {} ..."),
|
||||
]
|
||||
self._args_to_config_loop(config, configurations)
|
||||
|
||||
# Edge section:
|
||||
if self.args.get("stoploss_range"):
|
||||
txt_range = ast.literal_eval(self.args["stoploss_range"])
|
||||
config["edge"].update({"stoploss_range_min": txt_range[0]})
|
||||
config["edge"].update({"stoploss_range_max": txt_range[1]})
|
||||
config["edge"].update({"stoploss_range_step": txt_range[2]})
|
||||
logger.info("Parameter --stoplosses detected: %s ...", self.args["stoploss_range"])
|
||||
|
||||
# Hyperopt section
|
||||
|
||||
configurations = [
|
||||
@@ -405,11 +406,6 @@ class Configuration:
|
||||
self._args_to_config(
|
||||
config, argname="trading_mode", logstring="Detected --trading-mode: {}"
|
||||
)
|
||||
config["candle_type_def"] = CandleType.get_default(
|
||||
config.get("trading_mode", "spot") or "spot"
|
||||
)
|
||||
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
|
||||
config["margin_mode"] = MarginMode(config.get("margin_mode", "") or "")
|
||||
self._args_to_config(
|
||||
config, argname="candle_types", logstring="Detected --candle-types: {}"
|
||||
)
|
||||
|
||||
@@ -159,16 +159,6 @@ def process_temporary_deprecated_settings(config: Config) -> None:
|
||||
process_removed_setting(
|
||||
config, "ask_strategy", "ignore_roi_if_buy_signal", None, "ignore_roi_if_entry_signal"
|
||||
)
|
||||
if config.get("edge", {}).get(
|
||||
"enabled", False
|
||||
) and "capital_available_percentage" in config.get("edge", {}):
|
||||
raise ConfigurationError(
|
||||
"DEPRECATED: "
|
||||
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
|
||||
"'tradable_balance_ratio'. Please migrate your configuration to "
|
||||
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
|
||||
"from the edge configuration."
|
||||
)
|
||||
if "ticker_interval" in config:
|
||||
raise ConfigurationError(
|
||||
"DEPRECATED: 'ticker_interval' detected. "
|
||||
|
||||
@@ -43,15 +43,27 @@ def _flat_vars_to_nested_dict(env_dict: dict[str, Any], prefix: str) -> dict[str
|
||||
:return: Nested dict based on available and relevant variables.
|
||||
"""
|
||||
no_convert = ["CHAT_ID", "PASSWORD"]
|
||||
ccxt_config_keys = ["ccxt_config", "ccxt_sync_config", "ccxt_async_config"]
|
||||
relevant_vars: dict[str, Any] = {}
|
||||
|
||||
for env_var, val in sorted(env_dict.items()):
|
||||
if env_var.startswith(prefix):
|
||||
logger.info(f"Loading variable '{env_var}'")
|
||||
key = env_var.replace(prefix, "")
|
||||
for k in reversed(key.split("__")):
|
||||
key_parts = key.split("__")
|
||||
logger.info("Key parts: %s", key_parts)
|
||||
|
||||
# Check if any ccxt config key is in the key parts
|
||||
preserve_case = key_parts[0].lower() == "exchange" and any(
|
||||
ccxt_key in [part.lower() for part in key_parts] for ccxt_key in ccxt_config_keys
|
||||
)
|
||||
|
||||
for i, k in enumerate(reversed(key_parts)):
|
||||
# Preserve case for the final key if ccxt config is involved
|
||||
key_name = k if preserve_case and i == 0 else k.lower()
|
||||
|
||||
val = {
|
||||
k.lower(): (
|
||||
key_name: (
|
||||
_get_var_typed(val)
|
||||
if not isinstance(val, dict) and k not in no_convert
|
||||
else val
|
||||
|
||||
@@ -4,9 +4,8 @@ This module contains the argument manager class
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from typing_extensions import Self
|
||||
from datetime import UTC, datetime
|
||||
from typing import Self
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.exceptions import ConfigurationError
|
||||
@@ -151,9 +150,7 @@ class TimeRange:
|
||||
starts = rvals[index]
|
||||
if stype[0] == "date" and len(starts) == 8:
|
||||
start = int(
|
||||
datetime.strptime(starts, "%Y%m%d")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
.timestamp()
|
||||
datetime.strptime(starts, "%Y%m%d").replace(tzinfo=UTC).timestamp()
|
||||
)
|
||||
elif len(starts) == 13:
|
||||
start = int(starts) // 1000
|
||||
@@ -164,9 +161,7 @@ class TimeRange:
|
||||
stops = rvals[index]
|
||||
if stype[1] == "date" and len(stops) == 8:
|
||||
stop = int(
|
||||
datetime.strptime(stops, "%Y%m%d")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
.timestamp()
|
||||
datetime.strptime(stops, "%Y%m%d").replace(tzinfo=UTC).timestamp()
|
||||
)
|
||||
elif len(stops) == 13:
|
||||
stop = int(stops) // 1000
|
||||
|
||||
@@ -16,10 +16,7 @@ from .bt_fileutils import (
|
||||
load_backtest_data,
|
||||
load_backtest_metadata,
|
||||
load_backtest_stats,
|
||||
load_exit_signal_candles,
|
||||
load_file_from_zip,
|
||||
load_rejected_signals,
|
||||
load_signal_candles,
|
||||
load_trades,
|
||||
load_trades_from_db,
|
||||
trade_list_to_dataframe,
|
||||
|
||||
@@ -5,7 +5,7 @@ Helpers when analyzing backtest data
|
||||
import logging
|
||||
import zipfile
|
||||
from copy import copy
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from io import BytesIO, StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
@@ -155,33 +155,55 @@ def load_backtest_metadata(filename: Path | str) -> dict[str, Any]:
|
||||
raise OperationalException("Unexpected error while loading backtest metadata.") from e
|
||||
|
||||
|
||||
def load_backtest_stats(filename: Path | str) -> BacktestResultType:
|
||||
def _normalize_filename(file_or_directory: Path | str, filename: Path | str | None) -> Path:
|
||||
"""
|
||||
Normalize the filename by ensuring it is a Path object.
|
||||
:param file_or_directory: The directory or file to normalize.
|
||||
:param filename: The filename to normalize.
|
||||
:return: A Path object representing the normalized filename.
|
||||
"""
|
||||
if isinstance(file_or_directory, str):
|
||||
file_or_directory = Path(file_or_directory)
|
||||
if file_or_directory.is_dir():
|
||||
if not filename:
|
||||
filename = get_latest_backtest_filename(file_or_directory)
|
||||
if Path(filename).is_file():
|
||||
fn = Path(filename)
|
||||
else:
|
||||
fn = file_or_directory / filename
|
||||
else:
|
||||
fn = file_or_directory
|
||||
return fn
|
||||
|
||||
|
||||
def load_backtest_stats(
|
||||
file_or_directory: Path | str, filename: Path | str | None = None
|
||||
) -> BacktestResultType:
|
||||
"""
|
||||
Load backtest statistics file.
|
||||
:param filename: pathlib.Path object, or string pointing to the file.
|
||||
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
|
||||
or absolute/relative path to the backtest results file.
|
||||
:param filename: Optional filename to load from (if different from the main filename).
|
||||
Only valid when loading from a directory.
|
||||
:return: a dictionary containing the resulting file.
|
||||
"""
|
||||
if isinstance(filename, str):
|
||||
filename = Path(filename)
|
||||
if filename.is_dir():
|
||||
filename = filename / get_latest_backtest_filename(filename)
|
||||
if not filename.is_file():
|
||||
raise ValueError(f"File {filename} does not exist.")
|
||||
logger.info(f"Loading backtest result from {filename}")
|
||||
fn = _normalize_filename(file_or_directory, filename)
|
||||
|
||||
if filename.suffix == ".zip":
|
||||
if not fn.is_file():
|
||||
raise ValueError(f"File or directory {fn} does not exist.")
|
||||
logger.info(f"Loading backtest result from {fn}")
|
||||
|
||||
if fn.suffix == ".zip":
|
||||
data = json_load(
|
||||
StringIO(
|
||||
load_file_from_zip(filename, filename.with_suffix(".json").name).decode("utf-8")
|
||||
)
|
||||
StringIO(load_file_from_zip(fn, fn.with_suffix(".json").name).decode("utf-8"))
|
||||
)
|
||||
else:
|
||||
with filename.open() as file:
|
||||
with fn.open() as file:
|
||||
data = json_load(file)
|
||||
|
||||
# Legacy list format does not contain metadata.
|
||||
if isinstance(data, dict):
|
||||
data["metadata"] = load_backtest_metadata(filename)
|
||||
data["metadata"] = load_backtest_metadata(fn)
|
||||
return data
|
||||
|
||||
|
||||
@@ -324,7 +346,7 @@ def find_existing_backtest_stats(
|
||||
|
||||
if min_backtest_date is not None:
|
||||
backtest_date = strategy_metadata["backtest_start_time"]
|
||||
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
|
||||
backtest_date = datetime.fromtimestamp(backtest_date, tz=UTC)
|
||||
if backtest_date < min_backtest_date:
|
||||
# Do not use a cached result for this strategy as first result is too old.
|
||||
del run_ids[strategy_name]
|
||||
@@ -362,16 +384,21 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
||||
return df
|
||||
|
||||
|
||||
def load_backtest_data(filename: Path | str, strategy: str | None = None) -> pd.DataFrame:
|
||||
def load_backtest_data(
|
||||
file_or_directory: Path | str, strategy: str | None = None, filename: Path | str | None = None
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Load backtest data file.
|
||||
:param filename: pathlib.Path object, or string pointing to a file or directory
|
||||
Load backtest data file, returns a dataframe with the individual trades.
|
||||
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
|
||||
or absolute/relative path to the backtest results file.
|
||||
:param strategy: Strategy to load - mainly relevant for multi-strategy backtests
|
||||
Can also serve as protection to load the correct result.
|
||||
:param filename: Optional filename to load from (if different from the main filename).
|
||||
Only valid when loading from a directory.
|
||||
:return: a dataframe with the analysis results
|
||||
:raise: ValueError if loading goes wrong.
|
||||
"""
|
||||
data = load_backtest_stats(filename)
|
||||
data = load_backtest_stats(file_or_directory, filename)
|
||||
if not isinstance(data, list):
|
||||
# new, nested format
|
||||
if "strategy" not in data:
|
||||
@@ -430,20 +457,23 @@ def load_file_from_zip(zip_path: Path, filename: str) -> bytes:
|
||||
raise ValueError(f"Bad zip file: {zip_path}.") from None
|
||||
|
||||
|
||||
def load_backtest_analysis_data(backtest_dir: Path, name: str):
|
||||
def load_backtest_analysis_data(
|
||||
file_or_directory: Path,
|
||||
name: Literal["signals", "rejected", "exited"],
|
||||
filename: Path | str | None = None,
|
||||
):
|
||||
"""
|
||||
Load backtest analysis data either from a pickle file or from within a zip file
|
||||
:param backtest_dir: Directory containing backtest results
|
||||
:param file_or_directory: pathlib.Path object, or string pointing to the directory,
|
||||
or absolute/relative path to the backtest results file.
|
||||
:param name: Name of the analysis data to load (signals, rejected, exited)
|
||||
:param filename: Optional filename to load from (if different from the main filename).
|
||||
Only valid when loading from a directory.
|
||||
:return: Analysis data
|
||||
"""
|
||||
import joblib
|
||||
|
||||
if backtest_dir.is_dir():
|
||||
lbf = Path(get_latest_backtest_filename(backtest_dir))
|
||||
zip_path = backtest_dir / lbf
|
||||
else:
|
||||
zip_path = backtest_dir
|
||||
zip_path = _normalize_filename(file_or_directory, filename)
|
||||
|
||||
if zip_path.suffix == ".zip":
|
||||
# Load from zip file
|
||||
@@ -458,10 +488,10 @@ def load_backtest_analysis_data(backtest_dir: Path, name: str):
|
||||
|
||||
else:
|
||||
# Load from separate pickle file
|
||||
if backtest_dir.is_dir():
|
||||
scpf = Path(backtest_dir, f"{zip_path.stem}_{name}.pkl")
|
||||
if file_or_directory.is_dir():
|
||||
scpf = Path(file_or_directory, f"{zip_path.stem}_{name}.pkl")
|
||||
else:
|
||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
|
||||
scpf = Path(file_or_directory.parent / f"{file_or_directory.stem}_{name}.pkl")
|
||||
|
||||
try:
|
||||
with scpf.open("rb") as scp:
|
||||
@@ -473,27 +503,6 @@ def load_backtest_analysis_data(backtest_dir: Path, name: str):
|
||||
return None
|
||||
|
||||
|
||||
def load_rejected_signals(backtest_dir: Path):
|
||||
"""
|
||||
Load rejected signals from backtest directory
|
||||
"""
|
||||
return load_backtest_analysis_data(backtest_dir, "rejected")
|
||||
|
||||
|
||||
def load_signal_candles(backtest_dir: Path):
|
||||
"""
|
||||
Load signal candles from backtest directory
|
||||
"""
|
||||
return load_backtest_analysis_data(backtest_dir, "signals")
|
||||
|
||||
|
||||
def load_exit_signal_candles(backtest_dir: Path) -> dict[str, dict[str, pd.DataFrame]]:
|
||||
"""
|
||||
Load exit signal candles from backtest directory
|
||||
"""
|
||||
return load_backtest_analysis_data(backtest_dir, "exited")
|
||||
|
||||
|
||||
def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame:
|
||||
"""
|
||||
Convert list of Trade objects to pandas Dataframe
|
||||
|
||||
@@ -11,7 +11,7 @@ def get_tick_size_over_time(candles: DataFrame) -> Series:
|
||||
# count the number of significant digits for the open and close prices
|
||||
for col in ["open", "high", "low", "close"]:
|
||||
candles[f"{col}_count"] = (
|
||||
candles[col].round(14).astype(str).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
||||
candles[col].round(14).apply("{:.15f}".format).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
||||
)
|
||||
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
||||
axis=1
|
||||
|
||||
@@ -69,6 +69,10 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
||||
trades = pd.concat(dfs, ignore_index=True)
|
||||
del dfs
|
||||
|
||||
# drop any row not having a number in the column timestamp
|
||||
timestamp_numeric = pd.to_numeric(trades["timestamp"], errors="coerce")
|
||||
trades = trades[timestamp_numeric.notna()]
|
||||
|
||||
trades.loc[:, "timestamp"] = trades["timestamp"] * 1e3
|
||||
trades.loc[:, "cost"] = trades["price"] * trades["amount"]
|
||||
for col in DEFAULT_TRADES_COLUMNS:
|
||||
|
||||
@@ -7,7 +7,7 @@ Common Interface for bot and strategy to access data.
|
||||
|
||||
import logging
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
||||
@@ -98,7 +98,7 @@ class DataProvider:
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
pair_key = (pair, timeframe, candle_type)
|
||||
self.__cached_pairs[pair_key] = (dataframe, datetime.now(timezone.utc))
|
||||
self.__cached_pairs[pair_key] = (dataframe, datetime.now(UTC))
|
||||
|
||||
# For multiple producers we will want to merge the pairlists instead of overwriting
|
||||
def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"):
|
||||
@@ -131,7 +131,7 @@ class DataProvider:
|
||||
"data": {
|
||||
"key": pair_key,
|
||||
"df": dataframe.tail(1),
|
||||
"la": datetime.now(timezone.utc),
|
||||
"la": datetime.now(UTC),
|
||||
},
|
||||
}
|
||||
self.__rpc.send_msg(msg)
|
||||
@@ -164,7 +164,7 @@ class DataProvider:
|
||||
if producer_name not in self.__producer_pairs_df:
|
||||
self.__producer_pairs_df[producer_name] = {}
|
||||
|
||||
_last_analyzed = datetime.now(timezone.utc) if not last_analyzed else last_analyzed
|
||||
_last_analyzed = datetime.now(UTC) if not last_analyzed else last_analyzed
|
||||
|
||||
self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed)
|
||||
logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.")
|
||||
@@ -275,12 +275,12 @@ class DataProvider:
|
||||
# If we have no data from this Producer yet
|
||||
if producer_name not in self.__producer_pairs_df:
|
||||
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||
|
||||
# If we do have data from that Producer, but no data on this pair_key
|
||||
if pair_key not in self.__producer_pairs_df[producer_name]:
|
||||
# We don't have this data yet, return empty DataFrame and datetime (01-01-1970)
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||
|
||||
# We have it, return this data
|
||||
df, la = self.__producer_pairs_df[producer_name][pair_key]
|
||||
@@ -396,16 +396,16 @@ class DataProvider:
|
||||
if (max_index := self.__slice_index.get(pair)) is not None:
|
||||
df = df.iloc[max(0, max_index - MAX_DATAFRAME_CANDLES) : max_index]
|
||||
else:
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||
return df, date
|
||||
else:
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
|
||||
return (DataFrame(), datetime.fromtimestamp(0, tz=UTC))
|
||||
|
||||
@property
|
||||
def runmode(self) -> RunMode:
|
||||
"""
|
||||
Get runmode of the bot
|
||||
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
|
||||
can be "live", "dry-run", "backtest", "hyperopt" or "other".
|
||||
"""
|
||||
return RunMode(self._config.get("runmode", RunMode.OTHER))
|
||||
|
||||
|
||||
@@ -7,11 +7,9 @@ from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.btanalysis import (
|
||||
BT_DATA_COLUMNS,
|
||||
load_backtest_analysis_data,
|
||||
load_backtest_data,
|
||||
load_backtest_stats,
|
||||
load_exit_signal_candles,
|
||||
load_rejected_signals,
|
||||
load_signal_candles,
|
||||
)
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
from freqtrade.util import print_df_rich_table
|
||||
@@ -331,7 +329,9 @@ def process_entry_exit_reasons(config: Config):
|
||||
exit_only = config.get("exit_only", False)
|
||||
do_rejected = config.get("analysis_rejected", False)
|
||||
to_csv = config.get("analysis_to_csv", False)
|
||||
csv_path = Path(config.get("analysis_csv_path", config["exportfilename"]))
|
||||
csv_path = Path(
|
||||
config.get("analysis_csv_path", config["exportdirectory"]), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
if entry_only is True and exit_only is True:
|
||||
raise OperationalException(
|
||||
@@ -344,20 +344,30 @@ def process_entry_exit_reasons(config: Config):
|
||||
None if config.get("timerange") is None else str(config.get("timerange"))
|
||||
)
|
||||
try:
|
||||
backtest_stats = load_backtest_stats(config["exportfilename"])
|
||||
backtest_stats = load_backtest_stats(
|
||||
config["exportdirectory"], config["exportfilename"]
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ConfigurationError(e) from e
|
||||
|
||||
for strategy_name, results in backtest_stats["strategy"].items():
|
||||
trades = load_backtest_data(config["exportfilename"], strategy_name)
|
||||
trades = load_backtest_data(
|
||||
config["exportdirectory"], strategy_name, config["exportfilename"]
|
||||
)
|
||||
|
||||
if trades is not None and not trades.empty:
|
||||
signal_candles = load_signal_candles(config["exportfilename"])
|
||||
exit_signals = load_exit_signal_candles(config["exportfilename"])
|
||||
signal_candles = load_backtest_analysis_data(
|
||||
config["exportdirectory"], "signals", config["exportfilename"]
|
||||
)
|
||||
exit_signals = load_backtest_analysis_data(
|
||||
config["exportdirectory"], "exited", config["exportfilename"]
|
||||
)
|
||||
|
||||
rej_df = None
|
||||
if do_rejected:
|
||||
rejected_signals_dict = load_rejected_signals(config["exportfilename"])
|
||||
rejected_signals_dict = load_backtest_analysis_data(
|
||||
config["exportdirectory"], "rejected", config["exportfilename"]
|
||||
)
|
||||
rej_df = prepare_results(
|
||||
rejected_signals_dict,
|
||||
strategy_name,
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from pandas import DataFrame, to_datetime
|
||||
@@ -118,8 +118,8 @@ class IDataHandler(ABC):
|
||||
df = self._ohlcv_load(pair, timeframe, None, candle_type)
|
||||
if df.empty:
|
||||
return (
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=UTC),
|
||||
datetime.fromtimestamp(0, tz=UTC),
|
||||
0,
|
||||
)
|
||||
return df.iloc[0]["date"].to_pydatetime(), df.iloc[-1]["date"].to_pydatetime(), len(df)
|
||||
@@ -201,8 +201,8 @@ class IDataHandler(ABC):
|
||||
df = self._trades_load(pair, trading_mode)
|
||||
if df.empty:
|
||||
return (
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=UTC),
|
||||
datetime.fromtimestamp(0, tz=UTC),
|
||||
0,
|
||||
)
|
||||
return (
|
||||
|
||||
@@ -97,7 +97,7 @@ def load_data(
|
||||
"""
|
||||
result: dict[str, DataFrame] = {}
|
||||
if startup_candles > 0 and timerange:
|
||||
logger.info(f"Using indicator startup period: {startup_candles} ...")
|
||||
logger.debug(f"Using indicator startup period: {startup_candles} ...")
|
||||
|
||||
data_handler = get_datahandler(datadir, data_format)
|
||||
|
||||
|
||||
@@ -174,12 +174,18 @@ def calculate_underwater(
|
||||
|
||||
@dataclass()
|
||||
class DrawDownResult:
|
||||
# Max drawdown fields
|
||||
drawdown_abs: float = 0.0
|
||||
high_date: pd.Timestamp = None
|
||||
low_date: pd.Timestamp = None
|
||||
high_value: float = 0.0
|
||||
low_value: float = 0.0
|
||||
relative_account_drawdown: float = 0.0
|
||||
# Current drawdown fields
|
||||
current_high_date: pd.Timestamp = None
|
||||
current_high_value: float = 0.0
|
||||
current_drawdown_abs: float = 0.0
|
||||
current_relative_account_drawdown: float = 0.0
|
||||
|
||||
|
||||
def calculate_max_drawdown(
|
||||
@@ -191,29 +197,31 @@ def calculate_max_drawdown(
|
||||
relative: bool = False,
|
||||
) -> DrawDownResult:
|
||||
"""
|
||||
Calculate max drawdown and the corresponding close dates
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||
Calculate max drawdown and current drawdown with corresponding dates
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
|
||||
:param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
|
||||
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
|
||||
:param relative: If True, use relative drawdown for max calculation instead of absolute
|
||||
:return: DrawDownResult object
|
||||
with absolute max drawdown, high and low time and high and low value,
|
||||
and the relative account drawdown
|
||||
relative account drawdown, and current drawdown information.
|
||||
:raise: ValueError if trade-dataframe was found empty.
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
raise ValueError("Trade dataframe empty.")
|
||||
|
||||
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||
max_drawdown_df = _calc_drawdown_series(
|
||||
profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance
|
||||
)
|
||||
|
||||
# Calculate maximum drawdown
|
||||
idxmin = (
|
||||
max_drawdown_df["drawdown_relative"].idxmax()
|
||||
if relative
|
||||
else max_drawdown_df["drawdown"].idxmin()
|
||||
)
|
||||
|
||||
high_idx = max_drawdown_df.iloc[: idxmin + 1]["high_value"].idxmax()
|
||||
high_date = profit_results.loc[high_idx, date_col]
|
||||
low_date = profit_results.loc[idxmin, date_col]
|
||||
@@ -221,13 +229,27 @@ def calculate_max_drawdown(
|
||||
low_val = max_drawdown_df.loc[idxmin, "cumulative"]
|
||||
max_drawdown_rel = max_drawdown_df.loc[idxmin, "drawdown_relative"]
|
||||
|
||||
# Calculate current drawdown
|
||||
current_high_idx = max_drawdown_df["high_value"].iloc[:-1].idxmax()
|
||||
current_high_date = profit_results.loc[current_high_idx, date_col]
|
||||
current_high_value = max_drawdown_df.iloc[-1]["high_value"]
|
||||
current_cumulative = max_drawdown_df.iloc[-1]["cumulative"]
|
||||
current_drawdown_abs = current_high_value - current_cumulative
|
||||
current_drawdown_relative = max_drawdown_df.iloc[-1]["drawdown_relative"]
|
||||
|
||||
return DrawDownResult(
|
||||
# Max drawdown
|
||||
drawdown_abs=abs(max_drawdown_df.loc[idxmin, "drawdown"]),
|
||||
high_date=high_date,
|
||||
low_date=low_date,
|
||||
high_value=high_val,
|
||||
low_value=low_val,
|
||||
relative_account_drawdown=max_drawdown_rel,
|
||||
# Current drawdown
|
||||
current_high_date=current_high_date,
|
||||
current_high_value=current_high_value,
|
||||
current_drawdown_abs=current_drawdown_abs,
|
||||
current_relative_account_drawdown=current_drawdown_relative,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .edge_positioning import Edge, PairInfo # noqa: F401
|
||||
@@ -1,524 +0,0 @@
|
||||
# pragma pylint: disable=W0603
|
||||
"""Edge positioning package"""
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import numpy as np
|
||||
import utils_find_1st as utf1st
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT, Config
|
||||
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
||||
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.util import dt_now
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PairInfo(NamedTuple):
|
||||
stoploss: float
|
||||
winrate: float
|
||||
risk_reward_ratio: float
|
||||
required_risk_reward: float
|
||||
expectancy: float
|
||||
nb_trades: int
|
||||
avg_trade_duration: float
|
||||
|
||||
|
||||
class Edge:
|
||||
"""
|
||||
Calculates Win Rate, Risk Reward Ratio, Expectancy
|
||||
against historical data for a give set of markets and a strategy
|
||||
it then adjusts stoploss and position size accordingly
|
||||
and force it into the strategy
|
||||
Author: https://github.com/mishaker
|
||||
"""
|
||||
|
||||
_cached_pairs: dict[str, Any] = {} # Keeps a list of pairs
|
||||
|
||||
def __init__(self, config: Config, exchange, strategy) -> None:
|
||||
self.config = config
|
||||
self.exchange = exchange
|
||||
self.strategy: IStrategy = strategy
|
||||
|
||||
self.edge_config = self.config.get("edge", {})
|
||||
self._cached_pairs: dict[str, Any] = {} # Keeps a list of pairs
|
||||
self._final_pairs: list = []
|
||||
|
||||
# checking max_open_trades. it should be -1 as with Edge
|
||||
# the number of trades is determined by position size
|
||||
if self.config["max_open_trades"] != float("inf"):
|
||||
logger.critical("max_open_trades should be -1 in config !")
|
||||
|
||||
if self.config["stake_amount"] != UNLIMITED_STAKE_AMOUNT:
|
||||
raise OperationalException("Edge works only with unlimited stake amount")
|
||||
|
||||
self._capital_ratio: float = self.config["tradable_balance_ratio"]
|
||||
self._allowed_risk: float = self.edge_config.get("allowed_risk")
|
||||
self._since_number_of_days: int = self.edge_config.get("calculate_since_number_of_days", 14)
|
||||
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
||||
self._refresh_pairs = True
|
||||
|
||||
self._stoploss_range_min = float(self.edge_config.get("stoploss_range_min", -0.01))
|
||||
self._stoploss_range_max = float(self.edge_config.get("stoploss_range_max", -0.05))
|
||||
self._stoploss_range_step = float(self.edge_config.get("stoploss_range_step", -0.001))
|
||||
|
||||
# calculating stoploss range
|
||||
self._stoploss_range = np.arange(
|
||||
self._stoploss_range_min, self._stoploss_range_max, self._stoploss_range_step
|
||||
)
|
||||
|
||||
self._timerange: TimeRange = TimeRange.parse_timerange(
|
||||
f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-"
|
||||
)
|
||||
if config.get("fee"):
|
||||
self.fee = config["fee"]
|
||||
else:
|
||||
try:
|
||||
self.fee = self.exchange.get_fee(
|
||||
symbol=expand_pairlist(
|
||||
self.config["exchange"]["pair_whitelist"], list(self.exchange.markets)
|
||||
)[0]
|
||||
)
|
||||
except IndexError:
|
||||
self.fee = None
|
||||
|
||||
def calculate(self, pairs: list[str]) -> bool:
|
||||
if self.fee is None and pairs:
|
||||
self.fee = self.exchange.get_fee(pairs[0])
|
||||
|
||||
heartbeat = self.edge_config.get("process_throttle_secs")
|
||||
|
||||
if (self._last_updated > 0) and (
|
||||
self._last_updated + heartbeat > int(dt_now().timestamp())
|
||||
):
|
||||
return False
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
logger.info("Using stake_currency: %s ...", self.config["stake_currency"])
|
||||
logger.info("Using local backtesting data (using whitelist in given config) ...")
|
||||
|
||||
if self._refresh_pairs:
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
timerange_startup.subtract_start(
|
||||
timeframe_to_seconds(self.strategy.timeframe) * self.strategy.startup_candle_count
|
||||
)
|
||||
refresh_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=pairs,
|
||||
exchange=self.exchange,
|
||||
timeframe=self.strategy.timeframe,
|
||||
timerange=timerange_startup,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
# Download informative pairs too
|
||||
res = defaultdict(list)
|
||||
for pair, timeframe, _ in self.strategy.gather_informative_pairs():
|
||||
res[timeframe].append(pair)
|
||||
for timeframe, inf_pairs in res.items():
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
timerange_startup.subtract_start(
|
||||
timeframe_to_seconds(timeframe) * self.strategy.startup_candle_count
|
||||
)
|
||||
refresh_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=inf_pairs,
|
||||
exchange=self.exchange,
|
||||
timeframe=timeframe,
|
||||
timerange=timerange_startup,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
|
||||
data = load_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=pairs,
|
||||
timeframe=self.strategy.timeframe,
|
||||
timerange=self._timerange,
|
||||
startup_candles=self.strategy.startup_candle_count,
|
||||
data_format=self.config["dataformat_ohlcv"],
|
||||
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
|
||||
)
|
||||
|
||||
if not data:
|
||||
# Reinitializing cached pairs
|
||||
self._cached_pairs = {}
|
||||
logger.critical("No data found. Edge is stopped ...")
|
||||
return False
|
||||
# Fake run-mode to Edge
|
||||
prior_rm = self.config["runmode"]
|
||||
self.config["runmode"] = RunMode.EDGE
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
self.config["runmode"] = prior_rm
|
||||
|
||||
# Print timeframe
|
||||
min_date, max_date = get_timerange(preprocessed)
|
||||
logger.info(
|
||||
f"Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||
f"up to {max_date.strftime(DATETIME_PRINT_FORMAT)} "
|
||||
f"({(max_date - min_date).days} days).."
|
||||
)
|
||||
# TODO: Should edge support shorts? needs to be investigated further
|
||||
# * (add enter_short exit_short)
|
||||
headers = ["date", "open", "high", "low", "close", "enter_long", "exit_long"]
|
||||
|
||||
trades: list = []
|
||||
for pair, pair_data in preprocessed.items():
|
||||
# Sorting dataframe by date and reset index
|
||||
pair_data = pair_data.sort_values(by=["date"])
|
||||
pair_data = pair_data.reset_index(drop=True)
|
||||
|
||||
df_analyzed = self.strategy.ft_advise_signals(pair_data, {"pair": pair})[headers].copy()
|
||||
|
||||
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
|
||||
|
||||
# If no trade found then exit
|
||||
if len(trades) == 0:
|
||||
logger.info("No trades found.")
|
||||
return False
|
||||
|
||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||
self._cached_pairs = self._process_expectancy(trades_df)
|
||||
self._last_updated = int(dt_now().timestamp())
|
||||
|
||||
return True
|
||||
|
||||
def stake_amount(
|
||||
self, pair: str, free_capital: float, total_capital: float, capital_in_trade: float
|
||||
) -> float:
|
||||
stoploss = self.get_stoploss(pair)
|
||||
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
|
||||
allowed_capital_at_risk = available_capital * self._allowed_risk
|
||||
max_position_size = abs(allowed_capital_at_risk / stoploss)
|
||||
# Position size must be below available capital.
|
||||
position_size = min(min(max_position_size, free_capital), available_capital)
|
||||
if pair in self._cached_pairs:
|
||||
logger.info(
|
||||
"winrate: %s, expectancy: %s, position size: %s, pair: %s,"
|
||||
" capital in trade: %s, free capital: %s, total capital: %s,"
|
||||
" stoploss: %s, available capital: %s.",
|
||||
self._cached_pairs[pair].winrate,
|
||||
self._cached_pairs[pair].expectancy,
|
||||
position_size,
|
||||
pair,
|
||||
capital_in_trade,
|
||||
free_capital,
|
||||
total_capital,
|
||||
stoploss,
|
||||
available_capital,
|
||||
)
|
||||
return round(position_size, 15)
|
||||
|
||||
def get_stoploss(self, pair: str) -> float:
|
||||
if pair in self._cached_pairs:
|
||||
return self._cached_pairs[pair].stoploss
|
||||
else:
|
||||
logger.warning(
|
||||
f"Tried to access stoploss of non-existing pair {pair}, "
|
||||
"strategy stoploss is returned instead."
|
||||
)
|
||||
return self.strategy.stoploss
|
||||
|
||||
def adjust(self, pairs: list[str]) -> list:
|
||||
"""
|
||||
Filters out and sorts "pairs" according to Edge calculated pairs
|
||||
"""
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if (
|
||||
info.expectancy > float(self.edge_config.get("minimum_expectancy", 0.2))
|
||||
and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60))
|
||||
and pair in pairs
|
||||
):
|
||||
final.append(pair)
|
||||
|
||||
if self._final_pairs != final:
|
||||
self._final_pairs = final
|
||||
if self._final_pairs:
|
||||
logger.info(
|
||||
"Minimum expectancy and minimum winrate are met only for %s,"
|
||||
" so other pairs are filtered out.",
|
||||
self._final_pairs,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Edge removed all pairs as no pair with minimum expectancy "
|
||||
"and minimum winrate was found !"
|
||||
)
|
||||
|
||||
return self._final_pairs
|
||||
|
||||
def accepted_pairs(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
return a list of accepted pairs along with their winrate, expectancy and stoploss
|
||||
"""
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if info.expectancy > float(
|
||||
self.edge_config.get("minimum_expectancy", 0.2)
|
||||
) and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60)):
|
||||
final.append(
|
||||
{
|
||||
"Pair": pair,
|
||||
"Winrate": info.winrate,
|
||||
"Expectancy": info.expectancy,
|
||||
"Stoploss": info.stoploss,
|
||||
}
|
||||
)
|
||||
return final
|
||||
|
||||
def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
|
||||
"""
|
||||
The result frame contains a number of columns that are calculable
|
||||
from other columns. These are left blank till all rows are added,
|
||||
to be populated in single vector calls.
|
||||
|
||||
Columns to be populated are:
|
||||
- Profit
|
||||
- trade duration
|
||||
- profit abs
|
||||
:param result Dataframe
|
||||
:return: result Dataframe
|
||||
"""
|
||||
# We set stake amount to an arbitrary amount, as it doesn't change the calculation.
|
||||
# All returned values are relative, they are defined as ratios.
|
||||
stake = 0.015
|
||||
|
||||
result["trade_duration"] = result["close_date"] - result["open_date"]
|
||||
|
||||
result["trade_duration"] = result["trade_duration"].map(
|
||||
lambda x: int(x.total_seconds() / 60)
|
||||
)
|
||||
|
||||
# Spends, Takes, Profit, Absolute Profit
|
||||
|
||||
# Buy Price
|
||||
result["buy_vol"] = stake / result["open_rate"] # How many target are we buying
|
||||
result["buy_fee"] = stake * self.fee
|
||||
result["buy_spend"] = stake + result["buy_fee"] # How much we're spending
|
||||
|
||||
# Sell price
|
||||
result["sell_sum"] = result["buy_vol"] * result["close_rate"]
|
||||
result["sell_fee"] = result["sell_sum"] * self.fee
|
||||
result["sell_take"] = result["sell_sum"] - result["sell_fee"]
|
||||
|
||||
# profit_ratio
|
||||
result["profit_ratio"] = (result["sell_take"] - result["buy_spend"]) / result["buy_spend"]
|
||||
|
||||
# Absolute profit
|
||||
result["profit_abs"] = result["sell_take"] - result["buy_spend"]
|
||||
|
||||
return result
|
||||
|
||||
def _process_expectancy(self, results: DataFrame) -> dict[str, Any]:
|
||||
"""
|
||||
This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs
|
||||
The calculation will be done per pair and per strategy.
|
||||
"""
|
||||
# Removing pairs having less than min_trades_number
|
||||
min_trades_number = self.edge_config.get("min_trade_number", 10)
|
||||
results = results.groupby(["pair", "stoploss"]).filter(lambda x: len(x) > min_trades_number)
|
||||
###################################
|
||||
|
||||
# Removing outliers (Only Pumps) from the dataset
|
||||
# The method to detect outliers is to calculate standard deviation
|
||||
# Then every value more than (standard deviation + 2*average) is out (pump)
|
||||
#
|
||||
# Removing Pumps
|
||||
if self.edge_config.get("remove_pumps", False):
|
||||
results = results[
|
||||
results["profit_abs"]
|
||||
< 2 * results["profit_abs"].std() + results["profit_abs"].mean()
|
||||
]
|
||||
##########################################################################
|
||||
|
||||
# Removing trades having a duration more than X minutes (set in config)
|
||||
max_trade_duration = self.edge_config.get("max_trade_duration_minute", 1440)
|
||||
results = results[results.trade_duration < max_trade_duration]
|
||||
#######################################################################
|
||||
|
||||
if results.empty:
|
||||
return {}
|
||||
|
||||
groupby_aggregator = {
|
||||
"profit_abs": [
|
||||
("nb_trades", "count"), # number of all trades
|
||||
("profit_sum", lambda x: x[x > 0].sum()), # cumulative profit of all winning trades
|
||||
("loss_sum", lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades
|
||||
("nb_win_trades", lambda x: x[x > 0].count()), # number of winning trades
|
||||
],
|
||||
"trade_duration": [("avg_trade_duration", "mean")],
|
||||
}
|
||||
|
||||
# Group by (pair and stoploss) by applying above aggregator
|
||||
df = (
|
||||
results.groupby(["pair", "stoploss"])[["profit_abs", "trade_duration"]]
|
||||
.agg(groupby_aggregator)
|
||||
.reset_index(col_level=1)
|
||||
)
|
||||
|
||||
# Dropping level 0 as we don't need it
|
||||
df.columns = df.columns.droplevel(0)
|
||||
|
||||
# Calculating number of losing trades, average win and average loss
|
||||
df["nb_loss_trades"] = df["nb_trades"] - df["nb_win_trades"]
|
||||
df["average_win"] = np.where(
|
||||
df["nb_win_trades"] == 0, 0.0, df["profit_sum"] / df["nb_win_trades"]
|
||||
)
|
||||
df["average_loss"] = np.where(
|
||||
df["nb_loss_trades"] == 0, 0.0, df["loss_sum"] / df["nb_loss_trades"]
|
||||
)
|
||||
|
||||
# Win rate = number of profitable trades / number of trades
|
||||
df["winrate"] = df["nb_win_trades"] / df["nb_trades"]
|
||||
|
||||
# risk_reward_ratio = average win / average loss
|
||||
df["risk_reward_ratio"] = df["average_win"] / df["average_loss"]
|
||||
|
||||
# required_risk_reward = (1 / winrate) - 1
|
||||
df["required_risk_reward"] = (1 / df["winrate"]) - 1
|
||||
|
||||
# expectancy = (risk_reward_ratio * winrate) - (lossrate)
|
||||
df["expectancy"] = (df["risk_reward_ratio"] * df["winrate"]) - (1 - df["winrate"])
|
||||
|
||||
# sort by expectancy and stoploss
|
||||
df = (
|
||||
df.sort_values(by=["expectancy", "stoploss"], ascending=False)
|
||||
.groupby("pair")
|
||||
.first()
|
||||
.sort_values(by=["expectancy"], ascending=False)
|
||||
.reset_index()
|
||||
)
|
||||
|
||||
final = {}
|
||||
for x in df.itertuples():
|
||||
final[x.pair] = PairInfo(
|
||||
x.stoploss,
|
||||
x.winrate,
|
||||
x.risk_reward_ratio,
|
||||
x.required_risk_reward,
|
||||
x.expectancy,
|
||||
x.nb_trades,
|
||||
x.avg_trade_duration,
|
||||
)
|
||||
|
||||
# Returning a list of pairs in order of "expectancy"
|
||||
return final
|
||||
|
||||
def _find_trades_for_stoploss_range(self, df, pair: str, stoploss_range) -> list:
|
||||
buy_column = df["enter_long"].values
|
||||
sell_column = df["exit_long"].values
|
||||
date_column = df["date"].values
|
||||
ohlc_columns = df[["open", "high", "low", "close"]].values
|
||||
|
||||
result: list = []
|
||||
for stoploss in stoploss_range:
|
||||
result += self._detect_next_stop_or_sell_point(
|
||||
buy_column, sell_column, date_column, ohlc_columns, round(stoploss, 6), pair
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _detect_next_stop_or_sell_point(
|
||||
self, buy_column, sell_column, date_column, ohlc_columns, stoploss, pair: str
|
||||
):
|
||||
"""
|
||||
Iterate through ohlc_columns in order to find the next trade
|
||||
Next trade opens from the first buy signal noticed to
|
||||
The sell or stoploss signal after it.
|
||||
It then cuts OHLC, buy_column, sell_column and date_column.
|
||||
Cut from (the exit trade index) + 1.
|
||||
|
||||
Author: https://github.com/mishaker
|
||||
"""
|
||||
|
||||
result: list = []
|
||||
start_point = 0
|
||||
|
||||
while True:
|
||||
open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal)
|
||||
|
||||
# Return empty if we don't find trade entry (i.e. buy==1) or
|
||||
# we find a buy but at the end of array
|
||||
if open_trade_index == -1 or open_trade_index == len(buy_column) - 1:
|
||||
break
|
||||
else:
|
||||
# When a buy signal is seen,
|
||||
# trade opens in reality on the next candle
|
||||
open_trade_index += 1
|
||||
|
||||
open_price = ohlc_columns[open_trade_index, 0]
|
||||
stop_price = open_price * (stoploss + 1)
|
||||
|
||||
# Searching for the index where stoploss is hit
|
||||
stop_index = utf1st.find_1st(
|
||||
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller
|
||||
)
|
||||
|
||||
# If we don't find it then we assume stop_index will be far in future (infinite number)
|
||||
if stop_index == -1:
|
||||
stop_index = float("inf")
|
||||
|
||||
# Searching for the index where sell is hit
|
||||
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
|
||||
|
||||
# If we don't find it then we assume sell_index will be far in future (infinite number)
|
||||
if sell_index == -1:
|
||||
sell_index = float("inf")
|
||||
|
||||
# Check if we don't find any stop or sell point (in that case trade remains open)
|
||||
# It is not interesting for Edge to consider it so we simply ignore the trade
|
||||
# And stop iterating there is no more entry
|
||||
if stop_index == sell_index == float("inf"):
|
||||
break
|
||||
|
||||
if stop_index <= sell_index:
|
||||
exit_index = open_trade_index + stop_index
|
||||
exit_type = ExitType.STOP_LOSS
|
||||
exit_price = stop_price
|
||||
elif stop_index > sell_index:
|
||||
# If exit is SELL then we exit at the next candle
|
||||
exit_index = open_trade_index + sell_index + 1
|
||||
|
||||
# Check if we have the next candle
|
||||
if len(ohlc_columns) - 1 < exit_index:
|
||||
break
|
||||
|
||||
exit_type = ExitType.EXIT_SIGNAL
|
||||
exit_price = ohlc_columns[exit_index, 0]
|
||||
|
||||
trade = {
|
||||
"pair": pair,
|
||||
"stoploss": stoploss,
|
||||
"profit_ratio": "",
|
||||
"profit_abs": "",
|
||||
"open_date": date_column[open_trade_index],
|
||||
"close_date": date_column[exit_index],
|
||||
"trade_duration": "",
|
||||
"open_rate": round(open_price, 15),
|
||||
"close_rate": round(exit_price, 15),
|
||||
"exit_type": exit_type,
|
||||
}
|
||||
|
||||
result.append(trade)
|
||||
|
||||
# Giving a view of exit_index till the end of array
|
||||
buy_column = buy_column[exit_index:]
|
||||
sell_column = sell_column[exit_index:]
|
||||
date_column = date_column[exit_index:]
|
||||
ohlc_columns = ohlc_columns[exit_index:]
|
||||
start_point += exit_index
|
||||
|
||||
return result
|
||||
@@ -13,4 +13,4 @@ class MarginMode(str, Enum):
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
return f"{self.value.lower()}"
|
||||
|
||||
@@ -4,13 +4,12 @@ from enum import Enum
|
||||
class RunMode(str, Enum):
|
||||
"""
|
||||
Bot running mode (backtest, hyperopt, ...)
|
||||
can be "live", "dry-run", "backtest", "edge", "hyperopt".
|
||||
can be "live", "dry-run", "backtest", "hyperopt".
|
||||
"""
|
||||
|
||||
LIVE = "live"
|
||||
DRY_RUN = "dry_run"
|
||||
BACKTEST = "backtest"
|
||||
EDGE = "edge"
|
||||
HYPEROPT = "hyperopt"
|
||||
UTIL_EXCHANGE = "util_exchange"
|
||||
UTIL_NO_EXCHANGE = "util_no_exchange"
|
||||
@@ -20,5 +19,5 @@ class RunMode(str, Enum):
|
||||
|
||||
|
||||
TRADE_MODES = [RunMode.LIVE, RunMode.DRY_RUN]
|
||||
OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT]
|
||||
OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.HYPEROPT]
|
||||
NON_UTIL_MODES = TRADE_MODES + OPTIMIZE_MODES
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# flake8: noqa: F401
|
||||
# isort: off
|
||||
from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
|
||||
# isort: on
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.bingx import Bingx
|
||||
from freqtrade.exchange.bitget import Bitget
|
||||
from freqtrade.exchange.bitmart import Bitmart
|
||||
from freqtrade.exchange.bitpanda import Bitpanda
|
||||
from freqtrade.exchange.bitvavo import Bitvavo
|
||||
@@ -43,4 +44,6 @@ from freqtrade.exchange.idex import Idex
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.lbank import Lbank
|
||||
from freqtrade.exchange.okx import Okx
|
||||
from freqtrade.exchange.luno import Luno
|
||||
from freqtrade.exchange.modetrade import Modetrade
|
||||
from freqtrade.exchange.okx import MyOkx, Okx
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Binance exchange subclass"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import ccxt
|
||||
@@ -45,7 +45,6 @@ class Binance(Exchange):
|
||||
"funding_fee_candle_limit": 1000,
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
||||
"tickers_have_price": False,
|
||||
"floor_leverage": True,
|
||||
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
||||
@@ -63,7 +62,7 @@ class Binance(Exchange):
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||
(TradingMode.FUTURES, MarginMode.CROSS),
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||
@@ -76,7 +75,10 @@ class Binance(Exchange):
|
||||
:return: Proxy coin or stake currency
|
||||
"""
|
||||
if self.margin_mode == MarginMode.CROSS:
|
||||
return self._config.get("proxy_coin", self._config["stake_currency"])
|
||||
return self._config.get(
|
||||
"proxy_coin",
|
||||
self._config["stake_currency"],
|
||||
) # type: ignore[return-value]
|
||||
return self._config["stake_currency"]
|
||||
|
||||
def get_tickers(
|
||||
@@ -157,7 +159,7 @@ class Binance(Exchange):
|
||||
since_ms = x[3][0][0]
|
||||
logger.info(
|
||||
f"Candle-data for {pair} available starting with "
|
||||
f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}."
|
||||
f"{datetime.fromtimestamp(since_ms // 1000, tz=UTC).isoformat()}."
|
||||
)
|
||||
if until_ms and since_ms >= until_ms:
|
||||
logger.warning(
|
||||
@@ -396,7 +398,7 @@ class Binance(Exchange):
|
||||
trades = await self._api_async.fetch_trades(
|
||||
pair,
|
||||
params={
|
||||
self._trades_pagination_arg: "0",
|
||||
self._ft_has["trades_pagination_arg"]: "0",
|
||||
},
|
||||
limit=5,
|
||||
)
|
||||
|
||||
+28565
-18989
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.exceptions import (
|
||||
DDosProtection,
|
||||
OperationalException,
|
||||
RetryableOrderError,
|
||||
TemporaryError,
|
||||
)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Bitget(Exchange):
|
||||
"""
|
||||
Bitget exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: FtHas = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
}
|
||||
_ft_has_futures: FtHas = {
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
}
|
||||
|
||||
def ohlcv_candle_limit(
|
||||
self, timeframe: str, candle_type: CandleType, since_ms: int | None = None
|
||||
) -> int:
|
||||
"""
|
||||
Exchange ohlcv candle limit
|
||||
bitget has the following behaviour:
|
||||
* 1000 candles for up-to-date data
|
||||
* 200 candles for historic data (prior to a certain date)
|
||||
:param timeframe: Timeframe to check
|
||||
:param candle_type: Candle-type
|
||||
:param since_ms: Starting timestamp
|
||||
:return: Candle limit as integer
|
||||
"""
|
||||
timeframe_map = self._api.options["fetchOHLCV"]["maxRecentDaysPerTimeframe"]
|
||||
days = timeframe_map.get(timeframe, 30)
|
||||
|
||||
if candle_type in (CandleType.FUTURES, CandleType.SPOT, CandleType.MARK) and (
|
||||
not since_ms or dt_ts(dt_now() - timedelta(days=days)) < since_ms
|
||||
):
|
||||
return 1000
|
||||
|
||||
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
|
||||
|
||||
def _convert_stop_order(self, pair: str, order_id: str, order: CcxtOrder) -> CcxtOrder:
|
||||
if order.get("status", "open") == "closed":
|
||||
# Use orderID as cliendOrderId filter to fetch the regular followup order.
|
||||
# Could be done with "fetch_order" - but clientOid as filter doesn't seem to work
|
||||
# https://www.bitget.com/api-doc/spot/trade/Get-Order-Info
|
||||
|
||||
for method in (
|
||||
self._api.fetch_canceled_and_closed_orders,
|
||||
self._api.fetch_open_orders,
|
||||
):
|
||||
orders = method(pair)
|
||||
orders_f = [order for order in orders if order["clientOrderId"] == order_id]
|
||||
if orders_f:
|
||||
order_reg = orders_f[0]
|
||||
self._log_exchange_response("fetch_stoploss_order1", order_reg)
|
||||
order_reg["id_stop"] = order_reg["id"]
|
||||
order_reg["id"] = order_id
|
||||
order_reg["type"] = "stoploss"
|
||||
order_reg["status_stop"] = "triggered"
|
||||
return order_reg
|
||||
order = self._order_contracts_to_amount(order)
|
||||
order["type"] = "stoploss"
|
||||
return order
|
||||
|
||||
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
|
||||
params2 = {
|
||||
"stop": True,
|
||||
}
|
||||
for method in (
|
||||
self._api.fetch_open_orders,
|
||||
self._api.fetch_canceled_and_closed_orders,
|
||||
):
|
||||
try:
|
||||
orders = method(pair, params=params2)
|
||||
orders_f = [order for order in orders if order["id"] == order_id]
|
||||
if orders_f:
|
||||
order = orders_f[0]
|
||||
self._log_exchange_response("get_stop_order_fallback", order)
|
||||
return self._convert_stop_order(pair, order_id, order)
|
||||
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||
pass
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
||||
|
||||
@retrier(retries=API_RETRY_COUNT)
|
||||
def fetch_stoploss_order(
|
||||
self, order_id: str, pair: str, params: dict | None = None
|
||||
) -> CcxtOrder:
|
||||
if self._config["dry_run"]:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
|
||||
return self._fetch_stop_order_fallback(order_id, pair)
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bitpanda exchange subclass"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
@@ -34,5 +34,5 @@ class Bitpanda(Exchange):
|
||||
:param pair: Pair the order is for
|
||||
:param since: datetime object of the order creation time. Assumes object is in UTC.
|
||||
"""
|
||||
params = {"to": int(datetime.now(timezone.utc).timestamp() * 1000)}
|
||||
params = {"to": int(datetime.now(UTC).timestamp() * 1000)}
|
||||
return super().get_trades_for_order(order_id, pair, since, params)
|
||||
|
||||
+31
-18
@@ -1,8 +1,5 @@
|
||||
"""Bybit exchange subclass"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import ccxt
|
||||
|
||||
@@ -12,6 +9,7 @@ from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalExcep
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -64,9 +62,9 @@ class Bybit(Exchange):
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||
# (TradingMode.FUTURES, MarginMode.CROSS),
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
]
|
||||
|
||||
@property
|
||||
@@ -76,14 +74,11 @@ class Bybit(Exchange):
|
||||
config = {}
|
||||
if self.trading_mode == TradingMode.SPOT:
|
||||
config.update({"options": {"defaultType": "spot"}})
|
||||
config.update(super()._ccxt_config)
|
||||
elif self.trading_mode == TradingMode.FUTURES:
|
||||
config.update({"options": {"defaultSettle": self._config["stake_currency"]}})
|
||||
config = deep_merge_dicts(config, super()._ccxt_config)
|
||||
return config
|
||||
|
||||
def market_is_future(self, market: dict[str, Any]) -> bool:
|
||||
main = super().market_is_future(market)
|
||||
# For ByBit, we'll only support USDT markets for now.
|
||||
return main and market["settle"] == "USDT"
|
||||
|
||||
@retrier
|
||||
def additional_exchange_init(self) -> None:
|
||||
"""
|
||||
@@ -182,18 +177,36 @@ class Bybit(Exchange):
|
||||
PERPETUAL:
|
||||
bybit:
|
||||
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
|
||||
USDT:
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#b
|
||||
USDC:
|
||||
https://www.bybit.com/en/help-center/article/Liquidation-Price-Calculation-under-Isolated-Mode-Unified-Trading-Account#c
|
||||
|
||||
Long:
|
||||
Long USDT:
|
||||
Liquidation Price = (
|
||||
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
- (Extra Margin Added/Contract Quantity))
|
||||
Short USDT:
|
||||
Liquidation Price = (
|
||||
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
+ (Extra Margin Added/Contract Quantity))
|
||||
|
||||
Long USDC:
|
||||
Liquidation Price = (
|
||||
Entry Price - [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
- (Extra Margin Added/Contract Quantity))
|
||||
Short:
|
||||
Position Entry Price - [
|
||||
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
|
||||
]
|
||||
)
|
||||
|
||||
Short USDC:
|
||||
Liquidation Price = (
|
||||
Entry Price + [(Initial Margin - Maintenance Margin)/Contract Quantity]
|
||||
+ (Extra Margin Added/Contract Quantity))
|
||||
Position Entry Price + [
|
||||
(Initial Margin + Extra Margin Added - Maintenance Margin) / Position Size
|
||||
]
|
||||
)
|
||||
|
||||
Implementation Note: Extra margin is currently not used.
|
||||
Due to this - the liquidation formula between USDT and USDC is the same.
|
||||
|
||||
:param pair: Pair to calculate liquidation price for
|
||||
:param open_rate: Entry price of position
|
||||
|
||||
@@ -5,7 +5,6 @@ from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Any, TypeVar, cast, overload
|
||||
|
||||
from freqtrade.constants import ExchangeConfig
|
||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
|
||||
@@ -47,10 +46,9 @@ BAD_EXCHANGES = {
|
||||
|
||||
MAP_EXCHANGE_CHILDCLASS = {
|
||||
"binanceus": "binance",
|
||||
"binanceje": "binance",
|
||||
"binanceusdm": "binance",
|
||||
"okex": "okx",
|
||||
"myokx": "okx",
|
||||
"okxus": "okx",
|
||||
"gateio": "gate",
|
||||
"huboi": "htx",
|
||||
}
|
||||
@@ -65,6 +63,7 @@ SUPPORTED_EXCHANGES = [
|
||||
"hyperliquid",
|
||||
"kraken",
|
||||
"okx",
|
||||
"myokx",
|
||||
]
|
||||
|
||||
# either the main, or replacement methods (array) is required
|
||||
@@ -104,20 +103,6 @@ EXCHANGE_HAS_OPTIONAL = [
|
||||
]
|
||||
|
||||
|
||||
def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None:
|
||||
"""
|
||||
Removes exchange keys from the configuration and specifies dry-run
|
||||
Used for backtesting / hyperopt / edge and utils.
|
||||
Modifies the input dict!
|
||||
"""
|
||||
if dry_run:
|
||||
exchange_config["key"] = ""
|
||||
exchange_config["apiKey"] = ""
|
||||
exchange_config["secret"] = ""
|
||||
exchange_config["password"] = ""
|
||||
exchange_config["uid"] = ""
|
||||
|
||||
|
||||
def calculate_backoff(retrycount, max_retries):
|
||||
"""
|
||||
Calculate backoff
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
import signal
|
||||
from collections.abc import Coroutine, Generator
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from math import floor, isnan
|
||||
from threading import Lock
|
||||
from typing import Any, Literal, TypeGuard, TypeVar
|
||||
@@ -21,6 +21,7 @@ from ccxt import TICK_SIZE
|
||||
from dateutil import parser
|
||||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import remove_exchange_credentials
|
||||
from freqtrade.constants import (
|
||||
DEFAULT_AMOUNT_RESERVE_PERCENT,
|
||||
DEFAULT_TRADES_COLUMNS,
|
||||
@@ -64,7 +65,6 @@ from freqtrade.exceptions import (
|
||||
)
|
||||
from freqtrade.exchange.common import (
|
||||
API_FETCH_ORDER_RETRY_COUNT,
|
||||
remove_exchange_credentials,
|
||||
retrier,
|
||||
retrier_async,
|
||||
)
|
||||
@@ -137,6 +137,7 @@ class Exchange:
|
||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||
"ohlcv_partial_candle": True,
|
||||
"ohlcv_require_since": False,
|
||||
"always_require_api_keys": False, # purge API keys for Dry-run. Must default to false.
|
||||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||
"tickers_have_quoteVolume": True,
|
||||
@@ -168,7 +169,8 @@ class Exchange:
|
||||
_ft_has_futures: FtHas = {}
|
||||
|
||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
# Non-defined exchanges only support spot mode.
|
||||
(TradingMode.SPOT, MarginMode.NONE),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -197,7 +199,26 @@ class Exchange:
|
||||
self.loop = self._init_async_loop()
|
||||
self._config: Config = {}
|
||||
|
||||
# Leverage properties
|
||||
self.trading_mode: TradingMode = TradingMode(
|
||||
config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
|
||||
)
|
||||
self.margin_mode: MarginMode = MarginMode(
|
||||
MarginMode(config.get("margin_mode"))
|
||||
if config.get("margin_mode")
|
||||
else self._supported_trading_mode_margin_pairs[0][1]
|
||||
)
|
||||
config["trading_mode"] = self.trading_mode
|
||||
config["margin_mode"] = self.margin_mode
|
||||
config["candle_type_def"] = CandleType.get_default(self.trading_mode)
|
||||
self._config.update(config)
|
||||
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
|
||||
|
||||
exchange_conf: ExchangeConfig = exchange_config if exchange_config else config["exchange"]
|
||||
|
||||
# Deep merge ft_has with default ft_has options
|
||||
# Must be called before ft_has is used.
|
||||
self.build_ft_has(exchange_conf)
|
||||
|
||||
# Holds last candle refreshed time of each pair
|
||||
self._pairs_last_refresh_time: dict[PairWithTimeframe, int] = {}
|
||||
@@ -227,33 +248,17 @@ class Exchange:
|
||||
if config["dry_run"]:
|
||||
logger.info("Instance is running with dry_run enabled")
|
||||
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||
exchange_conf: dict[str, Any] = exchange_config if exchange_config else config["exchange"]
|
||||
remove_exchange_credentials(exchange_conf, config.get("dry_run", False))
|
||||
self.log_responses = exchange_conf.get("log_responses", False)
|
||||
|
||||
# Leverage properties
|
||||
self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
|
||||
self.margin_mode: MarginMode = (
|
||||
MarginMode(config.get("margin_mode")) if config.get("margin_mode") else MarginMode.NONE
|
||||
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
|
||||
remove_exchange_credentials(
|
||||
exchange_conf,
|
||||
not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
|
||||
)
|
||||
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
|
||||
|
||||
# Deep merge ft_has with default ft_has options
|
||||
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
|
||||
if exchange_conf.get("_ft_has_params"):
|
||||
self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
|
||||
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
|
||||
self.log_responses = exchange_conf.get("log_responses", False)
|
||||
|
||||
# Assign this directly for easy access
|
||||
self._ohlcv_partial_candle = self._ft_has["ohlcv_partial_candle"]
|
||||
|
||||
self._max_trades_limit = self._ft_has["trades_limit"]
|
||||
|
||||
self._trades_pagination = self._ft_has["trades_pagination"]
|
||||
self._trades_pagination_arg = self._ft_has["trades_pagination_arg"]
|
||||
|
||||
# Initialize ccxt objects
|
||||
ccxt_config = self._ccxt_config
|
||||
ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config)
|
||||
@@ -289,10 +294,6 @@ class Exchange:
|
||||
# Initial markets load
|
||||
self.reload_markets(True, load_leverage_tiers=False)
|
||||
self.validate_config(config)
|
||||
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
self._startup_candle_count, config.get("timeframe", "")
|
||||
)
|
||||
|
||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||
self.fill_leverage_tiers()
|
||||
@@ -331,6 +332,12 @@ class Exchange:
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop
|
||||
|
||||
def _set_startup_candle_count(self, config: Config) -> None:
|
||||
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
self._startup_candle_count, config.get("timeframe", "")
|
||||
)
|
||||
|
||||
def validate_config(self, config: Config) -> None:
|
||||
# Check if timeframe is available
|
||||
self.validate_timeframes(config.get("timeframe"))
|
||||
@@ -345,6 +352,8 @@ class Exchange:
|
||||
self.validate_orderflow(config["exchange"])
|
||||
self.validate_freqai(config)
|
||||
|
||||
self._set_startup_candle_count(config)
|
||||
|
||||
def _init_ccxt(
|
||||
self, exchange_config: dict[str, Any], sync: bool, ccxt_kwargs: dict[str, Any]
|
||||
) -> ccxt.Exchange:
|
||||
@@ -637,9 +646,9 @@ class Exchange:
|
||||
if self._exchange_ws:
|
||||
self._exchange_ws.reset_connections()
|
||||
|
||||
async def _api_reload_markets(self, reload: bool = False) -> dict[str, Any]:
|
||||
async def _api_reload_markets(self, reload: bool = False) -> None:
|
||||
try:
|
||||
return await self._api_async.load_markets(reload=reload, params={})
|
||||
await self._api_async.load_markets(reload=reload, params={})
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
@@ -649,15 +658,15 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise TemporaryError(e) from e
|
||||
|
||||
def _load_async_markets(self, reload: bool = False) -> dict[str, Any]:
|
||||
def _load_async_markets(self, reload: bool = False) -> None:
|
||||
try:
|
||||
with self._loop_lock:
|
||||
markets = self.loop.run_until_complete(self._api_reload_markets(reload=reload))
|
||||
|
||||
if isinstance(markets, Exception):
|
||||
raise markets
|
||||
return markets
|
||||
except asyncio.TimeoutError as e:
|
||||
return None
|
||||
except TimeoutError as e:
|
||||
logger.warning("Could not load markets. Reason: %s", e)
|
||||
raise TemporaryError from e
|
||||
|
||||
@@ -679,7 +688,8 @@ class Exchange:
|
||||
# on initial load, we retry 3 times to ensure we get the markets
|
||||
retries: int = 3 if force else 0
|
||||
# Reload async markets, then assign them to sync api
|
||||
self._markets = retrier(self._load_async_markets, retries=retries)(reload=True)
|
||||
retrier(self._load_async_markets, retries=retries)(reload=True)
|
||||
self._markets = self._api_async.markets
|
||||
self._api.set_markets(self._api_async.markets, self._api_async.currencies)
|
||||
# Assign options array, as it contains some temporary information from the exchange.
|
||||
self._api.options = self._api_async.options
|
||||
@@ -876,10 +886,24 @@ class Exchange:
|
||||
(trading_mode, margin_mode) not in self._supported_trading_mode_margin_pairs
|
||||
):
|
||||
mm_value = margin_mode and margin_mode.value
|
||||
raise OperationalException(
|
||||
f"Freqtrade does not support {mm_value} {trading_mode} on {self.name}"
|
||||
raise ConfigurationError(
|
||||
f"Freqtrade does not support '{mm_value}' '{trading_mode}' on {self.name}."
|
||||
)
|
||||
|
||||
def build_ft_has(self, exchange_conf: ExchangeConfig) -> None:
|
||||
"""
|
||||
Deep merge ft_has with default ft_has options
|
||||
and with exchange_conf._ft_has_params if available.
|
||||
This is called on initialization of the exchange object.
|
||||
It must be called before ft_has is used.
|
||||
"""
|
||||
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
|
||||
if exchange_conf.get("_ft_has_params"):
|
||||
self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
|
||||
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
|
||||
|
||||
def get_option(self, param: str, default: Any | None = None) -> Any:
|
||||
"""
|
||||
Get parameter value from _ft_has
|
||||
@@ -2207,7 +2231,7 @@ class Exchange:
|
||||
_params = params if params else {}
|
||||
my_trades = self._api.fetch_my_trades(
|
||||
pair,
|
||||
int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
|
||||
int((since.replace(tzinfo=UTC).timestamp() - 5) * 1000),
|
||||
params=_params,
|
||||
)
|
||||
matched_trades = [trade for trade in my_trades if trade["order"] == order_id]
|
||||
@@ -2583,10 +2607,12 @@ class Exchange:
|
||||
if ticks and cache:
|
||||
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
|
||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0]
|
||||
# keeping parsed dataframe in cache
|
||||
has_cache = cache and (pair, timeframe, c_type) in self._klines
|
||||
# in case of existing cache, fill_missing happens after concatenation
|
||||
ohlcv_df = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=drop_incomplete
|
||||
ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
|
||||
)
|
||||
# keeping parsed dataframe in cache
|
||||
if cache:
|
||||
if (pair, timeframe, c_type) in self._klines:
|
||||
old = self._klines[(pair, timeframe, c_type)]
|
||||
@@ -2994,7 +3020,7 @@ class Exchange:
|
||||
returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
|
||||
"""
|
||||
try:
|
||||
trades_limit = self._max_trades_limit
|
||||
trades_limit = self._ft_has["trades_limit"]
|
||||
# fetch trades asynchronously
|
||||
if params:
|
||||
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
|
||||
@@ -3038,7 +3064,7 @@ class Exchange:
|
||||
"""
|
||||
if not trades:
|
||||
return None
|
||||
if self._trades_pagination == "id":
|
||||
if self._ft_has["trades_pagination"] == "id":
|
||||
return trades[-1].get("id")
|
||||
else:
|
||||
return trades[-1].get("timestamp")
|
||||
@@ -3056,7 +3082,7 @@ class Exchange:
|
||||
) -> tuple[str, list[list]]:
|
||||
"""
|
||||
Asynchronously gets trade history using fetch_trades
|
||||
use this when exchange uses id-based iteration (check `self._trades_pagination`)
|
||||
use this when exchange uses id-based iteration (check `self._ft_has["trades_pagination"]`)
|
||||
:param pair: Pair to fetch trade data for
|
||||
:param since: Since as integer timestamp in milliseconds
|
||||
:param until: Until as integer timestamp in milliseconds
|
||||
@@ -3082,7 +3108,7 @@ class Exchange:
|
||||
while True:
|
||||
try:
|
||||
t, from_id_next = await self._async_fetch_trades(
|
||||
pair, params={self._trades_pagination_arg: from_id}
|
||||
pair, params={self._ft_has["trades_pagination_arg"]: from_id}
|
||||
)
|
||||
if t:
|
||||
trades.extend(t[x])
|
||||
@@ -3110,7 +3136,7 @@ class Exchange:
|
||||
) -> tuple[str, list[list]]:
|
||||
"""
|
||||
Asynchronously gets trade history using fetch_trades,
|
||||
when the exchange uses time-based iteration (check `self._trades_pagination`)
|
||||
when the exchange uses time-based iteration (check `self._ft_has["trades_pagination"]`)
|
||||
:param pair: Pair to fetch trade data for
|
||||
:param since: Since as integer timestamp in milliseconds
|
||||
:param until: Until as integer timestamp in milliseconds
|
||||
@@ -3164,9 +3190,9 @@ class Exchange:
|
||||
until = ccxt.Exchange.milliseconds()
|
||||
logger.debug(f"Exchange milliseconds: {until}")
|
||||
|
||||
if self._trades_pagination == "time":
|
||||
if self._ft_has["trades_pagination"] == "time":
|
||||
return await self._async_get_trade_history_time(pair=pair, since=since, until=until)
|
||||
elif self._trades_pagination == "id":
|
||||
elif self._ft_has["trades_pagination"] == "id":
|
||||
return await self._async_get_trade_history_id(
|
||||
pair=pair, since=since, until=until, from_id=from_id
|
||||
)
|
||||
@@ -3334,7 +3360,7 @@ class Exchange:
|
||||
if not filename.parent.is_dir():
|
||||
filename.parent.mkdir(parents=True)
|
||||
data = {
|
||||
"updated": datetime.now(timezone.utc),
|
||||
"updated": datetime.now(UTC),
|
||||
"data": tiers,
|
||||
}
|
||||
file_dump_json(filename, data)
|
||||
@@ -3356,7 +3382,7 @@ class Exchange:
|
||||
updated = tiers.get("updated")
|
||||
if updated:
|
||||
updated_dt = parser.parse(updated)
|
||||
if updated_dt < datetime.now(timezone.utc) - cache_time:
|
||||
if updated_dt < datetime.now(UTC) - cache_time:
|
||||
logger.info("Cached leverage tiers are outdated. Will update.")
|
||||
return None
|
||||
return tiers.get("data")
|
||||
@@ -3415,20 +3441,30 @@ class Exchange:
|
||||
# Find the appropriate tier based on stake_amount
|
||||
prior_max_lev = None
|
||||
for tier in pair_tiers:
|
||||
# Adjust notional by leverage to do a proper comparison
|
||||
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
||||
max_stake = tier["maxNotional"] / tier["maxLeverage"]
|
||||
prior_max_lev = tier["maxLeverage"]
|
||||
# Adjust notional by leverage to do a proper comparison
|
||||
if min_stake <= stake_amount <= max_stake:
|
||||
return tier["maxLeverage"]
|
||||
if stake_amount < min_stake and stake_amount <= max_stake:
|
||||
# TODO: Remove this warning eventually
|
||||
# Code could be simplified by removing the check for min-stake in the above
|
||||
# condition, making this branch unnecessary.
|
||||
logger.warning(
|
||||
f"Fallback to next higher leverage tier for {pair}, stake: {stake_amount}, "
|
||||
f"min_stake: {min_stake}."
|
||||
)
|
||||
return tier["maxLeverage"]
|
||||
|
||||
# else: # if on the last tier
|
||||
if stake_amount > max_stake:
|
||||
# If stake is > than max tradeable amount
|
||||
raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}")
|
||||
raise InvalidOrderException(f"Stake amount {stake_amount} too high for {pair}")
|
||||
|
||||
raise OperationalException(
|
||||
"Looped through all tiers without finding a max leverage. Should never be reached"
|
||||
f"Looped through all tiers without finding a max leverage for {pair}. "
|
||||
"Should never be reached."
|
||||
)
|
||||
|
||||
elif self.trading_mode == TradingMode.MARGIN: # Search markets.limits for max lev
|
||||
@@ -3570,7 +3606,7 @@ class Exchange:
|
||||
mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
|
||||
|
||||
if not close_date:
|
||||
close_date = datetime.now(timezone.utc)
|
||||
close_date = datetime.now(UTC)
|
||||
since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date))
|
||||
|
||||
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
|
||||
|
||||
@@ -24,6 +24,7 @@ class FtHas(TypedDict, total=False):
|
||||
ohlcv_require_since: bool
|
||||
ohlcv_volume_currency: str
|
||||
ohlcv_candle_limit_per_timeframe: dict[str, int]
|
||||
always_require_api_keys: bool
|
||||
# Tickers
|
||||
tickers_have_quoteVolume: bool
|
||||
tickers_have_percentage: bool
|
||||
|
||||
@@ -3,7 +3,7 @@ Exchange support utils
|
||||
"""
|
||||
|
||||
import inspect
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from math import ceil, floor, isnan
|
||||
from typing import Any
|
||||
|
||||
@@ -27,7 +27,7 @@ from freqtrade.exchange.common import (
|
||||
SUPPORTED_EXCHANGES,
|
||||
)
|
||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.ft_types import ValidExchangesType
|
||||
from freqtrade.ft_types import TradeModeType, ValidExchangesType
|
||||
from freqtrade.util import FtPrecise
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ def _build_exchange_list_entry(
|
||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||
}
|
||||
if resolved := exchangeClasses.get(mapped_exchange_name):
|
||||
supported_modes = [{"trading_mode": "spot", "margin_mode": ""}] + [
|
||||
supported_modes: list[TradeModeType] = [
|
||||
{"trading_mode": tm.value, "margin_mode": mm.value}
|
||||
for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs
|
||||
]
|
||||
@@ -148,7 +148,7 @@ def date_minus_candles(timeframe: str, candle_count: int, date: datetime | None
|
||||
|
||||
"""
|
||||
if not date:
|
||||
date = datetime.now(timezone.utc)
|
||||
date = datetime.now(UTC)
|
||||
|
||||
tf_min = timeframe_to_minutes(timeframe)
|
||||
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
|
||||
@@ -213,9 +213,9 @@ def amount_to_precision(
|
||||
amount = float(
|
||||
decimal_to_precision(
|
||||
amount,
|
||||
rounding_mode=TRUNCATE,
|
||||
precision=precision,
|
||||
counting_mode=precisionMode,
|
||||
TRUNCATE, # rounding_mode
|
||||
precision, # numPrecisionDigits
|
||||
precisionMode, # counting_mode
|
||||
)
|
||||
)
|
||||
|
||||
@@ -311,11 +311,11 @@ def price_to_precision(
|
||||
return float(
|
||||
decimal_to_precision(
|
||||
price,
|
||||
rounding_mode=rounding_mode,
|
||||
precision=int(price_precision)
|
||||
rounding_mode, # rounding mode
|
||||
int(price_precision)
|
||||
if precisionMode != TICK_SIZE
|
||||
else price_precision,
|
||||
counting_mode=precisionMode,
|
||||
else price_precision, # numPrecisionDigits
|
||||
precisionMode, # counting_mode
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user