Compare commits
857 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9adf0316f1 | |||
| 830166e1c7 | |||
| 50aaf2cfc9 | |||
| 1b741f9c58 | |||
| 606f3b7dfe | |||
| 2d9b13e65a | |||
| 1a476c4078 | |||
| 0c1c0990e8 | |||
| be37cafcb9 | |||
| 87a9f11773 | |||
| 0fedcf9eb1 | |||
| a54464905b | |||
| 7c8aeed806 | |||
| bd706557bd | |||
| d657a9760a | |||
| 0333e7e227 | |||
| 90945675bc | |||
| d3713cbf09 | |||
| 9003d5e9e0 | |||
| 7b197aa974 | |||
| ec65c88797 | |||
| ba6627cd13 | |||
| 22a6fdab13 | |||
| 580e5b945f | |||
| 83649ab787 | |||
| 8165fd6310 | |||
| 6cf46338ef | |||
| 0d84e80eb7 | |||
| 476abf80cc | |||
| a03ebc8472 | |||
| 1182c03b73 | |||
| 2152fd7325 | |||
| c7f84403d8 | |||
| 6dcea8bac1 | |||
| 217a71b834 | |||
| cf578a4b76 | |||
| f645d3e21a | |||
| 9c480b4f4d | |||
| 3a714cef14 | |||
| d67b54bcb5 | |||
| 9aebce306d | |||
| 8e7aed5013 | |||
| 4cee997d11 | |||
| 806a11c63a | |||
| 2e760ae1c5 | |||
| 674b2c11bd | |||
| 5a5dae22d3 | |||
| 0a1498af2d | |||
| e8cdbf0a8e | |||
| c0ee618bd0 | |||
| a4ed0a03f1 | |||
| 25d8d831ec | |||
| 3d91223c2b | |||
| 875daac375 | |||
| 03fac42047 | |||
| 5bcc4fbf03 | |||
| 0ea2258fa8 | |||
| a0627aa2fc | |||
| 764b32a0cf | |||
| fc634f42f6 | |||
| 8cce4be23c | |||
| 11ab80bfc4 | |||
| e00a1fd28c | |||
| b132a49f40 | |||
| f82e4ce9ec | |||
| fac00475a1 | |||
| 2ac99fc2bf | |||
| d935de08fe | |||
| a92e0b2545 | |||
| 8abdd496f6 | |||
| 443a245d82 | |||
| d95789a2e0 | |||
| 3a4adfac52 | |||
| 523216a39c | |||
| e5d4fd6b62 | |||
| 939ea01d12 | |||
| c587a1ccda | |||
| 6751b8e468 | |||
| ef1dca92eb | |||
| 0e33e69d8e | |||
| 021011df31 | |||
| c2e89e5f11 | |||
| ba89faab6a | |||
| ef38cb325b | |||
| e0df17ef33 | |||
| c2578dca06 | |||
| c8605ae8fb | |||
| 22ef7697e8 | |||
| 77cabd291f | |||
| 59b47a9a67 | |||
| e8eb5cdb4c | |||
| c9274a7d04 | |||
| c6a481a2dc | |||
| 0188d68a85 | |||
| 10c2ec39db | |||
| d0bf09b7e1 | |||
| d2cbae6f50 | |||
| 7e41443db8 | |||
| 011098209a | |||
| 885a147b48 | |||
| 1ad03ecb13 | |||
| c46272a7fc | |||
| 1d8034ab66 | |||
| 0b12dcfec9 | |||
| cf541f8e7c | |||
| 7c46980e9d | |||
| 7c9554349d | |||
| 2d3ccb841d | |||
| 68768748dc | |||
| 0b0a4e5df1 | |||
| 2b47d2e91d | |||
| 60cbf93b8f | |||
| 1b67a5b533 | |||
| 30832e702c | |||
| 8c8738e5f9 | |||
| f72d734e8c | |||
| 110ffde14e | |||
| 2d0ab2d973 | |||
| 8e9615ba6c | |||
| 55244fb098 | |||
| 2087e04a62 | |||
| 5ecd64d17e | |||
| 047a107ff3 | |||
| 4acdb2eb0c | |||
| 58a18e1e9c | |||
| 17b5e9bac7 | |||
| 610d03570c | |||
| 2ea4c4de2f | |||
| ad8dc57e9b | |||
| dd970f2be1 | |||
| 63819fa7e0 | |||
| bfe05868eb | |||
| 14d66100be | |||
| afc00c5a2e | |||
| 0b13ddfc0a | |||
| 820000fac4 | |||
| 5710fe4477 | |||
| 3c84ac6d3f | |||
| 3116139336 | |||
| 24e40d3a7a | |||
| 5b83dce052 | |||
| bab128b359 | |||
| 875c464a07 | |||
| 604fddd735 | |||
| bd247f3642 | |||
| ca6419228c | |||
| 003cc4dc38 | |||
| 41deb75279 | |||
| 5a8131caf6 | |||
| a5cb607311 | |||
| cf5ba35bb0 | |||
| 668f7b7e9c | |||
| e2879f11f5 | |||
| 700d4ea2ff | |||
| 1f94154220 | |||
| 14ee96172a | |||
| 159bde944b | |||
| 0a49903695 | |||
| e5f7d0cbb4 | |||
| f17e383157 | |||
| 093cc74774 | |||
| c34cd6a7dd | |||
| 17678f1819 | |||
| 057c51b0cd | |||
| 0858b82d3e | |||
| 5e624d658f | |||
| 3949efadfe | |||
| af1de46cd4 | |||
| 9d0fb9b025 | |||
| 6d096c20b0 | |||
| 05c3360892 | |||
| f9be1ac82c | |||
| 8e9d193791 | |||
| cdc01122d5 | |||
| a625cbecff | |||
| 2e4b92ea6c | |||
| f2f17f6d7b | |||
| 3e3436e432 | |||
| 9dfbe8cf63 | |||
| 85af9dd3fc | |||
| 72f9e9a051 | |||
| 2549808b52 | |||
| ef1e81f514 | |||
| eabaef0da2 | |||
| f78e9c9014 | |||
| ab6f3b8222 | |||
| 3e42dfff13 | |||
| af2e741465 | |||
| 0fa3459271 | |||
| ba3d29b100 | |||
| d1132d2b13 | |||
| 0850f048e7 | |||
| 56f6da9026 | |||
| 470a7bf285 | |||
| c16a951555 | |||
| 0013b3639d | |||
| fdf182df55 | |||
| f3d227270e | |||
| c1a8b5520a | |||
| d4a6fbc7c0 | |||
| 8c5f63127d | |||
| d9e0a2837a | |||
| 24bb7649eb | |||
| 4ddec7463a | |||
| 5ddd4d4583 | |||
| 11e35c467f | |||
| 053134297c | |||
| b97f037472 | |||
| 097ffd87b3 | |||
| cd58bba697 | |||
| 7d455c7e48 | |||
| 9d019f1018 | |||
| cd70e74d7b | |||
| b467eff8be | |||
| a02c84ecda | |||
| 84535a1404 | |||
| 832fb7044c | |||
| 55361f0a9e | |||
| c1795c79f3 | |||
| ee65ffc68f | |||
| 284446d782 | |||
| 6e270c5d06 | |||
| 143726edc1 | |||
| d27de49266 | |||
| 7e3e206ab3 | |||
| 12ccd292b1 | |||
| f1828d72ac | |||
| 0248c20999 | |||
| 63f2a8bb68 | |||
| b1747fe9ea | |||
| d55c479886 | |||
| b71f91a156 | |||
| 7105279654 | |||
| 812dc64cd7 | |||
| 019f1ab28d | |||
| ff7e6c3737 | |||
| ac3e0899a3 | |||
| 0568c7b945 | |||
| 3a31337e43 | |||
| 2c5dc72985 | |||
| 16547dbdbe | |||
| 412e112c0f | |||
| cd5ea969c4 | |||
| 15bba1b0d0 | |||
| 755a426992 | |||
| c19982dd36 | |||
| 74ba9d76a2 | |||
| 187d06b5ba | |||
| 202326875d | |||
| 895a9f351f | |||
| 48b67f7bfb | |||
| fcf5785367 | |||
| 85e84d8b69 | |||
| b8bd4604fa | |||
| f7080ac11d | |||
| a9b1fd219b | |||
| 3da3290224 | |||
| 2f8d82e0b0 | |||
| 94e52f5241 | |||
| df9469d195 | |||
| c9d4276c66 | |||
| 12f37b7575 | |||
| 51d61bc6a8 | |||
| 79ea1ba1d5 | |||
| f6f0180fc1 | |||
| 33211c8eb1 | |||
| c60d96922d | |||
| 45d4c5d036 | |||
| 09106ecbe0 | |||
| da9f592d3d | |||
| 22707d6c04 | |||
| 2d930f1fff | |||
| 44919adae4 | |||
| a28545a67a | |||
| 76c09299a9 | |||
| 173c75c08c | |||
| dfa2db8575 | |||
| 81fcd5ea5a | |||
| 5e0eb5da10 | |||
| 64ceb028ac | |||
| bc0c8ffb71 | |||
| 624be0c469 | |||
| bcd9023a8d | |||
| fba843cb61 | |||
| 6210927bdb | |||
| c2b95090f7 | |||
| 017df564ce | |||
| 97badd0d3b | |||
| f3c84d6a3c | |||
| 68d514db91 | |||
| f838db6409 | |||
| e8501dfb7a | |||
| e5a8aae831 | |||
| 174a857d72 | |||
| b66b0c3c9b | |||
| 85ce0a1840 | |||
| 5dd2ad532b | |||
| 68ea331703 | |||
| d93ae4e92d | |||
| 6dfc64b5b9 | |||
| 44880fdc0d | |||
| 0b838d2e7c | |||
| 6c218c8647 | |||
| 1479b0becd | |||
| 7792a0b86f | |||
| 7dc7511eef | |||
| c5a46dff84 | |||
| 48b178b353 | |||
| b5ac870eac | |||
| 968dea5b66 | |||
| c4aca0c2b9 | |||
| 3872906a57 | |||
| 55e4250bcd | |||
| 90d538d41a | |||
| 4762abd13f | |||
| 724ed63c41 | |||
| 1dbb0f0ee7 | |||
| e5baa27d51 | |||
| 7c43833a2d | |||
| ff7cddf4ca | |||
| 36b427cd3b | |||
| 8b204c75cb | |||
| f56ea7a6ca | |||
| 39569df42a | |||
| 08f0f28e90 | |||
| d8e0d41001 | |||
| 1ac68d6161 | |||
| e7f9059ff4 | |||
| 98fd645df6 | |||
| 86b8eae344 | |||
| 8e62158145 | |||
| 6a5fb9af42 | |||
| 02125f1883 | |||
| ed6899aad6 | |||
| 4cced59530 | |||
| 70b24fd0c1 | |||
| d026b67dee | |||
| c906e146fc | |||
| 8890cdbcdb | |||
| 6712d20578 | |||
| 9582bd3cd7 | |||
| 04990a888b | |||
| e6aa098c4e | |||
| c27f719fb5 | |||
| e45d781630 | |||
| 63ac3bef01 | |||
| 753bf99b95 | |||
| f13a02e2ec | |||
| b7b1bdda27 | |||
| 262c002a74 | |||
| a68d943eeb | |||
| a4c6cbcfd4 | |||
| f151639677 | |||
| 0f4d1df59f | |||
| 043281ba1b | |||
| a33def834a | |||
| d25124021c | |||
| 82ab8c4674 | |||
| 696af8ec07 | |||
| 7af050ecc4 | |||
| 13c872d079 | |||
| bc574509bf | |||
| 90d4447911 | |||
| 69ffc2c7a2 | |||
| 3d68d6aefc | |||
| de064505c5 | |||
| 4ba01b5c12 | |||
| aaadb01a6a | |||
| 8768fe90b3 | |||
| 51478baa47 | |||
| b6b9ae5eb0 | |||
| af815a3c76 | |||
| 754f24c8a6 | |||
| b45e086b50 | |||
| 7af322a197 | |||
| 75bfa87f74 | |||
| a40de0a59b | |||
| 66eb7f0199 | |||
| d001d91640 | |||
| dec3c3e13b | |||
| f99ccc5e3d | |||
| 78ccb68929 | |||
| e28da4b087 | |||
| 425ebeeedb | |||
| c3b2a73eef | |||
| 35806c26bf | |||
| a9bbc45ba5 | |||
| 5e6d3e265b | |||
| f9db85fcaf | |||
| 138b70a2bf | |||
| 518092a0ad | |||
| 5d3776309c | |||
| ab2e0e6d6f | |||
| fa48910e96 | |||
| 5d2a7d2187 | |||
| e4eee1aa1b | |||
| 803b4cae78 | |||
| 09ddef3716 | |||
| 5a847665ff | |||
| 50fd6d152e | |||
| b4961a2cb7 | |||
| 4cac724709 | |||
| 05bbc84bf2 | |||
| 21ac7d1969 | |||
| f125349587 | |||
| f7ddf46b32 | |||
| 623991c772 | |||
| 1237bb798c | |||
| a09036cd6e | |||
| 9891b8332b | |||
| 7b223f3d38 | |||
| 61a5ab8e1c | |||
| 98493dc9ed | |||
| 0835318b8f | |||
| b69a042f5b | |||
| 00d39bbb80 | |||
| a88dc8839d | |||
| cb8d68f395 | |||
| 7dfcf846d0 | |||
| 3a9160aace | |||
| 4904c7b9fd | |||
| 27c7a37531 | |||
| 9f9e13cec2 | |||
| 175e77794c | |||
| 1d7b1cd4ea | |||
| 323f42fc53 | |||
| 03fd0575ce | |||
| c12c2177cd | |||
| c884fd20ea | |||
| 0f9cab4231 | |||
| 97e7939a30 | |||
| 6e3c850807 | |||
| 7b49dc8c11 | |||
| cbd1a4c060 | |||
| 921cb4dad8 | |||
| bf5ec98918 | |||
| c877d267c7 | |||
| c66adf2bf1 | |||
| 1ede186484 | |||
| 680aeb89c3 | |||
| ba1092b726 | |||
| 9ddabbd849 | |||
| b5f31bf82d | |||
| db2309dfd2 | |||
| 418acb7034 | |||
| 8a284060d1 | |||
| 235c46ae12 | |||
| 1f32dbef9a | |||
| d105770ff6 | |||
| 15be0510fc | |||
| 6c24925522 | |||
| a8295de2b9 | |||
| d6a0a4ec6c | |||
| c7878130f1 | |||
| 51cf051fce | |||
| 1f15d28eeb | |||
| 281b627db3 | |||
| 2d2cee2c58 | |||
| 63869be376 | |||
| 37fae7ea71 | |||
| e52276e3da | |||
| cba49307b6 | |||
| b614ec4ef9 | |||
| ed560f995d | |||
| 21269b8a8d | |||
| ee745551a2 | |||
| 10cc857c51 | |||
| 11cb3ef416 | |||
| a3e7ee9895 | |||
| 3a76235c48 | |||
| 4ddb7fd43f | |||
| e3c316f311 | |||
| 1ebd75b32a | |||
| 65f494be26 | |||
| 8a98b6172c | |||
| 3e4f0df957 | |||
| 4f600eb078 | |||
| b5d400c50d | |||
| f96f9b7601 | |||
| f1fc3e1451 | |||
| a7c99b3ba7 | |||
| 57b42b8cf0 | |||
| cdffee23e1 | |||
| 384e0c5e5a | |||
| d1b378ead6 | |||
| 42a153953a | |||
| 8eb2edc0e2 | |||
| b0c14d1122 | |||
| 7e7e475d4a | |||
| c21b8587a6 | |||
| ea51c646cd | |||
| 36d2ed061f | |||
| 61e27462b1 | |||
| c47f5f9494 | |||
| 60ede009d9 | |||
| 89b6cabf77 | |||
| 4b9ab0a54f | |||
| 5782e39fc5 | |||
| f91e71e092 | |||
| 6a8d1fff8f | |||
| e703d6fea9 | |||
| a9f6fcc7bb | |||
| 230281f8f2 | |||
| 326ef6d49b | |||
| 5c129506ae | |||
| 9a94d97e69 | |||
| e0942e1328 | |||
| 88c3fd2bd5 | |||
| 6e11ce9bd5 | |||
| 5dccabe2d8 | |||
| b79a280e55 | |||
| 1615c0bd92 | |||
| 9df068c426 | |||
| e8fde061b5 | |||
| 8d024f5e08 | |||
| 6541450c7c | |||
| c29956866d | |||
| 0ed613a574 | |||
| c7dfd54eb3 | |||
| 697814ed6a | |||
| d5417d40ae | |||
| e386334130 | |||
| 57e489b9aa | |||
| e0d55bf5d3 | |||
| f169f99a5f | |||
| 0982b26d6b | |||
| 626e719942 | |||
| d82cac3895 | |||
| 3e40944b4e | |||
| 05081df48b | |||
| 6e6082fa2b | |||
| 3c43e3dae3 | |||
| 515a754dc4 | |||
| 1c98c2feeb | |||
| 697059a4a2 | |||
| 1b1508d8f2 | |||
| 48905e55e5 | |||
| a203f4129a | |||
| 0b3f3a94b5 | |||
| daf9918bb5 | |||
| 3bde73740c | |||
| 3f9eaba1ef | |||
| d557b4b298 | |||
| fd858b79af | |||
| e96c67e140 | |||
| faf28180a5 | |||
| e98959f3f0 | |||
| 0a2c3379c6 | |||
| b8f5df7cf6 | |||
| 04a4a97efb | |||
| 143d9a90c6 | |||
| c112fa6632 | |||
| cb46f72109 | |||
| 2eb5efed64 | |||
| 7657e37f67 | |||
| ec763e684f | |||
| d3847080e8 | |||
| 97305221ac | |||
| b65aa8671a | |||
| d35bc15562 | |||
| a9cabe1162 | |||
| bb6f4eb60d | |||
| cea1700eaa | |||
| bb2f4ac37e | |||
| 996123ea52 | |||
| 252b7b5e68 | |||
| fefba87922 | |||
| 434911c1cf | |||
| 1d947ba21a | |||
| 55a6591f10 | |||
| 266fe5c6cb | |||
| 3d4395ebe6 | |||
| 463f0050ed | |||
| b0b9c8d43a | |||
| 94d0080208 | |||
| 250156f1a3 | |||
| 724318c43e | |||
| 37dbaae8aa | |||
| 1bd0404503 | |||
| eba9c24510 | |||
| 6bf1901f9b | |||
| bd9697e6be | |||
| 200b9da2a2 | |||
| 32fecd7b15 | |||
| f8b99c6e66 | |||
| 0fda77a39c | |||
| 57c7edfe26 | |||
| 437d0a264c | |||
| 3a28c91635 | |||
| 1f4c188dbf | |||
| 7f9b1a9f90 | |||
| 5096ec8a50 | |||
| 35d812a271 | |||
| 1e79c1961b | |||
| bfd758d353 | |||
| b1ef0bf58d | |||
| d25b2ea10a | |||
| c533e48310 | |||
| a1e9b6bea8 | |||
| 0dfd7324bd | |||
| 3c4b07ce46 | |||
| d6e6c9d038 | |||
| 027ac3d66c | |||
| 8c1674b415 | |||
| eaa35d7b47 | |||
| 0dc26584cc | |||
| cb2e50abcf | |||
| f0aedc8d23 | |||
| 086e1d8bad | |||
| 83c2ca5a90 | |||
| 68c8dd4e18 | |||
| b25defb8b9 | |||
| b190079aa8 | |||
| a831a6ee34 | |||
| bfbae5d040 | |||
| bb2f19deaa | |||
| cfa4924b8e | |||
| 4f8291384d | |||
| f81e335b27 | |||
| 756f178ec3 | |||
| c68c6896d7 | |||
| 712fd8ac14 | |||
| cb6a1267b6 | |||
| 1c77bd6f5d | |||
| 1d0bcbe234 | |||
| 445a22222a | |||
| ba80329eda | |||
| a8d7a72cec | |||
| 6ed470884b | |||
| 0d4dadd63f | |||
| 0cab0e8ac5 | |||
| 9ddf464c03 | |||
| 06abc95b2e | |||
| b4daabe491 | |||
| e5a23f5afd | |||
| 7ee4338b1f | |||
| 3f6e7ca4c1 | |||
| 1345cfdbee | |||
| f2780c7c68 | |||
| 1254bd5bab | |||
| cef63725aa | |||
| 5af4147eb3 | |||
| ca9d4ed026 | |||
| 15359f57a8 | |||
| 4b23e9ccce | |||
| 35706eb9ec | |||
| 272771af38 | |||
| 2ed3db330a | |||
| 38d197b3bf | |||
| 6c588cce65 | |||
| 5752e52b0f | |||
| 2077f34a78 | |||
| 7718e8e1e9 | |||
| 06e2b8ae0d | |||
| 42361bc046 | |||
| 507ea87e9f | |||
| 60f71bbc2b | |||
| ea5ae9627d | |||
| 6fdeab2747 | |||
| 04ec24e737 | |||
| d9629c4d67 | |||
| 2f7e77a41e | |||
| 46e96e20a9 | |||
| 1a5f4127e0 | |||
| 51588c2ef1 | |||
| 271ee6d36e | |||
| 72995835ea | |||
| 1922f15735 | |||
| 633ff8c98c | |||
| 10b2a27342 | |||
| 6ed329bac3 | |||
| cebbb40c99 | |||
| adec491b3b | |||
| cbeae975f0 | |||
| a2b1c3710f | |||
| bbb1fecb02 | |||
| b7c89a72fa | |||
| 0a7fe730f0 | |||
| cadb269b58 | |||
| 168962f847 | |||
| e70e56df96 | |||
| 3242be2e29 | |||
| 7fc6463900 | |||
| 9560ba2144 | |||
| 257e26f2c0 | |||
| 27a650cf1c | |||
| 5dca913b9e | |||
| 6492f3d29e | |||
| 21c5cc7994 | |||
| 056bb213f8 | |||
| 5d3c65f7da | |||
| 5061554ada | |||
| 14e97e3f22 | |||
| 0f816ddfb7 | |||
| 92dd17ab62 | |||
| a8c887a5b0 | |||
| 009fa747e3 | |||
| 2744449773 | |||
| 537426d77a | |||
| e3994d1add | |||
| 720cd8b68c | |||
| 3846bd16f0 | |||
| 7863d4b854 | |||
| 7e8da54ad1 | |||
| ae06f90ca6 | |||
| 8a7b235b6e | |||
| daa0361c02 | |||
| 9136877bc7 | |||
| 2df3890917 | |||
| f2b930079b | |||
| f0a83c9e59 | |||
| b12ea975e0 | |||
| ab74dd785c | |||
| 39affd30f9 | |||
| ff6c96022e | |||
| fc3b8f37b0 | |||
| 26615dbd55 | |||
| 6411a76f03 | |||
| ce1734b176 | |||
| 2b3e3a5c46 | |||
| 0eddda2ee1 | |||
| 05ffc2dba0 | |||
| ec1aee1f85 | |||
| 9524fc0f81 | |||
| 3d56701e20 | |||
| d8d86ce861 | |||
| 503dfb7313 | |||
| 16717df1a9 | |||
| 6713cda109 | |||
| 0652339356 | |||
| 59a81dc07a | |||
| fcecbe780b | |||
| be00f2c26f | |||
| 10ba9e3105 | |||
| 1498860530 | |||
| 902bc75bf3 | |||
| bd6f891b07 | |||
| 36e259b347 | |||
| f1aaf99317 | |||
| 459a0decbd | |||
| 71598294a1 | |||
| e28eedaf1c | |||
| 3cd8b2e841 | |||
| 9925c4ac14 | |||
| 4170c943e7 | |||
| 70fe211a8b | |||
| 23a17859b1 | |||
| c1fa259ad7 | |||
| 4a9a630765 | |||
| a1cbfce316 | |||
| 5edfa675ef | |||
| 3158adad03 | |||
| ebf39b35b8 | |||
| 756b956a77 | |||
| b46ff76d58 | |||
| 6f039fa27c | |||
| acf3559205 | |||
| c05fe50b47 | |||
| a51ba0a2b4 | |||
| 9da9250865 | |||
| 881edddb6e | |||
| 0bd5086a0f | |||
| e563e07d40 | |||
| f34ad5fc42 | |||
| 6b6dcd8c52 | |||
| d5fa99696d | |||
| 2d75f002f9 | |||
| c31894be72 | |||
| 4d254952e3 | |||
| 24c57b5356 | |||
| 186e547ce1 | |||
| 555f653440 | |||
| 8ef5f54e61 | |||
| 6576528aad | |||
| 052e6f7865 | |||
| 560cb49d66 | |||
| c93ca48d17 | |||
| c2d682a5ed | |||
| 1e7c9cc285 | |||
| f0a9a6a7c0 | |||
| 47433ebe67 | |||
| e2034ed286 | |||
| cd5b9a0d56 | |||
| 8dd3fdafa9 | |||
| 6d6a9619a3 | |||
| a097727e32 | |||
| 14f3d7c819 | |||
| 7492f6d34e | |||
| 7a69e85cd6 | |||
| 27a39a4ec6 | |||
| 5e520136be | |||
| c6c30135c3 | |||
| 6f06569a11 | |||
| f3f6c7b73c | |||
| 958a9307fc | |||
| c49dc36a23 | |||
| 6cf73786e3 | |||
| 71f03ffbc6 | |||
| b57a7f207e | |||
| b283cabbcd | |||
| 38e64e06d1 | |||
| b35082e45e | |||
| 02475ebcce | |||
| d584544a6a | |||
| 428d1397a7 | |||
| aabda8d4f1 | |||
| 9cee5cc4d0 | |||
| 9dd2a9e5f5 | |||
| 2832c6b858 | |||
| bc0aeb367a | |||
| 1e33ea0059 | |||
| 0ca49bf1d3 | |||
| c1c2d24bdc | |||
| bbba9579dc | |||
| f92c66db56 | |||
| f08874d9b8 | |||
| da0e3912c6 | |||
| e15cd81383 | |||
| d1a17a2508 | |||
| da3c44137a | |||
| 192bd754df | |||
| ec46299ec0 | |||
| f1bc324eb3 | |||
| f5189f8846 | |||
| 29d24c7a9d | |||
| c360f8fe04 | |||
| 539b7a3a22 | |||
| 8a67eee960 | |||
| de0c275832 | |||
| 97062ef4b8 | |||
| ac5491bf6d | |||
| d49075b4e2 | |||
| e9f3543040 | |||
| 022134a652 | |||
| e551cb0bfe | |||
| 2731decf60 | |||
| 5dad681ab6 | |||
| 96f7c1e37b | |||
| bab787efb1 | |||
| 2601be0777 | |||
| e487b76d1e | |||
| 5788a75acc | |||
| 64f87fe187 | |||
| 84a680d6c3 | |||
| 56d73e0d74 | |||
| b8220a0a46 | |||
| 31bec6b65e | |||
| a73028796c | |||
| 3b59616cbe | |||
| 818162b630 | |||
| 991a1b1ab7 | |||
| b94b0926a7 | |||
| 8fb21e8f50 | |||
| 9719f28795 | |||
| 6814bc84aa | |||
| 783c365c10 | |||
| 455954e0e3 |
+13
-5
@@ -1,8 +1,10 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker # zizmor: ignore[dependabot-cooldown] Docker does not support cooldowns at the moment.
|
||||||
cooldown:
|
# Docker does not support cooldowns at the moment.
|
||||||
default-days: 7
|
# https://github.com/dependabot/dependabot-core/issues/14044
|
||||||
|
# cooldown:
|
||||||
|
# default-days: 7
|
||||||
directories:
|
directories:
|
||||||
- "/"
|
- "/"
|
||||||
- "/docker"
|
- "/docker"
|
||||||
@@ -47,7 +49,11 @@ updates:
|
|||||||
patterns:
|
patterns:
|
||||||
- "scipy"
|
- "scipy"
|
||||||
- "scipy-stubs"
|
- "scipy-stubs"
|
||||||
|
gymnasium:
|
||||||
|
patterns:
|
||||||
|
- "gymnasium"
|
||||||
|
- "stable-baselines3"
|
||||||
|
- "sb3-contrib"
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
cooldown:
|
cooldown:
|
||||||
@@ -61,5 +67,7 @@ updates:
|
|||||||
groups:
|
groups:
|
||||||
actions:
|
actions:
|
||||||
patterns:
|
patterns:
|
||||||
# Combine updates for github provided actions
|
|
||||||
- "actions/*"
|
- "actions/*"
|
||||||
|
docker:
|
||||||
|
patterns:
|
||||||
|
- "docker/*"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Binance Leverage tiers update
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "25 3 * * 4"
|
- cron: "25 2 * * 4"
|
||||||
# on demand
|
# on demand
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -24,19 +24,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- name: Install uv and Python 🐍
|
||||||
with:
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
python-version: "3.14"
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
|
|
||||||
- name: Install ccxt
|
- name: Install ccxt
|
||||||
run: uv pip install ccxt orjson
|
run: uv pip install $(grep -E "^ccxt==" requirements.txt) $(grep -E "^orjson==" requirements.txt)
|
||||||
|
|
||||||
- name: Run leverage tier update
|
- name: Run leverage tier update
|
||||||
env:
|
env:
|
||||||
@@ -46,7 +42,7 @@ jobs:
|
|||||||
run: python build_helpers/binance_update_lev_tiers.py
|
run: python build_helpers/binance_update_lev_tiers.py
|
||||||
|
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
||||||
@@ -55,7 +51,7 @@ jobs:
|
|||||||
Dependencies
|
Dependencies
|
||||||
branch: update/binance-leverage-tiers
|
branch: update/binance-leverage-tiers
|
||||||
title: Update Binance Leverage Tiers
|
title: Update Binance Leverage Tiers
|
||||||
commit-message: "chore: update pre-commit hooks"
|
commit-message: "chore: update binance leverage tiers"
|
||||||
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
committer: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||||
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||||
body: Update binance leverage tiers.
|
body: Update binance leverage tiers.
|
||||||
|
|||||||
+59
-71
@@ -32,13 +32,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Install uv and Python 🐍
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -55,7 +50,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Installation (python)
|
- name: Installation (python)
|
||||||
run: |
|
run: |
|
||||||
uv pip install --upgrade wheel
|
|
||||||
uv pip install -r requirements-dev.txt
|
uv pip install -r requirements-dev.txt
|
||||||
uv pip install -e ft_client/
|
uv pip install -e ft_client/
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
@@ -74,11 +68,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
||||||
|
|
||||||
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
|
|
||||||
- name: Cleanup codecov dirty state files
|
- name: Cleanup codecov dirty state files
|
||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04')
|
||||||
@@ -137,10 +131,6 @@ jobs:
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Sort imports (isort)
|
|
||||||
run: |
|
|
||||||
isort --check .
|
|
||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --output-format=github
|
ruff check --output-format=github
|
||||||
@@ -160,19 +150,22 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$PSVersionTable
|
$PSVersionTable
|
||||||
Get-PSRepository | Format-List *
|
Get-PSRepository | Format-List *
|
||||||
Set-PSRepository psgallery -InstallationPolicy trusted
|
if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) {
|
||||||
|
Register-PSRepository -Default
|
||||||
|
}
|
||||||
|
Set-PSRepository PSGallery -InstallationPolicy Trusted
|
||||||
Install-Module -Name Pester -RequiredVersion 5.7.1 -Confirm:$false -Force -SkipPublisherCheck
|
Install-Module -Name Pester -RequiredVersion 5.7.1 -Confirm:$false -Force -SkipPublisherCheck
|
||||||
$Error.clear()
|
$Error.clear()
|
||||||
Invoke-Pester -Path "tests" -CI
|
Invoke-Pester -Path "tests" -CI
|
||||||
if ($Error.Length -gt 0) {exit 1}
|
if ($Error.Length -gt 0) {exit 1}
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
|
||||||
if: ${{ failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
|
if: ${{ failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
|
||||||
with:
|
with:
|
||||||
severity: error
|
color: '#FF0000' # red
|
||||||
details: Freqtrade CI failed on ${{ matrix.os }} with Python ${{ matrix.python-version }}!
|
title: Freqtrade CI failed on ${{ matrix.os }} with Python ${{ matrix.python-version }}!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
|
|
||||||
mypy-version-check:
|
mypy-version-check:
|
||||||
name: "Mypy Version Check"
|
name: "Mypy Version Check"
|
||||||
@@ -182,14 +175,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Install uv and Python 🐍
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
activate-environment: true
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: pre-commit dependencies
|
- name: pre-commit dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install pyaml
|
uv pip install $(grep -E "^pyyaml==" requirements-dev.txt)
|
||||||
python build_helpers/pre_commit_update.py
|
python build_helpers/pre_commit_update.py
|
||||||
|
|
||||||
pre-commit:
|
pre-commit:
|
||||||
@@ -200,9 +194,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- name: Set up Python 🐍
|
||||||
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
|
|
||||||
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
||||||
|
|
||||||
docs-check:
|
docs-check:
|
||||||
@@ -217,51 +213,49 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Install uv and Python 🐍
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
activate-environment: true
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Documentation build
|
- name: Documentation build
|
||||||
run: |
|
run: |
|
||||||
pip install -r docs/requirements-docs.txt
|
uv pip install -r docs/requirements-docs.txt
|
||||||
mkdocs build
|
mkdocs build
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
severity: error
|
color: '#FF0000' # red
|
||||||
details: Freqtrade doc test failed!
|
title: Freqtrade doc test failed!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
|
|
||||||
|
|
||||||
build-linux-online:
|
build-linux-online:
|
||||||
# Run pytest with "live" checks
|
# Run pytest with "live" checks
|
||||||
name: "Tests and Linting - Online tests"
|
name: "Online / live tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.12"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Install uv and Python 🐍
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.12"
|
python-version: "${{ matrix.python-version }}"
|
||||||
cache-dependency-glob: "requirements**.txt"
|
cache-dependency-glob: "requirements**.txt"
|
||||||
cache-suffix: "3.12"
|
cache-suffix: "3.12"
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
run: |
|
run: |
|
||||||
uv pip install --upgrade wheel
|
|
||||||
uv pip install -r requirements-dev.txt
|
uv pip install -r requirements-dev.txt
|
||||||
uv pip install -e ft_client/
|
uv pip install -e ft_client/
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
@@ -285,22 +279,13 @@ jobs:
|
|||||||
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
|
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Check user permission
|
|
||||||
id: check
|
|
||||||
continue-on-error: true
|
|
||||||
uses: prince-chrismc/check-actor-permissions-action@d504e74ba31658f4cdf4fcfeb509d4c09736d88e # v3.0.2
|
|
||||||
with:
|
|
||||||
permission: "write"
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
|
||||||
if: steps.check.outputs.permitted == 'true' && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
|
||||||
with:
|
with:
|
||||||
severity: info
|
color: '#00FF00' # green
|
||||||
details: Test Completed!
|
title: Test Completed!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhook: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
|
|
||||||
build:
|
build:
|
||||||
if: always()
|
if: always()
|
||||||
@@ -312,6 +297,9 @@ jobs:
|
|||||||
pre-commit,
|
pre-commit,
|
||||||
]
|
]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
@@ -324,18 +312,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Install uv and Python 🐍
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
activate-environment: true
|
||||||
|
python-version: "${{ matrix.python-version }}"
|
||||||
|
|
||||||
- name: Build distribution
|
- name: Build distribution
|
||||||
run: |
|
run: |
|
||||||
pip install -U build
|
uv pip install $(grep -E "^build==" requirements-dev.txt)
|
||||||
python -m build --sdist --wheel
|
python -m build --sdist --wheel
|
||||||
|
|
||||||
- name: Upload artifacts 📦
|
- name: Upload artifacts 📦
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: freqtrade-build
|
name: freqtrade-build
|
||||||
path: |
|
path: |
|
||||||
@@ -344,11 +333,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Client distribution
|
- name: Build Client distribution
|
||||||
run: |
|
run: |
|
||||||
pip install -U build
|
|
||||||
python -m build --sdist --wheel ft_client
|
python -m build --sdist --wheel ft_client
|
||||||
|
|
||||||
- name: Upload artifacts 📦
|
- name: Upload artifacts 📦
|
||||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: freqtrade-client-build
|
name: freqtrade-client-build
|
||||||
path: |
|
path: |
|
||||||
@@ -372,14 +360,14 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: freqtrade*-build
|
pattern: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||||
with:
|
with:
|
||||||
repository-url: https://test.pypi.org/legacy/
|
repository-url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
@@ -401,14 +389,14 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
pattern: freqtrade*-build
|
pattern: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||||
|
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
@@ -422,9 +410,9 @@ jobs:
|
|||||||
packages: write # Needed to push package versions
|
packages: write # Needed to push package versions
|
||||||
contents: read
|
contents: read
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} # zizmor: ignore[secrets-outside-env] Intentionally not using environment variable.
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
packages-cleanup:
|
packages-cleanup:
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Install uv and Python 🐍
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
activate-environment: true
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
uv pip install -r docs/requirements-docs.txt
|
||||||
pip install -r docs/requirements-docs.txt
|
|
||||||
|
|
||||||
- name: Fetch gh-pages branch
|
- name: Fetch gh-pages branch
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Pre-build dev container image
|
- name: Pre-build dev container image
|
||||||
uses: devcontainers/ci@8bf61b26e9c3a98f69cb6ce2f88d24ff59b785c6 # v0.3.19
|
uses: devcontainers/ci@b63b30de439b47a52267f241112c5b453b673db5 # v0.3.1900000449
|
||||||
with:
|
with:
|
||||||
subFolder: .github
|
subFolder: .github
|
||||||
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ name: Docker Build and Deploy
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_PASSWORD:
|
DOCKERHUB_USERNAME:
|
||||||
required: true
|
required: true
|
||||||
DOCKER_USERNAME:
|
DOCKERHUB_TOKEN:
|
||||||
required: true
|
required: true
|
||||||
DISCORD_WEBHOOK:
|
DISCORD_WEBHOOK:
|
||||||
required: false
|
required: false
|
||||||
@@ -35,6 +35,8 @@ jobs:
|
|||||||
name: "Deploy Docker x64 and armv7l"
|
name: "Deploy Docker x64 and armv7l"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
environment:
|
||||||
|
name: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -57,19 +59,19 @@ jobs:
|
|||||||
uses: ./.github/actions/docker-tags
|
uses: ./.github/actions/docker-tags
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
with:
|
with:
|
||||||
cache-image: false
|
cache-image: false
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd #v4.0.0
|
||||||
|
|
||||||
- name: Available platforms
|
- name: Available platforms
|
||||||
run: echo ${PLATFORMS}
|
run: echo ${PLATFORMS}
|
||||||
@@ -168,6 +170,8 @@ jobs:
|
|||||||
# Only run on 64bit machines
|
# Only run on 64bit machines
|
||||||
runs-on: [self-hosted, linux, ARM64]
|
runs-on: [self-hosted, linux, ARM64]
|
||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
environment:
|
||||||
|
name: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -179,13 +183,13 @@ jobs:
|
|||||||
uses: ./.github/actions/docker-tags
|
uses: ./.github/actions/docker-tags
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to github
|
- name: Login to github
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -306,9 +310,8 @@ jobs:
|
|||||||
docker image prune -a --force --filter "until=24h"
|
docker image prune -a --force --filter "until=24h"
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@c2597273488aeda841dd1e891321952b51f7996f #v2.2.1
|
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1.16.0
|
||||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && (github.event_name != 'schedule')
|
||||||
with:
|
with:
|
||||||
severity: info
|
title: Deploy Succeeded!
|
||||||
details: Deploy Succeeded!
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- stable
|
- stable
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}
|
group: ${{ github.workflow }}
|
||||||
@@ -15,6 +16,8 @@ jobs:
|
|||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
name: "Update Docker Hub Description"
|
name: "Update Docker Hub Description"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: docker
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
@@ -23,6 +26,6 @@ jobs:
|
|||||||
- name: Docker Hub Description
|
- name: Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
repository: freqtradeorg/freqtrade
|
repository: freqtradeorg/freqtrade
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Pre-commit Types update
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "develop"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
|
||||||
|
cancel-in-progress: true
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mypy-version-update:
|
||||||
|
name: "Pre-commit mypy type versions update"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
# Only run this job for pull requests created by dependabot[bot]
|
||||||
|
if: >
|
||||||
|
github.event.pull_request.user.login == 'dependabot[bot]' &&
|
||||||
|
github.repository == github.event.pull_request.head.repo.full_name &&
|
||||||
|
github.event_name == 'pull_request' &&
|
||||||
|
startsWith(github.head_ref, 'dependabot/')
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: dependabot-pulls
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: true
|
||||||
|
token: ${{ secrets.REPO_SCOPED_TOKEN_DEP }}
|
||||||
|
ref: ${{ github.head_ref || github.ref }}
|
||||||
|
|
||||||
|
- name: Install uv and Python 🐍
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
activate-environment: true
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install PyYAML
|
||||||
|
run: |
|
||||||
|
|
||||||
|
- name: pre-commit dependencies
|
||||||
|
run: |
|
||||||
|
uv pip install $(grep -E "^pyyaml==" requirements-dev.txt)
|
||||||
|
python build_helpers/pre_commit_update.py --update
|
||||||
|
|
||||||
|
- uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7
|
||||||
|
with:
|
||||||
|
commit_message: "chore(deps): Apply pre-commit types update"
|
||||||
|
commit_user_name: Freqtrade Bot
|
||||||
|
commit_user_email: 154552126+freqtrade-bot@users.noreply.github.com
|
||||||
|
commit_author: Freqtrade Bot <154552126+freqtrade-bot@users.noreply.github.com>
|
||||||
@@ -2,7 +2,7 @@ name: Pre-commit auto-update
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 3 * * 2"
|
- cron: "13 1 * * 2"
|
||||||
# on demand
|
# on demand
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -17,23 +17,27 @@ jobs:
|
|||||||
auto-update:
|
auto-update:
|
||||||
name: Auto-update pre-commit hooks
|
name: Auto-update pre-commit hooks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: develop
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- name: Install uv and Python 🐍
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
activate-environment: true
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Install pre-commit
|
- name: Install pre-commit
|
||||||
run: pip install pre-commit
|
run: uv pip install $(grep -E "^pre-commit==" requirements-dev.txt)
|
||||||
|
|
||||||
- name: Run auto-update
|
- name: Run auto-update
|
||||||
run: pre-commit autoupdate
|
run: pre-commit autoupdate
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
add-paths: .pre-commit-config.yaml
|
add-paths: .pre-commit-config.yaml
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
|
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||||
|
|||||||
+10
-23
@@ -13,38 +13,25 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
additional_dependencies: ["python-rapidjson", "jsonschema"]
|
additional_dependencies: ["python-rapidjson", "jsonschema"]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/flake8
|
|
||||||
rev: "7.3.0"
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
additional_dependencies: [Flake8-pyproject]
|
|
||||||
# stages: [push]
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.19.1"
|
rev: "v2.1.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==6.2.0.20251022
|
- types-cachetools==7.0.0.20260518
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.32.4.20260107
|
- types-requests==2.33.0.20260518
|
||||||
- types-tabulate==0.9.0.20241207
|
- types-tabulate==0.10.0.20260508
|
||||||
- types-python-dateutil==2.9.0.20260124
|
- types-python-dateutil==2.9.0.20260518
|
||||||
- scipy-stubs==1.17.0.2
|
- scipy-stubs==1.17.1.4
|
||||||
- SQLAlchemy==2.0.46
|
- SQLAlchemy==2.0.49
|
||||||
# stages: [push]
|
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
|
||||||
rev: "8.0.0"
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
name: isort (python)
|
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.15.2'
|
rev: 'v0.15.13'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
@@ -75,7 +62,7 @@ repos:
|
|||||||
- id: strip-exif
|
- id: strip-exif
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.4.1
|
rev: v2.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
@@ -83,6 +70,6 @@ repos:
|
|||||||
|
|
||||||
# Ensure github actions remain safe
|
# Ensure github actions remain safe
|
||||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||||
rev: v1.22.0
|
rev: v1.24.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.13.12-slim-trixie AS base
|
FROM python:3.14.5-slim-trixie AS base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
[](https://doi.org/10.21105/joss.04864)
|
[](https://doi.org/10.21105/joss.04864)
|
||||||
[](https://codecov.io/gh/freqtrade/freqtrade)
|
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||||
[](https://www.freqtrade.io)
|
[](https://www.freqtrade.io)
|
||||||
[](https://discord.gg/p7nuUNVfP7)
|
[](https://discord.gg/p7nuUNVfP7)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ Please read the [exchange-specific notes](https://www.freqtrade.io/en/stable/exc
|
|||||||
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
||||||
- [X] [OKX](https://okx.com/)
|
- [X] [OKX](https://okx.com/)
|
||||||
- [X] [Bybit](https://bybit.com/)
|
- [X] [Bybit](https://bybit.com/)
|
||||||
|
- [X] [Kraken](https://www.kraken.com/features/futures)
|
||||||
|
|
||||||
Please make sure to read the [exchange specific notes](https://www.freqtrade.io/en/stable/exchanges/), as well as the [trading with leverage](https://www.freqtrade.io/en/stable/leverage/) documentation before diving in.
|
Please make sure to read the [exchange specific notes](https://www.freqtrade.io/en/stable/exchanges/), as well as the [trading with leverage](https://www.freqtrade.io/en/stable/leverage/) documentation before diving in.
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ def extract_command_partials():
|
|||||||
help_output = _get_help_output(subparser)
|
help_output = _get_help_output(subparser)
|
||||||
_write_partial_file(f"docs/commands/{command}.md", help_output)
|
_write_partial_file(f"docs/commands/{command}.md", help_output)
|
||||||
else:
|
else:
|
||||||
print(f" Warning: subcommand '{command}' not found in parser")
|
print(f" Warning: subcommand '{command}' not found in parser")
|
||||||
|
|
||||||
# freqtrade-client still uses subprocess as requested
|
# freqtrade-client still uses subprocess as requested
|
||||||
print("Running for freqtrade-client")
|
print("Running for freqtrade-client")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# File used in CI to ensure pre-commit dependencies are kept up-to-date.
|
# File used in CI to ensure pre-commit dependencies are kept up-to-date.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -10,6 +12,24 @@ pre_commit_file = Path(".pre-commit-config.yaml")
|
|||||||
require_dev = Path("requirements-dev.txt")
|
require_dev = Path("requirements-dev.txt")
|
||||||
require = Path("requirements.txt")
|
require = Path("requirements.txt")
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--update", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def replace_dependency_version(pre_commit_text: str, dependency: str) -> tuple[str, bool]:
|
||||||
|
"""
|
||||||
|
Regex-based replacement of a dependency version in the pre-commit config file.
|
||||||
|
using regex here ensures we only replace the version of the dependency while
|
||||||
|
keeping the overall file intact.
|
||||||
|
"""
|
||||||
|
package_name = dependency.split("==", 1)[0]
|
||||||
|
pattern = re.compile(rf"^(\s*-\s+){re.escape(package_name)}==.*$", re.MULTILINE)
|
||||||
|
updated_text, replacements = pattern.subn(rf"\1{dependency}", pre_commit_text, count=1)
|
||||||
|
return updated_text, replacements > 0 and updated_text != pre_commit_text
|
||||||
|
|
||||||
|
|
||||||
with require_dev.open("r") as rfile:
|
with require_dev.open("r") as rfile:
|
||||||
requirements = rfile.readlines()
|
requirements = rfile.readlines()
|
||||||
|
|
||||||
@@ -23,6 +43,18 @@ supported = ("types-", "SQLAlchemy", "scipy-stubs")
|
|||||||
# Only keep the first part of the line up to the first space
|
# 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)]
|
type_reqs = [r.strip("\n").split()[0] for r in requirements if r.startswith(supported)]
|
||||||
|
|
||||||
|
with pre_commit_file.open("r") as file:
|
||||||
|
pre_commit_text = file.read()
|
||||||
|
|
||||||
|
updated = False
|
||||||
|
for req in type_reqs:
|
||||||
|
pre_commit_text, req_updated = replace_dependency_version(pre_commit_text, req)
|
||||||
|
updated = updated or req_updated
|
||||||
|
|
||||||
|
if args.update and updated:
|
||||||
|
with pre_commit_file.open("w") as file:
|
||||||
|
file.write(pre_commit_text)
|
||||||
|
|
||||||
with pre_commit_file.open("r") as file:
|
with pre_commit_file.open("r") as file:
|
||||||
f = yaml.load(file, Loader=yaml.SafeLoader)
|
f = yaml.load(file, Loader=yaml.SafeLoader)
|
||||||
|
|
||||||
@@ -40,12 +72,20 @@ for hook in hooks:
|
|||||||
|
|
||||||
for req in type_reqs:
|
for req in type_reqs:
|
||||||
if req not in hooks:
|
if req not in hooks:
|
||||||
errors.append(f"{req} is missing in pre-config file.")
|
errors.append(f"{req} is missing in pre-commit config file.")
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
if args.update:
|
||||||
|
errors.append(".pre-commit-config.yaml was updated to match the requirements files.")
|
||||||
|
else:
|
||||||
|
errors.append(
|
||||||
|
".pre-commit-config.yaml is outdated. Run build_helpers/pre_commit_update.py --update."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
for e in errors:
|
for e in errors:
|
||||||
print(e)
|
print(e)
|
||||||
sys.exit(1)
|
sys.exit(1 if not (args.update and updated) else 0)
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -283,6 +283,10 @@
|
|||||||
"month"
|
"month"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"skip_wallet_history_migration": {
|
||||||
|
"description": "Disable wallet history migration.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"hyperopt_path": {
|
"hyperopt_path": {
|
||||||
"description": "Specify additional lookup path for Hyperopt Loss functions.",
|
"description": "Specify additional lookup path for Hyperopt Loss functions.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -649,6 +653,7 @@
|
|||||||
"ProducerPairList",
|
"ProducerPairList",
|
||||||
"RemotePairList",
|
"RemotePairList",
|
||||||
"MarketCapPairList",
|
"MarketCapPairList",
|
||||||
|
"CrossMarketPairList",
|
||||||
"AgeFilter",
|
"AgeFilter",
|
||||||
"DelistFilter",
|
"DelistFilter",
|
||||||
"FullTradesFilter",
|
"FullTradesFilter",
|
||||||
@@ -1058,7 +1063,8 @@
|
|||||||
"jwt_secret_key": {
|
"jwt_secret_key": {
|
||||||
"description": "Secret key for JWT authentication.",
|
"description": "Secret key for JWT authentication.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "somethingRandomSomethingRandom123"
|
"default": "somethingRandomSomethingRandom123",
|
||||||
|
"minLength": 32
|
||||||
},
|
},
|
||||||
"CORS_origins": {
|
"CORS_origins": {
|
||||||
"description": "List of allowed CORS origins.",
|
"description": "List of allowed CORS origins.",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11.14-slim-bookworm AS base
|
FROM python:3.11.15-slim-bookworm AS base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
|
|||||||
+203
-162
@@ -160,117 +160,131 @@ The most important in the backtesting is to understand the result.
|
|||||||
A backtesting result will look like that:
|
A backtesting result will look like that:
|
||||||
|
|
||||||
```
|
```
|
||||||
BACKTESTING REPORT
|
BACKTESTING REPORT
|
||||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit ┃ 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 │
|
│ LTC/USDT:USDT │ 16 │ 1.01 │ 56.882 │ 5.69 │ 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 │
|
│ ETC/USDT:USDT │ 12 │ 0.73 │ 31.513 │ 3.15 │ 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 │
|
│ ETH/USDT:USDT │ 8 │ 0.69 │ 18.659 │ 1.87 │ 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 │
|
│ XLM/USDT:USDT │ 10 │ 0.3 │ 10.694 │ 1.07 │ 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 │
|
│ BTC/USDT:USDT │ 8 │ 0.22 │ 7.502 │ 0.75 │ 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 │
|
│ XRP/USDT:USDT │ 9 │ -0.13 │ -6.837 │ -0.68 │ 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 │
|
│ DOT/USDT:USDT │ 6 │ -0.39 │ -9.169 │ -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 │
|
│ ADA/USDT:USDT │ 8 │ -1.75 │ -52.089 │ -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 │
|
│ TOTAL │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
└───────────────┴────────┴──────────────┴─────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||||
LEFT OPEN TRADES REPORT
|
LEFT OPEN TRADES REPORT
|
||||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit ┃ 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 │
|
│ 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 │
|
│ 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 │
|
│ DOT/USDT:USDT │ 1 │ -5.29 │ -19.166 │ -1.92 │ 11:30:00 │ 0 0 1 0 │
|
||||||
│ TOTAL │ 3 │ -4.56 │ -44.420 │ -4.44 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
│ TOTAL │ 3 │ -4.56 │ -44.461 │ -4.45 │ 6 days, 2:03:00 │ 0 0 3 0 │
|
||||||
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
|
└───────────────┴────────┴──────────────┴─────────────┴──────────────┴──────────────────┴────────────────────────┘
|
||||||
ENTER TAG STATS
|
ENTER TAG STATS
|
||||||
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
┃ Enter Tag ┃ Entries ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
┃ Enter Tag ┃ Entries ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||||
┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
│ OTHER │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
│ OTHER │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
│ TOTAL │ 77 │ 0.22 │ 54.774 │ 5.48 │ 22:12:00 │ 67 0 10 87.0 │
|
│ TOTAL │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
└───────────┴─────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┘
|
└───────────┴─────────┴──────────────┴─────────────┴──────────────┴──────────────┴────────────────────────┘
|
||||||
EXIT REASON STATS
|
EXIT REASON STATS
|
||||||
┏━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
┏━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
┃ Exit Reason ┃ Exits ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
┃ Exit Reason ┃ Exits ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||||
┡━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
┡━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
│ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
|
│ roi │ 67 │ 1.06 │ 245.117 │ 24.51 │ 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 │
|
│ exit_signal │ 4 │ -2.23 │ -31.226 │ -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 │
|
│ force_exit │ 3 │ -4.56 │ -44.461 │ -4.45 │ 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 │
|
│ stop_loss │ 3 │ -10.14 │ -112.273 │ -11.23 │ 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 │
|
│ TOTAL │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
└─────────────┴───────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
└─────────────┴───────┴──────────────┴─────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||||
MIXED TAG STATS
|
MIXED TAG STATS
|
||||||
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
┃ Enter Tag ┃ Exit Reason ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
┃ Enter Tag ┃ Exit Reason ┃ Trades ┃ Avg Profit % ┃ Tot Profit ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
|
||||||
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
│ │ roi │ 67 │ 1.05 │ 242.179 │ 24.22 │ 15:49:00 │ 67 0 0 100 │
|
│ │ roi │ 67 │ 1.06 │ 245.117 │ 24.51 │ 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 │
|
│ │ exit_signal │ 4 │ -2.23 │ -31.226 │ -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 │
|
│ │ force_exit │ 3 │ -4.56 │ -44.461 │ -4.45 │ 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 │
|
│ │ stop_loss │ 3 │ -10.14 │ -112.273 │ -11.23 │ 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 │
|
│ TOTAL │ │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │
|
||||||
└───────────┴─────────────┴────────┴──────────────┴─────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
└───────────┴─────────────┴────────┴──────────────┴─────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||||
SUMMARY METRICS
|
SUMMARY METRICS
|
||||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
┃ Metric ┃ Value ┃
|
┃ Metric ┃ Value ┃
|
||||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
│ Backtesting from │ 2025-07-01 00:00:00 │
|
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||||
│ Backtesting to │ 2025-08-01 00:00:00 │
|
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||||
│ Trading Mode │ Isolated Futures │
|
│ Trading Mode │ Isolated Futures │
|
||||||
│ Max open trades │ 3 │
|
│ Max open trades │ 3 │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
||||||
│ Starting balance │ 1000 USDT │
|
│ Starting balance │ 1000 USDT │
|
||||||
│ Final balance │ 1054.774 USDT │
|
│ Final balance │ 1057.157 USDT │
|
||||||
│ Absolute profit │ 54.774 USDT │
|
│ Absolute profit │ 57.157 USDT │
|
||||||
│ Total profit % │ 5.48% │
|
│ Total profit % │ 5.72% │
|
||||||
│ CAGR % │ 87.36% │
|
│ CAGR % │ 92.41% │
|
||||||
│ Sortino │ 2.48 │
|
│ Sharpe (closed trades) │ 3.89 │
|
||||||
│ Sharpe │ 3.75 │
|
│ Sortino (closed trades) │ 2.57 │
|
||||||
│ Calmar │ 40.99 │
|
│ Calmar (closed trades) │ 43.03 │
|
||||||
│ SQN │ 0.69 │
|
│ SQN │ 0.71 │
|
||||||
│ Profit factor │ 1.29 │
|
│ Profit factor │ 1.30 │
|
||||||
│ Expectancy (Ratio) │ 0.71 (0.04) │
|
│ Expectancy (Ratio) │ 0.74 (0.04) │
|
||||||
│ Avg. daily profit │ 1.767 USDT │
|
│ Avg. daily profit │ 1.844 USDT │
|
||||||
│ Avg. stake amount │ 345.016 USDT │
|
│ Avg. stake amount │ 345.478 USDT │
|
||||||
│ Total trade volume │ 53316.954 USDT │
|
│ Market change │ 30.51% │
|
||||||
│ │ │
|
│ Total trade volume │ 53390.788 USDT │
|
||||||
│ Long / Short trades │ 67 / 10 │
|
│ │ │
|
||||||
│ Long / Short profit % │ 8.94% / -3.47% │
|
│ Long / Short trades │ 67 / 10 │
|
||||||
│ Long / Short profit USDT │ 89.425 / -34.651 │
|
│ Long / Short profit % │ 9.19% / -3.48% │
|
||||||
│ │ │
|
│ Long / Short profit USDT │ 91.940 / -34.783 │
|
||||||
│ Best Pair │ LTC/USDT:USDT 5.62% │
|
│ │ │
|
||||||
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
│ Best Pair │ LTC/USDT:USDT 5.69% │
|
||||||
│ Best trade │ ETC/USDT:USDT 2.00% │
|
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
||||||
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
│ Best trade │ XRP/USDT:USDT 2.00% │
|
||||||
│ Best day │ 26.91 USDT │
|
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||||
│ Worst day │ -47.741 USDT │
|
│ Best day │ 27.031 USDT │
|
||||||
│ Days win/draw/lose │ 20 / 6 / 5 │
|
│ Worst day │ -47.826 USDT │
|
||||||
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
│ Days win/draw/lose │ 20 / 6 / 5 │
|
||||||
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
||||||
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
||||||
│ Rejected Entry signals │ 258 │
|
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
||||||
│ Entry/Exit Timeouts │ 0 / 0 │
|
│ Rejected Entry signals │ 258 │
|
||||||
│ │ │
|
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||||
│ Min balance │ 1003.168 USDT │
|
│ │ │
|
||||||
│ Max balance │ 1149.421 USDT │
|
│ Min/Max balance (closed trades) │ 1003.205 USDT / 1151.425 USDT │
|
||||||
│ Max % of account underwater │ 8.23% │
|
│ Max % of account underwater │ 8.19% │
|
||||||
│ Absolute drawdown │ 94.647 USDT (8.23%) │
|
│ Absolute drawdown │ 94.268 USDT (8.19%) │
|
||||||
│ Drawdown duration │ 9 days 08:50:00 │
|
│ Drawdown duration │ 9 days 08:50:00 │
|
||||||
│ Profit at drawdown start │ 149.421 USDT │
|
│ Profit at drawdown start │ 151.425 USDT │
|
||||||
│ Profit at drawdown end │ 54.774 USDT │
|
│ Profit at drawdown end │ 57.157 USDT │
|
||||||
│ Drawdown start │ 2025-07-22 15:10:00 │
|
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||||
│ Drawdown end │ 2025-08-01 00:00:00 │
|
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||||
│ Market change │ 30.51% │
|
│ │ │
|
||||||
└───────────────────────────────┴─────────────────────────────────┘
|
│ Wallet based Metrics │ │
|
||||||
|
│ Min/Max balance (wallet balance) │ 1000 USDT / 1151.425 USDT │
|
||||||
|
│ Min/Max balance dates (wallet balance) │ 2025-07-01 00:05:00 / 2025-07-22 15:15:00 │
|
||||||
|
│ Max % of account underwater (balance) │ 5.01% │
|
||||||
|
│ Absolute drawdown (wallet balance) │ 54.76 USDT (4.76%) │
|
||||||
|
│ Drawdown duration │ 7 days 20:35:00 │
|
||||||
|
│ Profit at drawdown start │ 151.425 USDT │
|
||||||
|
│ Profit at drawdown end │ 96.664 USDT │
|
||||||
|
│ Drawdown start │ 2025-07-22 15:15:00 │
|
||||||
|
│ Drawdown end │ 2025-07-30 11:50:00 │
|
||||||
|
│ Sharpe (daily wallet balance) │ 4.42 │
|
||||||
|
│ Sortino (daily wallet balance) │ 4.35 │
|
||||||
|
│ Calmar (daily wallet balance) │ 136.07 │
|
||||||
|
└────────────────────────────────────────┴───────────────────────────────────────────┘
|
||||||
|
|
||||||
Backtested 2025-07-01 00:00:00 -> 2025-08-01 00:00:00 | Max open trades : 3
|
Backtested 2025-07-01 00:00:00 -> 2025-08-01 00:00:00 | Max open trades : 3
|
||||||
STRATEGY SUMMARY
|
STRATEGY SUMMARY
|
||||||
┏━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
|
┏━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
|
||||||
┃ Strategy ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃ Drawdown ┃
|
┃ Strategy ┃ Trades ┃ Avg Profit % ┃ Tot Profit ┃ 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% │
|
│ SampleStrategy │ 77 │ 0.23 │ 57.157 │ 5.72 │ 22:12:00 │ 67 0 10 87.0 │ 94.268 8.19% │
|
||||||
└────────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┴────────────────────┘
|
└────────────────┴────────┴──────────────┴─────────────┴──────────────┴──────────────┴────────────────────────┴────────────────┘
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backtesting report table
|
### Backtesting report table
|
||||||
@@ -329,54 +343,72 @@ The last element of the backtest report is the summary metrics table.
|
|||||||
It contains key metrics about the performance of your strategy on backtesting data.
|
It contains key metrics about the performance of your strategy on backtesting data.
|
||||||
|
|
||||||
```
|
```
|
||||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
SUMMARY METRICS
|
||||||
┃ Metric ┃ Value ┃
|
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
┃ Metric ┃ Value ┃
|
||||||
│ Backtesting from │ 2025-07-01 00:00:00 │
|
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
│ Backtesting to │ 2025-08-01 00:00:00 │
|
│ Backtesting from │ 2025-07-01 00:00:00 │
|
||||||
│ Trading Mode │ Isolated Futures │
|
│ Backtesting to │ 2025-08-01 00:00:00 │
|
||||||
│ Max open trades │ 3 │
|
│ Trading Mode │ Isolated Futures │
|
||||||
│ │ │
|
│ Max open trades │ 3 │
|
||||||
│ Total/Daily Avg Trades │ 72 / 2.32 │
|
│ │ │
|
||||||
│ Starting balance │ 1000 USDT │
|
│ Total/Daily Avg Trades │ 77 / 2.48 │
|
||||||
│ Final balance │ 1106.734 USDT │
|
│ Starting balance │ 1000 USDT │
|
||||||
│ Absolute profit │ 106.734 USDT │
|
│ Final balance │ 1057.157 USDT │
|
||||||
│ Total profit % │ 10.67% │
|
│ Absolute profit │ 57.157 USDT │
|
||||||
│ CAGR % │ 230.04% │
|
│ Total profit % │ 5.72% │
|
||||||
│ Sortino │ 4.99 │
|
│ CAGR % │ 92.41% │
|
||||||
│ Sharpe │ 8.00 │
|
│ Sharpe (closed trades) │ 3.89 │
|
||||||
│ Calmar │ 77.76 │
|
│ Sortino (closed trades) │ 2.57 │
|
||||||
│ SQN │ 1.52 │
|
│ Calmar (closed trades) │ 43.03 │
|
||||||
│ Profit factor │ 1.79 │
|
│ SQN │ 0.71 │
|
||||||
│ Expectancy (Ratio) │ 1.48 (0.07) │
|
│ Profit factor │ 1.30 │
|
||||||
│ Avg. daily profit │ 3.443 USDT │
|
│ Expectancy (Ratio) │ 0.74 (0.04) │
|
||||||
│ Avg. stake amount │ 363.133 USDT │
|
│ Avg. daily profit │ 1.844 USDT │
|
||||||
│ Total trade volume │ 52466.174 USDT │
|
│ Avg. stake amount │ 345.478 USDT │
|
||||||
│ │ │
|
│ Market change │ 30.51% │
|
||||||
│ Best Pair │ LTC/USDT:USDT 4.48% │
|
│ Total trade volume │ 53390.788 USDT │
|
||||||
│ Worst Pair │ ADA/USDT:USDT -1.78% │
|
│ │ │
|
||||||
│ Best trade │ ETC/USDT:USDT 2.00% │
|
│ Long / Short trades │ 67 / 10 │
|
||||||
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
│ Long / Short profit % │ 9.19% / -3.48% │
|
||||||
│ Best day │ 23.535 USDT │
|
│ Long / Short profit USDT │ 91.940 / -34.783 │
|
||||||
│ Worst day │ -49.813 USDT │
|
│ │ │
|
||||||
│ Days win/draw/lose │ 21 / 6 / 4 │
|
│ Best Pair │ LTC/USDT:USDT 5.69% │
|
||||||
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:30 │
|
│ Worst Pair │ ADA/USDT:USDT -5.21% │
|
||||||
│ Min/Max/Avg. Duration Losers │ 0d 12:00 / 17d 08:00 / 3d 23:28 │
|
│ Best trade │ XRP/USDT:USDT 2.00% │
|
||||||
│ Max Consecutive Wins / Loss │ 58 / 4 │
|
│ Worst trade │ ADA/USDT:USDT -10.17% │
|
||||||
│ Rejected Entry signals │ 254 │
|
│ Best day │ 27.031 USDT │
|
||||||
│ Entry/Exit Timeouts │ 0 / 0 │
|
│ Worst day │ -47.826 USDT │
|
||||||
│ │ │
|
│ Days win/draw/lose │ 20 / 6 / 5 │
|
||||||
│ Min balance │ 1003.168 USDT │
|
│ Min/Max/Avg. Duration Winners │ 0d 00:35 / 5d 18:15 / 0d 15:49 │
|
||||||
│ Max balance │ 1209 USDT │
|
│ Min/Max/Avg. Duration Losers │ 0d 10:40 / 17d 08:00 / 2d 17:00 │
|
||||||
│ Max % of account underwater │ 8.46% │
|
│ Max Consecutive Wins / Loss │ 36 / 3 │
|
||||||
│ Absolute drawdown │ 102.266 USDT (8.46%) │
|
│ Rejected Entry signals │ 258 │
|
||||||
│ Drawdown duration │ 9 days 08:50:00 │
|
│ Entry/Exit Timeouts │ 0 / 0 │
|
||||||
│ Profit at drawdown start │ 209 USDT │
|
│ │ │
|
||||||
│ Profit at drawdown end │ 106.734 USDT │
|
│ Min/Max balance (closed trades) │ 1003.205 USDT / 1151.425 USDT │
|
||||||
│ Drawdown start │ 2025-07-22 15:10:00 │
|
│ Max % of account underwater │ 8.19% │
|
||||||
│ Drawdown end │ 2025-08-01 00:00:00 │
|
│ Absolute drawdown │ 94.268 USDT (8.19%) │
|
||||||
│ Market change │ 30.51% │
|
│ Drawdown duration │ 9 days 08:50:00 │
|
||||||
└───────────────────────────────┴─────────────────────────────────┘
|
│ Profit at drawdown start │ 151.425 USDT │
|
||||||
|
│ Profit at drawdown end │ 57.157 USDT │
|
||||||
|
│ Drawdown start │ 2025-07-22 15:10:00 │
|
||||||
|
│ Drawdown end │ 2025-08-01 00:00:00 │
|
||||||
|
│ │ │
|
||||||
|
│ Wallet based Metrics │ │
|
||||||
|
│ Min/Max balance (wallet balance) │ 1000 USDT / 1151.425 USDT │
|
||||||
|
│ Min/Max balance dates (wallet balance) │ 2025-07-01 00:05:00 / 2025-07-22 15:15:00 │
|
||||||
|
│ Max % of account underwater (balance) │ 5.01% │
|
||||||
|
│ Absolute drawdown (wallet balance) │ 54.76 USDT (4.76%) │
|
||||||
|
│ Drawdown duration │ 7 days 20:35:00 │
|
||||||
|
│ Profit at drawdown start │ 151.425 USDT │
|
||||||
|
│ Profit at drawdown end │ 96.664 USDT │
|
||||||
|
│ Drawdown start │ 2025-07-22 15:15:00 │
|
||||||
|
│ Drawdown end │ 2025-07-30 11:50:00 │
|
||||||
|
│ Sharpe (daily wallet balance) │ 4.42 │
|
||||||
|
│ Sortino (daily wallet balance) │ 4.35 │
|
||||||
|
│ Calmar (daily wallet balance) │ 136.07 │
|
||||||
|
└────────────────────────────────────────┴───────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
||||||
@@ -388,14 +420,15 @@ It contains key metrics about the performance of your strategy on backtesting da
|
|||||||
- `Absolute profit`: Profit made in stake currency.
|
- `Absolute profit`: Profit made in stake currency.
|
||||||
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
||||||
- `CAGR %`: Compound annual growth rate.
|
- `CAGR %`: Compound annual growth rate.
|
||||||
- `Sortino`: Annualized Sortino ratio.
|
- `Sharpe (closed trades)`: Annualized Sharpe ratio including only closed trades (ignoring open trades with profits or losses).
|
||||||
- `Sharpe`: Annualized Sharpe ratio.
|
- `Sortino (closed trades)`: Annualized Sortino ratio including only closed trades (ignoring open trades with profits or losses).
|
||||||
- `Calmar`: Annualized Calmar ratio.
|
- `Calmar (closed trades)`: Annualized Calmar ratio including only closed trades (ignoring open trades with profits or losses).
|
||||||
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
- `SQN`: System Quality Number (SQN) - by Van Tharp.
|
||||||
- `Profit factor`: Sum of the profits of all winning trades divided by the sum of the losses of all losing trades.
|
- `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.
|
- `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. 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.
|
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||||
|
- `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.
|
||||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||||
- `Long / Short trades`: Split long/short trade counts (only shown when short trades were made).
|
- `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 %`: Profit percentage for long and short trades (only shown when short trades were made).
|
||||||
@@ -409,13 +442,21 @@ It contains key metrics about the performance of your strategy on backtesting da
|
|||||||
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
|
- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row.
|
||||||
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
|
||||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
- `Min/Max balance (closed trades)`: Lowest and Highest Wallet balance during the backtest period based on closed trades trades.
|
||||||
- `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)`.
|
- `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)`..
|
- `Absolute drawdown`: Maximum absolute drawdown experienced, including percentage relative to the account calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`..
|
||||||
|
- `Absolute drawdown (wallet balance)`: Maximum absolute drawdown experienced based on the unrealized balance, including percentage relative to the account calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||||
- `Drawdown duration`: Duration of the largest drawdown period.
|
- `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.
|
- `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).
|
- `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.
|
- `Min/Max balance (wallet balance)`: Lowest and Highest Wallet balance during the backtest period - including capital tied in open trades.
|
||||||
|
- `Min/Max balance dates (wallet balance)`: Dates when the minimum and maximum unrealized balance occurred.
|
||||||
|
- `Sharpe (wallet balance)` Annualized Sharpe ratio calculation including unrealized profits.
|
||||||
|
- `Sortino (wallet balance)` Annualized Sortino ratio calculation including unrealized profits.
|
||||||
|
- `Calmar (wallet balance)` Annualized Calmar ratio calculation including unrealized profits.
|
||||||
|
|
||||||
|
!!! Tip "Wallet based Metrics"
|
||||||
|
The metrics under the "Wallet based Metrics" section are calculated based on the unrealized balance, which includes the capital tied in open trades. This provides a more comprehensive view of the strategy's performance, as it accounts for both realized and unrealized profits and losses.
|
||||||
|
|
||||||
### Daily / Weekly / Monthly / Yearly breakdown
|
### Daily / Weekly / Monthly / Yearly breakdown
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| | **Unfilled timeout**
|
| | **Unfilled timeout**
|
||||||
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
|
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set `unfilledtimeout.unit` to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
|
||||||
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
||||||
| | **Pricing**
|
| | **Pricing**
|
||||||
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||||
@@ -229,7 +229,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||||
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`*<br> **Datatype:** float
|
||||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||||
| `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
|
| `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
|
| `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
|
||||||
@@ -240,7 +240,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
||||||
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `true`.<br> **Datatype:** boolean
|
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `true`.*<br> **Datatype:** boolean
|
||||||
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
||||||
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
||||||
| | **Webhook**
|
| | **Webhook**
|
||||||
@@ -280,8 +280,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
|
| `add_config_files` | Additional config files. These files will be loaded and merged with the current config file. The files are resolved relative to the initial file.<br> *Defaults to `[]`*. <br> **Datatype:** List of strings
|
||||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `feather`*. <br> **Datatype:** String
|
||||||
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing backtesting/hyperopt and in FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage (and decreasing train/inference timing backtesting/hyperopt and in FreqAI). <br> Default: `False`. <br> **Datatype:** Boolean.
|
||||||
| `log_config` | Dictionary containing the log config for python logging. [more info](advanced-setup.md#advanced-logging) <br> **Datatype:** dict. <br> Default: `FtRichHandler`
|
| `log_config` | Dictionary containing the log config for python logging. [more info](advanced-setup.md#advanced-logging) <br> Default: `FtRichHandler` <br> **Datatype:** dict.
|
||||||
|
|
||||||
### Parameters in the strategy
|
### Parameters in the strategy
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,8 @@ If `--convert` is also provided, the resample step will happen automatically and
|
|||||||
!!! Note "Kraken user"
|
!!! Note "Kraken user"
|
||||||
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
|
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
|
||||||
|
|
||||||
|
Kraken Futures uses standard OHLCV downloads and does not require `--dl-trades`.
|
||||||
|
|
||||||
Example call:
|
Example call:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+69
-9
@@ -217,6 +217,32 @@ freqtrade download-data --exchange kraken --dl-trades -p BTC/EUR BCH/EUR
|
|||||||
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests/sec rate.
|
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests/sec rate.
|
||||||
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
|
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
|
||||||
|
|
||||||
|
## Kraken Futures
|
||||||
|
|
||||||
|
Kraken Futures uses the exchange id `krakenfutures` and supports isolated futures mode.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"exchange": {
|
||||||
|
"name": "krakenfutures",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret"
|
||||||
|
},
|
||||||
|
"trading_mode": "futures",
|
||||||
|
"margin_mode": "isolated",
|
||||||
|
"stake_currency": "USD"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Kraken Futures supports `stoploss_on_exchange` with both `limit` and `market` stop orders.
|
||||||
|
Use `order_types.stoploss_price_type` to select the trigger price source (`mark`, `last`, or `index`).
|
||||||
|
|
||||||
|
!!! Note "Collateral"
|
||||||
|
Kraken Futures is USD-settled. Use USD as your stake currency.
|
||||||
|
|
||||||
|
!!! Note "Flex (Multi-collateral) Accounts"
|
||||||
|
Kraken Futures flex accounts allow collateral in multiple currencies, while trading remains USD-settled.
|
||||||
|
Freqtrade derives the `USD` balance from Kraken margin fields, so keep `stake_currency` set to `USD`.
|
||||||
|
|
||||||
## Kucoin
|
## Kucoin
|
||||||
|
|
||||||
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
@@ -319,6 +345,15 @@ API Keys for live futures trading must have the following permissions:
|
|||||||
|
|
||||||
We do strongly recommend to limit all API keys to the IP you're going to use it from.
|
We do strongly recommend to limit all API keys to the IP you're going to use it from.
|
||||||
|
|
||||||
|
### Bybit Demo Mode
|
||||||
|
|
||||||
|
Bybit has a [demo mode](https://learn.bybit.com/en/bybit-guide/how-to-use-bybit-demo-trading) - which can be activated by setting `exchange.demo_trading` to `true` in the configuration.
|
||||||
|
Bybit uses live markets to simulate your trades (without market impact) - making it work very similar to freqtrade's dry-run mode.
|
||||||
|
|
||||||
|
You'll need to use separate API keys for demo trading, which you can create on bybit's demo page.
|
||||||
|
|
||||||
|
Demo mode is incompatible with dry-run.
|
||||||
|
|
||||||
## Bitmart
|
## Bitmart
|
||||||
|
|
||||||
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
||||||
@@ -368,6 +403,11 @@ On startup, freqtrade will set the position mode to "One-way Mode" for the whole
|
|||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Hyperliquid supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it.
|
Hyperliquid supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
|
||||||
|
!!! Warning "Unified accounts"
|
||||||
|
Hyperliquid unified accounts are supported - though this relies freqtrade's assumption of "owning" the account, and being the only one trading on it (in this case, extended to both spot and futures).
|
||||||
|
We hence recommend the usage of subaccounts where possible, and to avoid manual trading on the same account while the bot is running.
|
||||||
|
Freqtrade will attempt to detect the account type on startup - changing the account type mid-trading is not supported and may lead to exceptions and errors.
|
||||||
|
|
||||||
Hyperliquid is a Decentralized Exchange (DEX). Decentralized exchanges work a bit different compared to normal exchanges. Instead of authenticating private API calls using an API key, private API calls need to be signed with the private key of your wallet (We recommend using an api Wallet for this, generated either on Hyperliquid or in your wallet of choice).
|
Hyperliquid is a Decentralized Exchange (DEX). Decentralized exchanges work a bit different compared to normal exchanges. Instead of authenticating private API calls using an API key, private API calls need to be signed with the private key of your wallet (We recommend using an api Wallet for this, generated either on Hyperliquid or in your wallet of choice).
|
||||||
This needs to be configured like this:
|
This needs to be configured like this:
|
||||||
|
|
||||||
@@ -398,30 +438,50 @@ 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.
|
* 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.
|
* 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.
|
!!! Warning "Vaults and Subaccounts"
|
||||||
To use these with Freqtrade, you will need to use the following configuration pattern:
|
You can only use either a vault or a subaccount - not both at the same time.
|
||||||
|
|
||||||
|
### Hyperliquid Subaccount
|
||||||
|
|
||||||
|
Hyperliquid allows you to create subaccounts with sufficient previous trading volume.
|
||||||
|
To use subaccounts with Freqtrade, you will need to use the following configuration pattern:
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "hyperliquid",
|
"name": "hyperliquid",
|
||||||
"walletAddress": "your_master_wallet_address", // Your master wallet address (not the API wallet address and not the vault/subaccount address).
|
"walletAddress": "your_master_wallet_address", // Your master wallet address (not the API wallet or vault address - but not subaccount address).
|
||||||
"privateKey": "your_api_private_key", // API wallet private key (see https://app.hyperliquid.xyz/API). You'll only need the private key.
|
"privateKey": "your_api_private_key", // API wallet private key (see https://app.hyperliquid.xyz/API). You'll only need the private key.
|
||||||
"ccxt_config": {
|
"ccxt_config": {
|
||||||
"options": {
|
"options": {
|
||||||
"vaultAddress": "your_vault_address", // Optional, only if you want to use a vault ...
|
"subAccountAddress": "your_subaccount_address" // Required if you want to use a subaccount.
|
||||||
"subAccountAddress": "your_subaccount_address" // OR optional, only if you want to use a subaccount
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Your balance and trades will now be used from your vault / subaccount - and no longer from your main account.
|
Your balance and trades will now be used from your subaccount - and no longer from your main account.
|
||||||
|
|
||||||
!!! Note
|
### Hyperliquid Vault
|
||||||
You can only use either a vault or a subaccount - not both at the same time.
|
|
||||||
|
Hyperliquid allows you to create vaults. To use vaults with Freqtrade, you will need to use the following configuration pattern:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"exchange": {
|
||||||
|
"name": "hyperliquid",
|
||||||
|
"walletAddress": "your_vault_address", // Your vault wallet address (Must also be added below in the ccxt_config.options.vaultAddress field)
|
||||||
|
"privateKey": "your_api_private_key", // API wallet private key (see https://app.hyperliquid.xyz/API). You'll only need the private key.
|
||||||
|
"ccxt_config": {
|
||||||
|
"options": {
|
||||||
|
"vaultAddress": "your_vault_address", // Optional, only if you want to use a vault ... (vault address must also be added to walletAdress)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Your balance and trades will now be used from your vault - and no longer from your main account.
|
||||||
|
|
||||||
### Historic Hyperliquid data
|
### Historic Hyperliquid data
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Supported Markets
|
## Supported Markets
|
||||||
|
|
||||||
Freqtrade supports spot trading, as well as futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
|
Freqtrade supports spot trading, as well as futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges) for an up-to-date list of supported exchanges.
|
||||||
|
|
||||||
### Can my bot open short positions?
|
### Can my bot open short positions?
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ In spot markets, you can in some cases use leveraged spot tokens, which reflect
|
|||||||
|
|
||||||
### Can my bot trade options or futures?
|
### Can my bot trade options or futures?
|
||||||
|
|
||||||
Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges.
|
Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges) for an up-to-date list of supported exchanges.
|
||||||
|
|
||||||
## Beginner Tips & Tricks
|
## Beginner Tips & Tricks
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,23 @@ On this page, you can also interact with the bot by starting and stopping it and
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|
The dashboard view provides an overview of the bot's performance and status.
|
||||||
|
If multiple bots are connected, the dashboard will show an overview of all connected bots, allowing you to easily switch between them or show just a subset of available bots.
|
||||||
|
|
||||||
|
#### Wallet Balance
|
||||||
|
|
||||||
|
New in freqtrade 2026.4: This shows the balance of the bot over time.
|
||||||
|
|
||||||
|
Compared to the "cumulative Profit" chart, this chart will show the actual balance of the bot over time, including unrealized profit and losses, as well as deposits and withdrawals.
|
||||||
|
|
||||||
|
Historic data has re-populated based on available exchange data - however is assumed to be best-effort and may not be 100% accurate.
|
||||||
|
More specifically, it won't cover deposits and withdrawals, and will assume a starting balance of current balance - profit/losses.
|
||||||
|
|
||||||
|
For clarity - a "Capture start" marker line is shown on the chart, which indicates the point at which the migration to the new wallet balance tracking system happened.
|
||||||
|
Only beyond this point, the wallet balance is expected to be accurate.
|
||||||
|
|
||||||
### Plot Configurator
|
### Plot Configurator
|
||||||
|
|
||||||
FreqUI Plots can be configured either via a `plot_config` configuration object in the strategy (which can be loaded via "from strategy" button) or via the UI.
|
FreqUI Plots can be configured either via a `plot_config` configuration object in the strategy (which can be loaded via "from strategy" button) or via the UI.
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ Below are the values you can expect to include/use inside a typical strategy dat
|
|||||||
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
||||||
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
||||||
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%` (see details below). <br> **Datatype:** Depends on the feature created by the user.
|
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%` (see details below). <br> **Datatype:** Depends on the feature created by the user.
|
||||||
| `df['%%*']` | Any dataframe column prepended with `%%` in `feature_engineering_*()` is treated as a training feature, just the same as the above `%` prepend. However, in this case, the features are returned back to the strategy for FreqUI/plot-dataframe plotting and monitoring in Dry/Live/Backtesting <br> **Datatype:** Depends on the feature created by the user. Please note that features created in `feature_engineering_expand()` will have automatic FreqAI naming schemas depending on the expansions that you configured (i.e. `include_timeframes`, `include_corr_pairlist`, `indicators_periods_candles`, `include_shifted_candles`). So if you want to plot `%%-rsi` from `feature_engineering_expand_all()`, the final naming scheme for your plotting config would be: `%%-rsi-period_10_ETH/USDT:USDT_1h` for the `rsi` feature with `period=10`, `timeframe=1h`, and `pair=ETH/USDT:USDT` (the `:USDT` is added if you are using futures pairs). It is useful to simply add `print(dataframe.columns)` in your `populate_indicators()` after `self.freqai.start()` to see the full list of available features that are returned to the strategy for plotting purposes.
|
| `df['%%*']` | Any dataframe column prepended with `%%` in `feature_engineering_*()` is treated as a training feature, just the same as the above `%` prepend. However, in this case, the features are returned back to the strategy for FreqUI/plot-dataframe plotting and monitoring in Dry/Live/Backtesting <br> **Datatype:** Depends on the feature created by the user. <br>*Please note* that features created in `feature_engineering_expand()` will have automatic FreqAI naming schemas depending on the expansions that you configured (i.e. `include_timeframes`, `include_corr_pairlist`, `indicators_periods_candles`, `include_shifted_candles`). So if you want to plot `%%-rsi` from `feature_engineering_expand_all()`, the final naming scheme for your plotting config would be: `%%-rsi-period_10_ETH/USDT:USDT_1h` for the `rsi` feature with `period=10`, `timeframe=1h`, and `pair=ETH/USDT:USDT` (the `:USDT` is added if you are using futures pairs). It is useful to simply add `print(dataframe.columns)` in your `populate_indicators()` after `self.freqai.start()` to see the full list of available features that are returned to the strategy for plotting purposes.
|
||||||
|
|
||||||
## Setting the `startup_candle_count`
|
## Setting the `startup_candle_count`
|
||||||
|
|
||||||
@@ -260,6 +260,10 @@ freqtrade trade --config config_examples/config_freqai.example.json --strategy F
|
|||||||
|
|
||||||
PyTorch dropped support for macOS x64 (intel based Apple devices) in version 2.3. Subsequently, freqtrade also dropped support for PyTorch on this platform.
|
PyTorch dropped support for macOS x64 (intel based Apple devices) in version 2.3. Subsequently, freqtrade also dropped support for PyTorch on this platform.
|
||||||
|
|
||||||
|
!!! Danger "Security notice"
|
||||||
|
Loading saved models from disk can cause security issues if using remote model files (files you downloaded from the internet or received from an untrusted source) due to having the necessity to have `weights_only=False`, which can cause security problems.
|
||||||
|
As long as you only load models that you have trained yourself, there is no risk.
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
#### Model
|
#### Model
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset. Overrides `n_steps`. Either `n_epochs` or `n_steps` must be set. <br><br> **Datatype:** int. optional. <br> Default: `10`.
|
| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset. Overrides `n_steps`. Either `n_epochs` or `n_steps` must be set. <br><br> **Datatype:** int. optional. <br> Default: `10`.
|
||||||
| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. Ignored if `n_epochs` is set. A simplified version of the function: <br><br> n_epochs = n_steps / (n_obs / batch_size) <br><br> The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points. <br> <br> **Datatype:** int. optional. <br> Default: `None`.
|
| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. Ignored if `n_epochs` is set. A simplified version of the function: <br><br> n_epochs = n_steps / (n_obs / batch_size) <br><br> The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points. <br> <br> **Datatype:** int. optional. <br> Default: `None`.
|
||||||
| `batch_size` | The size of the batches to use during training. <br><br> **Datatype:** int. <br> Default: `64`.
|
| `batch_size` | The size of the batches to use during training. <br><br> **Datatype:** int. <br> Default: `64`.
|
||||||
|
| `early_stopping_patience` | Number of epochs with no improvement in validation loss before training is stopped early. This helps prevent overfitting by halting training when the model stops improving. Set to `0` to disable early stopping. Requires a test/validation split (`test_size > 0`). <br><br> **Datatype:** int. <br> Default: `0` (disabled).
|
||||||
|
|
||||||
### Additional parameters
|
### Additional parameters
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ To save the models generated during a particular backtest so that you can start
|
|||||||
To ensure that the model can be reused, freqAI will call your strategy with a dataframe of length 1.
|
To ensure that the model can be reused, freqAI will call your strategy with a dataframe of length 1.
|
||||||
If your strategy requires more data than this to generate the same features, you can't reuse backtest predictions for live deployment and need to update your `identifier` for each new backtest.
|
If your strategy requires more data than this to generate the same features, you can't reuse backtest predictions for live deployment and need to update your `identifier` for each new backtest.
|
||||||
|
|
||||||
|
!!! Danger "Security notice"
|
||||||
|
Loading saved models from disk can cause security issues if using remote model files (files you downloaded from the internet or received from an untrusted source) due to having the necessity to have `weights_only=False`, which can cause security problems.
|
||||||
|
As long as you only load models that you have trained yourself, there is no risk.
|
||||||
|
|
||||||
### Backtest live collected predictions
|
### Backtest live collected predictions
|
||||||
|
|
||||||
FreqAI allow you to reuse live historic predictions through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse predictions generated in dry/run for comparison or other study.
|
FreqAI allow you to reuse live historic predictions through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse predictions generated in dry/run for comparison or other study.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
| [Hyperliquid](exchanges.md#hyperliquid) | spot | | ❌ (not supported) |
|
| [Hyperliquid](exchanges.md#hyperliquid) | spot | | ❌ (not supported) |
|
||||||
| [Hyperliquid](exchanges.md#hyperliquid) | futures | isolated, cross | limit |
|
| [Hyperliquid](exchanges.md#hyperliquid) | futures | isolated, cross | limit |
|
||||||
| [Kraken](exchanges.md#kraken) | spot | | market, limit |
|
| [Kraken](exchanges.md#kraken) | spot | | market, limit |
|
||||||
|
| [Kraken](exchanges.md#kraken-futures) | futures | isolated | market, limit |
|
||||||
| [OKX](exchanges.md#okx) | spot | | limit |
|
| [OKX](exchanges.md#okx) | spot | | limit |
|
||||||
| [OKX](exchanges.md#okx) | futures | isolated | limit |
|
| [OKX](exchanges.md#okx) | futures | isolated | limit |
|
||||||
| [Bitvavo](exchanges.md#bitvavo) | spot | | ❌ (not supported) |
|
| [Bitvavo](exchanges.md#bitvavo) | spot | | ❌ (not supported) |
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
||||||
|
|
||||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
|
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list), [`CrossMarketPairList`](#crossmarketpairlist), [`MarketCapPairlist`](#marketcappairlist) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
|
||||||
|
|
||||||
Additionally, [`AgeFilter`](#agefilter), [`DelistFilter`](#delistfilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
Additionally, [`AgeFilter`](#agefilter), [`DelistFilter`](#delistfilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
||||||
|
|
||||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList` or `PercentChangePairList` as the starting Pairlist Handler.
|
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList`, `PercentChangePairList` or `CrossMarketPairList` as the starting Pairlist Handler.
|
||||||
|
|
||||||
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
|
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
|||||||
* [`ProducerPairList`](#producerpairlist)
|
* [`ProducerPairList`](#producerpairlist)
|
||||||
* [`RemotePairList`](#remotepairlist)
|
* [`RemotePairList`](#remotepairlist)
|
||||||
* [`MarketCapPairList`](#marketcappairlist)
|
* [`MarketCapPairList`](#marketcappairlist)
|
||||||
|
* [`CrossMarketPairList`](#crossmarketpairlist)
|
||||||
* [`AgeFilter`](#agefilter)
|
* [`AgeFilter`](#agefilter)
|
||||||
* [`DelistFilter`](#delistfilter)
|
* [`DelistFilter`](#delistfilter)
|
||||||
* [`FullTradesFilter`](#fulltradesfilter)
|
* [`FullTradesFilter`](#fulltradesfilter)
|
||||||
@@ -303,6 +304,8 @@ The optional `mode` option specifies if the pairlist should be used as a `blackl
|
|||||||
|
|
||||||
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter".
|
The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter".
|
||||||
|
|
||||||
|
The optional `number_assets` option in the RemotePairList configuration determines how many pairs will be returned if used in whitelist `mode`. By default, all pairs will be returned. In blacklist `mode`, this option will be ignored.
|
||||||
|
|
||||||
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out.
|
||||||
|
|
||||||
In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering.
|
In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering.
|
||||||
@@ -402,6 +405,12 @@ Coins like 1000PEPE/USDT or KPEPE/USDT:USDT are detected on a best effort basis,
|
|||||||
!!! Danger "Duplicate symbols in coingecko"
|
!!! Danger "Duplicate symbols in coingecko"
|
||||||
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
|
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
|
||||||
|
|
||||||
|
#### CrossMarketPairList
|
||||||
|
|
||||||
|
Generate or filter pairs based of their availability on the opposite market.
|
||||||
|
|
||||||
|
The `pairs_exist_on` setting defines whether the pairs should exists on both spot and futures market (`both_markets`) or only exist on the specified trading mode (`current_market_only`). By default, the plugin will be in `both_markets` setting, which means whitelisted pairs have to exists on both spot and futures markets.
|
||||||
|
|
||||||
#### AgeFilter
|
#### AgeFilter
|
||||||
|
|
||||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,7 @@
|
|||||||
[](https://doi.org/10.21105/joss.04864)
|
[](https://doi.org/10.21105/joss.04864)
|
||||||
[](https://codecov.io/gh/freqtrade/freqtrade)
|
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||||
[](https://www.freqtrade.io)
|
[](https://www.freqtrade.io)
|
||||||
[](https://discord.gg/p7nuUNVfP7)
|
[](https://discord.gg/p7nuUNVfP7)
|
||||||
|
|
||||||
<!-- GitHub action buttons -->
|
<!-- GitHub action buttons -->
|
||||||
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
[:octicons-star-16: Star](https://github.com/freqtrade/freqtrade){ .md-button .md-button--sm }
|
||||||
@@ -62,6 +62,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
|||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
||||||
- [X] [OKX](https://okx.com/)
|
- [X] [OKX](https://okx.com/)
|
||||||
|
- [X] [Kraken](https://www.kraken.com/features/futures)
|
||||||
|
|
||||||
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -111,10 +111,10 @@ It also allows multiple subplots to display both MACD and RSI at the same time.
|
|||||||
|
|
||||||
Plot type can be configured using `type` key. Possible types are:
|
Plot type can be configured using `type` key. Possible types are:
|
||||||
|
|
||||||
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
* `scatter` corresponding a scatter plot.
|
||||||
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
* `bar` corresponding to a bar plot.
|
||||||
|
|
||||||
Extra parameters to `plotly.graph_objects.*` constructor can be specified in `plotly` dict.
|
Extra parameters to `plotly.graph_objects.*` constructor can be specified in `plotly` dict - these are only supported when using plotly as plotting library and will be ignored when using freq-ui.
|
||||||
|
|
||||||
Sample configuration with inline comments explaining the process:
|
Sample configuration with inline comments explaining the process:
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ def plot_config(self):
|
|||||||
```
|
```
|
||||||
|
|
||||||
??? Note "As attribute (former method)"
|
??? Note "As attribute (former method)"
|
||||||
Assigning plot_config is also possible as Attribute (this used to be the default way).
|
Assigning `plot_config` is also possible as Attribute (this used to be the default way).
|
||||||
This has the disadvantage that strategy parameters are not available, preventing certain configurations from working.
|
This has the disadvantage that strategy parameters are not available, preventing certain configurations from working.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
markdown==3.10.2
|
markdown==3.10.2
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.7.1
|
mkdocs-material==9.7.6
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.21
|
pymdown-extensions==10.21.3
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
mike==2.1.3
|
mike==2.2.0
|
||||||
|
|||||||
+9
-9
@@ -202,20 +202,20 @@ All endpoints in the below table need to be prefixed with the base URL of the AP
|
|||||||
| `/blacklist` | GET | Show the current blacklist.
|
| `/blacklist` | GET | Show the current blacklist.
|
||||||
| `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `blacklist` (`str`)
|
| `/blacklist` | POST | Adds the specified pair to the blacklist.<br/>*Params:*<br/>- `blacklist` (`str`)
|
||||||
| `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`)
|
| `/blacklist` | DELETE | Deletes the specified list of pairs from the blacklist.<br/>*Params:*<br/>- `[pair,pair]` (`list[str]`)
|
||||||
| `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
|
| `/pair_candles` | GET | Returns dataframe for a pair / timeframe combination while the bot is running.
|
||||||
| `/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_candles` | POST | Returns dataframe for a pair / timeframe combination while the bot is running, filtered by a provided list of columns to return.<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
|
||||||
| `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
|
| `/pair_history` | GET | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy.
|
||||||
| `/pair_history` | POST | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy, filtered by a provided list of columns to return. **Alpha**<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
|
| `/pair_history` | POST | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy, filtered by a provided list of columns to return.<br/>*Params:*<br/>- `<column_list>` (`list[str]`)
|
||||||
| `/plot_config` | GET | Get plot config from the strategy (or nothing if not configured). **Alpha**
|
| `/plot_config` | GET | Get plot config from the strategy (or nothing if not configured).
|
||||||
| `/strategies` | GET | List strategies in strategy directory. **Alpha**
|
| `/strategies` | GET | List strategies in strategy directory.
|
||||||
| `/strategy/<strategy>` | GET | Get specific Strategy content by strategy class name. **Alpha**<br/>*Params:*<br/>- `<strategy>` (`str`)
|
| `/strategy/<strategy>` | GET | Get specific Strategy content by strategy class name.<br/>*Params:*<br/>- `<strategy>` (`str`)
|
||||||
| `/available_pairs` | GET | List available backtest data. **Alpha**
|
| `/available_pairs` | GET | List available backtest data.
|
||||||
| `/version` | GET | Show version.
|
| `/version` | GET | Show version.
|
||||||
| `/sysinfo` | GET | Show information about the system load.
|
| `/sysinfo` | GET | Show information about the system load.
|
||||||
| `/health` | GET | Show bot health (last bot loop).
|
| `/health` | GET | Show bot health (last bot loop).
|
||||||
|
|
||||||
!!! Warning "Alpha status"
|
!!! Warning "Alpha status"
|
||||||
Endpoints labeled with *Alpha status* above may change at any time without notice.
|
Endpoints labeled with *Alpha status* or *Beta status* above may change at any time without notice.
|
||||||
|
|
||||||
### Message WebSocket
|
### Message WebSocket
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ WHERE id=31;
|
|||||||
### Remove trade from the database
|
### Remove trade from the database
|
||||||
|
|
||||||
!!! Tip "Use RPC Methods to delete trades"
|
!!! Tip "Use RPC Methods to delete trades"
|
||||||
Consider using `/delete <tradeid>` via telegram or rest API. That's the recommended way to deleting trades.
|
Consider using `/delete <tradeid>` via telegram or rest API. That's the recommended way to deleting trades, as it will also remove the corresponding orders and custom data, and it will also trigger the necessary events in the bot to keep everything in sync.
|
||||||
|
|
||||||
If you'd still like to remove a trade from the database directly, you can use the below query.
|
If you'd still like to remove a trade from the database directly, you can use the below query.
|
||||||
|
|
||||||
@@ -113,9 +113,14 @@ If you'd still like to remove a trade from the database directly, you can use th
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
DELETE FROM trades WHERE id = <tradeid>;
|
DELETE FROM trades WHERE id = <tradeid>;
|
||||||
|
DELETE FROM orders WHERE ft_trade_id = <tradeid>;
|
||||||
|
DELETE FROM trade_custom_data WHERE ft_trade_id = <tradeid>;
|
||||||
|
|
||||||
|
|
||||||
DELETE FROM trades WHERE id = 31;
|
DELETE FROM trades WHERE id = 31;
|
||||||
|
DELETE FROM orders WHERE ft_trade_id = 31;
|
||||||
|
DELETE FROM trade_custom_data WHERE ft_trade_id = 31;
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause.
|
This will remove the specified trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause.
|
||||||
|
|||||||
+19
-2
@@ -39,6 +39,22 @@ The Order-type will be ignored if only one mode is available.
|
|||||||
In that case, the bot will fallback to using the `emergency_exit` order type to place a market order as placing the stoploss order failed.
|
In that case, the bot will fallback to using the `emergency_exit` order type to place a market order as placing the stoploss order failed.
|
||||||
Freqtrade currently does not implement a limitation to avoid this situation, so please ensure your stoploss values are within reasonable limits for your exchange or disable stoploss on exchange.
|
Freqtrade currently does not implement a limitation to avoid this situation, so please ensure your stoploss values are within reasonable limits for your exchange or disable stoploss on exchange.
|
||||||
|
|
||||||
|
### Which order type is used for stoploss on exchange?
|
||||||
|
|
||||||
|
The order type used for stoploss on exchange is determined by the `stoploss` value and the exchange capabilities.
|
||||||
|
If your selected exchange supports both stop-limit and stop-market orders, then the `stoploss` value will determine which order type is used for stoploss on exchange.
|
||||||
|
If your exchange only supports one of the two order types, you must configure your `stoploss` value accordingly, otherwise the bot will fail to start.
|
||||||
|
|
||||||
|
### Which order type should i use for stoploss on exchange?
|
||||||
|
|
||||||
|
If we translate the two stoploss order types into human words - they would be something like this:
|
||||||
|
|
||||||
|
* **stoploss-market** -> "when stop triggers, get me the hell out of here at whatever price".
|
||||||
|
* **stoploss-limit** -> "when stop triggers, place a limit order x% below the stoploss price. I accept a loss of "stoploss + 1%" at worst - but if price jumps further - i accept to wait for price to get back down to me, potentially resulting in a much bigger loss than "stoploss + 1%".
|
||||||
|
|
||||||
|
As a consequence, we recommend using stoploss-market orders whenever possible, as the main point of a stoploss is to get you out of a position when the market is crashing, and in such situations, you'll want to exit the position immediately at the best available price, rather than risking a limit order not getting filled and potentially incurring even greater losses.
|
||||||
|
The choice is ultimately up to you, but please be aware of the risk of using stoploss-limit orders, especially in volatile markets.
|
||||||
|
|
||||||
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
||||||
|
|
||||||
Enable or Disable stop loss on exchange.
|
Enable or Disable stop loss on exchange.
|
||||||
@@ -66,9 +82,10 @@ This same logic will reapply a stoploss order on the exchange should you cancel
|
|||||||
### stoploss_price_type
|
### stoploss_price_type
|
||||||
|
|
||||||
!!! Warning "Only applies to futures"
|
!!! Warning "Only applies to futures"
|
||||||
`stoploss_price_type` only applies to futures markets (on exchanges where it's available).
|
`stoploss_price_type` only applies to futures markets (on exchanges where it's available).
|
||||||
Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected.
|
Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected.
|
||||||
Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports.
|
Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports.
|
||||||
|
In spot markets, this setting is ignored and not validated, as most exchanges only support one price type for stoploss orders on spot markets.
|
||||||
|
|
||||||
Stoploss on exchange on futures markets can trigger on different price types.
|
Stoploss on exchange on futures markets can trigger on different price types.
|
||||||
The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index".
|
The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index".
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
trade_entry_type = trade.get_custom_data(key='entry_type')
|
trade_entry_type = trade.get_custom_data(key='entry_type')
|
||||||
if trade_entry_type is None:
|
if trade_entry_type is None:
|
||||||
trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip'
|
trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip'
|
||||||
elif fills > 1:
|
elif len(fills) > 1:
|
||||||
trade_entry_type = 'buy_up'
|
trade_entry_type = 'buy_up'
|
||||||
trade.set_custom_data(key='entry_type', value=trade_entry_type)
|
trade.set_custom_data(key='entry_type', value=trade_entry_type)
|
||||||
return super().bot_loop_start(**kwargs)
|
return super().bot_loop_start(**kwargs)
|
||||||
|
|||||||
@@ -696,6 +696,9 @@ However, freqtrade also offers a custom callback for both order types, which all
|
|||||||
Backtesting fills orders if their price falls within the candle's low/high range.
|
Backtesting fills orders if their price falls within the candle's low/high range.
|
||||||
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
|
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
|
||||||
|
|
||||||
|
!!! Tip "Replacing orders"
|
||||||
|
If you'd like to replace an order with a different price instead of just cancelling it, you might want to look at [`adjust_order_price()`](#adjust-order-price) instead, which will allow you to both cancel the order, as well as replace it with a new price.
|
||||||
|
|
||||||
### Custom order timeout example
|
### Custom order timeout example
|
||||||
|
|
||||||
Called for every open order until that order is either filled or cancelled.
|
Called for every open order until that order is either filled or cancelled.
|
||||||
|
|||||||
@@ -910,6 +910,8 @@ if self.dp.runmode.value in ('live', 'dry_run'):
|
|||||||
|
|
||||||
### *check_delisting(pair)*
|
### *check_delisting(pair)*
|
||||||
|
|
||||||
|
Return Datetime of the pair delisting schedule if any, otherwise return None
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs):
|
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs):
|
||||||
if self.dp.runmode.value in ('live', 'dry_run'):
|
if self.dp.runmode.value in ('live', 'dry_run'):
|
||||||
|
|||||||
+1
-1
@@ -416,6 +416,6 @@ Your original strategy will remain available in the `user_data/strategies_orig_u
|
|||||||
|
|
||||||
!!! Warning "Conversion results"
|
!!! Warning "Conversion results"
|
||||||
Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion.
|
Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion.
|
||||||
We also recommend to run a python formatter (e.g. `black`) to format results in a sane manner.
|
We also recommend to run a python formatter (e.g. `ruff format`) to format results in a sane manner.
|
||||||
|
|
||||||
--8<-- "commands/strategy-updater.md"
|
--8<-- "commands/strategy-updater.md"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Freqtrade bot"""
|
"""Freqtrade bot"""
|
||||||
|
|
||||||
__version__ = "2026.2"
|
__version__ = "2026.5-dev"
|
||||||
|
|
||||||
if "dev" in __version__:
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -8,15 +8,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def start_convert_db(args: dict[str, Any]) -> None:
|
def start_convert_db(args: dict[str, Any]) -> None:
|
||||||
from sqlalchemy import func, select
|
|
||||||
from sqlalchemy.orm import make_transient
|
|
||||||
|
|
||||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||||
from freqtrade.persistence import Order, Trade, init_db
|
from freqtrade.persistence import Trade, init_db
|
||||||
from freqtrade.persistence.custom_data import _CustomData
|
from freqtrade.persistence.db_migration import migrate_db
|
||||||
from freqtrade.persistence.key_value_store import _KeyValueStoreModel
|
|
||||||
from freqtrade.persistence.migrations import set_sequence_ids
|
|
||||||
from freqtrade.persistence.pairlock import PairLock
|
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
@@ -24,56 +19,4 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
|||||||
session_target = Trade.session
|
session_target = Trade.session
|
||||||
init_db(config["db_url_from"])
|
init_db(config["db_url_from"])
|
||||||
logger.info("Starting db migration.")
|
logger.info("Starting db migration.")
|
||||||
|
migrate_db(session_target)
|
||||||
trade_count = 0
|
|
||||||
pairlock_count = 0
|
|
||||||
kv_count = 0
|
|
||||||
custom_data_count = 0
|
|
||||||
for trade in Trade.get_trades():
|
|
||||||
trade_count += 1
|
|
||||||
make_transient(trade)
|
|
||||||
for o in trade.orders:
|
|
||||||
make_transient(o)
|
|
||||||
|
|
||||||
session_target.add(trade)
|
|
||||||
|
|
||||||
session_target.commit()
|
|
||||||
|
|
||||||
for pairlock in PairLock.get_all_locks():
|
|
||||||
pairlock_count += 1
|
|
||||||
make_transient(pairlock)
|
|
||||||
session_target.add(pairlock)
|
|
||||||
session_target.commit()
|
|
||||||
|
|
||||||
for kv in _KeyValueStoreModel.session.scalars(select(_KeyValueStoreModel)):
|
|
||||||
kv_count += 1
|
|
||||||
make_transient(kv)
|
|
||||||
session_target.add(kv)
|
|
||||||
session_target.commit()
|
|
||||||
|
|
||||||
for cd in _CustomData.session.scalars(select(_CustomData)):
|
|
||||||
custom_data_count += 1
|
|
||||||
make_transient(cd)
|
|
||||||
session_target.add(cd)
|
|
||||||
session_target.commit()
|
|
||||||
|
|
||||||
# Update sequences
|
|
||||||
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
|
||||||
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
|
||||||
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
|
||||||
max_kv_id = session_target.scalar(select(func.max(_KeyValueStoreModel.id)))
|
|
||||||
max_custom_data_id = session_target.scalar(select(func.max(_CustomData.id)))
|
|
||||||
|
|
||||||
set_sequence_ids(
|
|
||||||
session_target.get_bind(),
|
|
||||||
trade_id=(max_trade_id or 0) + 1,
|
|
||||||
order_id=(max_order_id or 0) + 1,
|
|
||||||
pairlock_id=(max_pairlock_id or 0) + 1,
|
|
||||||
kv_id=(max_kv_id or 0) + 1,
|
|
||||||
custom_data_id=(max_custom_data_id or 0) + 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Migrated {trade_count} Trades, {pairlock_count} Pairlocks, "
|
|
||||||
f"{kv_count} Key-Value pairs, and {custom_data_count} Custom Data entries."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ def start_show_trades(args: dict[str, Any]) -> None:
|
|||||||
tfilter = []
|
tfilter = []
|
||||||
|
|
||||||
if config.get("trade_ids"):
|
if config.get("trade_ids"):
|
||||||
tfilter.append(Trade.id.in_(config["trade_ids"]))
|
tfilter.append(Trade.id.in_(int(tid) for tid in config["trade_ids"]))
|
||||||
|
|
||||||
trades = Trade.get_trades(tfilter).all()
|
trades = Trade.get_trades(tfilter).all()
|
||||||
logger.info(f"Printing {len(trades)} Trades: ")
|
logger.info(f"Printing {len(trades)} Trades: ")
|
||||||
|
|||||||
@@ -236,6 +236,10 @@ CONF_SCHEMA = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": BACKTEST_CACHE_AGE,
|
"enum": BACKTEST_CACHE_AGE,
|
||||||
},
|
},
|
||||||
|
"skip_wallet_history_migration": {
|
||||||
|
"description": "Disable wallet history migration.",
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
# Hyperopt
|
# Hyperopt
|
||||||
"hyperopt_path": {
|
"hyperopt_path": {
|
||||||
"description": "Specify additional lookup path for Hyperopt Loss functions.",
|
"description": "Specify additional lookup path for Hyperopt Loss functions.",
|
||||||
@@ -753,6 +757,7 @@ CONF_SCHEMA = {
|
|||||||
"description": "Secret key for JWT authentication.",
|
"description": "Secret key for JWT authentication.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "somethingRandomSomethingRandom123",
|
"default": "somethingRandomSomethingRandom123",
|
||||||
|
"minLength": 32,
|
||||||
},
|
},
|
||||||
"CORS_origins": {
|
"CORS_origins": {
|
||||||
"description": "List of allowed CORS origins.",
|
"description": "List of allowed CORS origins.",
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = Fal
|
|||||||
_validate_consumers(conf)
|
_validate_consumers(conf)
|
||||||
validate_migrated_strategy_settings(conf)
|
validate_migrated_strategy_settings(conf)
|
||||||
_validate_orderflow(conf)
|
_validate_orderflow(conf)
|
||||||
|
_validate_demo_trading(conf)
|
||||||
|
|
||||||
# validate configuration before returning
|
# validate configuration before returning
|
||||||
logger.info("Validating configuration ...")
|
logger.info("Validating configuration ...")
|
||||||
@@ -413,6 +414,11 @@ def _validate_orderflow(conf: dict[str, Any]) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_demo_trading(conf: dict[str, Any]) -> None:
|
||||||
|
if conf.get("exchange", {}).get("demo_trading", False) and conf.get("dry_run", False):
|
||||||
|
raise ConfigurationError("Demo trading cannot be used together with dry_run.")
|
||||||
|
|
||||||
|
|
||||||
def _strategy_settings(conf: dict[str, Any]) -> None:
|
def _strategy_settings(conf: dict[str, Any]) -> None:
|
||||||
process_deprecated_setting(conf, None, "use_sell_signal", None, "use_exit_signal")
|
process_deprecated_setting(conf, None, "use_sell_signal", None, "use_exit_signal")
|
||||||
process_deprecated_setting(conf, None, "sell_profit_only", None, "exit_profit_only")
|
process_deprecated_setting(conf, None, "sell_profit_only", None, "exit_profit_only")
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ class Configuration:
|
|||||||
("include_inactive", "Detected --include-inactive-pairs: {}"),
|
("include_inactive", "Detected --include-inactive-pairs: {}"),
|
||||||
("no_parallel_download", "Detected --no-parallel-download: {}"),
|
("no_parallel_download", "Detected --no-parallel-download: {}"),
|
||||||
("download_trades", "Detected --dl-trades: {}"),
|
("download_trades", "Detected --dl-trades: {}"),
|
||||||
("convert_trades", "Detected --convert: {} - Converting Trade data to OHCV {}"),
|
("convert_trades", "Detected --convert: {} - Converting trade data to OHLCV."),
|
||||||
("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
|
("dataformat_ohlcv", 'Using "{}" to store OHLCV data.'),
|
||||||
("dataformat_trades", 'Using "{}" to store trades data.'),
|
("dataformat_trades", 'Using "{}" to store trades data.'),
|
||||||
("show_timerange", "Detected --show-timerange"),
|
("show_timerange", "Detected --show-timerange"),
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ AVAILABLE_PAIRLISTS = [
|
|||||||
"ProducerPairList",
|
"ProducerPairList",
|
||||||
"RemotePairList",
|
"RemotePairList",
|
||||||
"MarketCapPairList",
|
"MarketCapPairList",
|
||||||
|
"CrossMarketPairList",
|
||||||
"AgeFilter",
|
"AgeFilter",
|
||||||
"DelistFilter",
|
"DelistFilter",
|
||||||
"FullTradesFilter",
|
"FullTradesFilter",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .bt_fileutils import (
|
|||||||
get_backtest_market_change,
|
get_backtest_market_change,
|
||||||
get_backtest_result,
|
get_backtest_result,
|
||||||
get_backtest_resultlist,
|
get_backtest_resultlist,
|
||||||
|
get_backtest_wallet_change,
|
||||||
get_latest_backtest_filename,
|
get_latest_backtest_filename,
|
||||||
get_latest_hyperopt_file,
|
get_latest_hyperopt_file,
|
||||||
get_latest_hyperopt_filename,
|
get_latest_hyperopt_filename,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from io import BytesIO, StringIO
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||||
@@ -308,10 +307,31 @@ def get_backtest_market_change(filename: Path, include_ts: bool = True) -> pd.Da
|
|||||||
else:
|
else:
|
||||||
df = pd.read_feather(filename)
|
df = pd.read_feather(filename)
|
||||||
if include_ts:
|
if include_ts:
|
||||||
df.loc[:, "__date_ts"] = df.loc[:, "date"].astype(np.int64) // 1000 // 1000
|
df.loc[:, "__date_ts"] = df.loc[:, "date"].dt.as_unit("ms").astype("int64")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def get_backtest_wallet_change(filename: Path, strategy_name: str) -> pd.DataFrame | None:
|
||||||
|
"""
|
||||||
|
Read backtest wallet change file.
|
||||||
|
:param filename: Path to the backtest result zip file
|
||||||
|
:param strategy_name: Name of the strategy to load
|
||||||
|
:return: DataFrame with wallet change data
|
||||||
|
"""
|
||||||
|
if filename.suffix != ".zip":
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = load_file_from_zip(filename, f"{filename.stem}_{strategy_name}_wallet.feather")
|
||||||
|
df = pd.read_feather(BytesIO(data))
|
||||||
|
|
||||||
|
df.loc[:, "__date_ts"] = df.loc[:, "date"].dt.as_unit("ms").astype("int64")
|
||||||
|
return df
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def find_existing_backtest_stats(
|
def find_existing_backtest_stats(
|
||||||
dirname: Path | str, run_ids: dict[str, str], min_backtest_date: datetime | None = None
|
dirname: Path | str, run_ids: dict[str, str], min_backtest_date: datetime | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -503,13 +523,16 @@ def load_backtest_analysis_data(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame:
|
def trade_list_to_dataframe(
|
||||||
|
trades: list[Trade] | list[LocalTrade], *, minified: bool = True
|
||||||
|
) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Convert list of Trade objects to pandas Dataframe
|
Convert list of Trade objects to pandas Dataframe
|
||||||
:param trades: List of trade objects
|
:param trades: List of trade objects
|
||||||
|
:param minified: Whether to use minified version of trade JSON
|
||||||
:return: Dataframe with BT_DATA_COLUMNS
|
:return: Dataframe with BT_DATA_COLUMNS
|
||||||
"""
|
"""
|
||||||
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
|
df = pd.DataFrame.from_records([t.to_json(minified) for t in trades], columns=BT_DATA_COLUMNS)
|
||||||
if len(df) > 0:
|
if len(df) > 0:
|
||||||
df["close_date"] = pd.to_datetime(df["close_timestamp"], unit="ms", utc=True)
|
df["close_date"] = pd.to_datetime(df["close_timestamp"], unit="ms", utc=True)
|
||||||
df["open_date"] = pd.to_datetime(df["open_timestamp"], unit="ms", utc=True)
|
df["open_date"] = pd.to_datetime(df["open_timestamp"], unit="ms", utc=True)
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import IntOrInf
|
from freqtrade.constants import IntOrInf
|
||||||
|
from freqtrade.exchange import (
|
||||||
|
timeframe_to_prev_date,
|
||||||
|
timeframe_to_resample_freq,
|
||||||
|
)
|
||||||
|
from freqtrade.util import dt_from_ts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -58,3 +64,95 @@ def evaluate_result_multi(
|
|||||||
"""
|
"""
|
||||||
df_final = analyze_trade_parallelism(trades, timeframe)
|
df_final = analyze_trade_parallelism(trades, timeframe)
|
||||||
return df_final[df_final["open_trades"] > max_open_trades]
|
return df_final[df_final["open_trades"] > max_open_trades]
|
||||||
|
|
||||||
|
|
||||||
|
def balance_distribution_over_time(
|
||||||
|
trades: pd.DataFrame,
|
||||||
|
min_date: datetime,
|
||||||
|
max_date: datetime,
|
||||||
|
timeframe: str,
|
||||||
|
stake_currency: str,
|
||||||
|
start_balance: float,
|
||||||
|
pairlist: list[str],
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Return a dataframe with stake_currency and the pairlist as columns
|
||||||
|
Each column will contain the amount of the currency at the given time
|
||||||
|
Columns added are:
|
||||||
|
- stake_currency: amount of stake currency
|
||||||
|
- <pair>: amount of base currency in the pair
|
||||||
|
- <pair>_leverage: leverage used for the pair at the time (NaN if no open trade)
|
||||||
|
- <pair>_is_short: 1 if the open trade is short, 0 if long (NaN if no open trade)
|
||||||
|
- <pair>_collateral: amount of stake currency used as collateral for open trades
|
||||||
|
:param trades: Trades Dataframe - can be loaded from backtest, or created
|
||||||
|
via trade_list_to_dataframe
|
||||||
|
:param timeframe: Frequency to use for the resulting dataframe
|
||||||
|
:param min_date: start date
|
||||||
|
:param max_date: End date (will be rounded down to timeframe)
|
||||||
|
:param stake_currency: The stake currency
|
||||||
|
:param start_balance: Starting balance in stake currency
|
||||||
|
:param pairlist: List of trading pairs to include in the dataframe
|
||||||
|
Can be obtained via trade_df["pair"].unique()
|
||||||
|
For pairs without trades, the column will be all zeros
|
||||||
|
:return: Dataframe with balance distribution over time
|
||||||
|
"""
|
||||||
|
min_date_res = timeframe_to_prev_date(timeframe, min_date)
|
||||||
|
max_date_res = timeframe_to_prev_date(timeframe, max_date)
|
||||||
|
index = pd.date_range(min_date_res, max_date_res, freq=timeframe_to_resample_freq(timeframe))
|
||||||
|
pairs_lev = [f"{pair}_leverage" for pair in pairlist]
|
||||||
|
pairs_is_short = [f"{pair}_is_short" for pair in pairlist]
|
||||||
|
pairs_collateral = [f"{pair}_collateral" for pair in pairlist]
|
||||||
|
pairs_lev += pairs_is_short
|
||||||
|
|
||||||
|
df = pd.DataFrame(
|
||||||
|
index=index, columns=[stake_currency] + pairlist + pairs_lev + pairs_collateral, dtype=float
|
||||||
|
)
|
||||||
|
# Initialize variables to starting values
|
||||||
|
df[stake_currency] = float(start_balance)
|
||||||
|
df[pairlist + pairs_collateral] = 0.0
|
||||||
|
df[pairs_lev] = np.nan
|
||||||
|
|
||||||
|
for trade in trades.sort_values(by=["open_date"]).itertuples():
|
||||||
|
pair = trade.pair
|
||||||
|
end_date = trade.close_date if trade.close_date is not pd.NaT else None
|
||||||
|
# Exclude open orders - these won't have order_filled_timestamp set.
|
||||||
|
df.loc[trade.open_date : end_date, f"{pair}_leverage"] = trade.leverage
|
||||||
|
df.loc[trade.open_date : end_date, f"{pair}_is_short"] = 1 if trade.is_short else 0
|
||||||
|
orders = [o for o in trade.orders if o["order_filled_timestamp"]]
|
||||||
|
current_position = 0
|
||||||
|
current_collateral = 0
|
||||||
|
for order in sorted(orders, key=lambda x: x["order_filled_timestamp"]):
|
||||||
|
filled_at = pd.Timestamp(dt_from_ts(order["order_filled_timestamp"]))
|
||||||
|
real_amount = order.get("filled", order["amount"])
|
||||||
|
stake = order["safe_price"] * real_amount
|
||||||
|
stake_no_lev = stake / trade.leverage
|
||||||
|
if order["ft_is_entry"]:
|
||||||
|
# Entry order: lock collateral and pay fee
|
||||||
|
# For both long and short: balance decreases by collateral + fee
|
||||||
|
fee_open = stake * trade.fee_open
|
||||||
|
current_position += real_amount
|
||||||
|
current_collateral += stake_no_lev
|
||||||
|
df.loc[filled_at:end_date, pair] += real_amount
|
||||||
|
df.loc[filled_at:end_date, f"{pair}_collateral"] += stake_no_lev
|
||||||
|
df.loc[filled_at:, stake_currency] -= stake_no_lev + fee_open
|
||||||
|
else:
|
||||||
|
# Exit order: release collateral and realize profit/loss
|
||||||
|
fee_close = stake * trade.fee_close
|
||||||
|
if trade.is_short:
|
||||||
|
# For SHORT
|
||||||
|
df.loc[filled_at:, stake_currency] += (
|
||||||
|
current_collateral * (1 + trade.leverage) - stake
|
||||||
|
) - fee_close
|
||||||
|
else:
|
||||||
|
# For LONG
|
||||||
|
df.loc[filled_at:, stake_currency] += (
|
||||||
|
stake - current_collateral * (trade.leverage - 1) - fee_close
|
||||||
|
)
|
||||||
|
df.loc[filled_at:end_date, pair] -= real_amount
|
||||||
|
df.loc[filled_at:end_date, f"{pair}_collateral"] -= stake_no_lev
|
||||||
|
current_position -= real_amount
|
||||||
|
current_collateral -= stake_no_lev
|
||||||
|
|
||||||
|
# Round to avoid floating point issues
|
||||||
|
df = df.round(14)
|
||||||
|
return df
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ def ohlcv_to_dataframe(
|
|||||||
df = DataFrame(ohlcv, columns=cols)
|
df = DataFrame(ohlcv, columns=cols)
|
||||||
|
|
||||||
# Floor date to seconds to account for exchange imprecisions
|
# Floor date to seconds to account for exchange imprecisions
|
||||||
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s")
|
from freqtrade.exchange import timeframe_to_floor_freq
|
||||||
|
|
||||||
|
resample_interval = timeframe_to_floor_freq(timeframe)
|
||||||
|
|
||||||
|
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor(resample_interval)
|
||||||
|
|
||||||
# Some exchanges return int values for Volume and even for OHLC.
|
# Some exchanges return int values for Volume and even for OHLC.
|
||||||
# Convert them since TA-LIB indicators used in the strategy assume floats
|
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||||
@@ -59,14 +63,14 @@ def ohlcv_to_dataframe(
|
|||||||
|
|
||||||
|
|
||||||
def clean_ohlcv_dataframe(
|
def clean_ohlcv_dataframe(
|
||||||
data: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
|
dataframe: DataFrame, timeframe: str, pair: str, *, fill_missing: bool, drop_incomplete: bool
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Cleanse a OHLCV dataframe by
|
Cleanse a OHLCV dataframe by
|
||||||
* Grouping it by date (removes duplicate tics)
|
* Grouping it by date (removes duplicate tics)
|
||||||
* dropping last candles if requested
|
* dropping last candles if requested
|
||||||
* Filling up missing data (if requested)
|
* Filling up missing data (if requested)
|
||||||
:param data: DataFrame containing candle (OHLCV) data.
|
:param dataframe: DataFrame containing candle (OHLCV) data.
|
||||||
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
|
:param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
|
||||||
:param pair: Pair this data is for (used to warn if fillup was necessary)
|
:param pair: Pair this data is for (used to warn if fillup was necessary)
|
||||||
:param fill_missing: fill up missing candles with 0 candles
|
:param fill_missing: fill up missing candles with 0 candles
|
||||||
@@ -75,7 +79,7 @@ def clean_ohlcv_dataframe(
|
|||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
# group by index and aggregate results to eliminate duplicate ticks
|
# group by index and aggregate results to eliminate duplicate ticks
|
||||||
data = data.groupby(by="date", as_index=False, sort=True).agg(
|
dataframe = dataframe.groupby(by="date", as_index=False, sort=True).agg(
|
||||||
{
|
{
|
||||||
"open": "first",
|
"open": "first",
|
||||||
"high": "max",
|
"high": "max",
|
||||||
@@ -86,13 +90,13 @@ def clean_ohlcv_dataframe(
|
|||||||
)
|
)
|
||||||
# eliminate partial candle
|
# eliminate partial candle
|
||||||
if drop_incomplete:
|
if drop_incomplete:
|
||||||
data.drop(data.tail(1).index, inplace=True)
|
dataframe.drop(dataframe.tail(1).index, inplace=True)
|
||||||
logger.debug("Dropping last candle")
|
logger.debug("Dropping last candle")
|
||||||
|
|
||||||
if fill_missing:
|
if fill_missing:
|
||||||
return ohlcv_fill_up_missing_data(data, timeframe, pair)
|
return ohlcv_fill_up_missing_data(dataframe, timeframe, pair)
|
||||||
else:
|
else:
|
||||||
return data
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
|
def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pandas import DataFrame, read_feather, to_datetime
|
from pandas import DataFrame, read_feather
|
||||||
from pyarrow import dataset
|
from pyarrow import dataset
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
@@ -71,7 +71,7 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
"volume": "float",
|
"volume": "float",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
pairdata["date"] = pairdata["date"].dt.as_unit("ms")
|
||||||
return pairdata
|
return pairdata
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class IDataHandler(ABC):
|
class IDataHandler(ABC):
|
||||||
_OHLCV_REGEX = r"^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
|
_OHLCV_REGEX = r"^([\w-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
|
||||||
_TRADES_REGEX = r"^([a-zA-Z_\d-]+)\-(trades)?(?=\.)"
|
_TRADES_REGEX = r"^([\w-]+)\-(trades)?(?=\.)"
|
||||||
|
|
||||||
def __init__(self, datadir: Path) -> None:
|
def __init__(self, datadir: Path) -> None:
|
||||||
self._datadir = datadir
|
self._datadir = datadir
|
||||||
@@ -336,11 +336,10 @@ class IDataHandler(ABC):
|
|||||||
def rebuild_pair_from_filename(pair: str) -> str:
|
def rebuild_pair_from_filename(pair: str) -> str:
|
||||||
"""
|
"""
|
||||||
Rebuild pair name from filename
|
Rebuild pair name from filename
|
||||||
Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names.
|
Replaces the first '_' with '/' and the second '_' (if present) with ':'.
|
||||||
|
e.g. BTC_USDT -> BTC/USDT, BTC_USDT_USDT -> BTC/USDT:USDT
|
||||||
"""
|
"""
|
||||||
res = re.sub(r"^(([A-Za-z\d]{1,10})|^([A-Za-z\-]{1,6}))(_)", r"\g<1>/", pair, count=1)
|
return pair.replace("_", "/", 1).replace("_", ":", 1)
|
||||||
res = re.sub("_", ":", res, count=1)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def ohlcv_load(
|
def ohlcv_load(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from pandas import DataFrame, read_json, to_datetime
|
from pandas import DataFrame, read_json, to_datetime
|
||||||
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
@@ -35,8 +34,8 @@ class JsonDataHandler(IDataHandler):
|
|||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||||
self.create_dir_if_needed(filename)
|
self.create_dir_if_needed(filename)
|
||||||
_data = data.copy()
|
_data = data.copy()
|
||||||
# Convert date to int
|
# Convert date to int (milliseconds)
|
||||||
_data["date"] = _data["date"].astype(np.int64) // 1000 // 1000
|
_data["date"] = _data["date"].dt.as_unit("ms").astype("int64")
|
||||||
|
|
||||||
# Reset index, select only appropriate columns and save as json
|
# Reset index, select only appropriate columns and save as json
|
||||||
_data.reset_index(drop=True).loc[:, self._columns].to_json(
|
_data.reset_index(drop=True).loc[:, self._columns].to_json(
|
||||||
@@ -81,7 +80,7 @@ class JsonDataHandler(IDataHandler):
|
|||||||
"volume": "float",
|
"volume": "float",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True).dt.as_unit("ms")
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
@@ -105,6 +104,9 @@ class JsonDataHandler(IDataHandler):
|
|||||||
:param trading_mode: Trading mode to use (used to determine the filename)
|
:param trading_mode: Trading mode to use (used to determine the filename)
|
||||||
"""
|
"""
|
||||||
filename = self._pair_trades_filename(self._datadir, pair, trading_mode)
|
filename = self._pair_trades_filename(self._datadir, pair, trading_mode)
|
||||||
|
# Convert StringDtype columns to object to avoid NaN serialization issues
|
||||||
|
for col in data.select_dtypes(include="string").columns:
|
||||||
|
data[col] = data[col].astype(object).where(data[col].notna(), other=None)
|
||||||
trades = data.values.tolist()
|
trades = data.values.tolist()
|
||||||
misc.file_dump_json(filename, trades, is_zip=self._use_zip)
|
misc.file_dump_json(filename, trades, is_zip=self._use_zip)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pandas import DataFrame, read_parquet, to_datetime
|
from pandas import DataFrame, read_parquet
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
||||||
@@ -68,7 +68,7 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
"volume": "float",
|
"volume": "float",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
pairdata["date"] = pairdata["date"].dt.as_unit("ms")
|
||||||
return pairdata
|
return pairdata
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
|
|||||||
+197
-26
@@ -140,7 +140,7 @@ def _calc_drawdown_series(
|
|||||||
max_drawdown_df["drawdown_relative"] = (max_balance - cumulative_balance) / max_balance
|
max_drawdown_df["drawdown_relative"] = (max_balance - cumulative_balance) / max_balance
|
||||||
else:
|
else:
|
||||||
# NOTE: This is not completely accurate,
|
# NOTE: This is not completely accurate,
|
||||||
# but might good enough if starting_balance is not available
|
# but will be good enough if starting_balance is not available
|
||||||
max_drawdown_df["drawdown_relative"] = (
|
max_drawdown_df["drawdown_relative"] = (
|
||||||
max_drawdown_df["high_value"] - max_drawdown_df["cumulative"]
|
max_drawdown_df["high_value"] - max_drawdown_df["cumulative"]
|
||||||
) / max_drawdown_df["high_value"]
|
) / max_drawdown_df["high_value"]
|
||||||
@@ -296,7 +296,7 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
|||||||
:param final_balance: Final balance to calculate CAGR against
|
:param final_balance: Final balance to calculate CAGR against
|
||||||
:return: CAGR
|
:return: CAGR
|
||||||
"""
|
"""
|
||||||
if final_balance < 0:
|
if (final_balance < 0) or (starting_balance <= 0) or (days_passed <= 0):
|
||||||
# With leveraged trades, final_balance can become negative.
|
# With leveraged trades, final_balance can become negative.
|
||||||
return 0
|
return 0
|
||||||
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||||
@@ -333,6 +333,72 @@ def calculate_expectancy(trades: pd.DataFrame) -> tuple[float, float]:
|
|||||||
return expectancy, expectancy_ratio
|
return expectancy, expectancy_ratio
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_annualized_ratio(
|
||||||
|
expected_returns_mean: float,
|
||||||
|
denominator: float,
|
||||||
|
annualization_factor: int = 365,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Helper function to calculate annualized ratios like Sharpe and Sortino.
|
||||||
|
:param expected_returns_mean: Mean of the returns (expected returns)
|
||||||
|
:param denominator: Denominator of the ratio (e.g. standard deviation for Sharpe)
|
||||||
|
:param annualization_factor: Factor to annualize the ratio (default is 365 for daily returns)
|
||||||
|
:return: Annualized ratio, or -100.0 if denominator is zero or NaN to indicate this is
|
||||||
|
not optimal.
|
||||||
|
"""
|
||||||
|
if denominator != 0 and not np.isnan(denominator):
|
||||||
|
return float(expected_returns_mean / denominator * np.sqrt(annualization_factor))
|
||||||
|
|
||||||
|
# Define high (negative) ratio to be clear that this is NOT optimal.
|
||||||
|
return -100.0
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_daily_returns_from_balance(
|
||||||
|
balance_history: pd.DataFrame,
|
||||||
|
date_col: str,
|
||||||
|
balance_col: str,
|
||||||
|
) -> pd.Series:
|
||||||
|
wallet = _prepare_balance_history(balance_history, date_col, balance_col)
|
||||||
|
if len(wallet) == 0:
|
||||||
|
return pd.DataFrame(columns=[date_col, balance_col])
|
||||||
|
|
||||||
|
# Sample balance to daily end-of-day values to normalize variable snapshot frequency.
|
||||||
|
daily_balance = (
|
||||||
|
wallet.set_index(date_col)[balance_col].resample("1D").last().dropna().rename(balance_col)
|
||||||
|
)
|
||||||
|
daily_balance = daily_balance.reset_index()
|
||||||
|
|
||||||
|
if len(daily_balance) < 2:
|
||||||
|
return pd.Series(dtype=float)
|
||||||
|
|
||||||
|
return daily_balance[balance_col].pct_change().dropna()
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_balance_history(
|
||||||
|
balance_history: pd.DataFrame,
|
||||||
|
date_col: str,
|
||||||
|
balance_col: str,
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Prepare balance history for calculations by filtering out rows with
|
||||||
|
missing date or balance values.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
len(balance_history) == 0
|
||||||
|
or date_col not in balance_history
|
||||||
|
or balance_col not in balance_history
|
||||||
|
):
|
||||||
|
return pd.DataFrame(columns=[date_col, balance_col])
|
||||||
|
|
||||||
|
wallet = balance_history.loc[:, [date_col, balance_col]].copy()
|
||||||
|
wallet = wallet.dropna(subset=[date_col, balance_col]).sort_values(date_col)
|
||||||
|
|
||||||
|
if len(wallet) == 0:
|
||||||
|
return pd.DataFrame(columns=[date_col, balance_col])
|
||||||
|
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
def calculate_sortino(
|
def calculate_sortino(
|
||||||
trades: pd.DataFrame,
|
trades: pd.DataFrame,
|
||||||
min_date: datetime | None,
|
min_date: datetime | None,
|
||||||
@@ -354,14 +420,31 @@ def calculate_sortino(
|
|||||||
|
|
||||||
down_stdev = np.std(trades.loc[trades["profit_abs"] < 0, "profit_abs"] / starting_balance)
|
down_stdev = np.std(trades.loc[trades["profit_abs"] < 0, "profit_abs"] / starting_balance)
|
||||||
|
|
||||||
if down_stdev != 0 and not np.isnan(down_stdev):
|
return _calculate_annualized_ratio(expected_returns_mean, down_stdev)
|
||||||
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
|
|
||||||
else:
|
|
||||||
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
|
|
||||||
sortino_ratio = -100
|
|
||||||
|
|
||||||
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
|
||||||
return sortino_ratio
|
def calculate_sortino_from_balance(
|
||||||
|
balance_history: pd.DataFrame,
|
||||||
|
date_col: str = "date",
|
||||||
|
balance_col: str = "total_quote",
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate sortino ratio from historical balance snapshots.
|
||||||
|
|
||||||
|
:param balance_history: DataFrame containing at least date and balance columns
|
||||||
|
:param date_col: Column containing timestamps
|
||||||
|
:param balance_col: Column containing historical balance values
|
||||||
|
:return: sortino
|
||||||
|
"""
|
||||||
|
daily_returns = _calculate_daily_returns_from_balance(balance_history, date_col, balance_col)
|
||||||
|
|
||||||
|
if len(daily_returns) == 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
expected_returns_mean = daily_returns.mean()
|
||||||
|
downside_returns = daily_returns[daily_returns < 0]
|
||||||
|
down_stdev = downside_returns.std(ddof=0)
|
||||||
|
return _calculate_annualized_ratio(expected_returns_mean, down_stdev)
|
||||||
|
|
||||||
|
|
||||||
def calculate_sharpe(
|
def calculate_sharpe(
|
||||||
@@ -384,14 +467,67 @@ def calculate_sharpe(
|
|||||||
expected_returns_mean = total_profit.sum() / days_period
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
up_stdev = np.std(total_profit)
|
up_stdev = np.std(total_profit)
|
||||||
|
|
||||||
if up_stdev != 0:
|
return _calculate_annualized_ratio(expected_returns_mean, up_stdev)
|
||||||
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
|
||||||
else:
|
|
||||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
|
||||||
sharp_ratio = -100
|
|
||||||
|
|
||||||
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
|
||||||
return sharp_ratio
|
def calculate_sharpe_from_balance(
|
||||||
|
balance_history: pd.DataFrame,
|
||||||
|
date_col: str = "date",
|
||||||
|
balance_col: str = "total_quote",
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate sharpe ratio from historical balance snapshots.
|
||||||
|
|
||||||
|
:param balance_history: DataFrame containing at least date and balance columns
|
||||||
|
:param date_col: Column containing timestamps
|
||||||
|
:param balance_col: Column containing historical balance values
|
||||||
|
:return: sharpe
|
||||||
|
"""
|
||||||
|
daily_returns = _calculate_daily_returns_from_balance(balance_history, date_col, balance_col)
|
||||||
|
|
||||||
|
if len(daily_returns) == 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
expected_returns_mean = daily_returns.mean()
|
||||||
|
up_stdev = daily_returns.std(ddof=0)
|
||||||
|
return _calculate_annualized_ratio(expected_returns_mean, up_stdev)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_max_drawdown_from_balance(
|
||||||
|
balance_history: pd.DataFrame,
|
||||||
|
date_col: str = "date",
|
||||||
|
balance_col: str = "total_quote",
|
||||||
|
relative: bool = False,
|
||||||
|
) -> DrawDownResult:
|
||||||
|
"""
|
||||||
|
Calculate max drawdown from historical balance snapshots.
|
||||||
|
|
||||||
|
:param balance_history: DataFrame containing at least date and balance columns
|
||||||
|
:param date_col: Column containing timestamps
|
||||||
|
:param balance_col: Column containing historical balance values
|
||||||
|
:param relative: If True, use relative drawdown for max calculation instead of absolute
|
||||||
|
:return: DrawDownResult object
|
||||||
|
:raise: ValueError if balance-history dataframe was found empty.
|
||||||
|
"""
|
||||||
|
wallet = _prepare_balance_history(
|
||||||
|
balance_history=balance_history,
|
||||||
|
date_col=date_col,
|
||||||
|
balance_col=balance_col,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(wallet) < 2:
|
||||||
|
raise ValueError("Balance-history dataframe empty.")
|
||||||
|
|
||||||
|
starting_balance = float(wallet[balance_col].iloc[0])
|
||||||
|
wallet.loc[:, "total_balance"] = wallet[balance_col].diff().fillna(0.0)
|
||||||
|
|
||||||
|
return calculate_max_drawdown(
|
||||||
|
wallet,
|
||||||
|
date_col=date_col,
|
||||||
|
value_col="total_balance",
|
||||||
|
starting_balance=starting_balance,
|
||||||
|
relative=relative,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def calculate_calmar(
|
def calculate_calmar(
|
||||||
@@ -401,12 +537,12 @@ def calculate_calmar(
|
|||||||
starting_balance: float,
|
starting_balance: float,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate calmar
|
Calculate calmar from trades data.
|
||||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||||
:return: calmar
|
:return: calmar
|
||||||
"""
|
"""
|
||||||
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
return 0
|
return 0.0
|
||||||
|
|
||||||
total_profit = trades["profit_abs"].sum() / starting_balance
|
total_profit = trades["profit_abs"].sum() / starting_balance
|
||||||
days_period = max(1, (max_date - min_date).days)
|
days_period = max(1, (max_date - min_date).days)
|
||||||
@@ -422,16 +558,51 @@ def calculate_calmar(
|
|||||||
)
|
)
|
||||||
max_drawdown = drawdown.relative_account_drawdown
|
max_drawdown = drawdown.relative_account_drawdown
|
||||||
except ValueError:
|
except ValueError:
|
||||||
max_drawdown = 0
|
return 0.0
|
||||||
|
|
||||||
if max_drawdown != 0:
|
return _calculate_annualized_ratio(expected_returns_mean, max_drawdown)
|
||||||
calmar_ratio = expected_returns_mean / max_drawdown * math.sqrt(365)
|
|
||||||
else:
|
|
||||||
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
|
||||||
calmar_ratio = -100
|
|
||||||
|
|
||||||
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
|
||||||
return calmar_ratio
|
def calculate_calmar_from_balance(
|
||||||
|
balance_history: pd.DataFrame,
|
||||||
|
date_col: str = "date",
|
||||||
|
balance_col: str = "total_quote",
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculate calmar ratio from historical balance snapshots.
|
||||||
|
|
||||||
|
:param balance_history: DataFrame containing at least date and balance columns
|
||||||
|
:param date_col: Column containing timestamps
|
||||||
|
:param balance_col: Column containing historical balance values
|
||||||
|
:return: calmar
|
||||||
|
"""
|
||||||
|
wallet = _prepare_balance_history(
|
||||||
|
balance_history=balance_history,
|
||||||
|
date_col=date_col,
|
||||||
|
balance_col=balance_col,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(wallet) < 2:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
starting_balance = float(wallet[balance_col].iloc[0])
|
||||||
|
final_balance = float(wallet[balance_col].iloc[-1])
|
||||||
|
days_period = max(1, (wallet[date_col].iloc[-1] - wallet[date_col].iloc[0]).days)
|
||||||
|
|
||||||
|
total_profit = (final_balance - starting_balance) / starting_balance
|
||||||
|
expected_returns_mean = total_profit / days_period * 100
|
||||||
|
|
||||||
|
try:
|
||||||
|
drawdown = calculate_max_drawdown_from_balance(
|
||||||
|
wallet,
|
||||||
|
date_col=date_col,
|
||||||
|
balance_col=balance_col,
|
||||||
|
)
|
||||||
|
max_drawdown = drawdown.relative_account_drawdown
|
||||||
|
except ValueError:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return _calculate_annualized_ratio(expected_returns_mean, max_drawdown)
|
||||||
|
|
||||||
|
|
||||||
def calculate_sqn(trades: pd.DataFrame, starting_balance: float) -> float:
|
def calculate_sqn(trades: pd.DataFrame, starting_balance: float) -> float:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from freqtrade.exchange.exchange_utils import (
|
|||||||
validate_exchange,
|
validate_exchange,
|
||||||
)
|
)
|
||||||
from freqtrade.exchange.exchange_utils_timeframe import (
|
from freqtrade.exchange.exchange_utils_timeframe import (
|
||||||
|
timeframe_to_floor_freq,
|
||||||
timeframe_to_minutes,
|
timeframe_to_minutes,
|
||||||
timeframe_to_msecs,
|
timeframe_to_msecs,
|
||||||
timeframe_to_next_date,
|
timeframe_to_next_date,
|
||||||
@@ -43,6 +44,7 @@ from freqtrade.exchange.htx import Htx
|
|||||||
from freqtrade.exchange.hyperliquid import Hyperliquid
|
from freqtrade.exchange.hyperliquid import Hyperliquid
|
||||||
from freqtrade.exchange.idex import Idex
|
from freqtrade.exchange.idex import Idex
|
||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
|
from freqtrade.exchange.krakenfutures import Krakenfutures
|
||||||
from freqtrade.exchange.kucoin import Kucoin
|
from freqtrade.exchange.kucoin import Kucoin
|
||||||
from freqtrade.exchange.lbank import Lbank
|
from freqtrade.exchange.lbank import Lbank
|
||||||
from freqtrade.exchange.luno import Luno
|
from freqtrade.exchange.luno import Luno
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ class Binance(Exchange):
|
|||||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||||
"ws_enabled": True,
|
"ws_enabled": True,
|
||||||
"has_delisting": True,
|
"has_delisting": True,
|
||||||
|
# Demo trading
|
||||||
|
# https://www.binance.com/en/support/faq/detail/9be58f73e5e14338809e3b705b9687dd
|
||||||
|
# Intentionally Disabled as it's a separate market - not a simulated live market.
|
||||||
|
"supports_demo_trading": False,
|
||||||
}
|
}
|
||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
"ohlcv_candle_limit": 499,
|
"ohlcv_candle_limit": 499,
|
||||||
@@ -555,7 +559,7 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
|
|
||||||
class Binanceusdm(Binance):
|
class Binanceusdm(Binance):
|
||||||
"""Binacne USDM Exchange
|
"""Binance USDM Exchange
|
||||||
Same as Binance - only futures trading is supported (via ccxt).
|
Same as Binance - only futures trading is supported (via ccxt).
|
||||||
|
|
||||||
Not actually necessary, binance should be preferred.
|
Not actually necessary, binance should be preferred.
|
||||||
|
|||||||
+16919
-12164
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,10 @@ from datetime import datetime, timedelta
|
|||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import (
|
from freqtrade.exceptions import (
|
||||||
DDosProtection,
|
DDosProtection,
|
||||||
|
InvalidOrderException,
|
||||||
OperationalException,
|
OperationalException,
|
||||||
RetryableOrderError,
|
RetryableOrderError,
|
||||||
TemporaryError,
|
TemporaryError,
|
||||||
@@ -38,6 +39,13 @@ class Bitget(Exchange):
|
|||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
"funding_fee_candle_limit": 100,
|
"funding_fee_candle_limit": 100,
|
||||||
"has_delisting": True,
|
"has_delisting": True,
|
||||||
|
"stop_price_param": "stopLossPrice",
|
||||||
|
"stop_price_prop": "stopLossPrice",
|
||||||
|
"stop_price_type_field": "triggerType",
|
||||||
|
"stop_price_type_value_mapping": {
|
||||||
|
PriceType.LAST: "fill_price",
|
||||||
|
PriceType.MARK: "mark_price",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
@@ -94,30 +102,36 @@ class Bitget(Exchange):
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
|
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> CcxtOrder:
|
||||||
params2 = {
|
# old stoploss orders
|
||||||
"stop": True,
|
paramsold = {"stop": True}
|
||||||
}
|
# new stoploss orders with stopLossPrice (used in futures starting 2026.4)
|
||||||
for method in (
|
paramsnew = {"planType": "profit_loss"}
|
||||||
self._api.fetch_open_orders,
|
params_to_try = (
|
||||||
self._api.fetch_canceled_and_closed_orders,
|
(paramsnew, paramsold) if self.trading_mode == TradingMode.FUTURES else (paramsold,)
|
||||||
):
|
)
|
||||||
try:
|
|
||||||
orders = method(pair, params=params2)
|
for params2 in params_to_try:
|
||||||
orders_f = [order for order in orders if order["id"] == order_id]
|
for method in (
|
||||||
if orders_f:
|
self._api.fetch_open_orders,
|
||||||
order = orders_f[0]
|
self._api.fetch_canceled_and_closed_orders,
|
||||||
self._log_exchange_response("get_stop_order_fallback", order)
|
):
|
||||||
return self._convert_stop_order(pair, order_id, order)
|
try:
|
||||||
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
orders = method(pair, params=params2)
|
||||||
pass
|
orders_f = [order for order in orders if order["id"] == order_id]
|
||||||
except ccxt.DDoSProtection as e:
|
if orders_f:
|
||||||
raise DDosProtection(e) from e
|
order = orders_f[0]
|
||||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
self._log_exchange_response("get_stop_order_fallback", order)
|
||||||
raise TemporaryError(
|
return self._convert_stop_order(pair, order_id, order)
|
||||||
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||||
) from e
|
pass
|
||||||
except ccxt.BaseError as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise OperationalException(e) from 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}).")
|
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
||||||
|
|
||||||
@retrier(retries=API_RETRY_COUNT)
|
@retrier(retries=API_RETRY_COUNT)
|
||||||
@@ -129,6 +143,19 @@ class Bitget(Exchange):
|
|||||||
|
|
||||||
return self._fetch_stop_order_fallback(order_id, pair)
|
return self._fetch_stop_order_fallback(order_id, pair)
|
||||||
|
|
||||||
|
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||||
|
cancel_params = params.copy() if params else {}
|
||||||
|
cancel_params["stop"] = True
|
||||||
|
|
||||||
|
if self.trading_mode != TradingMode.FUTURES:
|
||||||
|
return self.cancel_order(order_id, pair, cancel_params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.cancel_order(order_id, pair, {**cancel_params, "planType": "pos_loss"})
|
||||||
|
except (InvalidOrderException, IndexError):
|
||||||
|
# Keep compatibility with stoploss orders created by older versions.
|
||||||
|
return self.cancel_order(order_id, pair, cancel_params)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def additional_exchange_init(self) -> None:
|
def additional_exchange_init(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -150,12 +177,6 @@ class Bitget(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
|
||||||
if self.trading_mode != TradingMode.SPOT:
|
|
||||||
# Explicitly setting margin_mode is not necessary as marginMode can be set per order.
|
|
||||||
# self.set_margin_mode(pair, self.margin_mode, accept_fail)
|
|
||||||
self._set_leverage(leverage, pair, accept_fail)
|
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
side: BuySell,
|
side: BuySell,
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class Bybit(Exchange):
|
|||||||
# TODO: Can be removed once bybit fully forces all accounts to unified mode.
|
# TODO: Can be removed once bybit fully forces all accounts to unified mode.
|
||||||
"fetchOrder": False,
|
"fetchOrder": False,
|
||||||
},
|
},
|
||||||
|
# Demo trading
|
||||||
|
# https://learn.bybit.com/en/bybit-guide/how-to-use-bybit-demo-trading
|
||||||
|
"supports_demo_trading": True,
|
||||||
}
|
}
|
||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
|
|||||||
@@ -51,12 +51,10 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool:
|
|||||||
if not valid:
|
if not valid:
|
||||||
if check_for_bad:
|
if check_for_bad:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.'
|
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.')
|
||||||
f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}.'
|
|
||||||
)
|
|
||||||
|
|
||||||
if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES:
|
if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES:
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ BAD_EXCHANGES = {
|
|||||||
"bitmex": "Various reasons",
|
"bitmex": "Various reasons",
|
||||||
"probit": "Requires additional, regular calls to `signIn()`",
|
"probit": "Requires additional, regular calls to `signIn()`",
|
||||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders",
|
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders",
|
||||||
"krakenfutures": "Unsupported futures exchange",
|
|
||||||
"kucoinfutures": "Unsupported futures exchange",
|
"kucoinfutures": "Unsupported futures exchange",
|
||||||
"poloniexfutures": "Unsupported futures exchange",
|
"poloniexfutures": "Unsupported futures exchange",
|
||||||
"binancecoinm": "Unsupported futures exchange",
|
"binancecoinm": "Unsupported futures exchange",
|
||||||
@@ -63,6 +62,7 @@ SUPPORTED_EXCHANGES = [
|
|||||||
"htx",
|
"htx",
|
||||||
"hyperliquid",
|
"hyperliquid",
|
||||||
"kraken",
|
"kraken",
|
||||||
|
"krakenfutures",
|
||||||
"okx",
|
"okx",
|
||||||
"myokx",
|
"myokx",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from datetime import UTC, datetime, timedelta
|
|||||||
from math import floor, isnan
|
from math import floor, isnan
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Literal, TypeGuard, TypeVar
|
from typing import Any, Literal, TypeGuard, TypeVar
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import ccxt.pro as ccxt_pro
|
import ccxt.pro as ccxt_pro
|
||||||
@@ -249,7 +250,7 @@ class Exchange:
|
|||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
# Holds all open sell orders for dry_run
|
||||||
self._dry_run_open_orders: dict[str, Any] = {}
|
self._dry_run_open_orders: dict[str, Any] = {}
|
||||||
|
self._is_demo_trading = exchange_conf.get("demo_trading", False)
|
||||||
if self._config["dry_run"]:
|
if self._config["dry_run"]:
|
||||||
logger.info("Instance is running with dry_run enabled")
|
logger.info("Instance is running with dry_run enabled")
|
||||||
logger.info(f"Using CCXT {ccxt.__version__}")
|
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||||
@@ -365,6 +366,7 @@ class Exchange:
|
|||||||
self.validate_pricing(config["exit_pricing"])
|
self.validate_pricing(config["exit_pricing"])
|
||||||
self.validate_pricing(config["entry_pricing"])
|
self.validate_pricing(config["entry_pricing"])
|
||||||
self.validate_orderflow(config["exchange"])
|
self.validate_orderflow(config["exchange"])
|
||||||
|
self.validate_demo_trading(config["exchange"])
|
||||||
self.validate_freqai(config)
|
self.validate_freqai(config)
|
||||||
|
|
||||||
self._set_startup_candle_count(config)
|
self._set_startup_candle_count(config)
|
||||||
@@ -418,6 +420,9 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
|
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
|
||||||
|
|
||||||
|
if self.get_option("supports_demo_trading") and exchange_config.get("demo_trading", False):
|
||||||
|
api.enable_demo_trading(True)
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -433,12 +438,12 @@ class Exchange:
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""exchange Name (from ccxt)"""
|
"""exchange Name (from ccxt)"""
|
||||||
return self._api.name
|
return self._api.name if not self._is_demo_trading else f"{self._api.name} (Demo)"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> str:
|
def id(self) -> str:
|
||||||
"""exchange ccxt id"""
|
"""exchange ccxt id"""
|
||||||
return self._api.id
|
return self._api.id if not self._is_demo_trading else f"{self._api.id}_demo"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timeframes(self) -> list[str]:
|
def timeframes(self) -> list[str]:
|
||||||
@@ -825,7 +830,8 @@ class Exchange:
|
|||||||
and order_types["stoploss_price_type"] not in price_mapping
|
and order_types["stoploss_price_type"] not in price_mapping
|
||||||
):
|
):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f"On exchange stoploss price type is not supported for {self.name}."
|
f"On exchange stoploss price type '{order_types['stoploss_price_type']}' "
|
||||||
|
f"is not supported for {self.name}."
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_pricing(self, pricing: dict) -> None:
|
def validate_pricing(self, pricing: dict) -> None:
|
||||||
@@ -869,6 +875,16 @@ class Exchange:
|
|||||||
"fetching historic OHLCV data, otherwise freqAI will not work."
|
"fetching historic OHLCV data, otherwise freqAI will not work."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_demo_trading(self, exchange_conf: dict) -> None:
|
||||||
|
"""Validate demo trading configuration
|
||||||
|
Prevents accidental configuration with wrong expectations.
|
||||||
|
"""
|
||||||
|
if exchange_conf.get("demo_trading", False):
|
||||||
|
if not self.get_option("supports_demo_trading"):
|
||||||
|
raise ConfigurationError(f"Demo trading is not supported for {self.name}.")
|
||||||
|
else:
|
||||||
|
logger.info(f"Demo trading enabled for {self.name}")
|
||||||
|
|
||||||
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
|
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
|
||||||
"""
|
"""
|
||||||
Checks if required startup_candles is more than ohlcv_candle_limit().
|
Checks if required startup_candles is more than ohlcv_candle_limit().
|
||||||
@@ -1137,7 +1153,7 @@ class Exchange:
|
|||||||
stop_price: float | None = None,
|
stop_price: float | None = None,
|
||||||
) -> CcxtOrder:
|
) -> CcxtOrder:
|
||||||
now = dt_now()
|
now = dt_now()
|
||||||
order_id = f"dry_run_{side}_{pair}_{now.timestamp()}"
|
order_id = f"dry_run_{side}_{pair}_{uuid4()}"
|
||||||
# Rounding here must respect to contract sizes
|
# Rounding here must respect to contract sizes
|
||||||
_amount = self._contracts_to_amount(
|
_amount = self._contracts_to_amount(
|
||||||
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||||
@@ -1896,9 +1912,12 @@ class Exchange:
|
|||||||
orders = []
|
orders = []
|
||||||
if self.exchange_has("fetchClosedOrders"):
|
if self.exchange_has("fetchClosedOrders"):
|
||||||
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
||||||
if self.exchange_has("fetchOpenOrders"):
|
if self.exchange_has("fetchCanceledOrders"):
|
||||||
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
orders_canceled = self._api.fetch_canceled_orders(pair, since=since_ms)
|
||||||
orders.extend(orders_open)
|
orders.extend(orders_canceled)
|
||||||
|
if self.exchange_has("fetchOpenOrders"):
|
||||||
|
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
||||||
|
orders.extend(orders_open)
|
||||||
return orders
|
return orders
|
||||||
|
|
||||||
@retrier(retries=0)
|
@retrier(retries=0)
|
||||||
@@ -2651,11 +2670,11 @@ class Exchange:
|
|||||||
if self._can_use_websocket(self._exchange_ws, pair, timeframe, candle_type):
|
if self._can_use_websocket(self._exchange_ws, pair, timeframe, candle_type):
|
||||||
candle_ts = dt_ts(timeframe_to_prev_date(timeframe))
|
candle_ts = dt_ts(timeframe_to_prev_date(timeframe))
|
||||||
prev_candle_ts = dt_ts(date_minus_candles(timeframe, 1))
|
prev_candle_ts = dt_ts(date_minus_candles(timeframe, 1))
|
||||||
candles = self._exchange_ws.ohlcvs(pair, timeframe)
|
candles, last_refresh_time = self._exchange_ws.get_ohlcv_with_refresh(
|
||||||
half_candle = int(candle_ts - (candle_ts - prev_candle_ts) * 0.5)
|
pair, timeframe, candle_type
|
||||||
last_refresh_time = int(
|
|
||||||
self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0)
|
|
||||||
)
|
)
|
||||||
|
last_refresh_time = int(last_refresh_time)
|
||||||
|
half_candle = int(candle_ts - (candle_ts - prev_candle_ts) * 0.5)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
candles
|
candles
|
||||||
@@ -3931,7 +3950,6 @@ class Exchange:
|
|||||||
is_short: bool,
|
is_short: bool,
|
||||||
open_date: datetime,
|
open_date: datetime,
|
||||||
close_date: datetime,
|
close_date: datetime,
|
||||||
time_in_ratio: float | None = None,
|
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
calculates the sum of all funding fees that occurred for a pair during a futures trade
|
calculates the sum of all funding fees that occurred for a pair during a futures trade
|
||||||
@@ -3941,7 +3959,6 @@ class Exchange:
|
|||||||
:param is_short: trade direction
|
:param is_short: trade direction
|
||||||
:param open_date: The date and time that the trade started
|
:param open_date: The date and time that the trade started
|
||||||
:param close_date: The date and time that the trade ended
|
:param close_date: The date and time that the trade ended
|
||||||
:param time_in_ratio: Not used by most exchange classes
|
|
||||||
"""
|
"""
|
||||||
fees: float = 0
|
fees: float = 0
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ class FtHas(TypedDict, total=False):
|
|||||||
|
|
||||||
# Delisting check
|
# Delisting check
|
||||||
has_delisting: bool
|
has_delisting: bool
|
||||||
|
# Demo mode - this is not sandbox but an exchange-provided demo mode.
|
||||||
|
supports_demo_trading: bool
|
||||||
|
|
||||||
|
|
||||||
class Ticker(TypedDict):
|
class Ticker(TypedDict):
|
||||||
|
|||||||
@@ -29,6 +29,21 @@ def timeframe_to_msecs(timeframe: str) -> int:
|
|||||||
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
||||||
|
|
||||||
|
|
||||||
|
def timeframe_to_floor_freq(timeframe: str) -> str:
|
||||||
|
"""
|
||||||
|
Translates the timeframe interval value written in the human readable
|
||||||
|
form ('1m', '5m', '1h', '1d', '1w', etc.) to the desired floor frequency used by pandas
|
||||||
|
("1m", "5m", "1h", "1d", "1w", etc.).
|
||||||
|
Will use minute for most higher timeframes.
|
||||||
|
"""
|
||||||
|
timeframe_seconds = timeframe_to_seconds(timeframe)
|
||||||
|
timeframe_minutes = timeframe_seconds // 60
|
||||||
|
if timeframe_minutes <= 1:
|
||||||
|
return "1s"
|
||||||
|
else:
|
||||||
|
return "1min"
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_resample_freq(timeframe: str) -> str:
|
def timeframe_to_resample_freq(timeframe: str) -> str:
|
||||||
"""
|
"""
|
||||||
Translates the timeframe interval value written in the human readable
|
Translates the timeframe interval value written in the human readable
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from threading import Thread
|
from threading import Event, RLock, Thread
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
@@ -24,49 +23,71 @@ class ExchangeWS:
|
|||||||
self.config = config
|
self.config = config
|
||||||
self._ccxt_object = ccxt_object
|
self._ccxt_object = ccxt_object
|
||||||
self._background_tasks: set[asyncio.Task] = set()
|
self._background_tasks: set[asyncio.Task] = set()
|
||||||
|
self._state_lock = RLock()
|
||||||
|
self._loop_ready = Event()
|
||||||
|
|
||||||
self._klines_watching: set[PairWithTimeframe] = set()
|
self._klines_watching: set[PairWithTimeframe] = set()
|
||||||
self._klines_scheduled: set[PairWithTimeframe] = set()
|
self._klines_scheduled: set[PairWithTimeframe] = set()
|
||||||
self.klines_last_refresh: dict[PairWithTimeframe, float] = {}
|
self._klines_last_refresh: dict[PairWithTimeframe, float] = {}
|
||||||
self.klines_last_request: dict[PairWithTimeframe, float] = {}
|
self._klines_last_request: dict[PairWithTimeframe, float] = {}
|
||||||
self._thread = Thread(name="ccxt_ws", target=self._start_forever)
|
self._thread = Thread(name="ccxt_ws", target=self._start_forever)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
self.__cleanup_called = False
|
|
||||||
|
|
||||||
def _start_forever(self) -> None:
|
def _start_forever(self) -> None:
|
||||||
self._loop = asyncio.new_event_loop()
|
self._loop = asyncio.new_event_loop()
|
||||||
|
self._loop_ready.set()
|
||||||
try:
|
try:
|
||||||
self._loop.run_forever()
|
self._loop.run_forever()
|
||||||
finally:
|
finally:
|
||||||
if self._loop.is_running():
|
if not self._loop.is_closed():
|
||||||
self._loop.stop()
|
# Cancel remaining tasks and close the loop in the owning thread.
|
||||||
|
pending = asyncio.all_tasks(self._loop)
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
if pending:
|
||||||
|
self._loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||||
|
self._loop.run_until_complete(self._loop.shutdown_asyncgens())
|
||||||
|
self._loop.close()
|
||||||
|
self._loop_ready.clear()
|
||||||
|
|
||||||
|
def _wait_for_loop(self, timeout: float = 1.0) -> bool:
|
||||||
|
"""
|
||||||
|
Wait for the event loop to be ready
|
||||||
|
Returns True once the loop is ready.
|
||||||
|
Will probably only return false during startup/shutdown.
|
||||||
|
"""
|
||||||
|
if hasattr(self, "_loop"):
|
||||||
|
return True
|
||||||
|
return self._loop_ready.wait(timeout=timeout) and hasattr(self, "_loop")
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
logger.debug("Cleanup called - stopping")
|
logger.debug("Cleanup called - stopping")
|
||||||
self._klines_watching.clear()
|
with self._state_lock:
|
||||||
for task in self._background_tasks:
|
self._klines_watching.clear()
|
||||||
|
tasks = list(self._background_tasks)
|
||||||
|
for task in tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
if hasattr(self, "_loop") and not self._loop.is_closed():
|
if self._wait_for_loop(timeout=0.2) and not self._loop.is_closed():
|
||||||
self.reset_connections()
|
self.reset_connections(cleanup=True)
|
||||||
|
|
||||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||||
time.sleep(0.1)
|
self._thread.join(timeout=5)
|
||||||
if not self._loop.is_closed():
|
if self._thread.is_alive():
|
||||||
self._loop.close()
|
logger.warning("Websocket loop thread did not stop within timeout.")
|
||||||
|
|
||||||
self._thread.join()
|
|
||||||
logger.debug("Stopped")
|
logger.debug("Stopped")
|
||||||
|
|
||||||
def reset_connections(self) -> None:
|
def reset_connections(self, cleanup: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Reset all connections - avoids "connection-reset" errors that happen after ~9 days
|
Reset all connections - avoids "connection-reset" errors that happen after ~9 days
|
||||||
"""
|
"""
|
||||||
if hasattr(self, "_loop") and not self._loop.is_closed():
|
if self._wait_for_loop() and not self._loop.is_closed():
|
||||||
logger.info("Resetting WS connections.")
|
logger.info(f"{'Cleaning up' if cleanup else 'Resetting'} exchange WS connections.")
|
||||||
asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop)
|
try:
|
||||||
while not self.__cleanup_called:
|
fut = asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop)
|
||||||
time.sleep(0.1)
|
fut.result(timeout=10)
|
||||||
self.__cleanup_called = False
|
except TimeoutError:
|
||||||
|
logger.warning("Timed out while resetting websocket connections.")
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Exception while resetting websocket connections")
|
||||||
|
|
||||||
async def _cleanup_async(self) -> None:
|
async def _cleanup_async(self) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -76,15 +97,14 @@ class ExchangeWS:
|
|||||||
self._ccxt_object.ohlcvs.clear()
|
self._ccxt_object.ohlcvs.clear()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Exception in _cleanup_async")
|
logger.exception("Exception in _cleanup_async")
|
||||||
finally:
|
|
||||||
self.__cleanup_called = True
|
|
||||||
|
|
||||||
def _pop_history(self, paircomb: PairWithTimeframe) -> None:
|
def _pop_history(self, paircomb: PairWithTimeframe) -> None:
|
||||||
"""
|
"""
|
||||||
Remove history for a pair/timeframe combination from ccxt cache
|
Remove history for a pair/timeframe combination from ccxt cache
|
||||||
"""
|
"""
|
||||||
self._ccxt_object.ohlcvs.get(paircomb[0], {}).pop(paircomb[1], None)
|
with self._state_lock:
|
||||||
self.klines_last_refresh.pop(paircomb, None)
|
self._ccxt_object.ohlcvs.get(paircomb[0], {}).pop(paircomb[1], None)
|
||||||
|
self._klines_last_refresh.pop(paircomb, None)
|
||||||
|
|
||||||
@retrier(retries=3)
|
@retrier(retries=3)
|
||||||
def ohlcvs(self, pair: str, timeframe: str) -> list[list]:
|
def ohlcvs(self, pair: str, timeframe: str) -> list[list]:
|
||||||
@@ -100,81 +120,129 @@ class ExchangeWS:
|
|||||||
# TemporaryError does not cause backoff - so we're essentially retrying immediately
|
# TemporaryError does not cause backoff - so we're essentially retrying immediately
|
||||||
raise TemporaryError(f"Error deepcopying: {e}") from e
|
raise TemporaryError(f"Error deepcopying: {e}") from e
|
||||||
|
|
||||||
|
def get_ohlcv_with_refresh(
|
||||||
|
self, pair: str, timeframe: str, candle_type: CandleType
|
||||||
|
) -> tuple[list[list], float]:
|
||||||
|
"""
|
||||||
|
Get deepcopied klines and update the last refresh time
|
||||||
|
"""
|
||||||
|
ohlcvs = self.ohlcvs(pair, timeframe)
|
||||||
|
with self._state_lock:
|
||||||
|
last_refresh = self._klines_last_refresh.get((pair, timeframe, candle_type), 0)
|
||||||
|
return ohlcvs, last_refresh
|
||||||
|
|
||||||
def cleanup_expired(self) -> None:
|
def cleanup_expired(self) -> None:
|
||||||
"""
|
"""
|
||||||
Remove pairs from watchlist if they've not been requested within
|
Remove pairs from watchlist if they've not been requested within
|
||||||
the last timeframe (+ offset)
|
the last timeframe (+ offset)
|
||||||
"""
|
"""
|
||||||
changed = False
|
changed = False
|
||||||
for p in list(self._klines_watching):
|
with self._state_lock:
|
||||||
_, timeframe, _ = p
|
for p in list(self._klines_watching):
|
||||||
timeframe_s = timeframe_to_seconds(timeframe)
|
_, timeframe, _ = p
|
||||||
last_refresh = self.klines_last_request.get(p, 0)
|
timeframe_s = timeframe_to_seconds(timeframe)
|
||||||
if last_refresh > 0 and (dt_ts() - last_refresh) > ((timeframe_s + 20) * 1000):
|
last_refresh = self._klines_last_request.get(p, 0)
|
||||||
logger.info(f"Removing {p} from websocket watchlist.")
|
if last_refresh > 0 and (dt_ts() - last_refresh) > ((timeframe_s + 20) * 1000):
|
||||||
self._klines_watching.discard(p)
|
logger.info(f"Removing {p} from websocket watchlist.")
|
||||||
# Pop history to avoid getting stale data
|
self._klines_watching.discard(p)
|
||||||
self._pop_history(p)
|
# Pop history to avoid getting stale data
|
||||||
changed = True
|
self._pop_history(p)
|
||||||
|
changed = True
|
||||||
if changed:
|
if changed:
|
||||||
logger.info(f"Removal done: new watch list ({len(self._klines_watching)})")
|
logger.info(f"Removal done: new watch list ({len(self._klines_watching)})")
|
||||||
|
|
||||||
async def _schedule_while_true(self) -> None:
|
async def _schedule_while_true(self) -> None:
|
||||||
# For the ones we should be watching
|
# For the ones we should be watching
|
||||||
for p in self._klines_watching:
|
with self._state_lock:
|
||||||
|
pairs_to_check = list(self._klines_watching)
|
||||||
|
|
||||||
|
for p in pairs_to_check:
|
||||||
# Check if they're already scheduled
|
# Check if they're already scheduled
|
||||||
if p not in self._klines_scheduled:
|
with self._state_lock:
|
||||||
|
if p in self._klines_scheduled:
|
||||||
|
continue
|
||||||
self._klines_scheduled.add(p)
|
self._klines_scheduled.add(p)
|
||||||
pair, timeframe, candle_type = p
|
pair, timeframe, candle_type = p
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
self._continuously_async_watch_ohlcv(pair, timeframe, candle_type)
|
self._continuously_async_watch_ohlcv(pair, timeframe, candle_type)
|
||||||
)
|
)
|
||||||
|
with self._state_lock:
|
||||||
self._background_tasks.add(task)
|
self._background_tasks.add(task)
|
||||||
task.add_done_callback(
|
task.add_done_callback(
|
||||||
partial(
|
partial(
|
||||||
self._continuous_stopped,
|
self._continuous_stopped,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
candle_type=candle_type,
|
candle_type=candle_type,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if exchange implements a specific API endpoint.
|
||||||
|
Wrapper around ccxt 'has' attribute
|
||||||
|
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
return endpoint in self._ccxt_object.has and self._ccxt_object.has[endpoint]
|
||||||
|
|
||||||
async def _unwatch_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None:
|
async def _unwatch_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None:
|
||||||
try:
|
try:
|
||||||
await self._ccxt_object.un_watch_ohlcv_for_symbols([[pair, timeframe]])
|
if self.exchange_has("unWatchOHLCVForSymbols"):
|
||||||
|
await self._ccxt_object.un_watch_ohlcv_for_symbols([[pair, timeframe]])
|
||||||
|
elif self.exchange_has("unWatchOHLCV"):
|
||||||
|
await self._ccxt_object.un_watch_ohlcv(pair, timeframe)
|
||||||
|
else:
|
||||||
|
logger.debug("un_watch_ohlcv not supported for %s, %s", pair, timeframe)
|
||||||
|
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
logger.debug("un_watch_ohlcv_for_symbols not supported: %s", e)
|
logger.debug("un_watch_ohlcv_for_symbols not supported: %s", e)
|
||||||
pass
|
pass
|
||||||
|
except ccxt.NetworkError as e:
|
||||||
|
# Network errors are common on shutdown so we can ignore them.
|
||||||
|
# It's a network error - which most likely means that the connection is already closed.
|
||||||
|
logger.debug("Network error during unwatch for %s, %s: %s", pair, timeframe, e)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Exception in _unwatch_ohlcv")
|
logger.exception(f"Exception in _unwatch_ohlcv for {pair}, {timeframe},")
|
||||||
|
|
||||||
def _continuous_stopped(
|
def _continuous_stopped(
|
||||||
self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType
|
self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType
|
||||||
):
|
) -> None:
|
||||||
self._background_tasks.discard(task)
|
with self._state_lock:
|
||||||
|
self._background_tasks.discard(task)
|
||||||
result = "done"
|
result = "done"
|
||||||
if task.cancelled():
|
try:
|
||||||
result = "cancelled"
|
if task.cancelled():
|
||||||
else:
|
result = "cancelled"
|
||||||
if (result1 := task.result()) is not None:
|
else:
|
||||||
result = str(result1)
|
if (result1 := task.result()) is not None:
|
||||||
|
result = str(result1)
|
||||||
|
except Exception:
|
||||||
|
result = "error"
|
||||||
|
logger.exception(f"Unhandled exception in watch task callback for {pair}, {timeframe}")
|
||||||
|
finally:
|
||||||
|
logger.info(f"{pair}, {timeframe}, {candle_type} - Task finished - {result}")
|
||||||
|
if hasattr(self, "_loop") and not self._loop.is_closed():
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self._unwatch_ohlcv(pair, timeframe, candle_type), loop=self._loop
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"{pair}, {timeframe}, {candle_type} - Task finished - {result}")
|
with self._state_lock:
|
||||||
asyncio.run_coroutine_threadsafe(
|
self._klines_scheduled.discard((pair, timeframe, candle_type))
|
||||||
self._unwatch_ohlcv(pair, timeframe, candle_type), loop=self._loop
|
self._pop_history((pair, timeframe, candle_type))
|
||||||
)
|
|
||||||
|
|
||||||
self._klines_scheduled.discard((pair, timeframe, candle_type))
|
|
||||||
self._pop_history((pair, timeframe, candle_type))
|
|
||||||
|
|
||||||
async def _continuously_async_watch_ohlcv(
|
async def _continuously_async_watch_ohlcv(
|
||||||
self, pair: str, timeframe: str, candle_type: CandleType
|
self, pair: str, timeframe: str, candle_type: CandleType
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
while (pair, timeframe, candle_type) in self._klines_watching:
|
while True:
|
||||||
|
with self._state_lock:
|
||||||
|
if (pair, timeframe, candle_type) not in self._klines_watching:
|
||||||
|
break
|
||||||
start = dt_ts()
|
start = dt_ts()
|
||||||
data = await self._ccxt_object.watch_ohlcv(pair, timeframe)
|
data = await self._ccxt_object.watch_ohlcv(pair, timeframe)
|
||||||
self.klines_last_refresh[(pair, timeframe, candle_type)] = dt_ts()
|
with self._state_lock:
|
||||||
|
self._klines_last_refresh[(pair, timeframe, candle_type)] = dt_ts()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"watch done {pair}, {timeframe}, data {len(data)} "
|
f"watch done {pair}, {timeframe}, data {len(data)} "
|
||||||
f"in {(dt_ts() - start) / 1000:.3f}s"
|
f"in {(dt_ts() - start) / 1000:.3f}s"
|
||||||
@@ -184,14 +252,19 @@ class ExchangeWS:
|
|||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception(f"Exception in continuously_async_watch_ohlcv for {pair}, {timeframe}")
|
logger.exception(f"Exception in continuously_async_watch_ohlcv for {pair}, {timeframe}")
|
||||||
finally:
|
finally:
|
||||||
self._klines_watching.discard((pair, timeframe, candle_type))
|
with self._state_lock:
|
||||||
|
self._klines_watching.discard((pair, timeframe, candle_type))
|
||||||
|
|
||||||
def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None:
|
def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None:
|
||||||
"""
|
"""
|
||||||
Schedule a pair/timeframe combination to be watched
|
Schedule a pair/timeframe combination to be watched
|
||||||
"""
|
"""
|
||||||
self._klines_watching.add((pair, timeframe, candle_type))
|
if not self._wait_for_loop():
|
||||||
self.klines_last_request[(pair, timeframe, candle_type)] = dt_ts()
|
logger.warning(f"Websocket loop not ready. Could not schedule {pair}, {timeframe}.")
|
||||||
|
return
|
||||||
|
with self._state_lock:
|
||||||
|
self._klines_watching.add((pair, timeframe, candle_type))
|
||||||
|
self._klines_last_request[(pair, timeframe, candle_type)] = dt_ts()
|
||||||
# asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop)
|
# asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop)
|
||||||
asyncio.run_coroutine_threadsafe(self._schedule_while_true(), loop=self._loop)
|
asyncio.run_coroutine_threadsafe(self._schedule_while_true(), loop=self._loop)
|
||||||
self.cleanup_expired()
|
self.cleanup_expired()
|
||||||
@@ -207,12 +280,10 @@ class ExchangeWS:
|
|||||||
Returns cached klines from ccxt's "watch" cache.
|
Returns cached klines from ccxt's "watch" cache.
|
||||||
:param candle_ts: timestamp of the end-time of the candle we expect.
|
:param candle_ts: timestamp of the end-time of the candle we expect.
|
||||||
"""
|
"""
|
||||||
# Deepcopy the response - as it might be modified in the background as new messages arrive
|
candles, refresh_date = self.get_ohlcv_with_refresh(pair, timeframe, candle_type)
|
||||||
candles = self.ohlcvs(pair, timeframe)
|
|
||||||
refresh_date = self.klines_last_refresh[(pair, timeframe, candle_type)]
|
|
||||||
received_ts = candles[-1][0] if candles else 0
|
received_ts = candles[-1][0] if candles else 0
|
||||||
drop_hint = received_ts >= candle_ts
|
drop_hint = received_ts >= candle_ts
|
||||||
if received_ts > refresh_date:
|
if refresh_date and received_ts > refresh_date:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{pair}, {timeframe} - Candle date > last refresh "
|
f"{pair}, {timeframe} - Candle date > last refresh "
|
||||||
f"({format_ms_time(received_ts)} > {format_ms_time_det(refresh_date)}). "
|
f"({format_ms_time(received_ts)} > {format_ms_time_det(refresh_date)}). "
|
||||||
|
|||||||
@@ -5,11 +5,20 @@ from copy import deepcopy
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES
|
from freqtrade.enums.runmode import NON_UTIL_MODES
|
||||||
from freqtrade.exceptions import ConfigurationError, ExchangeError, OperationalException
|
from freqtrade.exceptions import (
|
||||||
|
ConfigurationError,
|
||||||
|
DDosProtection,
|
||||||
|
ExchangeError,
|
||||||
|
OperationalException,
|
||||||
|
TemporaryError,
|
||||||
|
)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, CcxtPosition, FtHas
|
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, CcxtPosition, FtHas
|
||||||
from freqtrade.util.datetime_helpers import dt_from_ts
|
from freqtrade.util.datetime_helpers import dt_from_ts
|
||||||
|
|
||||||
@@ -22,6 +31,8 @@ class Hyperliquid(Exchange):
|
|||||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
unified_account = False
|
||||||
|
|
||||||
_ft_has: FtHas = {
|
_ft_has: FtHas = {
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
"l2_limit_range": [20],
|
"l2_limit_range": [20],
|
||||||
@@ -58,6 +69,38 @@ class Hyperliquid(Exchange):
|
|||||||
config.update(super()._ccxt_config)
|
config.update(super()._ccxt_config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def additional_exchange_init(self) -> None:
|
||||||
|
"""
|
||||||
|
Additional exchange initialization logic.
|
||||||
|
.api will be available at this point.
|
||||||
|
Query User account Account Type to determine unified account status
|
||||||
|
https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#query-a-users-abstraction-state
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.trading_mode == TradingMode.FUTURES and not self._config["dry_run"]:
|
||||||
|
# Determine account status
|
||||||
|
# Unified accounts must use the spot endpoint for balances
|
||||||
|
request = {
|
||||||
|
"type": "userAbstraction",
|
||||||
|
"user": self._api.walletAddress,
|
||||||
|
}
|
||||||
|
response = self._api.publicPostInfo(request)
|
||||||
|
self.unified_account = response in ('"unifiedAccount"', '"portfolioMargin"')
|
||||||
|
if self.unified_account:
|
||||||
|
logger.info("Unified Hyperliquid account detected.")
|
||||||
|
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def _get_configured_hip3_dexes(self) -> list[str]:
|
def _get_configured_hip3_dexes(self) -> list[str]:
|
||||||
"""Get list of configured HIP-3 DEXes."""
|
"""Get list of configured HIP-3 DEXes."""
|
||||||
return self._config.get("exchange", {}).get("hip3_dexes", [])
|
return self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||||
@@ -122,28 +165,33 @@ class Hyperliquid(Exchange):
|
|||||||
This override is not absolutely necessary and is only there for correct used / total values
|
This override is not absolutely necessary and is only there for correct used / total values
|
||||||
which are however not used by Freqtrade in futures mode at the moment.
|
which are however not used by Freqtrade in futures mode at the moment.
|
||||||
"""
|
"""
|
||||||
balances = super().get_balances()
|
params = params or {}
|
||||||
dexes = self._get_configured_hip3_dexes()
|
if self.unified_account:
|
||||||
for dex in dexes:
|
params["type"] = "spot"
|
||||||
try:
|
balances = super().get_balances(params)
|
||||||
dex_balance = super().get_balances(params={"dex": dex})
|
if not self.unified_account:
|
||||||
|
# In unified accounts, the balance already includes all DEXes
|
||||||
|
dexes = self._get_configured_hip3_dexes()
|
||||||
|
for dex in dexes:
|
||||||
|
try:
|
||||||
|
dex_balance = super().get_balances(params={"dex": dex})
|
||||||
|
|
||||||
for currency, amount_info in dex_balance.items():
|
for currency, amount_info in dex_balance.items():
|
||||||
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
|
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if currency not in balances:
|
if currency not in balances:
|
||||||
balances[currency] = amount_info
|
balances[currency] = amount_info
|
||||||
else:
|
else:
|
||||||
balances[currency]["free"] += amount_info["free"]
|
balances[currency]["free"] += amount_info["free"]
|
||||||
balances[currency]["used"] += amount_info["used"]
|
balances[currency]["used"] += amount_info["used"]
|
||||||
balances[currency]["total"] += amount_info["total"]
|
balances[currency]["total"] += amount_info["total"]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
||||||
|
|
||||||
if dexes:
|
if dexes:
|
||||||
self._log_exchange_response("fetch_balance", balances, add_info="combined")
|
self._log_exchange_response("fetch_balance", balances, add_info="combined")
|
||||||
return balances
|
return balances
|
||||||
|
|
||||||
def fetch_positions(
|
def fetch_positions(
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
"""Kraken exchange subclass"""
|
"""Kraken exchange subclass"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
@@ -40,7 +38,6 @@ class Kraken(Exchange):
|
|||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
(TradingMode.SPOT, MarginMode.NONE),
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||||
# (TradingMode.FUTURES, MarginMode.CROSS)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
||||||
@@ -114,18 +111,6 @@ class Kraken(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def _set_leverage(
|
|
||||||
self,
|
|
||||||
leverage: float,
|
|
||||||
pair: str | None = None,
|
|
||||||
accept_fail: bool = False,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Kraken set's the leverage as an option in the order object, so we need to
|
|
||||||
add it to params
|
|
||||||
"""
|
|
||||||
return
|
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
side: BuySell,
|
side: BuySell,
|
||||||
@@ -148,41 +133,6 @@ class Kraken(Exchange):
|
|||||||
params["postOnly"] = True
|
params["postOnly"] = True
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def calculate_funding_fees(
|
|
||||||
self,
|
|
||||||
df: DataFrame,
|
|
||||||
amount: float,
|
|
||||||
is_short: bool,
|
|
||||||
open_date: datetime,
|
|
||||||
close_date: datetime,
|
|
||||||
time_in_ratio: float | None = None,
|
|
||||||
) -> float:
|
|
||||||
"""
|
|
||||||
# ! This method will always error when run by Freqtrade because time_in_ratio is never
|
|
||||||
# ! passed to _get_funding_fee. For kraken futures to work in dry run and backtesting
|
|
||||||
# ! functionality must be added that passes the parameter time_in_ratio to
|
|
||||||
# ! _get_funding_fee when using Kraken
|
|
||||||
calculates the sum of all funding fees that occurred for a pair during a futures trade
|
|
||||||
:param df: Dataframe containing combined funding and mark rates
|
|
||||||
as `open_fund` and `open_mark`.
|
|
||||||
:param amount: The quantity of the trade
|
|
||||||
:param is_short: trade direction
|
|
||||||
:param open_date: The date and time that the trade started
|
|
||||||
:param close_date: The date and time that the trade ended
|
|
||||||
:param time_in_ratio: Not used by most exchange classes
|
|
||||||
"""
|
|
||||||
if not time_in_ratio:
|
|
||||||
raise OperationalException(
|
|
||||||
f"time_in_ratio is required for {self.name}._get_funding_fee"
|
|
||||||
)
|
|
||||||
fees: float = 0
|
|
||||||
|
|
||||||
if not df.empty:
|
|
||||||
df = df[(df["date"] >= open_date) & (df["date"] <= close_date)]
|
|
||||||
fees = sum(df["open_fund"] * df["open_mark"] * amount * time_in_ratio)
|
|
||||||
|
|
||||||
return fees if is_short else -fees
|
|
||||||
|
|
||||||
def _get_trade_pagination_next_value(self, trades: list[dict]):
|
def _get_trade_pagination_next_value(self, trades: list[dict]):
|
||||||
"""
|
"""
|
||||||
Extract pagination id for the next "from_id" value
|
Extract pagination id for the next "from_id" value
|
||||||
|
|||||||
@@ -0,0 +1,300 @@
|
|||||||
|
"""Kraken Futures exchange subclass"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
|
from freqtrade.exceptions import (
|
||||||
|
DDosProtection,
|
||||||
|
ExchangeError,
|
||||||
|
InvalidOrderException,
|
||||||
|
OperationalException,
|
||||||
|
TemporaryError,
|
||||||
|
)
|
||||||
|
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
|
||||||
|
from freqtrade.exchange.exchange import Exchange
|
||||||
|
from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, FtHas
|
||||||
|
from freqtrade.misc import safe_value_nested
|
||||||
|
from freqtrade.util.datetime_helpers import dt_from_ts
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Krakenfutures(Exchange):
|
||||||
|
"""Kraken Futures exchange class.
|
||||||
|
|
||||||
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
|
|
||||||
|
Key differences from spot Kraken:
|
||||||
|
- Stop orders use triggerPrice/triggerSignal instead of stopPrice
|
||||||
|
- Flex (multi-collateral) accounts need USD balance synthesis
|
||||||
|
"""
|
||||||
|
|
||||||
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
|
]
|
||||||
|
|
||||||
|
_ft_has: FtHas = {
|
||||||
|
"tickers_have_quoteVolume": False,
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {
|
||||||
|
"limit": "limit",
|
||||||
|
"market": "market",
|
||||||
|
},
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
|
"stop_price_param": "triggerPrice",
|
||||||
|
"stop_price_prop": "stopPrice",
|
||||||
|
"stop_price_type_field": "triggerSignal",
|
||||||
|
"stop_price_type_value_mapping": {
|
||||||
|
PriceType.LAST: "last",
|
||||||
|
PriceType.MARK: "mark",
|
||||||
|
PriceType.INDEX: "index",
|
||||||
|
},
|
||||||
|
"exchange_has_overrides": {"fetchOrders": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||||
|
"""
|
||||||
|
Fetch balances with USD synthesis for flex (multi-collateral) accounts.
|
||||||
|
|
||||||
|
Kraken Futures flex accounts hold multiple currencies as collateral.
|
||||||
|
CCXT returns per-currency balances but doesn't expose margin values
|
||||||
|
as a USD balance. This override synthesizes a USD entry from flex account data
|
||||||
|
when stake_currency is USD.
|
||||||
|
|
||||||
|
Field mapping (margin-centric for internal consistency):
|
||||||
|
- free: availableMargin (margin available for new positions)
|
||||||
|
- total: marginEquity (haircut-adjusted collateral + unrealized P&L)
|
||||||
|
- used: total - free (margin currently in use)
|
||||||
|
|
||||||
|
Fallback chain for total: marginEquity -> portfolioValue -> balanceValue
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
balances = self._api.fetch_balance(params or {})
|
||||||
|
|
||||||
|
# Only synthesize USD if stake_currency is USD
|
||||||
|
stake = str(self._config.get("stake_currency", "")).upper()
|
||||||
|
if stake == "USD":
|
||||||
|
# Only synthesize if USD stake - flex only applies for these currencies.
|
||||||
|
# For flex accounts, synthesize USD balance from margin values
|
||||||
|
info = balances.get("info", {})
|
||||||
|
accounts = info.get("accounts", {}) if isinstance(info, dict) else {}
|
||||||
|
flex = accounts.get("flex", {}) if isinstance(accounts, dict) else {}
|
||||||
|
|
||||||
|
if flex:
|
||||||
|
usd_free = self._safe_float(flex.get("availableMargin"))
|
||||||
|
# Prefer marginEquity for consistency (same basis as availableMargin)
|
||||||
|
raw_total = (
|
||||||
|
flex.get("marginEquity")
|
||||||
|
or flex.get("portfolioValue")
|
||||||
|
or flex.get("balanceValue")
|
||||||
|
)
|
||||||
|
usd_total = self._safe_float(raw_total)
|
||||||
|
if usd_free is not None or usd_total is not None:
|
||||||
|
# Use available value for both if only one is present
|
||||||
|
usd_free_value = usd_free if usd_free is not None else usd_total
|
||||||
|
usd_total_value = usd_total if usd_total is not None else usd_free
|
||||||
|
if usd_free_value is not None and usd_total_value is not None:
|
||||||
|
usd_used = max(0.0, usd_total_value - usd_free_value)
|
||||||
|
balances["USD"] = {
|
||||||
|
"free": usd_free_value,
|
||||||
|
"used": usd_used,
|
||||||
|
"total": usd_total_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove additional info from ccxt results (same as base class)
|
||||||
|
balances.pop("info", None)
|
||||||
|
balances.pop("free", None)
|
||||||
|
balances.pop("total", None)
|
||||||
|
balances.pop("used", None)
|
||||||
|
|
||||||
|
self._log_exchange_response("fetch_balance", balances, add_info=params)
|
||||||
|
return balances
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not get balance due to {e.__class__.__name__}. Message: {e}"
|
||||||
|
) from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_float(value: Any) -> float | None:
|
||||||
|
"""Convert value to float, returning None if conversion fails."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _order_contracts_to_amount(self, order: CcxtOrder) -> CcxtOrder:
|
||||||
|
"""Normalize order and apply Kraken Futures-specific order corrections."""
|
||||||
|
order = super()._order_contracts_to_amount(order)
|
||||||
|
return self._adjust_krakenfutures_order(order)
|
||||||
|
|
||||||
|
def _adjust_krakenfutures_order(self, order: CcxtOrder) -> CcxtOrder:
|
||||||
|
"""Apply Kraken Futures-specific order corrections.
|
||||||
|
|
||||||
|
For filled terminal orders, always fetch trades and compute VWAP because
|
||||||
|
CCXT's average is still unreliable.
|
||||||
|
|
||||||
|
See: https://github.com/ccxt/ccxt/issues/27996
|
||||||
|
"""
|
||||||
|
if order.get("status") == "canceled" and order.get("filled") is None:
|
||||||
|
# Workaround for missing filled parsing - https://github.com/ccxt/ccxt/issues/28210
|
||||||
|
order["filled"] = safe_value_nested(order, "info.order.filled", default_value=None)
|
||||||
|
|
||||||
|
filled = self._safe_float(order.get("filled")) or 0.0
|
||||||
|
if order.get("status") in ("canceled", "closed") and filled > 0:
|
||||||
|
# Compute VWAP and cost for filled orders.
|
||||||
|
trades = self.get_trades_for_order(
|
||||||
|
order["id"], order["symbol"], since=dt_from_ts(order["timestamp"])
|
||||||
|
)
|
||||||
|
if trades:
|
||||||
|
total_amount = sum(t["amount"] for t in trades)
|
||||||
|
if total_amount:
|
||||||
|
# Compute VWAP
|
||||||
|
order["average"] = sum(t["price"] * t["amount"] for t in trades) / total_amount
|
||||||
|
trade_costs = [t["cost"] for t in trades if t.get("cost") is not None]
|
||||||
|
if trade_costs:
|
||||||
|
order["cost"] = sum(trade_costs)
|
||||||
|
return order
|
||||||
|
|
||||||
|
def get_trades_for_order(
|
||||||
|
self, order_id: str, pair: str, since: datetime, params: dict | None = None
|
||||||
|
) -> list:
|
||||||
|
"""Fetch trades and enrich with calculated fees.
|
||||||
|
|
||||||
|
Kraken Futures' /fills endpoint does not include fee amounts — only
|
||||||
|
fillType (maker/taker). This enriches each trade with a calculated fee
|
||||||
|
using the market's fee schedule so Freqtrade's fee detection works.
|
||||||
|
"""
|
||||||
|
trades = super().get_trades_for_order(order_id, pair, since, params)
|
||||||
|
for trade in trades:
|
||||||
|
if trade.get("fee") is None or trade["fee"].get("cost") is None:
|
||||||
|
taker_or_maker = trade.get("takerOrMaker", "taker")
|
||||||
|
symbol = trade.get("symbol", pair)
|
||||||
|
market = self.markets.get(symbol, {})
|
||||||
|
fee_rate = market.get(taker_or_maker, market.get("taker", 0.0005))
|
||||||
|
cost = trade.get("cost")
|
||||||
|
if cost is not None and fee_rate is not None:
|
||||||
|
trade["fee"] = {
|
||||||
|
"cost": cost * fee_rate,
|
||||||
|
"currency": market.get("quote", "USD"),
|
||||||
|
"rate": fee_rate,
|
||||||
|
}
|
||||||
|
return trades
|
||||||
|
|
||||||
|
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||||
|
def fetch_order(
|
||||||
|
self, order_id: str, pair: str, params: dict[str, Any] | None = None
|
||||||
|
) -> CcxtOrder:
|
||||||
|
"""Fetch order with direct CCXT call and fallback to history endpoints."""
|
||||||
|
if self._config.get("dry_run"):
|
||||||
|
return self.fetch_dry_run_order(order_id)
|
||||||
|
|
||||||
|
params = params or {}
|
||||||
|
status_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")}
|
||||||
|
try:
|
||||||
|
order = self._api.fetch_order(order_id, pair, params=status_params)
|
||||||
|
self._log_exchange_response("fetch_order", order)
|
||||||
|
return self._order_contracts_to_amount(order)
|
||||||
|
except ccxt.OrderNotFound:
|
||||||
|
# Expected for older Kraken Futures orders not visible in orders/status.
|
||||||
|
pass
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
msg = f"Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}"
|
||||||
|
raise InvalidOrderException(msg) from e
|
||||||
|
except (ccxt.OperationFailed, ccxt.ExchangeError):
|
||||||
|
# Fallback to history endpoints for temporary/status endpoint gaps.
|
||||||
|
pass
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
order = self._fetch_order_fallback(order_id, pair, params)
|
||||||
|
if order is not None:
|
||||||
|
return order
|
||||||
|
|
||||||
|
# Order not in status, open, closed, or canceled endpoints - genuinely gone.
|
||||||
|
# Raise non-retrying InvalidOrderException (Kraken has limited history retention).
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f"Order not found in any endpoint (pair: {pair} id: {order_id})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fetch_order_fallback(
|
||||||
|
self, order_id: str, pair: str, params: dict[str, Any]
|
||||||
|
) -> CcxtOrder | None:
|
||||||
|
"""Search open, closed, and canceled order endpoints for order_id.
|
||||||
|
|
||||||
|
Kraken Futures' orders/status endpoint only returns currently open orders.
|
||||||
|
Older orders require querying history endpoints (closed/canceled).
|
||||||
|
For stoploss (trigger) orders, the caller should pass stop=True in params
|
||||||
|
(handled automatically via stoploss_query_requires_stop_flag in _ft_has)
|
||||||
|
so that closed/canceled queries hit the trigger history endpoint.
|
||||||
|
"""
|
||||||
|
order_id_str = str(order_id)
|
||||||
|
|
||||||
|
# Open orders include triggers by default. Avoid passing trigger/stop flags
|
||||||
|
# to prevent endpoint/filter mismatches.
|
||||||
|
open_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")}
|
||||||
|
order = self._find_order_in_list(
|
||||||
|
self._api.fetch_open_orders, pair, open_params, order_id_str
|
||||||
|
)
|
||||||
|
if order is not None:
|
||||||
|
return order
|
||||||
|
|
||||||
|
# Closed/canceled: pass params through (including stop=True for stoploss orders,
|
||||||
|
# which CCXT maps to the trigger history endpoint).
|
||||||
|
for fetch_fn in (self._api.fetch_closed_orders, self._api.fetch_canceled_orders):
|
||||||
|
order = self._find_order_in_list(fetch_fn, pair, params, order_id_str)
|
||||||
|
if order is not None:
|
||||||
|
return order
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_order_in_list(
|
||||||
|
self,
|
||||||
|
fetch_fn,
|
||||||
|
symbol: str | None,
|
||||||
|
params: dict[str, Any],
|
||||||
|
order_id_str: str,
|
||||||
|
) -> CcxtOrder | None:
|
||||||
|
"""Fetch orders and return matching order_id, or None."""
|
||||||
|
try:
|
||||||
|
orders = fetch_fn(symbol, params=params) or []
|
||||||
|
self._log_exchange_response(fetch_fn.__name__, orders)
|
||||||
|
for order in orders:
|
||||||
|
if str(order.get("id")) == order_id_str:
|
||||||
|
self._log_exchange_response("fetch_order_fallback", order)
|
||||||
|
|
||||||
|
return self._order_contracts_to_amount(order)
|
||||||
|
except (ccxt.OrderNotFound, ccxt.InvalidOrder) as e:
|
||||||
|
logger.debug(f"{fetch_fn.__name__} failed: {e}")
|
||||||
|
return None
|
||||||
|
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
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_funding_fees(self, pair: str, amount: float, is_short: bool, open_date) -> float:
|
||||||
|
"""Fetch funding fees, returning 0.0 if retrieval fails."""
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
try:
|
||||||
|
return self._fetch_and_calculate_funding_fees(pair, amount, is_short, open_date)
|
||||||
|
except ExchangeError:
|
||||||
|
logger.warning(f"Could not update funding fees for {pair}.")
|
||||||
|
return 0.0
|
||||||
@@ -361,7 +361,7 @@ class FreqaiDataDrawer:
|
|||||||
label_loc = df.columns.get_loc(label)
|
label_loc = df.columns.get_loc(label)
|
||||||
pred_label_loc = predictions.columns.get_loc(label)
|
pred_label_loc = predictions.columns.get_loc(label)
|
||||||
df.iloc[-1, label_loc] = predictions.iloc[-1, pred_label_loc]
|
df.iloc[-1, label_loc] = predictions.iloc[-1, pred_label_loc]
|
||||||
if df[label].dtype == object:
|
if pd.api.types.is_string_dtype(df[label].dtype):
|
||||||
continue
|
continue
|
||||||
label_mean_loc = df.columns.get_loc(f"{label}_mean")
|
label_mean_loc = df.columns.get_loc(f"{label}_mean")
|
||||||
label_std_loc = df.columns.get_loc(f"{label}_std")
|
label_std_loc = df.columns.get_loc(f"{label}_std")
|
||||||
@@ -614,9 +614,13 @@ class FreqaiDataDrawer:
|
|||||||
elif self.model_type == "pytorch":
|
elif self.model_type == "pytorch":
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
zipfile = torch.load(dk.data_path / f"{dk.model_filename}_model.zip")
|
zipfile = torch.load(
|
||||||
model = zipfile["pytrainer"]
|
dk.data_path / f"{dk.model_filename}_model.zip",
|
||||||
model = model.load_from_checkpoint(zipfile)
|
weights_only=False,
|
||||||
|
)
|
||||||
|
# weights_only is necessary due to pytrainer being a serialized python object.
|
||||||
|
_trainer = zipfile["pytrainer"]
|
||||||
|
model = _trainer.load_from_checkpoint(zipfile)
|
||||||
|
|
||||||
if not model:
|
if not model:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ from freqtrade.strategy import merge_informative_pair
|
|||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
|
|
||||||
pd.set_option("future.no_silent_downcasting", True)
|
|
||||||
|
|
||||||
SECONDS_IN_DAY = 86400
|
SECONDS_IN_DAY = 86400
|
||||||
SECONDS_IN_HOUR = 3600
|
SECONDS_IN_HOUR = 3600
|
||||||
|
|
||||||
@@ -239,16 +237,14 @@ class FreqaiDataKitchen:
|
|||||||
filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan)
|
filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan)
|
||||||
|
|
||||||
drop_index = pd.isnull(filtered_df).any(axis=1) # get the rows that have NaNs,
|
drop_index = pd.isnull(filtered_df).any(axis=1) # get the rows that have NaNs,
|
||||||
drop_index = drop_index.replace(True, 1).replace(False, 0).infer_objects(copy=False)
|
drop_index = drop_index.replace(True, 1).replace(False, 0).infer_objects()
|
||||||
if training_filter:
|
if training_filter:
|
||||||
# we don't care about total row number (total no. datapoints) in training, we only care
|
# we don't care about total row number (total no. datapoints) in training, we only care
|
||||||
# about removing any row with NaNs
|
# about removing any row with NaNs
|
||||||
# if labels has multiple columns (user wants to train multiple modelEs), we detect here
|
# if labels has multiple columns (user wants to train multiple modelEs), we detect here
|
||||||
labels = unfiltered_df.filter(label_list or [], axis=1)
|
labels = unfiltered_df.filter(label_list or [], axis=1)
|
||||||
drop_index_labels = pd.isnull(labels).any(axis=1)
|
drop_index_labels = pd.isnull(labels).any(axis=1)
|
||||||
drop_index_labels = (
|
drop_index_labels = drop_index_labels.replace(True, 1).replace(False, 0).infer_objects()
|
||||||
drop_index_labels.replace(True, 1).replace(False, 0).infer_objects(copy=False)
|
|
||||||
)
|
|
||||||
dates = unfiltered_df["date"]
|
dates = unfiltered_df["date"]
|
||||||
filtered_df = filtered_df[
|
filtered_df = filtered_df[
|
||||||
(drop_index == 0) & (drop_index_labels == 0)
|
(drop_index == 0) & (drop_index_labels == 0)
|
||||||
@@ -428,22 +424,28 @@ class FreqaiDataKitchen:
|
|||||||
Get backtest prediction from current backtest period
|
Get backtest prediction from current backtest period
|
||||||
"""
|
"""
|
||||||
|
|
||||||
append_df = DataFrame()
|
# Build dict first and construct DataFrame once to avoid
|
||||||
|
# column-by-column assignment which causes DataFrame fragmentation
|
||||||
|
# and PerformanceWarning on large prediction sets.
|
||||||
|
append_dict: dict[str, Any] = {}
|
||||||
|
|
||||||
for label in predictions.columns:
|
for label in predictions.columns:
|
||||||
append_df[label] = predictions[label]
|
append_dict[label] = predictions[label]
|
||||||
if append_df[label].dtype == object:
|
if pd.api.types.is_string_dtype(predictions[label].dtype):
|
||||||
continue
|
continue
|
||||||
if "labels_mean" in self.data:
|
if "labels_mean" in self.data and label in self.data["labels_mean"]:
|
||||||
append_df[f"{label}_mean"] = self.data["labels_mean"][label]
|
append_dict[f"{label}_mean"] = self.data["labels_mean"][label]
|
||||||
if "labels_std" in self.data:
|
if "labels_std" in self.data and label in self.data["labels_std"]:
|
||||||
append_df[f"{label}_std"] = self.data["labels_std"][label]
|
append_dict[f"{label}_std"] = self.data["labels_std"][label]
|
||||||
|
|
||||||
for extra_col in self.data["extra_returns_per_train"]:
|
for extra_col in self.data["extra_returns_per_train"]:
|
||||||
append_df[f"{extra_col}"] = self.data["extra_returns_per_train"][extra_col]
|
append_dict[f"{extra_col}"] = self.data["extra_returns_per_train"][extra_col]
|
||||||
|
|
||||||
append_df["do_predict"] = do_predict
|
append_dict["do_predict"] = do_predict
|
||||||
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
||||||
append_df["DI_values"] = self.DI_values
|
append_dict["DI_values"] = self.DI_values
|
||||||
|
|
||||||
|
append_df = DataFrame(append_dict)
|
||||||
|
|
||||||
user_cols = [col for col in dataframe_backtest.columns if col.startswith("%%")]
|
user_cols = [col for col in dataframe_backtest.columns if col.startswith("%%")]
|
||||||
cols = ["date"]
|
cols = ["date"]
|
||||||
@@ -873,7 +875,7 @@ class FreqaiDataKitchen:
|
|||||||
|
|
||||||
self.data["labels_mean"], self.data["labels_std"] = {}, {}
|
self.data["labels_mean"], self.data["labels_std"] = {}, {}
|
||||||
for label in self.data_dictionary["train_labels"].columns:
|
for label in self.data_dictionary["train_labels"].columns:
|
||||||
if self.data_dictionary["train_labels"][label].dtype == object:
|
if pd.api.types.is_string_dtype(self.data_dictionary["train_labels"][label].dtype):
|
||||||
continue
|
continue
|
||||||
f = spy.stats.norm.fit(self.data_dictionary["train_labels"][label])
|
f = spy.stats.norm.fit(self.data_dictionary["train_labels"][label])
|
||||||
self.data["labels_mean"][label], self.data["labels_std"][label] = f[0], f[1]
|
self.data["labels_mean"][label], self.data["labels_std"][label] = f[0], f[1]
|
||||||
@@ -899,7 +901,7 @@ class FreqaiDataKitchen:
|
|||||||
self.find_labels(dataframe)
|
self.find_labels(dataframe)
|
||||||
|
|
||||||
for key in self.label_list:
|
for key in self.label_list:
|
||||||
if dataframe[key].dtype == object:
|
if pd.api.types.is_string_dtype(dataframe[key].dtype):
|
||||||
self.unique_classes[key] = dataframe[key].dropna().unique()
|
self.unique_classes[key] = dataframe[key].dropna().unique()
|
||||||
|
|
||||||
if self.unique_classes:
|
if self.unique_classes:
|
||||||
@@ -984,7 +986,7 @@ class FreqaiDataKitchen:
|
|||||||
are populated.
|
are populated.
|
||||||
|
|
||||||
The main example use is when predicting maxima and minima, the argrelextrema
|
The main example use is when predicting maxima and minima, the argrelextrema
|
||||||
function cannot know the maxima/minima at the edges of the timerange. To improve
|
function cannot know the maxima/minima at the edges of the timerange. To improve
|
||||||
model accuracy, it is best to compute argrelextrema on the full timerange
|
model accuracy, it is best to compute argrelextrema on the full timerange
|
||||||
and then use this function to cut off the edges (buffer) by the kernel.
|
and then use this function to cut off the edges (buffer) by the kernel.
|
||||||
|
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ class IFreqaiModel(ABC):
|
|||||||
self.set_start_dry_live_date(strat_df)
|
self.set_start_dry_live_date(strat_df)
|
||||||
|
|
||||||
for label in hist_preds_df.columns:
|
for label in hist_preds_df.columns:
|
||||||
if hist_preds_df[label].dtype == object:
|
if pd.api.types.is_string_dtype(hist_preds_df[label].dtype):
|
||||||
continue
|
continue
|
||||||
hist_preds_df[f"{label}_mean"] = 0
|
hist_preds_df[f"{label}_mean"] = 0
|
||||||
hist_preds_df[f"{label}_std"] = 0
|
hist_preds_df[f"{label}_std"] = 0
|
||||||
@@ -706,7 +706,7 @@ class IFreqaiModel(ABC):
|
|||||||
num_candles = self.freqai_info.get("fit_live_predictions_candles", 100)
|
num_candles = self.freqai_info.get("fit_live_predictions_candles", 100)
|
||||||
dk.data["labels_mean"], dk.data["labels_std"] = {}, {}
|
dk.data["labels_mean"], dk.data["labels_std"] = {}, {}
|
||||||
for label in full_labels:
|
for label in full_labels:
|
||||||
if self.dd.historic_predictions[dk.pair][label].dtype == object:
|
if pd.api.types.is_string_dtype(self.dd.historic_predictions[dk.pair][label].dtype):
|
||||||
continue
|
continue
|
||||||
f = spy.stats.norm.fit(self.dd.historic_predictions[dk.pair][label].tail(num_candles))
|
f = spy.stats.norm.fit(self.dd.historic_predictions[dk.pair][label].tail(num_candles))
|
||||||
dk.data["labels_mean"][label], dk.data["labels_std"][label] = f[0], f[1]
|
dk.data["labels_mean"][label], dk.data["labels_std"][label] = f[0], f[1]
|
||||||
@@ -896,7 +896,7 @@ class IFreqaiModel(ABC):
|
|||||||
]
|
]
|
||||||
self.fit_live_predictions(self.dk, self.dk.pair)
|
self.fit_live_predictions(self.dk, self.dk.pair)
|
||||||
for label in label_columns:
|
for label in label_columns:
|
||||||
if dk.full_df[label].dtype == object:
|
if pd.api.types.is_string_dtype(dk.full_df[label].dtype):
|
||||||
continue
|
continue
|
||||||
if "labels_mean" in self.dk.data:
|
if "labels_mean" in self.dk.data:
|
||||||
dk.full_df.at[index, f"{label}_mean"] = self.dk.data["labels_mean"][
|
dk.full_df.at[index, f"{label}_mean"] = self.dk.data["labels_mean"][
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from lightgbm import LGBMClassifier
|
from lightgbm import LGBMClassifier
|
||||||
|
|
||||||
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
from freqtrade.freqai.tensorboard import LightGBMCallback
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -46,6 +48,10 @@ class LightGBMClassifier(BaseClassifierModel):
|
|||||||
init_model = self.get_init_model(dk.pair)
|
init_model = self.get_init_model(dk.pair)
|
||||||
|
|
||||||
model = LGBMClassifier(**self.model_training_parameters)
|
model = LGBMClassifier(**self.model_training_parameters)
|
||||||
|
activate_tensorboard = self.freqai_info.get("activate_tensorboard", True)
|
||||||
|
callbacks: list[Callable[..., Any]] = []
|
||||||
|
if LightGBMCallback is not None:
|
||||||
|
callbacks = [LightGBMCallback(dk.data_path, activate_tensorboard)]
|
||||||
model.fit(
|
model.fit(
|
||||||
X=X,
|
X=X,
|
||||||
y=y,
|
y=y,
|
||||||
@@ -53,6 +59,7 @@ class LightGBMClassifier(BaseClassifierModel):
|
|||||||
sample_weight=train_weights,
|
sample_weight=train_weights,
|
||||||
eval_sample_weight=[test_weights],
|
eval_sample_weight=[test_weights],
|
||||||
init_model=init_model,
|
init_model=init_model,
|
||||||
|
callbacks=callbacks,
|
||||||
)
|
)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from lightgbm import LGBMClassifier
|
|||||||
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
||||||
from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier
|
from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
from freqtrade.freqai.tensorboard import LightGBMCallback
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -53,6 +54,11 @@ class LightGBMClassifierMultiTarget(BaseClassifierModel):
|
|||||||
else:
|
else:
|
||||||
init_models = [None] * y.shape[1]
|
init_models = [None] * y.shape[1]
|
||||||
|
|
||||||
|
activate_tensorboard = self.freqai_info.get("activate_tensorboard", True)
|
||||||
|
callbacks = []
|
||||||
|
if LightGBMCallback is not None:
|
||||||
|
callbacks = [LightGBMCallback(dk.data_path, activate_tensorboard)]
|
||||||
|
|
||||||
fit_params = []
|
fit_params = []
|
||||||
for i in range(len(eval_sets)):
|
for i in range(len(eval_sets)):
|
||||||
fit_params.append(
|
fit_params.append(
|
||||||
@@ -60,6 +66,7 @@ class LightGBMClassifierMultiTarget(BaseClassifierModel):
|
|||||||
"eval_set": eval_sets[i],
|
"eval_set": eval_sets[i],
|
||||||
"eval_sample_weight": eval_weights,
|
"eval_sample_weight": eval_weights,
|
||||||
"init_model": init_models[i],
|
"init_model": init_models[i],
|
||||||
|
"callbacks": callbacks,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from lightgbm import LGBMRegressor
|
from lightgbm import LGBMRegressor
|
||||||
|
|
||||||
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
from freqtrade.freqai.tensorboard import LightGBMCallback
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -42,6 +44,11 @@ class LightGBMRegressor(BaseRegressionModel):
|
|||||||
|
|
||||||
model = LGBMRegressor(**self.model_training_parameters)
|
model = LGBMRegressor(**self.model_training_parameters)
|
||||||
|
|
||||||
|
activate_tensorboard = self.freqai_info.get("activate_tensorboard", True)
|
||||||
|
callbacks: list[Callable[..., Any]] = []
|
||||||
|
if LightGBMCallback is not None:
|
||||||
|
callbacks = [LightGBMCallback(dk.data_path, activate_tensorboard)]
|
||||||
|
|
||||||
model.fit(
|
model.fit(
|
||||||
X=X,
|
X=X,
|
||||||
y=y,
|
y=y,
|
||||||
@@ -49,6 +56,7 @@ class LightGBMRegressor(BaseRegressionModel):
|
|||||||
sample_weight=train_weights,
|
sample_weight=train_weights,
|
||||||
eval_sample_weight=[eval_weights],
|
eval_sample_weight=[eval_weights],
|
||||||
init_model=init_model,
|
init_model=init_model,
|
||||||
|
callbacks=callbacks,
|
||||||
)
|
)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from lightgbm import LGBMRegressor
|
|||||||
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
||||||
from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor
|
from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
from freqtrade.freqai.tensorboard import LightGBMCallback
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -55,6 +56,11 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel):
|
|||||||
else:
|
else:
|
||||||
init_models = [None] * y.shape[1]
|
init_models = [None] * y.shape[1]
|
||||||
|
|
||||||
|
activate_tensorboard = self.freqai_info.get("activate_tensorboard", True)
|
||||||
|
callbacks = []
|
||||||
|
if LightGBMCallback is not None:
|
||||||
|
callbacks = [LightGBMCallback(dk.data_path, activate_tensorboard)]
|
||||||
|
|
||||||
fit_params = []
|
fit_params = []
|
||||||
for i in range(len(eval_sets)):
|
for i in range(len(eval_sets)):
|
||||||
fit_params.append(
|
fit_params.append(
|
||||||
@@ -62,6 +68,7 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel):
|
|||||||
"eval_set": eval_sets[i],
|
"eval_set": eval_sets[i],
|
||||||
"eval_sample_weight": eval_weights,
|
"eval_sample_weight": eval_weights,
|
||||||
"init_model": init_models[i],
|
"init_model": init_models[i],
|
||||||
|
"callbacks": callbacks,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class SKLearnRandomForestClassifier(BaseClassifierModel):
|
|||||||
) -> tuple[DataFrame, npt.NDArray[np.int_]]:
|
) -> tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||||
"""
|
"""
|
||||||
Filter the prediction features data and predict with it.
|
Filter the prediction features data and predict with it.
|
||||||
:param unfiltered_df: Full dataframe for the current backtest period.
|
:param unfiltered_df: Full dataframe for the current backtest period.
|
||||||
:return:
|
:return:
|
||||||
:pred_df: dataframe containing the predictions
|
:pred_df: dataframe containing the predictions
|
||||||
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class XGBoostRFClassifier(BaseClassifierModel):
|
|||||||
) -> tuple[DataFrame, npt.NDArray[np.int_]]:
|
) -> tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||||
"""
|
"""
|
||||||
Filter the prediction features data and predict with it.
|
Filter the prediction features data and predict with it.
|
||||||
:param unfiltered_df: Full dataframe for the current backtest period.
|
:param unfiltered_df: Full dataframe for the current backtest period.
|
||||||
:return:
|
:return:
|
||||||
:pred_df: dataframe containing the predictions
|
:pred_df: dataframe containing the predictions
|
||||||
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# ensure users can still use a non-torch freqai version
|
# ensure users can still use a non-torch freqai version
|
||||||
try:
|
try:
|
||||||
|
from freqtrade.freqai.tensorboard.lightgbm_callback import LightGBMTensorboardCallback
|
||||||
from freqtrade.freqai.tensorboard.tensorboard import TensorBoardCallback, TensorboardLogger
|
from freqtrade.freqai.tensorboard.tensorboard import TensorBoardCallback, TensorboardLogger
|
||||||
|
|
||||||
TBLogger = TensorboardLogger
|
TBLogger = TensorboardLogger
|
||||||
TBCallback = TensorBoardCallback
|
TBCallback = TensorBoardCallback
|
||||||
|
LightGBMCallback = LightGBMTensorboardCallback
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
from freqtrade.freqai.tensorboard.base_tensorboard import (
|
from freqtrade.freqai.tensorboard.base_tensorboard import (
|
||||||
BaseTensorBoardCallback,
|
BaseTensorBoardCallback,
|
||||||
@@ -12,5 +14,6 @@ except ModuleNotFoundError:
|
|||||||
|
|
||||||
TBLogger = BaseTensorboardLogger # type: ignore
|
TBLogger = BaseTensorboardLogger # type: ignore
|
||||||
TBCallback = BaseTensorBoardCallback # type: ignore
|
TBCallback = BaseTensorBoardCallback # type: ignore
|
||||||
|
LightGBMCallback = None # type: ignore
|
||||||
|
|
||||||
__all__ = ("TBLogger", "TBCallback")
|
__all__ = ("TBLogger", "TBCallback", "LightGBMCallback")
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from freqtrade.freqai.tensorboard.tensorboard import TensorboardLogger
|
||||||
|
|
||||||
|
|
||||||
|
class LightGBMTensorboardCallback:
|
||||||
|
def __init__(self, logdir, activate: bool) -> None:
|
||||||
|
self.activate = activate
|
||||||
|
self.logger = TensorboardLogger(logdir, activate)
|
||||||
|
|
||||||
|
def __call__(self, env) -> None:
|
||||||
|
if not self.activate:
|
||||||
|
return
|
||||||
|
|
||||||
|
evals = getattr(env, "evaluation_result_list", None)
|
||||||
|
if not evals:
|
||||||
|
return
|
||||||
|
|
||||||
|
for data_name, metric_name, value, _ in evals:
|
||||||
|
self.logger.log_scalar(f"{data_name}-{metric_name}", value, env.iteration)
|
||||||
|
|
||||||
|
end_iteration = getattr(env, "end_iteration", None)
|
||||||
|
if end_iteration is not None and env.iteration + 1 >= end_iteration:
|
||||||
|
self.logger.close()
|
||||||
@@ -63,6 +63,11 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
self.tb_logger = tb_logger
|
self.tb_logger = tb_logger
|
||||||
self.test_batch_counter = 0
|
self.test_batch_counter = 0
|
||||||
|
|
||||||
|
# Early stopping parameters
|
||||||
|
self.early_stopping_patience: int = kwargs.get("early_stopping_patience", 0)
|
||||||
|
self.best_val_loss: float = float("inf")
|
||||||
|
self.patience_counter: int = 0
|
||||||
|
|
||||||
def fit(self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]):
|
def fit(self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]):
|
||||||
"""
|
"""
|
||||||
:param data_dictionary: the dictionary constructed by DataHandler to hold
|
:param data_dictionary: the dictionary constructed by DataHandler to hold
|
||||||
@@ -99,15 +104,40 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
|
|
||||||
# evaluation
|
# evaluation
|
||||||
if "test" in splits:
|
if "test" in splits:
|
||||||
self.estimate_loss(data_loaders_dictionary, "test")
|
val_loss = self.estimate_loss(data_loaders_dictionary, "test")
|
||||||
|
|
||||||
|
# Early stopping check
|
||||||
|
if self.early_stopping_patience > 0 and val_loss is not None:
|
||||||
|
if val_loss < self.best_val_loss:
|
||||||
|
self.best_val_loss = val_loss
|
||||||
|
self.patience_counter = 0
|
||||||
|
else:
|
||||||
|
self.patience_counter += 1
|
||||||
|
if self.patience_counter >= self.early_stopping_patience:
|
||||||
|
logger.info(
|
||||||
|
f"Early stopping triggered after {self.patience_counter} "
|
||||||
|
f"epochs without improvement. "
|
||||||
|
f"Best val_loss: {self.best_val_loss:.6f}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
def estimate_loss(
|
def estimate_loss(
|
||||||
self,
|
self,
|
||||||
data_loader_dictionary: dict[str, DataLoader],
|
data_loader_dictionary: dict[str, DataLoader],
|
||||||
split: str,
|
split: str,
|
||||||
) -> None:
|
) -> float | None:
|
||||||
|
"""
|
||||||
|
Estimate loss on a data split.
|
||||||
|
|
||||||
|
:param data_loader_dictionary: dictionary of data loaders.
|
||||||
|
:param split: split to estimate loss on (e.g. "test").
|
||||||
|
:return: average loss over all batches, or None if no batches.
|
||||||
|
"""
|
||||||
self.model.eval()
|
self.model.eval()
|
||||||
|
total_loss = 0.0
|
||||||
|
num_batches = 0
|
||||||
|
|
||||||
for _, batch_data in enumerate(data_loader_dictionary[split]):
|
for _, batch_data in enumerate(data_loader_dictionary[split]):
|
||||||
xb, yb = batch_data
|
xb, yb = batch_data
|
||||||
xb = xb.to(self.device)
|
xb = xb.to(self.device)
|
||||||
@@ -115,11 +145,17 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
|
|
||||||
yb_pred = self.model(xb)
|
yb_pred = self.model(xb)
|
||||||
loss = self.criterion(yb_pred, yb)
|
loss = self.criterion(yb_pred, yb)
|
||||||
|
total_loss += loss.item()
|
||||||
|
num_batches += 1
|
||||||
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), self.test_batch_counter)
|
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), self.test_batch_counter)
|
||||||
self.test_batch_counter += 1
|
self.test_batch_counter += 1
|
||||||
|
|
||||||
self.model.train()
|
self.model.train()
|
||||||
|
|
||||||
|
if num_batches > 0:
|
||||||
|
return total_loss / num_batches
|
||||||
|
return None
|
||||||
|
|
||||||
def create_data_loaders_dictionary(
|
def create_data_loaders_dictionary(
|
||||||
self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]
|
self, data_dictionary: dict[str, pd.DataFrame], splits: list[str]
|
||||||
) -> dict[str, DataLoader]:
|
) -> dict[str, DataLoader]:
|
||||||
@@ -179,16 +215,12 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
path,
|
path,
|
||||||
)
|
)
|
||||||
|
|
||||||
def load(self, path: Path):
|
|
||||||
checkpoint = torch.load(path)
|
|
||||||
return self.load_from_checkpoint(checkpoint)
|
|
||||||
|
|
||||||
def load_from_checkpoint(self, checkpoint: dict):
|
def load_from_checkpoint(self, checkpoint: dict):
|
||||||
"""
|
"""
|
||||||
when using continual_learning, DataDrawer will load the dictionary
|
when using continual_learning, DataDrawer will load the dictionary
|
||||||
(containing state dicts and model_meta_data) by calling torch.load(path).
|
(containing state dicts and model_meta_data) by calling torch.load(path).
|
||||||
you can access this dict from any class that inherits IFreqaiModel by calling
|
you can access this dict from any class that inherits IFreqaiModel by calling
|
||||||
get_init_model method.
|
the get_init_model method.
|
||||||
"""
|
"""
|
||||||
self.model.load_state_dict(checkpoint["model_state_dict"])
|
self.model.load_state_dict(checkpoint["model_state_dict"])
|
||||||
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
||||||
|
|||||||
+109
-99
@@ -92,100 +92,106 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
exchange_config: ExchangeConfig = deepcopy(config["exchange"])
|
exchange_config: ExchangeConfig = deepcopy(config["exchange"])
|
||||||
# Remove credentials from original exchange config to avoid accidental credential exposure
|
# Remove credentials from original exchange config to avoid accidental credential exposure
|
||||||
remove_exchange_credentials(config["exchange"], True)
|
remove_exchange_credentials(config["exchange"], True)
|
||||||
|
try:
|
||||||
self.exchange = ExchangeResolver.load_exchange(
|
self.exchange = ExchangeResolver.load_exchange(
|
||||||
self.config, exchange_config=exchange_config, load_leverage_tiers=True
|
self.config, exchange_config=exchange_config, load_leverage_tiers=True
|
||||||
)
|
|
||||||
|
|
||||||
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
|
||||||
|
|
||||||
# Check config consistency here since strategies can set certain options
|
|
||||||
validate_config_consistency(config)
|
|
||||||
# Re-validate exchange compatibility
|
|
||||||
self.exchange.validate_config(self.config)
|
|
||||||
|
|
||||||
init_db(self.config["db_url"])
|
|
||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange)
|
|
||||||
|
|
||||||
PairLocks.timeframe = self.config["timeframe"]
|
|
||||||
|
|
||||||
self.trading_mode: TradingMode = self.config.get("trading_mode", TradingMode.SPOT)
|
|
||||||
self.margin_mode: MarginMode = self.config.get("margin_mode", MarginMode.NONE)
|
|
||||||
self.last_process: datetime | None = None
|
|
||||||
|
|
||||||
# RPC runs in separate threads, can start handling external commands just after
|
|
||||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
|
||||||
# so anything in the Freqtradebot instance should be ready (initialized), including
|
|
||||||
# the initial state of the bot.
|
|
||||||
# Keep this at the end of this initialization method.
|
|
||||||
self.rpc: RPCManager = RPCManager(self)
|
|
||||||
|
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange, rpc=self.rpc)
|
|
||||||
self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider)
|
|
||||||
|
|
||||||
self.dataprovider.add_pairlisthandler(self.pairlists)
|
|
||||||
|
|
||||||
# Attach Dataprovider to strategy instance
|
|
||||||
self.strategy.dp = self.dataprovider
|
|
||||||
# Attach Wallets to strategy instance
|
|
||||||
self.strategy.wallets = self.wallets
|
|
||||||
|
|
||||||
# Init ExternalMessageConsumer if enabled
|
|
||||||
self.emc = (
|
|
||||||
ExternalMessageConsumer(self.config, self.dataprovider)
|
|
||||||
if self.config.get("external_message_consumer", {}).get("enabled", False)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Starting initial pairlist refresh")
|
|
||||||
with MeasureTime(
|
|
||||||
lambda duration, _: logger.info(f"Initial Pairlist refresh took {duration:.2f}s"), 0
|
|
||||||
):
|
|
||||||
self.active_pair_whitelist = self._refresh_active_whitelist()
|
|
||||||
|
|
||||||
# Set initial bot state from config
|
|
||||||
initial_state = self.config.get("initial_state")
|
|
||||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
|
||||||
|
|
||||||
# Protect exit-logic from forcesell and vice versa
|
|
||||||
self._exit_lock = Lock()
|
|
||||||
timeframe_secs = timeframe_to_seconds(self.strategy.timeframe)
|
|
||||||
self._exit_reason_cache = PeriodicCache(100, ttl=timeframe_secs)
|
|
||||||
LoggingMixin.__init__(self, logger, timeframe_secs)
|
|
||||||
|
|
||||||
self._schedule = Scheduler()
|
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
|
|
||||||
def update():
|
|
||||||
self.update_funding_fees()
|
|
||||||
self.update_all_liquidation_prices()
|
|
||||||
self.wallets.update()
|
|
||||||
|
|
||||||
# This would be more efficient if scheduled in utc time, and performed at each
|
|
||||||
# funding interval, specified by funding_fee_times on the exchange classes
|
|
||||||
# However, this reduces the precision - and might therefore lead to problems.
|
|
||||||
for time_slot in range(0, 24):
|
|
||||||
for minutes in [1, 31]:
|
|
||||||
t = str(time(time_slot, minutes, 2))
|
|
||||||
self._schedule.every().day.at(t).do(update)
|
|
||||||
|
|
||||||
self._schedule.every().day.at("00:02").do(self.exchange.ws_connection_reset)
|
|
||||||
|
|
||||||
self.strategy.ft_bot_start()
|
|
||||||
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
|
||||||
self.protections = ProtectionManager(self.config, self.strategy.protections)
|
|
||||||
|
|
||||||
def log_took_too_long(duration: float, time_limit: float):
|
|
||||||
logger.warning(
|
|
||||||
f"Strategy analysis took {duration:.2f}s, more than 25% of the timeframe "
|
|
||||||
f"({time_limit:.2f}s). This can lead to delayed orders and missed signals."
|
|
||||||
"Consider either reducing the amount of work your strategy performs "
|
|
||||||
"or reduce the amount of pairs in the Pairlist."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._measure_execution = MeasureTime(log_took_too_long, timeframe_secs * 0.25)
|
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
||||||
|
|
||||||
|
# Check config consistency here since strategies can set certain options
|
||||||
|
validate_config_consistency(config)
|
||||||
|
# Re-validate exchange compatibility
|
||||||
|
self.exchange.validate_config(self.config)
|
||||||
|
|
||||||
|
init_db(self.config["db_url"])
|
||||||
|
|
||||||
|
self.wallets = Wallets(self.config, self.exchange)
|
||||||
|
|
||||||
|
PairLocks.timeframe = self.config["timeframe"]
|
||||||
|
|
||||||
|
self.trading_mode: TradingMode = self.config.get("trading_mode", TradingMode.SPOT)
|
||||||
|
self.margin_mode: MarginMode = self.config.get("margin_mode", MarginMode.NONE)
|
||||||
|
self.last_process: datetime | None = None
|
||||||
|
|
||||||
|
# RPC runs in separate threads, can start handling external commands just after
|
||||||
|
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||||
|
# so anything in the Freqtradebot instance should be ready (initialized), including
|
||||||
|
# the initial state of the bot.
|
||||||
|
# Keep this at the end of this initialization method.
|
||||||
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
|
|
||||||
|
self.dataprovider = DataProvider(self.config, self.exchange, rpc=self.rpc)
|
||||||
|
self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider)
|
||||||
|
|
||||||
|
self.dataprovider.add_pairlisthandler(self.pairlists)
|
||||||
|
|
||||||
|
# Attach Dataprovider to strategy instance
|
||||||
|
self.strategy.dp = self.dataprovider
|
||||||
|
# Attach Wallets to strategy instance
|
||||||
|
self.strategy.wallets = self.wallets
|
||||||
|
|
||||||
|
# Init ExternalMessageConsumer if enabled
|
||||||
|
self.emc: ExternalMessageConsumer | None = (
|
||||||
|
ExternalMessageConsumer(self.config, self.dataprovider)
|
||||||
|
if self.config.get("external_message_consumer", {}).get("enabled", False)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Starting initial pairlist refresh")
|
||||||
|
with MeasureTime(
|
||||||
|
lambda duration, _: logger.info(f"Initial Pairlist refresh took {duration:.2f}s"), 0
|
||||||
|
):
|
||||||
|
self.active_pair_whitelist = self._refresh_active_whitelist()
|
||||||
|
|
||||||
|
# Set initial bot state from config
|
||||||
|
initial_state = self.config.get("initial_state")
|
||||||
|
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||||
|
|
||||||
|
# Protect exit-logic from forcesell and vice versa
|
||||||
|
self._exit_lock = Lock()
|
||||||
|
timeframe_secs = timeframe_to_seconds(self.strategy.timeframe)
|
||||||
|
self._exit_reason_cache = PeriodicCache(100, ttl=timeframe_secs)
|
||||||
|
LoggingMixin.__init__(self, logger, timeframe_secs)
|
||||||
|
|
||||||
|
self._schedule = Scheduler()
|
||||||
|
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
|
||||||
|
def update():
|
||||||
|
self.update_funding_fees()
|
||||||
|
self.update_all_liquidation_prices()
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
|
# This would be more efficient if scheduled in utc time, and performed at each
|
||||||
|
# funding interval, specified by funding_fee_times on the exchange classes
|
||||||
|
# However, this reduces the precision - and might therefore lead to problems.
|
||||||
|
for time_slot in range(0, 24):
|
||||||
|
for minutes in [1, 31]:
|
||||||
|
t = str(time(time_slot, minutes, 2))
|
||||||
|
self._schedule.every().day.at(t).do(update)
|
||||||
|
|
||||||
|
self._schedule.every().day.at("00:02").do(self.exchange.ws_connection_reset)
|
||||||
|
self._schedule.every().day.at("00:07").do(self.wallets.record_wallet_state)
|
||||||
|
|
||||||
|
self.strategy.ft_bot_start()
|
||||||
|
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
||||||
|
self.protections = ProtectionManager(self.config, self.strategy.protections)
|
||||||
|
|
||||||
|
def log_took_too_long(duration: float, time_limit: float):
|
||||||
|
logger.warning(
|
||||||
|
f"Strategy analysis took {duration:.2f}s, more than 25% of the timeframe "
|
||||||
|
f"({time_limit:.2f}s). This can lead to delayed orders and missed signals."
|
||||||
|
"Consider either reducing the amount of work your strategy performs "
|
||||||
|
"or reduce the amount of pairs in the Pairlist."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._measure_execution = MeasureTime(log_took_too_long, timeframe_secs * 0.25)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Graceful shutdown in case of failed initialization.
|
||||||
|
self.cleanup()
|
||||||
|
raise e from e
|
||||||
|
|
||||||
def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None:
|
def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -211,14 +217,18 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.warning(f"Exception during cleanup: {e.__class__.__name__} {e}")
|
logger.warning(f"Exception during cleanup: {e.__class__.__name__} {e}")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.strategy.ft_bot_cleanup()
|
if getattr(self, "strategy", None):
|
||||||
|
self.strategy.ft_bot_cleanup()
|
||||||
|
|
||||||
self.rpc.cleanup()
|
if getattr(self, "rpc", None):
|
||||||
if self.emc:
|
self.rpc.cleanup()
|
||||||
|
if hasattr(self, "emc") and self.emc:
|
||||||
self.emc.shutdown()
|
self.emc.shutdown()
|
||||||
self.exchange.close()
|
if getattr(self, "exchange", None):
|
||||||
|
self.exchange.close()
|
||||||
try:
|
try:
|
||||||
Trade.commit()
|
if hasattr(Trade, "session"):
|
||||||
|
Trade.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
# Exceptions here will be happening if the db disappeared.
|
# Exceptions here will be happening if the db disappeared.
|
||||||
# At which point we can no longer commit anyway.
|
# At which point we can no longer commit anyway.
|
||||||
@@ -229,7 +239,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Called on startup and after reloading the bot - triggers notifications and
|
Called on startup and after reloading the bot - triggers notifications and
|
||||||
performs startup tasks
|
performs startup tasks
|
||||||
"""
|
"""
|
||||||
migrate_live_content(self.config, self.exchange)
|
migrate_live_content(self.config, self.exchange, self.wallets.get_starting_balance())
|
||||||
set_startup_time()
|
set_startup_time()
|
||||||
|
|
||||||
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
|
||||||
@@ -555,7 +565,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if trade.base_currency
|
if trade.base_currency
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
if total < trade.amount:
|
if total < trade.amount or (total == 0 and trade.amount == 0):
|
||||||
if trade.fully_canceled_entry_order_count == len(trade.orders):
|
if trade.fully_canceled_entry_order_count == len(trade.orders):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Trade only had fully canceled entry orders. "
|
f"Trade only had fully canceled entry orders. "
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class BacktestContentTypeIcomplete(TypedDict, total=False):
|
|||||||
backtest_start_time: int
|
backtest_start_time: int
|
||||||
backtest_end_time: int
|
backtest_end_time: int
|
||||||
run_id: str
|
run_id: str
|
||||||
|
wallet_summary: DataFrame
|
||||||
|
|
||||||
|
|
||||||
class BacktestContentType(BacktestContentTypeIcomplete, total=True):
|
class BacktestContentType(BacktestContentTypeIcomplete, total=True):
|
||||||
|
|||||||
+3
-5
@@ -6,7 +6,6 @@ Read the documentation to know what cli arguments you need.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
# check min. python version
|
# check min. python version
|
||||||
@@ -35,7 +34,7 @@ def main(sysargv: list[str] | None = None) -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return_code: Any = 1
|
return_code: int | None = None
|
||||||
try:
|
try:
|
||||||
setup_logging_pre()
|
setup_logging_pre()
|
||||||
asyncio_setup()
|
asyncio_setup()
|
||||||
@@ -62,11 +61,9 @@ def main(sysargv: list[str] | None = None) -> None:
|
|||||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||||
)
|
)
|
||||||
|
|
||||||
except SystemExit as e: # pragma: no cover
|
|
||||||
return_code = e
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("SIGINT received, aborting ...")
|
logger.info("SIGINT received, aborting ...")
|
||||||
return_code = 0
|
return_code = 130
|
||||||
except ConfigurationError as e:
|
except ConfigurationError as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Configuration error: {e}\n"
|
f"Configuration error: {e}\n"
|
||||||
@@ -77,6 +74,7 @@ def main(sysargv: list[str] | None = None) -> None:
|
|||||||
return_code = 2
|
return_code = 2
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Fatal exception!")
|
logger.exception("Fatal exception!")
|
||||||
|
return_code = 1
|
||||||
finally:
|
finally:
|
||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,12 @@ def dataframe_to_json(dataframe: pd.DataFrame) -> str:
|
|||||||
:param dataframe: A pandas DataFrame
|
:param dataframe: A pandas DataFrame
|
||||||
:returns: A JSON string of the pandas DataFrame
|
:returns: A JSON string of the pandas DataFrame
|
||||||
"""
|
"""
|
||||||
|
date_columns = dataframe.select_dtypes(include=["datetime", "datetime64", "datetimetz"])
|
||||||
|
# Explicit conversion to ms
|
||||||
|
# This used to be part of to_json, but was deprecated in pandas 3
|
||||||
|
for date_column in date_columns:
|
||||||
|
dataframe[date_column] = date_columns[date_column].dt.as_unit("ms").astype("int64")
|
||||||
|
|
||||||
return dataframe.to_json(orient="split")
|
return dataframe.to_json(orient="split")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -126,14 +126,14 @@ class LookaheadAnalysisSubFunctions:
|
|||||||
csv_df = add_or_update_row(csv_df, new_row_data)
|
csv_df = add_or_update_row(csv_df, new_row_data)
|
||||||
|
|
||||||
# Fill NaN values with a default value (e.g., 0)
|
# Fill NaN values with a default value (e.g., 0)
|
||||||
csv_df["total_signals"] = csv_df["total_signals"].astype(int).fillna(0)
|
csv_df["total_signals"] = csv_df["total_signals"].astype("int64").fillna(0)
|
||||||
csv_df["biased_entry_signals"] = csv_df["biased_entry_signals"].astype(int).fillna(0)
|
csv_df["biased_entry_signals"] = csv_df["biased_entry_signals"].astype("int64").fillna(0)
|
||||||
csv_df["biased_exit_signals"] = csv_df["biased_exit_signals"].astype(int).fillna(0)
|
csv_df["biased_exit_signals"] = csv_df["biased_exit_signals"].astype("int64").fillna(0)
|
||||||
|
|
||||||
# Convert columns to integers
|
# Convert columns to integers
|
||||||
csv_df["total_signals"] = csv_df["total_signals"].astype(int)
|
csv_df["total_signals"] = csv_df["total_signals"].astype("int64")
|
||||||
csv_df["biased_entry_signals"] = csv_df["biased_entry_signals"].astype(int)
|
csv_df["biased_entry_signals"] = csv_df["biased_entry_signals"].astype("int64")
|
||||||
csv_df["biased_exit_signals"] = csv_df["biased_exit_signals"].astype(int)
|
csv_df["biased_exit_signals"] = csv_df["biased_exit_signals"].astype("int64")
|
||||||
|
|
||||||
logger.info(f"saving {config['lookahead_analysis_exportfilename']}")
|
logger.info(f"saving {config['lookahead_analysis_exportfilename']}")
|
||||||
csv_df.to_csv(config["lookahead_analysis_exportfilename"], index=False)
|
csv_df.to_csv(config["lookahead_analysis_exportfilename"], index=False)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from freqtrade.mixins import LoggingMixin
|
|||||||
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
||||||
from freqtrade.optimize.bt_progress import BTProgress
|
from freqtrade.optimize.bt_progress import BTProgress
|
||||||
from freqtrade.optimize.optimize_reports import (
|
from freqtrade.optimize.optimize_reports import (
|
||||||
|
convert_bt_wallet_collection,
|
||||||
generate_backtest_stats,
|
generate_backtest_stats,
|
||||||
generate_rejected_signals,
|
generate_rejected_signals,
|
||||||
generate_trade_signal_candles,
|
generate_trade_signal_candles,
|
||||||
@@ -137,6 +138,7 @@ class Backtesting:
|
|||||||
}
|
}
|
||||||
self.rejected_dict: dict[str, list] = {}
|
self.rejected_dict: dict[str, list] = {}
|
||||||
self.starting_balance: float = 0.0
|
self.starting_balance: float = 0.0
|
||||||
|
self.wallet_captures: list = []
|
||||||
|
|
||||||
self._exchange_name = self.config["exchange"]["name"]
|
self._exchange_name = self.config["exchange"]["name"]
|
||||||
self.__initial_backtest = exchange is None
|
self.__initial_backtest = exchange is None
|
||||||
@@ -451,6 +453,7 @@ class Backtesting:
|
|||||||
self.replaced_entry_orders = 0
|
self.replaced_entry_orders = 0
|
||||||
self.canceled_exit_orders = 0
|
self.canceled_exit_orders = 0
|
||||||
self.replaced_exit_orders = 0
|
self.replaced_exit_orders = 0
|
||||||
|
self.wallet_captures = []
|
||||||
self.dataprovider.clear_cache()
|
self.dataprovider.clear_cache()
|
||||||
if enable_protections:
|
if enable_protections:
|
||||||
self._load_protections(self.strategy)
|
self._load_protections(self.strategy)
|
||||||
@@ -754,7 +757,7 @@ class Backtesting:
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if an order is open and if it should've filled.
|
Check if an order is open and if it should've filled.
|
||||||
:return: True if the order filled.
|
:return: True if the order filled.
|
||||||
"""
|
"""
|
||||||
if order and self._get_order_filled(order.ft_price, row):
|
if order and self._get_order_filled(order.ft_price, row):
|
||||||
order.close_bt_order(current_date, trade)
|
order.close_bt_order(current_date, trade)
|
||||||
@@ -848,9 +851,7 @@ class Backtesting:
|
|||||||
exit_tag=exit_reason,
|
exit_tag=exit_reason,
|
||||||
)
|
)
|
||||||
if rate is not None and rate != close_rate:
|
if rate is not None and rate != close_rate:
|
||||||
close_rate = price_to_precision(
|
close_rate = rate
|
||||||
rate, trade.price_precision, trade.precision_mode_price
|
|
||||||
)
|
|
||||||
# We can't place orders lower than current low.
|
# We can't place orders lower than current low.
|
||||||
# freqtrade does not support this in live, and the order would fill immediately
|
# freqtrade does not support this in live, and the order would fill immediately
|
||||||
if trade.is_short:
|
if trade.is_short:
|
||||||
@@ -892,6 +893,9 @@ class Backtesting:
|
|||||||
self.order_id_counter += 1
|
self.order_id_counter += 1
|
||||||
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||||
order_type = self.strategy.order_types["exit"]
|
order_type = self.strategy.order_types["exit"]
|
||||||
|
close_rate = price_to_precision(
|
||||||
|
close_rate, trade.price_precision, trade.precision_mode_price
|
||||||
|
)
|
||||||
# amount = amount or trade.amount
|
# amount = amount or trade.amount
|
||||||
amount = amount_to_contract_precision(
|
amount = amount_to_contract_precision(
|
||||||
amount or trade.amount, trade.amount_precision, self.precision_mode, trade.contract_size
|
amount or trade.amount, trade.amount_precision, self.precision_mode, trade.contract_size
|
||||||
@@ -1602,6 +1606,7 @@ class Backtesting:
|
|||||||
pair_detail_cache: dict[str, list[tuple]] = {}
|
pair_detail_cache: dict[str, list[tuple]] = {}
|
||||||
pair_tradedir_cache: dict[str, LongShort | None] = {}
|
pair_tradedir_cache: dict[str, LongShort | None] = {}
|
||||||
pairs_with_open_trades = [t.pair for t in LocalTrade.bt_trades_open]
|
pairs_with_open_trades = [t.pair for t in LocalTrade.bt_trades_open]
|
||||||
|
self._capture_wallet(current_time, self.strategy.config["stake_currency"], 1)
|
||||||
|
|
||||||
for current_time_det, is_first, has_detail, idx, pair in self._time_pair_generator_det(
|
for current_time_det, is_first, has_detail, idx, pair in self._time_pair_generator_det(
|
||||||
current_time, pairs
|
current_time, pairs
|
||||||
@@ -1626,6 +1631,7 @@ class Backtesting:
|
|||||||
)
|
)
|
||||||
trade_dir = self.check_for_trade_entry(row)
|
trade_dir = self.check_for_trade_entry(row)
|
||||||
pair_tradedir_cache[pair] = trade_dir
|
pair_tradedir_cache[pair] = trade_dir
|
||||||
|
self._capture_wallet(current_time, pair.split("/")[0], row[OPEN_IDX])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Detail candle - from cache.
|
# Detail candle - from cache.
|
||||||
@@ -1679,6 +1685,15 @@ class Backtesting:
|
|||||||
yield current_time_det, pair, row, is_last_row, trade_dir
|
yield current_time_det, pair, row, is_last_row, trade_dir
|
||||||
self.progress.increment()
|
self.progress.increment()
|
||||||
|
|
||||||
|
def _capture_wallet(self, current_time: datetime, currency: str, price: float) -> None:
|
||||||
|
"""
|
||||||
|
Capture the current wallet state.
|
||||||
|
"""
|
||||||
|
if self.dataprovider.runmode != RunMode.BACKTEST:
|
||||||
|
return
|
||||||
|
if total := self.wallets.get_total(currency):
|
||||||
|
self.wallet_captures.append((current_time, currency, price, total))
|
||||||
|
|
||||||
def backtest(
|
def backtest(
|
||||||
self, processed: dict, start_date: datetime, end_date: datetime
|
self, processed: dict, start_date: datetime, end_date: datetime
|
||||||
) -> BacktestContentTypeIcomplete:
|
) -> BacktestContentTypeIcomplete:
|
||||||
@@ -1738,6 +1753,7 @@ class Backtesting:
|
|||||||
"canceled_entry_orders": self.canceled_entry_orders,
|
"canceled_entry_orders": self.canceled_entry_orders,
|
||||||
"replaced_entry_orders": self.replaced_entry_orders,
|
"replaced_entry_orders": self.replaced_entry_orders,
|
||||||
"final_balance": self.wallets.get_total(self.strategy.config["stake_currency"]),
|
"final_balance": self.wallets.get_total(self.strategy.config["stake_currency"]),
|
||||||
|
"wallet_summary": convert_bt_wallet_collection(self.wallet_captures),
|
||||||
}
|
}
|
||||||
|
|
||||||
def backtest_one_strategy(
|
def backtest_one_strategy(
|
||||||
@@ -1866,6 +1882,11 @@ class Backtesting:
|
|||||||
dt_appendix,
|
dt_appendix,
|
||||||
market_change_data=combined_res,
|
market_change_data=combined_res,
|
||||||
analysis_results=self.analysis_results,
|
analysis_results=self.analysis_results,
|
||||||
|
wallet_summary={
|
||||||
|
s: x["wallet_summary"]
|
||||||
|
for s, x in self.all_bt_content.items()
|
||||||
|
if "wallet_summary" in x
|
||||||
|
},
|
||||||
strategy_files={s.get_strategy_name(): s.__file__ for s in self.strategylist},
|
strategy_files={s.get_strategy_name(): s.__file__ for s in self.strategylist},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from freqtrade.optimize.optimize_reports.bt_output import (
|
|||||||
)
|
)
|
||||||
from freqtrade.optimize.optimize_reports.bt_storage import store_backtest_results
|
from freqtrade.optimize.optimize_reports.bt_storage import store_backtest_results
|
||||||
from freqtrade.optimize.optimize_reports.optimize_reports import (
|
from freqtrade.optimize.optimize_reports.optimize_reports import (
|
||||||
|
convert_bt_wallet_collection,
|
||||||
generate_all_periodic_breakdown_stats,
|
generate_all_periodic_breakdown_stats,
|
||||||
generate_backtest_stats,
|
generate_backtest_stats,
|
||||||
generate_daily_stats,
|
generate_daily_stats,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
|
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
|
||||||
from freqtrade.ft_types import BacktestResultType
|
from freqtrade.ft_types import BacktestResultType
|
||||||
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
|
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
|
||||||
@@ -9,6 +11,8 @@ from freqtrade.util import decimals_per_coin, fmt_coin, print_rich_table
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__EMPTY_LINE = ("", "")
|
||||||
|
|
||||||
|
|
||||||
def _get_line_floatfmt(stake_currency: str) -> list[str]:
|
def _get_line_floatfmt(stake_currency: str) -> list[str]:
|
||||||
"""
|
"""
|
||||||
@@ -201,7 +205,7 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
|||||||
|
|
||||||
short_metrics = (
|
short_metrics = (
|
||||||
[
|
[
|
||||||
("", ""), # Empty line to improve readability
|
__EMPTY_LINE, # Empty line to improve readability
|
||||||
(
|
(
|
||||||
"Long / Short trades",
|
"Long / Short trades",
|
||||||
f"{strat_results.get('trade_count_long', 'total_trades')} / "
|
f"{strat_results.get('trade_count_long', 'total_trades')} / "
|
||||||
@@ -222,7 +226,7 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
|||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
|
||||||
drawdown_metrics = []
|
drawdown_metrics: list[tuple[str | Text, str | Text]] = []
|
||||||
if "max_relative_drawdown" in strat_results:
|
if "max_relative_drawdown" in strat_results:
|
||||||
# Compatibility to show old hyperopt results
|
# Compatibility to show old hyperopt results
|
||||||
drawdown_metrics.append(
|
drawdown_metrics.append(
|
||||||
@@ -287,6 +291,79 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
|||||||
if "trading_mode" in strat_results
|
if "trading_mode" in strat_results
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
wallet_metrics: list[tuple[str, str]] = [
|
||||||
|
(
|
||||||
|
"Min/Max balance (closed trades)",
|
||||||
|
f"{fmt_coin(strat_results['csum_min'], stake)} / "
|
||||||
|
f"{fmt_coin(strat_results['csum_max'], stake)}",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
wallet_stats = strat_results.get("wallet_stats", {})
|
||||||
|
if wallet_stats:
|
||||||
|
drawdown_metrics.extend(
|
||||||
|
[
|
||||||
|
__EMPTY_LINE, # Empty line to improve readability
|
||||||
|
(Text("Wallet based Metrics", style="bold"), ""),
|
||||||
|
(
|
||||||
|
"Min/Max balance (wallet balance)",
|
||||||
|
f"{fmt_coin(wallet_stats['low_balance'], stake)} / "
|
||||||
|
f"{fmt_coin(wallet_stats['high_balance'], stake)}",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Min/Max balance dates (wallet balance)",
|
||||||
|
f"{wallet_stats['low_date']} / {wallet_stats['high_date']}",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if "max_drawdown_abs" in wallet_stats:
|
||||||
|
# Assume that if sharpe is there, all others are there as well.
|
||||||
|
drawdown_metrics.extend(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"Max % of account underwater (balance)",
|
||||||
|
f"{wallet_stats['max_relative_drawdown']:.2%}",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Absolute drawdown (wallet balance)",
|
||||||
|
f"{fmt_coin(wallet_stats['max_drawdown_abs'], stake)} "
|
||||||
|
f"({wallet_stats['max_drawdown_account']:.2%})",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Drawdown duration",
|
||||||
|
wallet_stats["drawdown_duration"]
|
||||||
|
if "drawdown_duration" in wallet_stats
|
||||||
|
else "N/A",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Profit at drawdown start",
|
||||||
|
fmt_coin(wallet_stats["max_drawdown_high"], stake),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Profit at drawdown end",
|
||||||
|
fmt_coin(wallet_stats["max_drawdown_low"], stake),
|
||||||
|
),
|
||||||
|
("Drawdown start", wallet_stats["drawdown_start"]),
|
||||||
|
("Drawdown end", wallet_stats["drawdown_end"]),
|
||||||
|
(
|
||||||
|
"Sharpe (daily wallet balance)",
|
||||||
|
f"{wallet_stats['sharpe']:.2f}"
|
||||||
|
if wallet_stats and "sharpe" in wallet_stats
|
||||||
|
else "N/A",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sortino (daily wallet balance)",
|
||||||
|
f"{wallet_stats['sortino']:.2f}"
|
||||||
|
if wallet_stats and "sortino" in wallet_stats
|
||||||
|
else "N/A",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Calmar (daily wallet balance)",
|
||||||
|
f"{wallet_stats['calmar']:.2f}"
|
||||||
|
if wallet_stats and "calmar" in wallet_stats
|
||||||
|
else "N/A",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
|
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
|
||||||
# command stores these results and newer version of freqtrade must be able to handle old
|
# command stores these results and newer version of freqtrade must be able to handle old
|
||||||
@@ -296,7 +373,7 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
|||||||
("Backtesting to", strat_results["backtest_end"]),
|
("Backtesting to", strat_results["backtest_end"]),
|
||||||
*trading_mode,
|
*trading_mode,
|
||||||
("Max open trades", strat_results["max_open_trades"]),
|
("Max open trades", strat_results["max_open_trades"]),
|
||||||
("", ""), # Empty line to improve readability
|
__EMPTY_LINE, # Empty line to improve readability
|
||||||
(
|
(
|
||||||
"Total/Daily Avg Trades",
|
"Total/Daily Avg Trades",
|
||||||
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}",
|
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}",
|
||||||
@@ -315,9 +392,18 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
|||||||
),
|
),
|
||||||
("Total profit %", f"{strat_results['profit_total']:.2%}"),
|
("Total profit %", f"{strat_results['profit_total']:.2%}"),
|
||||||
("CAGR %", f"{strat_results['cagr']:.2%}" if "cagr" in strat_results else "N/A"),
|
("CAGR %", f"{strat_results['cagr']:.2%}" if "cagr" in strat_results else "N/A"),
|
||||||
("Sortino", f"{strat_results['sortino']:.2f}" if "sortino" in strat_results else "N/A"),
|
(
|
||||||
("Sharpe", f"{strat_results['sharpe']:.2f}" if "sharpe" in strat_results else "N/A"),
|
"Sharpe (closed trades)",
|
||||||
("Calmar", f"{strat_results['calmar']:.2f}" if "calmar" in strat_results else "N/A"),
|
f"{strat_results['sharpe']:.2f}" if "sharpe" in strat_results else "N/A",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sortino (closed trades)",
|
||||||
|
f"{strat_results['sortino']:.2f}" if "sortino" in strat_results else "N/A",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Calmar (closed trades)",
|
||||||
|
f"{strat_results['calmar']:.2f}" if "calmar" in strat_results else "N/A",
|
||||||
|
),
|
||||||
("SQN", f"{strat_results['sqn']:.2f}" if "sqn" in strat_results else "N/A"),
|
("SQN", f"{strat_results['sqn']:.2f}" if "sqn" in strat_results else "N/A"),
|
||||||
(
|
(
|
||||||
"Profit factor",
|
"Profit factor",
|
||||||
@@ -346,12 +432,13 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
|||||||
"Avg. stake amount",
|
"Avg. stake amount",
|
||||||
fmt_coin(strat_results["avg_stake_amount"], stake),
|
fmt_coin(strat_results["avg_stake_amount"], stake),
|
||||||
),
|
),
|
||||||
|
("Market change", f"{strat_results['market_change']:.2%}"),
|
||||||
(
|
(
|
||||||
"Total trade volume",
|
"Total trade volume",
|
||||||
fmt_coin(strat_results["total_volume"], stake),
|
fmt_coin(strat_results["total_volume"], stake),
|
||||||
),
|
),
|
||||||
*short_metrics,
|
*short_metrics,
|
||||||
("", ""), # Empty line to improve readability
|
__EMPTY_LINE, # Empty line to improve readability
|
||||||
(
|
(
|
||||||
"Best Pair",
|
"Best Pair",
|
||||||
f"{strat_results['best_pair']['key']} "
|
f"{strat_results['best_pair']['key']} "
|
||||||
@@ -407,11 +494,9 @@ def text_table_add_metrics(strat_results: dict) -> None:
|
|||||||
f"{strat_results.get('timedout_exit_orders', 'N/A')}",
|
f"{strat_results.get('timedout_exit_orders', 'N/A')}",
|
||||||
),
|
),
|
||||||
*entry_adjustment_metrics,
|
*entry_adjustment_metrics,
|
||||||
("", ""), # Empty line to improve readability
|
__EMPTY_LINE, # Empty line to improve readability
|
||||||
("Min balance", fmt_coin(strat_results["csum_min"], stake)),
|
*wallet_metrics,
|
||||||
("Max balance", fmt_coin(strat_results["csum_max"], stake)),
|
|
||||||
*drawdown_metrics,
|
*drawdown_metrics,
|
||||||
("Market change", f"{strat_results['market_change']:.2%}"),
|
|
||||||
]
|
]
|
||||||
print_rich_table(metrics, ["Metric", "Value"], summary="SUMMARY METRICS", justify="left")
|
print_rich_table(metrics, ["Metric", "Value"], summary="SUMMARY METRICS", justify="left")
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ def store_backtest_results(
|
|||||||
dtappendix: str,
|
dtappendix: str,
|
||||||
*,
|
*,
|
||||||
market_change_data: DataFrame | None = None,
|
market_change_data: DataFrame | None = None,
|
||||||
|
wallet_summary: dict[str, DataFrame] | None = None,
|
||||||
analysis_results: dict[str, dict[str, DataFrame]] | None = None,
|
analysis_results: dict[str, dict[str, DataFrame]] | None = None,
|
||||||
strategy_files: dict[str, str] | None = None,
|
strategy_files: dict[str, str] | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
@@ -123,6 +124,15 @@ def store_backtest_results(
|
|||||||
market_change_buf.seek(0)
|
market_change_buf.seek(0)
|
||||||
zipf.writestr(market_change_name, market_change_buf.getvalue())
|
zipf.writestr(market_change_name, market_change_buf.getvalue())
|
||||||
|
|
||||||
|
# Add wallet summary if present
|
||||||
|
if wallet_summary is not None:
|
||||||
|
for strategy, df in wallet_summary.items():
|
||||||
|
wallet_name = f"{base_filename.stem}_{strategy}_wallet.feather"
|
||||||
|
wallet_buf = BytesIO()
|
||||||
|
df.reset_index().to_feather(wallet_buf, compression_level=9, compression="lz4")
|
||||||
|
wallet_buf.seek(0)
|
||||||
|
zipf.writestr(wallet_name, wallet_buf.getvalue())
|
||||||
|
|
||||||
# Add analysis results if present and running in backtest mode
|
# Add analysis results if present and running in backtest mode
|
||||||
if (
|
if (
|
||||||
config.get("export", "none") == "signals"
|
config.get("export", "none") == "signals"
|
||||||
|
|||||||
@@ -10,12 +10,16 @@ from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT
|
|||||||
from freqtrade.data.metrics import (
|
from freqtrade.data.metrics import (
|
||||||
calculate_cagr,
|
calculate_cagr,
|
||||||
calculate_calmar,
|
calculate_calmar,
|
||||||
|
calculate_calmar_from_balance,
|
||||||
calculate_csum,
|
calculate_csum,
|
||||||
calculate_expectancy,
|
calculate_expectancy,
|
||||||
calculate_market_change,
|
calculate_market_change,
|
||||||
calculate_max_drawdown,
|
calculate_max_drawdown,
|
||||||
|
calculate_max_drawdown_from_balance,
|
||||||
calculate_sharpe,
|
calculate_sharpe,
|
||||||
|
calculate_sharpe_from_balance,
|
||||||
calculate_sortino,
|
calculate_sortino,
|
||||||
|
calculate_sortino_from_balance,
|
||||||
calculate_sqn,
|
calculate_sqn,
|
||||||
)
|
)
|
||||||
from freqtrade.ft_types import (
|
from freqtrade.ft_types import (
|
||||||
@@ -29,6 +33,94 @@ from freqtrade.util import decimals_per_coin, fmt_coin, format_duration, get_dry
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_bt_wallet_collection(wallet_captures: list[tuple]) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Convert the wallet capture list to a DataFrame.
|
||||||
|
Assumes the wallet_captures list contains tuples with the following structure:
|
||||||
|
(date, currency, price, balance).
|
||||||
|
"""
|
||||||
|
if len(wallet_captures) == 0:
|
||||||
|
return DataFrame()
|
||||||
|
return DataFrame(
|
||||||
|
wallet_captures,
|
||||||
|
columns=["date", "currency", "rate", "balance"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_wallet_stats(wallet_df: DataFrame, stake_currency: str) -> dict[str, Any]:
|
||||||
|
"""Generate wallet statistics from the wallet DataFrame."""
|
||||||
|
if wallet_df is None or wallet_df.empty:
|
||||||
|
return {}
|
||||||
|
wallet_df.loc[:, "total_quote"] = wallet_df["rate"] * wallet_df["balance"]
|
||||||
|
# Group by date to get total wallet value at each timestamp
|
||||||
|
wallet = wallet_df.groupby("date")["total_quote"].sum().reset_index()
|
||||||
|
total_quote = wallet["total_quote"]
|
||||||
|
low_idx = total_quote.idxmin()
|
||||||
|
high_idx = total_quote.idxmax()
|
||||||
|
start_balance = wallet.iloc[0]["total_quote"]
|
||||||
|
end_balance = wallet.iloc[-1]["total_quote"]
|
||||||
|
high_balance = total_quote.loc[high_idx]
|
||||||
|
low_balance = total_quote.loc[low_idx]
|
||||||
|
low_date = wallet.loc[low_idx, "date"]
|
||||||
|
high_date = wallet.loc[high_idx, "date"]
|
||||||
|
sharpe = calculate_sharpe_from_balance(wallet)
|
||||||
|
sortino = calculate_sortino_from_balance(wallet)
|
||||||
|
calmar = calculate_calmar_from_balance(wallet)
|
||||||
|
try:
|
||||||
|
drawdown = calculate_max_drawdown_from_balance(wallet)
|
||||||
|
# max_relative_drawdown = Underwater
|
||||||
|
drawdown_duration = drawdown.low_date - drawdown.high_date
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
drawdown = None
|
||||||
|
drawdown_duration = timedelta()
|
||||||
|
try:
|
||||||
|
underwater = calculate_max_drawdown_from_balance(wallet, relative=True)
|
||||||
|
except ValueError:
|
||||||
|
underwater = None
|
||||||
|
return {
|
||||||
|
"start_balance": start_balance,
|
||||||
|
"end_balance": end_balance,
|
||||||
|
"high_balance": high_balance,
|
||||||
|
"low_balance": low_balance,
|
||||||
|
"sharpe": sharpe,
|
||||||
|
"sortino": sortino,
|
||||||
|
"calmar": calmar,
|
||||||
|
"low_date": low_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
|
"low_ts": int(low_date.timestamp() * 1000),
|
||||||
|
"high_date": high_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
|
"high_ts": int(high_date.timestamp() * 1000),
|
||||||
|
# Drawdown metrics
|
||||||
|
"max_drawdown_account": drawdown.relative_account_drawdown if drawdown else 0.0,
|
||||||
|
"max_relative_drawdown": underwater.relative_account_drawdown if underwater else 0.0,
|
||||||
|
"max_drawdown_abs": drawdown.drawdown_abs if drawdown else 0.0,
|
||||||
|
"drawdown_start": (
|
||||||
|
drawdown.high_date.strftime(DATETIME_PRINT_FORMAT)
|
||||||
|
if drawdown and drawdown.high_date is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"drawdown_start_ts": (
|
||||||
|
int(drawdown.high_date.timestamp() * 1000)
|
||||||
|
if drawdown and drawdown.high_date is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"drawdown_end": (
|
||||||
|
drawdown.low_date.strftime(DATETIME_PRINT_FORMAT)
|
||||||
|
if drawdown and drawdown.low_date is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"drawdown_end_ts": (
|
||||||
|
int(drawdown.low_date.timestamp() * 1000)
|
||||||
|
if drawdown and drawdown.low_date is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"drawdown_duration": drawdown_duration,
|
||||||
|
"drawdown_duration_s": drawdown_duration.total_seconds(),
|
||||||
|
"max_drawdown_low": drawdown.low_value if drawdown else 0.0,
|
||||||
|
"max_drawdown_high": drawdown.high_value if drawdown else 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def generate_trade_signal_candles(
|
def generate_trade_signal_candles(
|
||||||
preprocessed_df: dict[str, DataFrame], bt_results: BacktestContentType, date_col: str
|
preprocessed_df: dict[str, DataFrame], bt_results: BacktestContentType, date_col: str
|
||||||
) -> dict[str, DataFrame]:
|
) -> dict[str, DataFrame]:
|
||||||
@@ -155,7 +247,7 @@ def generate_pair_metrics( #
|
|||||||
skip_nan: bool = False,
|
skip_nan: bool = False,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Generates and returns a list for the given backtest data and the results dataframe
|
Generates and returns a list for the given backtest data and the results dataframe
|
||||||
:param pairlist: Pairlist used
|
:param pairlist: Pairlist used
|
||||||
:param stake_currency: stake-currency - used to correctly name headers
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
:param starting_balance: Starting balance
|
:param starting_balance: Starting balance
|
||||||
@@ -248,7 +340,7 @@ def generate_strategy_comparison(bt_stats: dict) -> list[dict]:
|
|||||||
|
|
||||||
def _get_resample_from_period(period: str) -> str:
|
def _get_resample_from_period(period: str) -> str:
|
||||||
if period == "day":
|
if period == "day":
|
||||||
return "1d"
|
return "1D"
|
||||||
if period == "week":
|
if period == "week":
|
||||||
# Weekly defaulting to Monday.
|
# Weekly defaulting to Monday.
|
||||||
return "1W-MON"
|
return "1W-MON"
|
||||||
@@ -438,8 +530,8 @@ def generate_daily_stats(results: DataFrame) -> dict[str, Any]:
|
|||||||
"losing_days": 0,
|
"losing_days": 0,
|
||||||
"daily_profit_list": [],
|
"daily_profit_list": [],
|
||||||
}
|
}
|
||||||
daily_profit_rel = results.resample("1d", on="close_date")["profit_ratio"].sum()
|
daily_profit_rel = results.resample("1D", on="close_date")["profit_ratio"].sum()
|
||||||
daily_profit = results.resample("1d", on="close_date")["profit_abs"].sum().round(10)
|
daily_profit = results.resample("1D", on="close_date")["profit_abs"].sum().round(10)
|
||||||
worst_rel = min(daily_profit_rel)
|
worst_rel = min(daily_profit_rel)
|
||||||
best_rel = max(daily_profit_rel)
|
best_rel = max(daily_profit_rel)
|
||||||
worst = min(daily_profit)
|
worst = min(daily_profit)
|
||||||
@@ -592,6 +684,7 @@ def generate_strategy_stats(
|
|||||||
"sharpe": calculate_sharpe(results, min_date, max_date, start_balance),
|
"sharpe": calculate_sharpe(results, min_date, max_date, start_balance),
|
||||||
"calmar": calculate_calmar(results, min_date, max_date, start_balance),
|
"calmar": calculate_calmar(results, min_date, max_date, start_balance),
|
||||||
"sqn": calculate_sqn(results, start_balance),
|
"sqn": calculate_sqn(results, start_balance),
|
||||||
|
"wallet_stats": generate_wallet_stats(content.get("wallet_summary"), stake_currency),
|
||||||
"profit_factor": profit_factor,
|
"profit_factor": profit_factor,
|
||||||
"backtest_start": min_date.strftime(DATETIME_PRINT_FORMAT),
|
"backtest_start": min_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
"backtest_start_ts": int(min_date.timestamp() * 1000),
|
"backtest_start_ts": int(min_date.timestamp() * 1000),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class SKDecimal(FloatDistribution):
|
|||||||
*,
|
*,
|
||||||
step: float | None = None,
|
step: float | None = None,
|
||||||
decimals: int | None = None,
|
decimals: int | None = None,
|
||||||
name=None,
|
name: str | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
FloatDistribution with a fixed step size.
|
FloatDistribution with a fixed step size.
|
||||||
@@ -26,7 +26,7 @@ class SKDecimal(FloatDistribution):
|
|||||||
raise ValueError("You must set one of decimals or step")
|
raise ValueError("You must set one of decimals or step")
|
||||||
# Convert decimals to step
|
# Convert decimals to step
|
||||||
self.step = step or (1 / 10**decimals if decimals else 1)
|
self.step = step or (1 / 10**decimals if decimals else 1)
|
||||||
self.name = name
|
self.name = name or ""
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
low=round(low, decimals) if decimals else low,
|
low=round(low, decimals) if decimals else low,
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ from freqtrade.persistence.usedb_context import (
|
|||||||
disable_database_use,
|
disable_database_use,
|
||||||
enable_database_use,
|
enable_database_use,
|
||||||
)
|
)
|
||||||
|
from freqtrade.persistence.wallet_history import WalletHistory
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.orm import make_transient
|
||||||
|
|
||||||
|
from freqtrade.persistence.base import SessionType
|
||||||
|
from freqtrade.persistence.custom_data import _CustomData
|
||||||
|
from freqtrade.persistence.key_value_store import _KeyValueStoreModel
|
||||||
|
from freqtrade.persistence.migrations import set_sequence_ids
|
||||||
|
from freqtrade.persistence.pairlock import PairLock
|
||||||
|
from freqtrade.persistence.trade_model import Order, Trade
|
||||||
|
from freqtrade.persistence.wallet_history import WalletHistory
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_db(session_target: SessionType):
|
||||||
|
|
||||||
|
trade_count = 0
|
||||||
|
pairlock_count = 0
|
||||||
|
kv_count = 0
|
||||||
|
custom_data_count = 0
|
||||||
|
wallet_history_count = 0
|
||||||
|
for trade in Trade.get_trades():
|
||||||
|
trade_count += 1
|
||||||
|
make_transient(trade)
|
||||||
|
for o in trade.orders:
|
||||||
|
make_transient(o)
|
||||||
|
|
||||||
|
session_target.add(trade)
|
||||||
|
|
||||||
|
session_target.commit()
|
||||||
|
|
||||||
|
for pairlock in PairLock.get_all_locks():
|
||||||
|
pairlock_count += 1
|
||||||
|
make_transient(pairlock)
|
||||||
|
session_target.add(pairlock)
|
||||||
|
session_target.commit()
|
||||||
|
|
||||||
|
for kv in _KeyValueStoreModel.session.scalars(select(_KeyValueStoreModel)):
|
||||||
|
kv_count += 1
|
||||||
|
make_transient(kv)
|
||||||
|
session_target.add(kv)
|
||||||
|
session_target.commit()
|
||||||
|
|
||||||
|
for cd in _CustomData.session.scalars(select(_CustomData)):
|
||||||
|
custom_data_count += 1
|
||||||
|
make_transient(cd)
|
||||||
|
session_target.add(cd)
|
||||||
|
session_target.commit()
|
||||||
|
|
||||||
|
for wh in WalletHistory.session.scalars(select(WalletHistory)):
|
||||||
|
wallet_history_count += 1
|
||||||
|
make_transient(wh)
|
||||||
|
session_target.add(wh)
|
||||||
|
session_target.commit()
|
||||||
|
|
||||||
|
# Update sequences
|
||||||
|
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
||||||
|
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
||||||
|
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
||||||
|
max_kv_id = session_target.scalar(select(func.max(_KeyValueStoreModel.id)))
|
||||||
|
max_custom_data_id = session_target.scalar(select(func.max(_CustomData.id)))
|
||||||
|
max_wallet_history_id = session_target.scalar(select(func.max(WalletHistory.id)))
|
||||||
|
|
||||||
|
set_sequence_ids(
|
||||||
|
session_target.get_bind(),
|
||||||
|
trade_id=(max_trade_id or 0) + 1,
|
||||||
|
order_id=(max_order_id or 0) + 1,
|
||||||
|
pairlock_id=(max_pairlock_id or 0) + 1,
|
||||||
|
kv_id=(max_kv_id or 0) + 1,
|
||||||
|
custom_data_id=(max_custom_data_id or 0) + 1,
|
||||||
|
wallet_history_id=(max_wallet_history_id or 0) + 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Migrated {trade_count} Trades, {pairlock_count} Pairlocks, "
|
||||||
|
f"{kv_count} Key-Value pairs, {custom_data_count} Custom Data entries, "
|
||||||
|
f"and {wallet_history_count} Wallet History entries."
|
||||||
|
)
|
||||||
@@ -18,10 +18,13 @@ class ValueTypesEnum(StrEnum):
|
|||||||
INT = "int"
|
INT = "int"
|
||||||
|
|
||||||
|
|
||||||
|
# must be < 50 characters to fit the database column
|
||||||
KeyStoreKeys = Literal[
|
KeyStoreKeys = Literal[
|
||||||
"bot_start_time",
|
"bot_start_time",
|
||||||
"startup_time",
|
"startup_time",
|
||||||
"binance_migration",
|
"binance_migration",
|
||||||
|
"wallet_history_migration",
|
||||||
|
"wallet_history_migration_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ class _KeyValueStoreModel(ModelBase):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
|
||||||
key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True)
|
key: Mapped[KeyStoreKeys] = mapped_column(String(50), nullable=False, index=True)
|
||||||
|
|
||||||
value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
|
value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False)
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,12 @@ def get_last_sequence_ids(engine, sequence_name: str, table_back_name: str) -> i
|
|||||||
|
|
||||||
if engine.name == "postgresql":
|
if engine.name == "postgresql":
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
last_id = connection.execute(text(f"select nextval('{sequence_name}')")).fetchone()[0]
|
last_id = connection.execute(
|
||||||
|
text(f"""select nextval('"{sequence_name}"')""")
|
||||||
|
).fetchone()[0]
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(
|
connection.execute(
|
||||||
text(f"ALTER SEQUENCE {sequence_name} rename to {table_back_name}_id_seq_bak")
|
text(f'ALTER SEQUENCE "{sequence_name}" rename to "{table_back_name}_id_seq_bak"')
|
||||||
)
|
)
|
||||||
|
|
||||||
return last_id
|
return last_id
|
||||||
@@ -51,6 +53,7 @@ def set_sequence_ids(
|
|||||||
pairlock_id: int | None = None,
|
pairlock_id: int | None = None,
|
||||||
kv_id: int | None = None,
|
kv_id: int | None = None,
|
||||||
custom_data_id: int | None = None,
|
custom_data_id: int | None = None,
|
||||||
|
wallet_history_id: int | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Set sequence ids to the given values.
|
Set sequence ids to the given values.
|
||||||
@@ -62,6 +65,7 @@ def set_sequence_ids(
|
|||||||
:param pairlock_id: value to set for pairlocks_id_seq (optional)
|
:param pairlock_id: value to set for pairlocks_id_seq (optional)
|
||||||
:param kv_id: value to set for KeyValueStore_id_seq (optional)
|
:param kv_id: value to set for KeyValueStore_id_seq (optional)
|
||||||
:param custom_data_id: value to set for trade_custom_data_id_seq (optional)
|
:param custom_data_id: value to set for trade_custom_data_id_seq (optional)
|
||||||
|
:param wallet_history_id: value to set for wallet_history_id_seq (optional)
|
||||||
"""
|
"""
|
||||||
if engine.name == "postgresql":
|
if engine.name == "postgresql":
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
@@ -81,6 +85,10 @@ def set_sequence_ids(
|
|||||||
connection.execute(
|
connection.execute(
|
||||||
text(f"ALTER SEQUENCE trade_custom_data_id_seq RESTART WITH {custom_data_id}")
|
text(f"ALTER SEQUENCE trade_custom_data_id_seq RESTART WITH {custom_data_id}")
|
||||||
)
|
)
|
||||||
|
if wallet_history_id:
|
||||||
|
connection.execute(
|
||||||
|
text(f"ALTER SEQUENCE wallet_history_id_seq RESTART WITH {wallet_history_id}")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def drop_index_on_table(engine, inspector, table_bak_name):
|
def drop_index_on_table(engine, inspector, table_bak_name):
|
||||||
@@ -88,9 +96,9 @@ def drop_index_on_table(engine, inspector, table_bak_name):
|
|||||||
# drop indexes on backup table in new session
|
# drop indexes on backup table in new session
|
||||||
for index in inspector.get_indexes(table_bak_name):
|
for index in inspector.get_indexes(table_bak_name):
|
||||||
if engine.name == "mysql":
|
if engine.name == "mysql":
|
||||||
connection.execute(text(f"drop index {index['name']} on {table_bak_name}"))
|
connection.execute(text(f'drop index "{index["name"]}" on {table_bak_name}'))
|
||||||
else:
|
else:
|
||||||
connection.execute(text(f"drop index {index['name']}"))
|
connection.execute(text(f'drop index "{index["name"]}"'))
|
||||||
|
|
||||||
|
|
||||||
def migrate_trades_and_orders_table(
|
def migrate_trades_and_orders_table(
|
||||||
@@ -315,6 +323,31 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st
|
|||||||
set_sequence_ids(engine, pairlock_id=pairlock_id)
|
set_sequence_ids(engine, pairlock_id=pairlock_id)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_kv_store_table(decl_base, inspector, engine, kv_store_back_name: str, cols: list):
|
||||||
|
# Schema migration necessary
|
||||||
|
with engine.begin() as connection:
|
||||||
|
connection.execute(text(f'alter table "KeyValueStore" rename to "{kv_store_back_name}"'))
|
||||||
|
|
||||||
|
drop_index_on_table(engine, inspector, kv_store_back_name)
|
||||||
|
kv_store_id = get_last_sequence_ids(engine, "KeyValueStore_id_seq", kv_store_back_name)
|
||||||
|
|
||||||
|
# let SQLAlchemy create the schema as required
|
||||||
|
decl_base.metadata.create_all(engine)
|
||||||
|
# Copy data back - following the correct schema
|
||||||
|
with engine.begin() as connection:
|
||||||
|
connection.execute(
|
||||||
|
text(
|
||||||
|
f"""insert into "KeyValueStore"
|
||||||
|
(id, key, value_type, string_value, datetime_value, float_value, int_value)
|
||||||
|
select id, key, value_type, string_value, datetime_value, float_value, int_value
|
||||||
|
from "{kv_store_back_name}"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
set_sequence_ids(engine, kv_id=kv_store_id)
|
||||||
|
|
||||||
|
|
||||||
def set_sqlite_to_wal(engine):
|
def set_sqlite_to_wal(engine):
|
||||||
if engine.name == "sqlite" and str(engine.url) != "sqlite://":
|
if engine.name == "sqlite" and str(engine.url) != "sqlite://":
|
||||||
# Set Mode to
|
# Set Mode to
|
||||||
@@ -385,12 +418,15 @@ def check_migrate(engine: Engine, decl_base, previous_tables: list[str]) -> None
|
|||||||
cols_trades = inspector.get_columns("trades")
|
cols_trades = inspector.get_columns("trades")
|
||||||
cols_orders = inspector.get_columns("orders")
|
cols_orders = inspector.get_columns("orders")
|
||||||
cols_pairlocks = inspector.get_columns("pairlocks")
|
cols_pairlocks = inspector.get_columns("pairlocks")
|
||||||
|
cols_kv_store = inspector.get_columns("KeyValueStore")
|
||||||
tabs = get_table_names_for_table(inspector, "trades")
|
tabs = get_table_names_for_table(inspector, "trades")
|
||||||
table_back_name = get_backup_name(tabs, "trades_bak")
|
table_back_name = get_backup_name(tabs, "trades_bak")
|
||||||
order_tabs = get_table_names_for_table(inspector, "orders")
|
order_tabs = get_table_names_for_table(inspector, "orders")
|
||||||
order_table_bak_name = get_backup_name(order_tabs, "orders_bak")
|
order_table_bak_name = get_backup_name(order_tabs, "orders_bak")
|
||||||
pairlock_tabs = get_table_names_for_table(inspector, "pairlocks")
|
pairlock_tabs = get_table_names_for_table(inspector, "pairlocks")
|
||||||
pairlock_table_bak_name = get_backup_name(pairlock_tabs, "pairlocks_bak")
|
pairlock_table_bak_name = get_backup_name(pairlock_tabs, "pairlocks_bak")
|
||||||
|
kv_store_tabs = get_table_names_for_table(inspector, "KeyValueStore")
|
||||||
|
kv_store_back_name = get_backup_name(kv_store_tabs, "KeyValueStore_bak")
|
||||||
|
|
||||||
# Check if migration necessary
|
# Check if migration necessary
|
||||||
# Migrates both trades and orders table!
|
# Migrates both trades and orders table!
|
||||||
@@ -421,6 +457,16 @@ def check_migrate(engine: Engine, decl_base, previous_tables: list[str]) -> None
|
|||||||
migrate_pairlocks_table(
|
migrate_pairlocks_table(
|
||||||
decl_base, inspector, engine, pairlock_table_bak_name, cols_pairlocks
|
decl_base, inspector, engine, pairlock_table_bak_name, cols_pairlocks
|
||||||
)
|
)
|
||||||
|
if "KeyValueStore" in previous_tables:
|
||||||
|
key_column = next(filter(lambda x: x["name"] == "key", cols_kv_store), None)
|
||||||
|
# length of key column < 50, recreate table with correct length and migrate data
|
||||||
|
if key_column and getattr(key_column["type"], "length", -1) < 50:
|
||||||
|
migrating = True
|
||||||
|
logger.info(
|
||||||
|
f"Running database migration for KeyValueStore - backup: {kv_store_back_name}"
|
||||||
|
)
|
||||||
|
migrate_kv_store_table(decl_base, inspector, engine, kv_store_back_name, cols_kv_store)
|
||||||
|
|
||||||
if "orders" not in previous_tables and "trades" in previous_tables:
|
if "orders" not in previous_tables and "trades" in previous_tables:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Your database seems to be very old. "
|
"Your database seems to be very old. "
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from freqtrade.persistence.key_value_store import _KeyValueStoreModel
|
|||||||
from freqtrade.persistence.migrations import check_migrate
|
from freqtrade.persistence.migrations import check_migrate
|
||||||
from freqtrade.persistence.pairlock import PairLock
|
from freqtrade.persistence.pairlock import PairLock
|
||||||
from freqtrade.persistence.trade_model import Order, Trade
|
from freqtrade.persistence.trade_model import Order, Trade
|
||||||
|
from freqtrade.persistence.wallet_history import WalletHistory
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -91,6 +92,7 @@ def init_db(db_url: str) -> None:
|
|||||||
_CustomData.session = scoped_session(
|
_CustomData.session = scoped_session(
|
||||||
sessionmaker(bind=engine, autoflush=True), scopefunc=get_request_or_thread_id
|
sessionmaker(bind=engine, autoflush=True), scopefunc=get_request_or_thread_id
|
||||||
)
|
)
|
||||||
|
WalletHistory.session = Trade.session
|
||||||
|
|
||||||
previous_tables = inspect(engine).get_table_names()
|
previous_tables = inspect(engine).get_table_names()
|
||||||
ModelBase.metadata.create_all(engine)
|
ModelBase.metadata.create_all(engine)
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ class PairLock(ModelBase):
|
|||||||
|
|
||||||
active: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)
|
active: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lock_end_time_utc(self) -> datetime:
|
||||||
|
"""Lock end time with UTC timezoneinfo"""
|
||||||
|
return self.lock_end_time.replace(tzinfo=UTC)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
||||||
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
|
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class PairLocks:
|
|||||||
) -> PairLock:
|
) -> PairLock:
|
||||||
"""
|
"""
|
||||||
Create PairLock from now to "until".
|
Create PairLock from now to "until".
|
||||||
|
Doesn't create a new lock if there is already a lock with the same Reason, side and endtime.
|
||||||
Uses database by default, unless PairLocks.use_db is set to False,
|
Uses database by default, unless PairLocks.use_db is set to False,
|
||||||
in which case a list is maintained.
|
in which case a list is maintained.
|
||||||
:param pair: pair to lock. use '*' to lock all pairs
|
:param pair: pair to lock. use '*' to lock all pairs
|
||||||
@@ -50,10 +51,19 @@ class PairLocks:
|
|||||||
:param now: Current timestamp. Used to determine lock start time.
|
:param now: Current timestamp. Used to determine lock start time.
|
||||||
:param side: Side to lock pair, can be 'long', 'short' or '*'
|
:param side: Side to lock pair, can be 'long', 'short' or '*'
|
||||||
"""
|
"""
|
||||||
|
lock_end_time = timeframe_to_next_date(PairLocks.timeframe, until)
|
||||||
|
existing_locks = PairLocks.get_pair_locks(pair, now, side=side)
|
||||||
|
for lock in existing_locks:
|
||||||
|
if (
|
||||||
|
lock.reason == reason
|
||||||
|
and lock.lock_end_time_utc == lock_end_time
|
||||||
|
and lock.side == side
|
||||||
|
):
|
||||||
|
return lock
|
||||||
lock = PairLock(
|
lock = PairLock(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
lock_time=now or datetime.now(UTC),
|
lock_time=now or datetime.now(UTC),
|
||||||
lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until),
|
lock_end_time=lock_end_time,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
side=side,
|
side=side,
|
||||||
active=True,
|
active=True,
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ class Order(ModelBase):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
|
f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "
|
||||||
f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
|
f"side={self.side or self.ft_order_side}, filled={self.safe_filled}, "
|
||||||
f"amount={self.amount}, "
|
f"price={self.safe_price}, amount={self.amount}, "
|
||||||
f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})"
|
f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -858,9 +858,9 @@ class LocalTrade:
|
|||||||
higher_stop = stop_loss_norm > self.stop_loss
|
higher_stop = stop_loss_norm > self.stop_loss
|
||||||
lower_stop = stop_loss_norm < self.stop_loss
|
lower_stop = stop_loss_norm < self.stop_loss
|
||||||
|
|
||||||
# stop losses only walk up, never down!,
|
# stop losses only walk up, never down!
|
||||||
# ? But adding more to a leveraged trade would create a lower liquidation price,
|
# but adding more to a leveraged trade would create a lower liquidation price,
|
||||||
# ? decreasing the minimum stoploss
|
# decreasing the minimum stoploss
|
||||||
if (
|
if (
|
||||||
allow_refresh
|
allow_refresh
|
||||||
or (higher_stop and not self.is_short)
|
or (higher_stop and not self.is_short)
|
||||||
@@ -1248,12 +1248,16 @@ class LocalTrade:
|
|||||||
close_profit_abs = 0.0
|
close_profit_abs = 0.0
|
||||||
# Reset funding fees
|
# Reset funding fees
|
||||||
self.funding_fees = 0.0
|
self.funding_fees = 0.0
|
||||||
funding_fees = 0.0
|
# Total funding fees - cumulated over all orders
|
||||||
ordercount = len(self.orders) - 1
|
total_funding_fees = 0.0
|
||||||
|
# current funding fees - resetting on every exit to be aligned with profit calculation,
|
||||||
|
# as funding fees are part of the profit
|
||||||
|
current_funding_fee = 0.0
|
||||||
for i, o in enumerate(self.orders):
|
for i, o in enumerate(self.orders):
|
||||||
if o.ft_is_open or not o.filled:
|
if o.ft_is_open or not o.filled:
|
||||||
continue
|
continue
|
||||||
funding_fees += o.funding_fee or 0.0
|
current_funding_fee += o.funding_fee or 0.0
|
||||||
|
total_funding_fees += o.funding_fee or 0.0
|
||||||
tmp_amount = FtPrecise(o.safe_amount_after_fee)
|
tmp_amount = FtPrecise(o.safe_amount_after_fee)
|
||||||
tmp_price = FtPrecise(o.safe_price)
|
tmp_price = FtPrecise(o.safe_price)
|
||||||
|
|
||||||
@@ -1268,11 +1272,8 @@ class LocalTrade:
|
|||||||
avg_price = current_stake / current_amount
|
avg_price = current_stake / current_amount
|
||||||
|
|
||||||
if is_exit:
|
if is_exit:
|
||||||
# Process exits
|
# Intermediate funding fees for profit calculation
|
||||||
if i == ordercount and is_closing:
|
self.funding_fees = current_funding_fee
|
||||||
# Apply funding fees only to the last closing order
|
|
||||||
self.funding_fees = funding_fees
|
|
||||||
|
|
||||||
exit_rate = o.safe_price
|
exit_rate = o.safe_price
|
||||||
exit_amount = o.safe_amount_after_fee
|
exit_amount = o.safe_amount_after_fee
|
||||||
prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
|
prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price))
|
||||||
@@ -1281,10 +1282,12 @@ class LocalTrade:
|
|||||||
# This needs to be calculated based on the last occurring exit to be aligned
|
# This needs to be calculated based on the last occurring exit to be aligned
|
||||||
# with realized_profit.
|
# with realized_profit.
|
||||||
close_profit = (close_profit_abs / total_stake) * self.leverage
|
close_profit = (close_profit_abs / total_stake) * self.leverage
|
||||||
|
current_funding_fee = 0.0
|
||||||
else:
|
else:
|
||||||
total_stake += self._calc_open_trade_value(tmp_amount, price)
|
total_stake += self._calc_open_trade_value(tmp_amount, price)
|
||||||
max_stake_amount += tmp_amount * price
|
max_stake_amount += tmp_amount * price
|
||||||
self.funding_fees = funding_fees
|
# Assign cumulated funding fees after all orders have been processed
|
||||||
|
self.funding_fees = total_funding_fees
|
||||||
self.max_stake_amount = float(max_stake_amount) / (self.leverage or 1.0)
|
self.max_stake_amount = float(max_stake_amount) / (self.leverage or 1.0)
|
||||||
|
|
||||||
if close_profit:
|
if close_profit:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user