Compare commits
816 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e2313be7b | |||
| f535c4cff4 | |||
| 5f901d837c | |||
| f7b11c5166 | |||
| 2e92fccd7b | |||
| 09c60b9543 | |||
| 166476b21c | |||
| 3a775a5b65 | |||
| ab9b6989be | |||
| 1f69cafd16 | |||
| 7030b0bfcc | |||
| f0dd3e4ab8 | |||
| 402ef216c3 | |||
| 5f585e330f | |||
| 6d04874e26 | |||
| 4b45e3f3d2 | |||
| 4359de7cc5 | |||
| 48015a9b1b | |||
| 34df7b1793 | |||
| 429276cac6 | |||
| e7154c93a9 | |||
| 523a9d644b | |||
| 3a6311b7fc | |||
| f6dbca35f3 | |||
| 07e1088839 | |||
| 74d373e37d | |||
| 6ce4d1a7c5 | |||
| 402a3bb2c2 | |||
| 961eaef2a5 | |||
| 6e49a90730 | |||
| 246a9049e6 | |||
| 816f81e5b4 | |||
| 49e32b2600 | |||
| b6993f0b24 | |||
| 58079a8b50 | |||
| bdf080155d | |||
| 52fede6307 | |||
| 7d6c80fa7c | |||
| 814111adb9 | |||
| f0f4191dcf | |||
| 9a80a11577 | |||
| 5551a287a8 | |||
| b0f7c91548 | |||
| 9232e17af4 | |||
| d962137d24 | |||
| 22c91137bb | |||
| 92b8bdd113 | |||
| 3c5178b002 | |||
| 8231005225 | |||
| 2646313c11 | |||
| 12b4875e24 | |||
| 6a98df1c71 | |||
| a5d24f14cb | |||
| 50f914d6d5 | |||
| b4422131fb | |||
| f61b50dbfc | |||
| ce590cced7 | |||
| a417af1ff0 | |||
| d2e650d188 | |||
| 088516b2ed | |||
| 797ea46bb3 | |||
| 96c0a6296f | |||
| e524e030ef | |||
| e19d96f85c | |||
| 0672d8f094 | |||
| 780858f538 | |||
| 7ec495be67 | |||
| d27aef2063 | |||
| d7d07bde75 | |||
| f54ccde5fc | |||
| e1d7c7a081 | |||
| d593384219 | |||
| 5c737d977d | |||
| 22c0af9bb1 | |||
| 1c21b092e6 | |||
| 2325e5f535 | |||
| dbaece9462 | |||
| edd33ab77e | |||
| 38934ddda2 | |||
| 5c9455c32a | |||
| 9e2eca3b8b | |||
| 82a487ec50 | |||
| 466db15ccb | |||
| c7dfb005ba | |||
| 82ae8b64e4 | |||
| 1ffc2e47a1 | |||
| a5c392e176 | |||
| 8f9754dd54 | |||
| b3b3b1d134 | |||
| 26fd70ce4b | |||
| 18aed385f9 | |||
| e6db0ab00d | |||
| 8ed1e6c394 | |||
| da62e614d7 | |||
| ca94909ccc | |||
| 131c1aaf13 | |||
| 5f06b1d43e | |||
| c999de6291 | |||
| a42c8faeca | |||
| ecfbfa2a35 | |||
| 6b86214ca8 | |||
| df6f53ba30 | |||
| 0888f6dba0 | |||
| 5d3c798dfd | |||
| 93d2397f72 | |||
| 05e759a38c | |||
| 67eaa4ac49 | |||
| e8aeebb123 | |||
| d121e97067 | |||
| bdf5c4de8e | |||
| 7492cf8bf5 | |||
| d29d4cba66 | |||
| 655d61e560 | |||
| ba6d210662 | |||
| aff46acdd0 | |||
| 5a16cbe602 | |||
| 519f2d93ba | |||
| 646c091e02 | |||
| 12c3698480 | |||
| c86acd20d6 | |||
| 60b787edc9 | |||
| eec33190d2 | |||
| 6e38759bd1 | |||
| 6a242356a0 | |||
| 04d2ea8773 | |||
| d78ffd7a05 | |||
| 3f21a18f46 | |||
| c4514f94cf | |||
| af39ec1e07 | |||
| c02530759d | |||
| 6df42502ac | |||
| fea13d1d3d | |||
| c7685febcf | |||
| abf5f2b49a | |||
| 24319585b4 | |||
| 812f357d7e | |||
| 51431d9533 | |||
| eb81c70523 | |||
| b52805cd96 | |||
| cb1266225e | |||
| 33ba2ad17c | |||
| 2f2b36f648 | |||
| aa1de750e9 | |||
| 68410f2f37 | |||
| 1f00c9d680 | |||
| a91fd66abc | |||
| 4036375759 | |||
| 9eb668d08a | |||
| ed22b4e09b | |||
| 52090d7b8e | |||
| 3a065c27e8 | |||
| a14f54735c | |||
| c5697c7d68 | |||
| 6381a32685 | |||
| 42230fdd0e | |||
| 4934a83a2d | |||
| 0ef0ff2a78 | |||
| 80d23f1ba1 | |||
| 59b05ece77 | |||
| e467405e60 | |||
| 1f92c3fd70 | |||
| 74e8875280 | |||
| 3c44a013e5 | |||
| 8546542a16 | |||
| f56460cc1f | |||
| 54f25b5b8f | |||
| 541b976b8e | |||
| b6c7e24cad | |||
| 22d624a25d | |||
| d482117fd1 | |||
| 835f00b106 | |||
| 55c3b83fa8 | |||
| 348afa46f0 | |||
| a7890a964f | |||
| ba57ab9a1b | |||
| ac6b396ed3 | |||
| 570bfef4b3 | |||
| 100babc056 | |||
| 5ab8338c3e | |||
| 600b6a7d23 | |||
| f6e465a0ba | |||
| bea006880b | |||
| a7e35f5d78 | |||
| 5fb963abbf | |||
| 351fe62d18 | |||
| 65a713a569 | |||
| 646531cbfc | |||
| 8233ca67bb | |||
| d991069efc | |||
| d86bca9f00 | |||
| 82f71d8e4b | |||
| 315a1baf2c | |||
| e5bd33061b | |||
| 06ed6e6b23 | |||
| 4a578148e2 | |||
| 2c0a029187 | |||
| a3b83afa84 | |||
| 6c2a5b460e | |||
| aa302fe531 | |||
| f2c2e84a74 | |||
| d73a173c6e | |||
| 110b50a3cb | |||
| 36f5afe910 | |||
| 15f79c19a0 | |||
| 7135466092 | |||
| e4c25ed07e | |||
| 273ac9a9c0 | |||
| 7007e71dcf | |||
| 14247854ce | |||
| d863adc8af | |||
| 2e33b1dc56 | |||
| d56ae991c2 | |||
| 2913af90ca | |||
| 553e45c948 | |||
| 2364a08fd6 | |||
| eed5de4045 | |||
| 71a4f88c8b | |||
| db1162e0a1 | |||
| 40a791985e | |||
| 157669b775 | |||
| 3d8d101bfc | |||
| 7e3c8fc8ab | |||
| c58ac19adf | |||
| 35d038cbf7 | |||
| 13a5c54338 | |||
| c0a439fb5f | |||
| 5ce2203786 | |||
| f3931c8587 | |||
| 828abf6326 | |||
| 33ebbab6a8 | |||
| 0a49816b24 | |||
| c77ac9f101 | |||
| 820a13f37a | |||
| 2a29fccd1d | |||
| b857e19392 | |||
| b4bdc66c39 | |||
| 98b56a49c8 | |||
| 791fbe8054 | |||
| cedfb2a51f | |||
| 93b6c52aad | |||
| 0ef85e161e | |||
| 26aed91bbf | |||
| 3c2ddda203 | |||
| c86484b152 | |||
| beec9123d4 | |||
| a33eb51f36 | |||
| 373cd8141c | |||
| 360f294f45 | |||
| a64ab2629b | |||
| 5d88923e03 | |||
| 84cbb1eb01 | |||
| 1c764d622d | |||
| fb6627335b | |||
| 436d3b1bbc | |||
| 308c80c7b2 | |||
| a4a69bd16d | |||
| 9bd68dd63a | |||
| 670cfa2087 | |||
| 7a69cfa62b | |||
| 142dd3fd25 | |||
| d102d3cac8 | |||
| 2b7bcdb594 | |||
| 9ccbf15080 | |||
| 16717f06e9 | |||
| 7d77e16271 | |||
| f501619434 | |||
| 7c9a618c72 | |||
| 16374ef7f1 | |||
| bb4d2f8f1a | |||
| 52fc180c75 | |||
| 87b313cd53 | |||
| 74c88f77c0 | |||
| 6cb459f37b | |||
| 00848c9a96 | |||
| e8adf01b18 | |||
| d28fce7ed8 | |||
| 390d504cf2 | |||
| 1799aa33f9 | |||
| dcff1c4317 | |||
| 121bdab8fe | |||
| 9a48feea35 | |||
| 0955ccadfe | |||
| c778e44e80 | |||
| 18933a6e20 | |||
| 182bdaceb5 | |||
| 6ae4394628 | |||
| 1bdb1539b1 | |||
| 2765277cd0 | |||
| 085a9be186 | |||
| e279c0333f | |||
| 5e7246d23b | |||
| fe86163730 | |||
| b5d467c943 | |||
| c9fd8a619a | |||
| 690080e180 | |||
| 583431e9d5 | |||
| 4b82f82d9c | |||
| 63d6001415 | |||
| ad6cf90037 | |||
| 558c65a85c | |||
| 8cb72e1fa3 | |||
| 4870d45b04 | |||
| 4436acea68 | |||
| e8eb6d7018 | |||
| f1a6e61dbd | |||
| 4abb871c16 | |||
| a6eccaec69 | |||
| 1747724206 | |||
| 616900330b | |||
| bbe5e22730 | |||
| 8b396ae3a2 | |||
| 7f511cbe82 | |||
| 12f5b9bc5b | |||
| d152e3e47d | |||
| 39908c60e2 | |||
| 74a1e48a1c | |||
| b507aebf1c | |||
| ad293c28d0 | |||
| e4c7b5511a | |||
| 85fd070899 | |||
| e658490593 | |||
| 621e78e7ce | |||
| fc5801a0b7 | |||
| 22cd1357f8 | |||
| ead68e0a51 | |||
| 80b87d2592 | |||
| 0ac43042cc | |||
| 590a69ca42 | |||
| e44a36cedd | |||
| 131ab77085 | |||
| ba07de8d00 | |||
| 6ee34aa482 | |||
| d5686d548c | |||
| 4917a0abe5 | |||
| 93f2cf5db7 | |||
| 38a470098f | |||
| b0eb5c77ec | |||
| 0d5bd94f43 | |||
| 735284dccd | |||
| deb0dd80c8 | |||
| 12f815e157 | |||
| 059636f1ea | |||
| 793ca9c3da | |||
| 323a28c0bc | |||
| 77567f6315 | |||
| dda448c10d | |||
| b7df0de33c | |||
| a89ef23407 | |||
| b90538f47d | |||
| ee6b7f2f95 | |||
| b4aa6231e0 | |||
| 6bd57cc3b5 | |||
| 26db014782 | |||
| 8e3ca302b0 | |||
| 14aeb56ff9 | |||
| 9cbb8ec1c2 | |||
| faa5c4be39 | |||
| 45b6ec95a4 | |||
| 91b921cc88 | |||
| 123897ea89 | |||
| 31cf2dd46d | |||
| 06a53924d7 | |||
| 993771c5d3 | |||
| f08f19d1cb | |||
| 515912f14d | |||
| df8465a9f3 | |||
| 1333cb3181 | |||
| 18fa981690 | |||
| 60425e6237 | |||
| 3b5f1d52cd | |||
| 519721b58b | |||
| 3ae7823983 | |||
| 7182e6960e | |||
| 8596042065 | |||
| c2e92250b5 | |||
| d2ed5c6736 | |||
| 8e023a7bf3 | |||
| b3711fdf6c | |||
| 681876e0e5 | |||
| c3502991d3 | |||
| 6cc22fcf74 | |||
| 2f011e5421 | |||
| 4322acbbb8 | |||
| 50cba51008 | |||
| 7498f20baa | |||
| a1b3e59491 | |||
| 4fcbce2ce5 | |||
| 22699ee6ed | |||
| aec5450036 | |||
| 2c5a150a7b | |||
| f845dc71b1 | |||
| 61274252af | |||
| 00d903cb3f | |||
| 29f235441b | |||
| 872b59c9cd | |||
| be7f5f4e26 | |||
| 82c1f096de | |||
| ed7887c426 | |||
| e58f7b0344 | |||
| 32eaae5dcf | |||
| 38c74e5d1a | |||
| e89a31c6d1 | |||
| 3eb8de633c | |||
| 0ee3e4304f | |||
| 228dfe8ab2 | |||
| bfcc2aa153 | |||
| 39e2ecb6a2 | |||
| 0fcfd933ea | |||
| b9a4918d61 | |||
| 0d7127bb6a | |||
| cfd048e74e | |||
| c911332da3 | |||
| 43a849d45d | |||
| 3ad502b073 | |||
| 6d4ad393d5 | |||
| 69cc238700 | |||
| 9a9b4e15f3 | |||
| fcab9460fb | |||
| 58130572a9 | |||
| f82d2fe796 | |||
| 577464c12d | |||
| 00bd3e5a5c | |||
| bc2be74b36 | |||
| 3543437fc5 | |||
| 4b278941b8 | |||
| 8a33b71351 | |||
| a09ca38de2 | |||
| 4d4fee9874 | |||
| f2dfe1788a | |||
| 0e82c0458a | |||
| 2ec2626277 | |||
| 04f4f327df | |||
| 34ad58e2da | |||
| f78aa7b39a | |||
| a174461952 | |||
| 6c164683b7 | |||
| 9cdd8659ec | |||
| 795df94817 | |||
| 40f87c0d30 | |||
| 1380940565 | |||
| 595309c9ed | |||
| 78dde7d1bc | |||
| 2a0da08413 | |||
| fd7aff6063 | |||
| c41584a548 | |||
| 1e6d832f71 | |||
| 6f2ba13b0c | |||
| 78141c7216 | |||
| 8e91fea11f | |||
| 0f8c6d456e | |||
| 2a1b3ae7ba | |||
| 0a94a6d332 | |||
| 75a3651493 | |||
| 0dad2b36ab | |||
| 70834b6491 | |||
| 31269c7ce0 | |||
| 9f00a1d0d2 | |||
| f0a5bbe511 | |||
| 863124d67e | |||
| 6d1e4d8f45 | |||
| 5f12c81768 | |||
| 9a37d7bfbb | |||
| c9c08906e5 | |||
| 31341afcef | |||
| 383b305b74 | |||
| 84244e7ca7 | |||
| cc06e9baec | |||
| d5ac8a5b0b | |||
| 00a4a743d2 | |||
| 650cb1ef8c | |||
| 76ea4ea14f | |||
| 90502a6ac0 | |||
| f9fe909847 | |||
| 7085103e43 | |||
| c5ee96b66a | |||
| d454a10dea | |||
| e3d03f586d | |||
| e8d748535f | |||
| 1cdb386374 | |||
| 8205cb25b8 | |||
| 511b817e84 | |||
| de94170424 | |||
| 6e9b60c0b0 | |||
| b8cf38cb6f | |||
| b394705545 | |||
| 9b99c5b3f3 | |||
| e77e901df1 | |||
| b26c37af62 | |||
| 414fe81b27 | |||
| 00de8077f7 | |||
| 1dde1ad0ed | |||
| 2708343a2d | |||
| 61eb3306dd | |||
| e71329c82b | |||
| dc4adccaaf | |||
| 45b9c8c53f | |||
| 5c02ce934d | |||
| 260512d678 | |||
| 8b1b16cb08 | |||
| cacd52db1c | |||
| 4e809947c7 | |||
| 89c4aa23f3 | |||
| 1bd043a912 | |||
| 3ce23f99de | |||
| c5fa5c5c69 | |||
| 21d0b60168 | |||
| ba3925cdcd | |||
| 9cf6d47a43 | |||
| e01993b9c1 | |||
| c354a76ae2 | |||
| f296213088 | |||
| d80dbc1644 | |||
| 91ee8652ef | |||
| 75dfc95221 | |||
| 05fe35ce57 | |||
| 95e36013db | |||
| c5c96d9610 | |||
| ad9a7279be | |||
| dafb2fda7b | |||
| 04fea4ac29 | |||
| 896250fe88 | |||
| 8485abc9b8 | |||
| 2013a2edda | |||
| 11eb0c0802 | |||
| 78709a9d76 | |||
| f55a90d98c | |||
| 4b3ff02d49 | |||
| ea52deb98e | |||
| 7b33242aa0 | |||
| 46ee092119 | |||
| 931cd2e7ba | |||
| b6899264a8 | |||
| ca47882fa9 | |||
| c21ce5ef94 | |||
| 9baa6eebbe | |||
| 0feec257e3 | |||
| 802d42cc00 | |||
| f546146d40 | |||
| d2c4bd1b50 | |||
| 5e5495e050 | |||
| 51f3f0d65a | |||
| 0019867da8 | |||
| 335e632bbc | |||
| d0ac462dd3 | |||
| fc113f7bbf | |||
| 4f6b1b6fd0 | |||
| 006d2305af | |||
| 5c9f140c5d | |||
| c6f3b00819 | |||
| 1780d2db23 | |||
| 78d602f7b4 | |||
| 4823f66c4e | |||
| a3c19d10f7 | |||
| ebb318a0f0 | |||
| 35d6e6e832 | |||
| 37bc93e0d3 | |||
| 2cd30b0604 | |||
| 2f30cda9d7 | |||
| 9e1c760222 | |||
| 53884cae2f | |||
| 1b16b450b7 | |||
| 18ba3241ce | |||
| 6f8ad595b7 | |||
| 5479f204b5 | |||
| c54b94b5a4 | |||
| 5ed590d862 | |||
| 3f5ed399b5 | |||
| a9a686c64e | |||
| 978ca8f852 | |||
| ea30f906f9 | |||
| 811112f0ce | |||
| 5ebffb27aa | |||
| 0cb676a677 | |||
| 6a6dce323d | |||
| 0d62fab367 | |||
| a1b4a4dbfb | |||
| bcd033bd0d | |||
| 092da6aee5 | |||
| 13c63c0bf5 | |||
| aa38a36b56 | |||
| ce8732b0cd | |||
| bcf6c244b0 | |||
| 9023ca4f56 | |||
| e9847d9549 | |||
| 0beb76ce48 | |||
| 21d723b35d | |||
| f002ce67b1 | |||
| 5fdc8acbe9 | |||
| 1811f9581f | |||
| 66235f3198 | |||
| 6c8b8f642a | |||
| 29b7cbbb12 | |||
| b2d5343a4e | |||
| 6905b8059d | |||
| 301aff141a | |||
| 245ca4bc2a | |||
| f32d25daac | |||
| 1143aba671 | |||
| 278bd0e97d | |||
| d5b296c75c | |||
| c63fba2e57 | |||
| c8d74baaa1 | |||
| a9ebff3965 | |||
| 8859b9009a | |||
| d4aee7e433 | |||
| f0908a1043 | |||
| cf6bf1b7b5 | |||
| 84e9251fcd | |||
| 5800002d42 | |||
| 6a98c19dab | |||
| 025707d9ad | |||
| 4540e2ceea | |||
| eb3e8b82f9 | |||
| 98da54ae82 | |||
| fa5210aa4b | |||
| 289e53ed5a | |||
| 789f4aa4f8 | |||
| 2045fa6427 | |||
| 8c62816972 | |||
| c0d40a03aa | |||
| 551831c901 | |||
| 67850fd2a2 | |||
| 715ef21da0 | |||
| b8defa5d45 | |||
| 6d3dac828a | |||
| 968a9a3750 | |||
| 03b5cdb240 | |||
| 7dc0d5d146 | |||
| c20f511418 | |||
| 484b3ff2f3 | |||
| 5d25adfc42 | |||
| 1099fd2525 | |||
| 293989f019 | |||
| 57d1e24275 | |||
| e52b5aef45 | |||
| a16d2a1ef9 | |||
| 13c86452e9 | |||
| fe18731057 | |||
| e26529b695 | |||
| bac6219cc1 | |||
| 0ed3bdc747 | |||
| 8af0631ff0 | |||
| 2e3d276304 | |||
| bd5630a104 | |||
| b406219515 | |||
| 31d3a19836 | |||
| 83b372a32d | |||
| 51e0b204b6 | |||
| 6848f9197e | |||
| 6d2c30abca | |||
| 93bde7dc46 | |||
| 97e2e0a405 | |||
| 12206f028b | |||
| 4c7944ac77 | |||
| d4ced7b416 | |||
| 451eef5c99 | |||
| ae8f059de0 | |||
| 878bd7cbc7 | |||
| 6d017c9a6c | |||
| 4c3d9b8c70 | |||
| 23a4260859 | |||
| d15d08a2d5 | |||
| 6aeab16ce4 | |||
| 46538d9a5b | |||
| f0f48395c5 | |||
| b3a1442a69 | |||
| bbafb1dabd | |||
| 645a9159e4 | |||
| 2d3ff2f8ca | |||
| 5919736904 | |||
| 732610e200 | |||
| 309985503d | |||
| 072ed705fc | |||
| 3689b52309 | |||
| 01fbf31405 | |||
| 00f687f3f5 | |||
| 96849fcafe | |||
| 80d5b6e24c | |||
| f33fd98c83 | |||
| c7636734de | |||
| 994e61fe42 | |||
| 359eba462b | |||
| 38e48c0c5e | |||
| 1e187e0945 | |||
| e4fc5df1cf | |||
| cde886b884 | |||
| 9f4e167455 | |||
| f5e6504e22 | |||
| 0ec1066b34 | |||
| 62d4da3b94 | |||
| 253950deb6 | |||
| ed92d6beb9 | |||
| ebb362d9fa | |||
| f23fad420e | |||
| aaf23221ff | |||
| 156c1a99a9 | |||
| cb55ef5c59 | |||
| 6540fbb8e7 | |||
| 412392aea9 | |||
| e6030b7f59 | |||
| c1c968666e | |||
| f8d6363d19 | |||
| acc69e0d2e | |||
| 5110d0bdde | |||
| 597cc0592b | |||
| 01b0a8fa42 | |||
| cf6b7a847b | |||
| 4897080827 | |||
| 3ca8e0fb5c | |||
| 40f4ff04c2 | |||
| 3bd911982f | |||
| 730383ab18 | |||
| b70f10dca6 | |||
| 17009ac59f | |||
| 07fbf2b467 | |||
| 3f0be5e41f | |||
| 2845568f61 | |||
| e3229935f6 | |||
| b1ee115b77 | |||
| d6060f04bc | |||
| 1ae5310d2f | |||
| 417a0817a7 | |||
| 41a82eff21 | |||
| d41acc77f7 | |||
| 271fc6b585 | |||
| 262f4ffa4c | |||
| 338c1c5424 | |||
| 8ca25b1757 | |||
| 68e54248fd | |||
| e792bafe21 | |||
| fe95581acd | |||
| 7f831f8a9c | |||
| 3fb7382cc4 | |||
| 83f70b3c27 | |||
| 922d5893f8 | |||
| e6e14ab67f | |||
| 37792e7d86 | |||
| a63ea6adb8 | |||
| 06a8f3d097 | |||
| e76b9bb64a | |||
| 606dbe5db1 | |||
| 81e4e0d301 | |||
| a3e97527d1 | |||
| 3ebc7bfcfc | |||
| c892b07eef | |||
| 7a23da4deb | |||
| 670999abda | |||
| e2b5e209f6 | |||
| 7137461891 | |||
| 10e7d279f0 | |||
| 971cb94ce4 | |||
| 19526f1df2 | |||
| 9c73c8671e | |||
| 0f5427f4a0 | |||
| a013793b2f | |||
| 1a77f1b203 | |||
| faf552837a | |||
| 4b2d099258 | |||
| 4e6ea1d2ba | |||
| 197623839d | |||
| 6d8fa76708 | |||
| d7eb49d5d0 | |||
| 659509e085 | |||
| 2b729c2527 | |||
| 38151423b8 | |||
| 2f990b773d | |||
| 20d253dd41 | |||
| 6e2165b5e5 | |||
| 6860904bb9 | |||
| 450dc23374 | |||
| d781f09b46 | |||
| dbde8332f7 | |||
| f5f1785b17 | |||
| 7d01928df0 | |||
| 31f712749b | |||
| bcdf83100f | |||
| dc88ea0ed9 | |||
| 543457a1ef | |||
| 5d781d0114 | |||
| 45b1dabe0a | |||
| ffea373f86 | |||
| ba650de562 | |||
| 39233ff240 | |||
| 8950423418 | |||
| 760a2b9ff7 | |||
| a13f488b84 | |||
| 09b1582704 | |||
| 61ab49a124 | |||
| 1a95b04d79 | |||
| 691a6b0a72 | |||
| 1d0a902251 | |||
| 1de9f5a2fb | |||
| 7a5342cc93 | |||
| 52c73eabeb | |||
| 02de1f0a1e | |||
| 3453bdf607 | |||
| ac2723c3a0 | |||
| 1536c09df3 | |||
| 645f70e216 | |||
| b59db3c690 | |||
| fade66afd9 | |||
| f63484d0b0 | |||
| 3543e96ec5 | |||
| c83ea0db4f | |||
| ad256367be | |||
| 73b427370b | |||
| ca7234e33f | |||
| cd7b267171 | |||
| 6cef73947c | |||
| 33dbe32e5c | |||
| 8caf7a888e | |||
| 033050e6cf | |||
| 0340d36af2 | |||
| ad7816d51e | |||
| 73127c8179 | |||
| ea6d23bd2d |
@@ -46,8 +46,9 @@ runs:
|
|||||||
id: tags
|
id: tags
|
||||||
env:
|
env:
|
||||||
BRANCH_NAME_INPUT: ${{ github.event.inputs.branch_name }}
|
BRANCH_NAME_INPUT: ${{ github.event.inputs.branch_name }}
|
||||||
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||||
BRANCH_NAME="${BRANCH_NAME_INPUT}"
|
BRANCH_NAME="${BRANCH_NAME_INPUT}"
|
||||||
else
|
else
|
||||||
BRANCH_NAME="${GITHUB_REF##*/}"
|
BRANCH_NAME="${GITHUB_REF##*/}"
|
||||||
|
|||||||
+15
-8
@@ -2,7 +2,7 @@ version: 2
|
|||||||
updates:
|
updates:
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 4
|
default-days: 7
|
||||||
directories:
|
directories:
|
||||||
- "/"
|
- "/"
|
||||||
- "/docker"
|
- "/docker"
|
||||||
@@ -16,7 +16,7 @@ updates:
|
|||||||
- package-ecosystem: devcontainers
|
- package-ecosystem: devcontainers
|
||||||
directory: "/"
|
directory: "/"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 4
|
default-days: 7
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
@@ -24,13 +24,13 @@ updates:
|
|||||||
- package-ecosystem: pip
|
- package-ecosystem: pip
|
||||||
directory: "/"
|
directory: "/"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 4
|
default-days: 7
|
||||||
exclude:
|
exclude:
|
||||||
- ccxt
|
- ccxt
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: "cron"
|
||||||
time: "03:00"
|
# Monday at 03:00
|
||||||
timezone: "Etc/UTC"
|
cronjob: "0 3 * * 1"
|
||||||
open-pull-requests-limit: 15
|
open-pull-requests-limit: 15
|
||||||
target-branch: develop
|
target-branch: develop
|
||||||
groups:
|
groups:
|
||||||
@@ -51,8 +51,15 @@ updates:
|
|||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 4
|
default-days: 7
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "cron"
|
||||||
|
# Monday at 03:00
|
||||||
|
cronjob: "0 3 * * 1"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
target-branch: develop
|
target-branch: develop
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
# Combine updates for github provided actions
|
||||||
|
- "actions/*"
|
||||||
|
|||||||
@@ -2,29 +2,41 @@ name: Binance Leverage tiers update
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 3 * * 4"
|
- cron: "25 3 * * 4"
|
||||||
# on demand
|
# on demand
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-update:
|
auto-update:
|
||||||
|
name: "Auto Update Binance Leverage Tiers"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
environment:
|
||||||
name: develop
|
name: develop
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.14"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||||
|
with:
|
||||||
|
activate-environment: true
|
||||||
|
enable-cache: false
|
||||||
|
python-version: "3.14"
|
||||||
|
|
||||||
- name: Install ccxt
|
- name: Install ccxt
|
||||||
run: pip install ccxt
|
run: uv pip install ccxt orjson
|
||||||
|
|
||||||
- name: Run leverage tier update
|
- name: Run leverage tier update
|
||||||
env:
|
env:
|
||||||
@@ -34,7 +46,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@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||||
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
|
||||||
|
|||||||
+34
-34
@@ -16,8 +16,8 @@ on:
|
|||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
|
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
permissions:
|
permissions: {}
|
||||||
repository-projects: read
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: "Tests and Linting"
|
name: "Tests and Linting"
|
||||||
@@ -25,20 +25,20 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "ubuntu-22.04", "ubuntu-24.04", "macos-14", "macos-15" , "windows-2022", "windows-2025" ]
|
os: [ "ubuntu-22.04", "ubuntu-24.04", "macos-14", "macos-15" , "windows-2022", "windows-2025" ]
|
||||||
python-version: ["3.11", "3.12", "3.13"]
|
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -74,7 +74,7 @@ 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@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
- uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||||
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
|
||||||
@@ -87,12 +87,12 @@ jobs:
|
|||||||
rm -rf codecov codecov.SHA256SUM codecov.SHA256SUM.sig
|
rm -rf codecov codecov.SHA256SUM codecov.SHA256SUM.sig
|
||||||
|
|
||||||
- name: Run json schema extract
|
- name: Run json schema extract
|
||||||
# This should be kept before the repository check to ensure that the schema is up-to-date
|
# This must be kept before the repository check to ensure that the schema is up-to-date
|
||||||
run: |
|
run: |
|
||||||
python build_helpers/extract_config_json_schema.py
|
python build_helpers/extract_config_json_schema.py
|
||||||
|
|
||||||
- name: Run command docs partials extract
|
- name: Run command docs partials extract
|
||||||
# This should be kept before the repository check to ensure that the docs are up-to-date
|
# This must be kept before the repository check to ensure that the docs are up-to-date
|
||||||
if: ${{ (matrix.python-version == '3.13') }}
|
if: ${{ (matrix.python-version == '3.13') }}
|
||||||
run: |
|
run: |
|
||||||
python build_helpers/create_command_partials.py
|
python build_helpers/create_command_partials.py
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check for repository changes - Windows
|
- name: Check for repository changes - Windows
|
||||||
if: ${{ runner.os == 'Windows' && (matrix.python-version != '3.13') }}
|
if: ${{ runner.os == 'Windows' }}
|
||||||
run: |
|
run: |
|
||||||
if (git status --porcelain) {
|
if (git status --porcelain) {
|
||||||
Write-Host "Repository is dirty, changes detected:"
|
Write-Host "Repository is dirty, changes detected:"
|
||||||
@@ -159,8 +159,9 @@ jobs:
|
|||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
$PSVersionTable
|
$PSVersionTable
|
||||||
|
Get-PSRepository | Format-List *
|
||||||
Set-PSRepository psgallery -InstallationPolicy trusted
|
Set-PSRepository psgallery -InstallationPolicy trusted
|
||||||
Install-Module -Name Pester -RequiredVersion 5.3.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}
|
||||||
@@ -177,12 +178,12 @@ jobs:
|
|||||||
name: "Mypy Version Check"
|
name: "Mypy Version Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -195,11 +196,11 @@ jobs:
|
|||||||
name: "Pre-commit checks"
|
name: "Pre-commit checks"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
||||||
@@ -208,7 +209,7 @@ jobs:
|
|||||||
name: "Documentation build"
|
name: "Documentation build"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -217,7 +218,7 @@ jobs:
|
|||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -240,17 +241,17 @@ jobs:
|
|||||||
name: "Tests and Linting - Online tests"
|
name: "Tests and Linting - Online tests"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||||
with:
|
with:
|
||||||
activate-environment: true
|
activate-environment: true
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -274,6 +275,7 @@ jobs:
|
|||||||
|
|
||||||
# Notify only once - when CI completes (and after deploy) in case it's successful
|
# Notify only once - when CI completes (and after deploy) in case it's successful
|
||||||
notify-complete:
|
notify-complete:
|
||||||
|
name: "Notify CI Completion"
|
||||||
needs: [
|
needs: [
|
||||||
build,
|
build,
|
||||||
build-linux-online
|
build-linux-online
|
||||||
@@ -281,8 +283,6 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
# Discord notification can't handle schedule events
|
# Discord notification can't handle schedule events
|
||||||
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
|
if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade'
|
||||||
permissions:
|
|
||||||
repository-projects: read
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Check user permission
|
- name: Check user permission
|
||||||
@@ -320,12 +320,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
jobs: ${{ toJSON(needs) }}
|
jobs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ jobs:
|
|||||||
python -m build --sdist --wheel
|
python -m build --sdist --wheel
|
||||||
|
|
||||||
- name: Upload artifacts 📦
|
- name: Upload artifacts 📦
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
||||||
with:
|
with:
|
||||||
name: freqtrade-build
|
name: freqtrade-build
|
||||||
path: |
|
path: |
|
||||||
@@ -348,7 +348,7 @@ jobs:
|
|||||||
python -m build --sdist --wheel ft_client
|
python -m build --sdist --wheel ft_client
|
||||||
|
|
||||||
- name: Upload artifacts 📦
|
- name: Upload artifacts 📦
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.1.0
|
||||||
with:
|
with:
|
||||||
name: freqtrade-client-build
|
name: freqtrade-client-build
|
||||||
path: |
|
path: |
|
||||||
@@ -364,15 +364,15 @@ jobs:
|
|||||||
name: testpypi
|
name: testpypi
|
||||||
url: https://test.pypi.org/p/freqtrade
|
url: https://test.pypi.org/p/freqtrade
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write # Needed for pypa/gh-action-pypi-publish
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
pattern: freqtrade*-build
|
pattern: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
@@ -393,15 +393,15 @@ jobs:
|
|||||||
name: pypi
|
name: pypi
|
||||||
url: https://pypi.org/p/freqtrade
|
url: https://pypi.org/p/freqtrade
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write # Needed for pypa/gh-action-pypi-publish
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download artifact 📦
|
- name: Download artifact 📦
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||||
with:
|
with:
|
||||||
pattern: freqtrade*-build
|
pattern: freqtrade*-build
|
||||||
path: dist
|
path: dist
|
||||||
@@ -419,7 +419,7 @@ jobs:
|
|||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
uses: ./.github/workflows/docker-build.yml
|
uses: ./.github/workflows/docker-build.yml
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write # Needed to push package versions
|
||||||
contents: read
|
contents: read
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@@ -433,6 +433,6 @@ jobs:
|
|||||||
# Only run on push, schedule, or release events
|
# Only run on push, schedule, or release events
|
||||||
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write # Needed to delete package versions
|
||||||
with:
|
with:
|
||||||
package_name: 'freqtrade'
|
package_name: 'freqtrade'
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ on:
|
|||||||
# disable permissions for all of the available permissions
|
# disable permissions for all of the available permissions
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docs:
|
build-docs:
|
||||||
@@ -19,12 +22,12 @@ jobs:
|
|||||||
name: Deploy Docs through mike
|
name: Deploy Docs through mike
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
|
|||||||
@@ -17,18 +17,21 @@ concurrency:
|
|||||||
group: "${{ github.workflow }}"
|
group: "${{ github.workflow }}"
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
|
name: "Build and Push Devcontainer Image"
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write # Needed to push package versions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -40,3 +43,14 @@ jobs:
|
|||||||
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
imageName: ghcr.io/${{ github.repository }}-devcontainer
|
||||||
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
cacheFrom: ghcr.io/${{ github.repository }}-devcontainer
|
||||||
push: always
|
push: always
|
||||||
|
|
||||||
|
|
||||||
|
packages-cleanup:
|
||||||
|
name: "Docker Package Cleanup"
|
||||||
|
uses: ./.github/workflows/packages-cleanup.yml
|
||||||
|
# Only run on push, schedule, or release events
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
|
||||||
|
permissions:
|
||||||
|
packages: write # Needed to delete package versions
|
||||||
|
with:
|
||||||
|
package_name: 'freqtrade-devcontainer'
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ on:
|
|||||||
default: 'develop'
|
default: 'develop'
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@@ -33,16 +37,27 @@ jobs:
|
|||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Visualize disk usage before build
|
||||||
|
run: df -h
|
||||||
|
|
||||||
|
- name: Cleanup some disk space
|
||||||
|
run: |
|
||||||
|
docker system prune -a --force || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
|
||||||
|
- name: Visualize disk usage after cleanup
|
||||||
|
run: df -h
|
||||||
|
|
||||||
- name: Set docker tag names
|
- name: Set docker tag names
|
||||||
id: tags
|
id: tags
|
||||||
uses: ./.github/actions/docker-tags
|
uses: ./.github/actions/docker-tags
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@@ -54,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 #v3.11.1
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f #v3.12.0
|
||||||
|
|
||||||
- name: Available platforms
|
- name: Available platforms
|
||||||
run: echo ${PLATFORMS}
|
run: echo ${PLATFORMS}
|
||||||
@@ -142,17 +157,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker images
|
docker images
|
||||||
|
|
||||||
|
- name: Visualize disk usage after build
|
||||||
|
run: df -h
|
||||||
|
|
||||||
deploy-arm:
|
deploy-arm:
|
||||||
name: "Deploy Docker ARM64"
|
name: "Deploy Docker ARM64"
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write # Needed to push package versions
|
||||||
needs: [ deploy-docker ]
|
needs: [ deploy-docker ]
|
||||||
# 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'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -161,13 +179,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@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to github
|
- name: Login to github
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -276,6 +294,7 @@ jobs:
|
|||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
--tag ${GHCR_IMAGE_NAME}:${TAG} \
|
--tag ${GHCR_IMAGE_NAME}:${TAG} \
|
||||||
--tag ${GHCR_IMAGE_NAME}:latest \
|
--tag ${GHCR_IMAGE_NAME}:latest \
|
||||||
|
--tag ${IMAGE_NAME}:latest \
|
||||||
${IMAGE_NAME}:${TAG}
|
${IMAGE_NAME}:${TAG}
|
||||||
|
|
||||||
- name: Docker images
|
- name: Docker images
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- stable
|
- stable
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
# disable permissions for all of the available permissions
|
# disable permissions for all of the available permissions
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
|
name: "Update Docker Hub Description"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -25,20 +25,26 @@ on:
|
|||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PACKAGE_NAME: "freqtrade"
|
PACKAGE_NAME: "freqtrade"
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-docker:
|
deploy-docker:
|
||||||
name: "Delete Packages"
|
name: "Delete Packages"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: github.repository == 'freqtrade/freqtrade'
|
if: github.repository == 'freqtrade/freqtrade'
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write # Needed to delete package versions
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Delete untagged Package Versions"
|
- name: "Delete untagged Package Versions"
|
||||||
uses: actions/delete-package-versions@v5
|
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5.0.0
|
||||||
with:
|
with:
|
||||||
package-name: ${{ inputs.package_name || env.PACKAGE_NAME }}
|
package-name: ${{ inputs.package_name || env.PACKAGE_NAME }}
|
||||||
package-type: 'container'
|
package-type: 'container'
|
||||||
|
|||||||
@@ -9,15 +9,20 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-update:
|
auto-update:
|
||||||
|
name: Auto-update pre-commit hooks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
@@ -28,7 +33,7 @@ jobs:
|
|||||||
- name: Run auto-update
|
- name: Run auto-update
|
||||||
run: pre-commit autoupdate
|
run: pre-commit autoupdate
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
add-paths: .pre-commit-config.yaml
|
add-paths: .pre-commit-config.yaml
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- stable
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- stable
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
zizmor:
|
|
||||||
name: Run zizmor 🌈
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
# contents: read # only needed for private repos
|
|
||||||
# actions: read # only needed for private repos
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
|
||||||
uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- stable
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- stable
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
zizmor:
|
||||||
|
name: Run zizmor 🌈
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||||
|
# contents: read # Only needed for private repos. Needed to clone the repo.
|
||||||
|
# actions: read # Only needed for private repos. Needed for upload-sarif to read workflow run info.
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Run zizmor 🌈
|
||||||
|
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
|
||||||
@@ -21,22 +21,22 @@ repos:
|
|||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.18.2"
|
rev: "v1.19.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==6.2.0.20251022
|
- types-cachetools==6.2.0.20251022
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.32.4.20250913
|
- types-requests==2.32.4.20260107
|
||||||
- types-tabulate==0.9.0.20241207
|
- types-tabulate==0.9.0.20241207
|
||||||
- types-python-dateutil==2.9.0.20251115
|
- types-python-dateutil==2.9.0.20260124
|
||||||
- scipy-stubs==1.16.3.0
|
- scipy-stubs==1.17.0.2
|
||||||
- SQLAlchemy==2.0.44
|
- SQLAlchemy==2.0.46
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: "7.0.0"
|
rev: "8.0.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
@@ -44,7 +44,7 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.14.6'
|
rev: 'v0.15.2'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
@@ -83,6 +83,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.16.3
|
rev: v1.22.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: zizmor
|
- id: zizmor
|
||||||
|
|||||||
+3
-3
@@ -12,7 +12,8 @@ Few pointers for contributions:
|
|||||||
- Stick to english in both commit messages, PR descriptions and code comments and variable names.
|
- Stick to english in both commit messages, PR descriptions and code comments and variable names.
|
||||||
- New features need to contain unit tests, must pass CI (run pre-commit and pytest to get an early feedback) and should be documented with the introduction PR.
|
- New features need to contain unit tests, must pass CI (run pre-commit and pytest to get an early feedback) and should be documented with the introduction PR.
|
||||||
- PR's can be declared as draft - signaling Work in Progress for Pull Requests (which are not finished). We'll still aim to provide feedback on draft PR's in a timely manner.
|
- PR's can be declared as draft - signaling Work in Progress for Pull Requests (which are not finished). We'll still aim to provide feedback on draft PR's in a timely manner.
|
||||||
- If you're using AI for your PR, please both mention it in the PR description and do a thorough review of the generated code. The final responsibility for the code with the PR author, not with the AI.
|
- If you're using AI for your PR, please both mention it in the PR description and do a thorough review of the generated code yourself.
|
||||||
|
The final responsibility for the code with the PR author, not with the AI, which also means that commits must be linked to your (human) account, not some generic AI account.
|
||||||
|
|
||||||
If you are unsure, discuss the feature on our [discord server](https://discord.gg/p7nuUNVfP7) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a Pull Request.
|
If you are unsure, discuss the feature on our [discord server](https://discord.gg/p7nuUNVfP7) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a Pull Request.
|
||||||
|
|
||||||
@@ -24,8 +25,7 @@ Best start by reading the [documentation](https://www.freqtrade.io/) to get a fe
|
|||||||
|
|
||||||
### 1. Run unit tests
|
### 1. Run unit tests
|
||||||
|
|
||||||
All unit tests must pass. If a unit test is broken, change your code to
|
All unit tests must pass. If a unit test is broken, change your code to make it pass. It means you have introduced a regression.
|
||||||
make it pass. It means you have introduced a regression.
|
|
||||||
|
|
||||||
#### Test the whole project
|
#### Test the whole project
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.13.8-slim-bookworm AS base
|
FROM python:3.13.12-slim-trixie AS base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
||||||
[](https://doi.org/10.21105/joss.04864)
|
[](https://doi.org/10.21105/joss.04864)
|
||||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||||
[](https://www.freqtrade.io)
|
[](https://www.freqtrade.io)
|
||||||
|
[](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.
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ This software is for educational purposes only. Do not risk money which
|
|||||||
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
|
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
|
||||||
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
||||||
|
|
||||||
Always start by running a trading bot in Dry-run and do not engage money
|
Always start by running a trading bot in Dry-Run and do not engage money
|
||||||
before you understand how it works and what profit/loss you should
|
before you understand how it works and what profit/loss you should
|
||||||
expect.
|
expect.
|
||||||
|
|
||||||
@@ -24,7 +25,9 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
|
|
||||||
## Supported Exchange marketplaces
|
## Supported Exchange marketplaces
|
||||||
|
|
||||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange-specific notes](https://www.freqtrade.io/en/stable/exchanges/) to learn about special configurations that maybe needed for each exchange.
|
||||||
|
|
||||||
|
### Supported Spot Exchanges
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
||||||
@@ -39,7 +42,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
|||||||
- [X] [MyOKX](https://okx.com/) (OKX EEA)
|
- [X] [MyOKX](https://okx.com/) (OKX EEA)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Supported Futures Exchanges (experimental)
|
### Supported Futures Exchanges
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bitget](https://www.bitget.com/)
|
- [X] [Bitget](https://www.bitget.com/)
|
||||||
@@ -48,7 +51,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
|||||||
- [X] [OKX](https://okx.com/)
|
- [X] [OKX](https://okx.com/)
|
||||||
- [X] [Bybit](https://bybit.com/)
|
- [X] [Bybit](https://bybit.com/)
|
||||||
|
|
||||||
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) 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.
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
|
||||||
@@ -140,7 +143,7 @@ options:
|
|||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
|
|
||||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/stable/telegram-usage/)
|
||||||
|
|
||||||
- `/start`: Starts the trader.
|
- `/start`: Starts the trader.
|
||||||
- `/stop`: Stops the trader.
|
- `/stop`: Stops the trader.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import os
|
||||||
import subprocess # noqa: S404, RUF100
|
import subprocess # noqa: S404, RUF100
|
||||||
import sys
|
import sys
|
||||||
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +10,20 @@ def _write_partial_file(filename: str, content: str):
|
|||||||
f.write(f"``` output\n{content}\n```\n")
|
f.write(f"``` output\n{content}\n```\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_help_output(parser) -> str:
|
||||||
|
"""Capture the help output from a parser."""
|
||||||
|
output = StringIO()
|
||||||
|
parser.print_help(file=output)
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def extract_command_partials():
|
def extract_command_partials():
|
||||||
|
# Set terminal width to 80 columns for consistent output formatting
|
||||||
|
os.environ["COLUMNS"] = "80"
|
||||||
|
|
||||||
|
# Import Arguments here to avoid circular imports and ensure COLUMNS is set
|
||||||
|
from freqtrade.commands.arguments import Arguments
|
||||||
|
|
||||||
subcommands = [
|
subcommands = [
|
||||||
"trade",
|
"trade",
|
||||||
"create-userdir",
|
"create-userdir",
|
||||||
@@ -46,16 +61,35 @@ def extract_command_partials():
|
|||||||
"recursive-analysis",
|
"recursive-analysis",
|
||||||
]
|
]
|
||||||
|
|
||||||
result = subprocess.run(["freqtrade", "--help"], capture_output=True, text=True)
|
# Build the Arguments class to get the parser with all subcommands
|
||||||
|
args = Arguments(None)
|
||||||
|
args._build_subcommands()
|
||||||
|
|
||||||
_write_partial_file("docs/commands/main.md", result.stdout)
|
# Get main help output
|
||||||
|
main_help = _get_help_output(args.parser)
|
||||||
|
_write_partial_file("docs/commands/main.md", main_help)
|
||||||
|
|
||||||
|
# Get subparsers from the main parser
|
||||||
|
# The subparsers are stored in _subparsers._group_actions[0].choices
|
||||||
|
subparsers_action = None
|
||||||
|
for action in args.parser._subparsers._group_actions:
|
||||||
|
if hasattr(action, "choices"):
|
||||||
|
subparsers_action = action
|
||||||
|
break
|
||||||
|
|
||||||
|
if subparsers_action is None:
|
||||||
|
raise RuntimeError("Could not find subparsers in the main parser")
|
||||||
|
|
||||||
for command in subcommands:
|
for command in subcommands:
|
||||||
print(f"Running for {command}")
|
print(f"Running for {command}")
|
||||||
result = subprocess.run(["freqtrade", command, "--help"], capture_output=True, text=True)
|
if command in subparsers_action.choices:
|
||||||
|
subparser = subparsers_action.choices[command]
|
||||||
_write_partial_file(f"docs/commands/{command}.md", result.stdout)
|
help_output = _get_help_output(subparser)
|
||||||
|
_write_partial_file(f"docs/commands/{command}.md", help_output)
|
||||||
|
else:
|
||||||
|
print(f" Warning: subcommand '{command}' not found in parser")
|
||||||
|
|
||||||
|
# freqtrade-client still uses subprocess as requested
|
||||||
print("Running for freqtrade-client")
|
print("Running for freqtrade-client")
|
||||||
result_client = subprocess.run(["freqtrade-client", "--show"], capture_output=True, text=True)
|
result_client = subprocess.run(["freqtrade-client", "--show"], capture_output=True, text=True)
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -1057,7 +1057,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"
|
||||||
},
|
},
|
||||||
"CORS_origins": {
|
"CORS_origins": {
|
||||||
"description": "List of allowed CORS origins.",
|
"description": "List of allowed CORS origins.",
|
||||||
@@ -1080,7 +1081,8 @@
|
|||||||
"listen_ip_address",
|
"listen_ip_address",
|
||||||
"listen_port",
|
"listen_port",
|
||||||
"username",
|
"username",
|
||||||
"password"
|
"password",
|
||||||
|
"jwt_secret_key"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"db_url": {
|
"db_url": {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"listen_ip_address": "127.0.0.1",
|
"listen_ip_address": "127.0.0.1",
|
||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "freqtrader",
|
"username": "freqtrader",
|
||||||
"password": "SuperSecurePassword"
|
"password": "SuperSecurePassword"
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"enable_openapi": false,
|
"enable_openapi": false,
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "freqtrader",
|
"username": "freqtrader",
|
||||||
"password": "SuperSecurePassword",
|
"password": "SuperSecurePassword",
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
"listen_ip_address": "127.0.0.1",
|
"listen_ip_address": "127.0.0.1",
|
||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "freqtrader",
|
"username": "freqtrader",
|
||||||
"password": "SuperSecurePassword"
|
"password": "SuperSecurePassword"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11.13-slim-bookworm AS base
|
FROM python:3.11.14-slim-bookworm AS base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
|
|||||||
* 1: profit summaries grouped by enter_tag
|
* 1: profit summaries grouped by enter_tag
|
||||||
* 2: profit summaries grouped by enter_tag and exit_tag
|
* 2: profit summaries grouped by enter_tag and exit_tag
|
||||||
* 3: profit summaries grouped by pair and enter_tag
|
* 3: profit summaries grouped by pair and enter_tag
|
||||||
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
* 4: profit summaries grouped by pair, enter_tag and exit_tag (this can get quite large)
|
||||||
* 5: profit summaries grouped by exit_tag
|
* 5: profit summaries grouped by exit_tag
|
||||||
|
|
||||||
More options are available by running with the `-h` option.
|
More options are available by running with the `-h` option.
|
||||||
@@ -52,11 +52,10 @@ By default, `backtesting-analysis` processes the most recent backtest results in
|
|||||||
If you want to analyze results from an earlier backtest, use the `--backtest-filename` option to specify the desired file. This lets you revisit and re-analyze historical backtest outputs at any time by providing the filename of the relevant backtest result:
|
If you want to analyze results from an earlier backtest, use the `--backtest-filename` option to specify the desired file. This lets you revisit and re-analyze historical backtest outputs at any time by providing the filename of the relevant backtest result:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
freqtrade backtesting -c <config.json> --strategy <strategy_name> --timerange <timerange> --export signals --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see some output similar to below in the logs with the name of the timestamped
|
You should see some output similar to below in the logs with the name of the timestamped filename that was exported:
|
||||||
filename that was exported:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "mystrat_backtest-2022-06-14_16-28-32.json"
|
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "mystrat_backtest-2022-06-14_16-28-32.json"
|
||||||
@@ -64,14 +63,14 @@ filename that was exported:
|
|||||||
|
|
||||||
You can then use that filename in `backtesting-analysis`:
|
You can then use that filename in `backtesting-analysis`:
|
||||||
|
|
||||||
```
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --backtest-filename=mystrat_backtest-2022-06-14_16-28-32.json
|
freqtrade backtesting-analysis -c <config.json> --backtest-filename=backtest-result-2025-03-05_20-38-34.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
To use a result from a different results directory, you can use `--backtest-directory` to specify the directory
|
To use a result from a different results directory, you can use `--backtest-directory` to specify the directory
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename mystrat_backtest-2022-06-14_16-28-32.json
|
freqtrade backtesting-analysis -c <config.json> --backtest-directory custom_results/ --backtest-filename backtest-result-2025-03-05_20-38-34.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tuning the buy tags and sell tags to display
|
### Tuning the buy tags and sell tags to display
|
||||||
@@ -85,7 +84,7 @@ To show only certain buy and sell tags in the displayed output, use the followin
|
|||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss
|
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -96,7 +95,7 @@ values present on signal candles to allow fine-grained investigation and tuning
|
|||||||
indicators. To print out a column for a given set of indicators, use the `--indicator-list`
|
indicators. To print out a column for a given set of indicators, use the `--indicator-list`
|
||||||
option:
|
option:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss --indicator-list rsi rsi_1h bb_lowerband ema_9 macd macdsignal
|
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss --indicator-list rsi rsi_1h bb_lowerband ema_9 macd macdsignal
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,21 +110,21 @@ output.
|
|||||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||||
automatically accessible by including them on the indicator-list, and these include:
|
automatically accessible by including them on the indicator-list, and these include:
|
||||||
|
|
||||||
- **open_date :** trade open datetime
|
* **open_date :** trade open datetime
|
||||||
- **close_date :** trade close datetime
|
* **close_date :** trade close datetime
|
||||||
- **min_rate :** minimum price seen throughout the position
|
* **min_rate :** minimum price seen throughout the position
|
||||||
- **max_rate :** maximum price seen throughout the position
|
* **max_rate :** maximum price seen throughout the position
|
||||||
- **open :** signal candle open price
|
* **open :** signal candle open price
|
||||||
- **close :** signal candle close price
|
* **close :** signal candle close price
|
||||||
- **high :** signal candle high price
|
* **high :** signal candle high price
|
||||||
- **low :** signal candle low price
|
* **low :** signal candle low price
|
||||||
- **volume :** signal candle volume
|
* **volume :** signal candle volume
|
||||||
- **profit_ratio :** trade profit ratio
|
* **profit_ratio :** trade profit ratio
|
||||||
- **profit_abs :** absolute profit return of the trade
|
* **profit_abs :** absolute profit return of the trade
|
||||||
|
|
||||||
#### Sample Output for Indicator Values
|
#### Sample Output for Indicator Values
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen
|
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -158,13 +157,13 @@ The `--indicator-list` option, by default, displays indicator values for both en
|
|||||||
|
|
||||||
Example: Display indicator values at entry signals:
|
Example: Display indicator values at entry signals:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --entry-only
|
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --entry-only
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: Display indicator values at exit signals:
|
Example: Display indicator values at exit signals:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --exit-only
|
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --exit-only
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -181,7 +180,7 @@ To show only trades between dates within your backtested timerange, supply the u
|
|||||||
|
|
||||||
For example, if your backtest timerange was `20220101-20221231` but you only want to output trades in January:
|
For example, if your backtest timerange was `20220101-20221231` but you only want to output trades in January:
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -189,7 +188,7 @@ freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
|||||||
|
|
||||||
Use the `--rejected-signals` option to print out rejected signals.
|
Use the `--rejected-signals` option to print out rejected signals.
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -198,13 +197,13 @@ freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
|||||||
Some of the tabular outputs can become large, so printing them out to the terminal is not preferable.
|
Some of the tabular outputs can become large, so printing them out to the terminal is not preferable.
|
||||||
Use the `--analysis-to-csv` option to disable printing out of tables to standard out and write them to CSV files.
|
Use the `--analysis-to-csv` option to disable printing out of tables to standard out and write them to CSV files.
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv
|
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv
|
||||||
```
|
```
|
||||||
|
|
||||||
By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g.
|
By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g.
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --rejected-signals --analysis-groups 0 1
|
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --rejected-signals --analysis-groups 0 1
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -216,6 +215,6 @@ This will write to `user_data/backtest_results`:
|
|||||||
|
|
||||||
To override where the files will be written, also specify the `--analysis-csv-path` option.
|
To override where the files will be written, also specify the `--analysis-csv-path` option.
|
||||||
|
|
||||||
```bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --analysis-csv-path another/data/path/
|
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --analysis-csv-path another/data/path/
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Define a custom max_open_trades space
|
# Define a custom max_open_trades space
|
||||||
def max_open_trades_space(self) -> List[Dimension]:
|
def max_open_trades_space() -> List[Dimension]:
|
||||||
return [
|
return [
|
||||||
Integer(-1, 10, name='max_open_trades'),
|
Integer(-1, 10, name='max_open_trades'),
|
||||||
]
|
]
|
||||||
@@ -142,7 +142,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
!!! Note
|
!!! Note
|
||||||
All overrides are optional and can be mixed/matched as necessary.
|
All overrides are optional and can be mixed/matched as necessary.
|
||||||
|
|
||||||
### Dynamic parameters
|
## Dynamic parameters
|
||||||
|
|
||||||
Parameters can also be defined dynamically, but must be available to the instance once the [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
Parameters can also be defined dynamically, but must be available to the instance once the [`bot_start()` callback](strategy-callbacks.md#bot-start) has been called.
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
!!! Warning
|
!!! Warning
|
||||||
Parameters created this way will not show up in the `list-strategies` parameter count.
|
Parameters created this way will not show up in the `list-strategies` parameter count.
|
||||||
|
|
||||||
### Overriding Base estimator
|
## Overriding Base estimator
|
||||||
|
|
||||||
You can define your own optuna sampler for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
|
You can define your own optuna sampler for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
|
||||||
|
|
||||||
@@ -208,7 +208,6 @@ Some research will be necessary to find additional Samplers (from optunahub) for
|
|||||||
|
|
||||||
Obviously the same approach will work for all other Samplers optuna supports.
|
Obviously the same approach will work for all other Samplers optuna supports.
|
||||||
|
|
||||||
|
|
||||||
## Space options
|
## Space options
|
||||||
|
|
||||||
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
|
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- "./user_data:/freqtrade/user_data"
|
- "./user_data:/freqtrade/user_data"
|
||||||
# Expose api on port 8080 (localhost only)
|
# Expose api on port 8080 (localhost only)
|
||||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||||
# before enabling this.
|
# before enabling this.
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
@@ -100,7 +100,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- "./user_data:/freqtrade/user_data"
|
- "./user_data:/freqtrade/user_data"
|
||||||
# Expose api on port 8080 (localhost only)
|
# Expose api on port 8080 (localhost only)
|
||||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||||
# before enabling this.
|
# before enabling this.
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8081:8080"
|
- "127.0.0.1:8081:8080"
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ options:
|
|||||||
separated.
|
separated.
|
||||||
--eps, --enable-position-stacking
|
--eps, --enable-position-stacking
|
||||||
Allow buying the same pair multiple times (position
|
Allow buying the same pair multiple times (position
|
||||||
stacking).
|
stacking). Only applicable to backtesting and
|
||||||
|
hyperopt. Results archived by this cannot be
|
||||||
|
reproduced in dry/live trading.
|
||||||
--enable-protections, --enableprotections
|
--enable-protections, --enableprotections
|
||||||
Enable protections for backtesting. Will slow
|
Enable protections for backtesting. Will slow
|
||||||
backtesting down by a considerable amount, but will
|
backtesting down by a considerable amount, but will
|
||||||
@@ -62,18 +64,15 @@ options:
|
|||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that timeframe needs to be set
|
backtest. Please note that timeframe needs to be set
|
||||||
either in config or via command line. When using this
|
either in config or via command line.
|
||||||
together with `--export trades`, the strategy-name is
|
|
||||||
injected into the filename (so `backtest-data.json`
|
|
||||||
becomes `backtest-data-SampleStrategy.json`
|
|
||||||
--export {none,trades,signals}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--backtest-filename, --export-filename PATH
|
--backtest-filename, --export-filename PATH
|
||||||
Use this filename for backtest results.Example:
|
DEPRECATED: This option is deprecated for backtesting
|
||||||
`--backtest-
|
and will be removed in a future release. Using a
|
||||||
filename=backtest_results_2020-09-27_16-20-48.json`.
|
custom filename for backtest results is no longer
|
||||||
Assumes either `user_data/backtest_results/` or
|
supported. Use `--backtest-directory` to specify the
|
||||||
`--export-directory` as base directory.
|
directory.
|
||||||
--backtest-directory, --export-directory PATH
|
--backtest-directory, --export-directory PATH
|
||||||
Directory to use for backtest results. Example:
|
Directory to use for backtest results. Example:
|
||||||
`--export-directory=user_data/backtest_results/`.
|
`--export-directory=user_data/backtest_results/`.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ usage: freqtrade download-data [-h] [-v] [--no-color] [--logfile FILE] [-V]
|
|||||||
[--data-format-ohlcv {json,jsongz,feather,parquet}]
|
[--data-format-ohlcv {json,jsongz,feather,parquet}]
|
||||||
[--data-format-trades {json,jsongz,feather,parquet}]
|
[--data-format-trades {json,jsongz,feather,parquet}]
|
||||||
[--trading-mode {spot,margin,futures}]
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
[--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]]
|
||||||
[--prepend]
|
[--prepend]
|
||||||
|
|
||||||
options:
|
options:
|
||||||
@@ -50,6 +51,11 @@ options:
|
|||||||
`feather`).
|
`feather`).
|
||||||
--trading-mode, --tradingmode {spot,margin,futures}
|
--trading-mode, --tradingmode {spot,margin,futures}
|
||||||
Select Trading mode
|
Select Trading mode
|
||||||
|
--candle-types {spot,futures,mark,index,premiumIndex,funding_rate} [{spot,futures,mark,index,premiumIndex,funding_rate} ...]
|
||||||
|
Select candle type to download. Defaults to the
|
||||||
|
necessary candles for the selected trading mode (e.g.
|
||||||
|
'spot' or ('futures', 'funding_rate' and 'mark') for
|
||||||
|
futures).
|
||||||
--prepend Allow data prepending. (Data-appending is disabled)
|
--prepend Allow data prepending. (Data-appending is disabled)
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ options:
|
|||||||
functions.
|
functions.
|
||||||
--eps, --enable-position-stacking
|
--eps, --enable-position-stacking
|
||||||
Allow buying the same pair multiple times (position
|
Allow buying the same pair multiple times (position
|
||||||
stacking).
|
stacking). Only applicable to backtesting and
|
||||||
|
hyperopt. Results archived by this cannot be
|
||||||
|
reproduced in dry/live trading.
|
||||||
--enable-protections, --enableprotections
|
--enable-protections, --enableprotections
|
||||||
Enable protections for backtesting. Will slow
|
Enable protections for backtesting. Will slow
|
||||||
backtesting down by a considerable amount, but will
|
backtesting down by a considerable amount, but will
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ options:
|
|||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that timeframe needs to be set
|
backtest. Please note that timeframe needs to be set
|
||||||
either in config or via command line. When using this
|
either in config or via command line.
|
||||||
together with `--export trades`, the strategy-name is
|
|
||||||
injected into the filename (so `backtest-data.json`
|
|
||||||
becomes `backtest-data-SampleStrategy.json`
|
|
||||||
--export {none,trades,signals}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--backtest-filename, --export-filename PATH
|
--backtest-filename, --export-filename PATH
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ options:
|
|||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that timeframe needs to be set
|
backtest. Please note that timeframe needs to be set
|
||||||
either in config or via command line. When using this
|
either in config or via command line.
|
||||||
together with `--export trades`, the strategy-name is
|
|
||||||
injected into the filename (so `backtest-data.json`
|
|
||||||
becomes `backtest-data-SampleStrategy.json`
|
|
||||||
--strategy-path PATH Specify additional strategy lookup path.
|
--strategy-path PATH Specify additional strategy lookup path.
|
||||||
--recursive-strategy-search
|
--recursive-strategy-search
|
||||||
Recursively search for a strategy in the strategies
|
Recursively search for a strategy in the strategies
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ freqtrade download-data --exchange binance --pairs ".*/USDT"
|
|||||||
* Given starting points are ignored if data is already available, downloading only missing data up to today.
|
* Given starting points are ignored if data is already available, downloading only missing data up to today.
|
||||||
* Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
* Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||||
* To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
* To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||||
|
* When downloading futures data (`--trading-mode futures` or a configuration specifying futures mode), freqtrade will automatically download the necessary candle types (e.g. `mark` and `funding_rate` candles) unless specified otherwise via `--candle-types`.
|
||||||
|
|
||||||
??? Note "Permission denied errors"
|
??? Note "Permission denied errors"
|
||||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||||
|
|||||||
@@ -98,3 +98,50 @@ Please use configuration based [log setup](advanced-setup.md#advanced-logging) i
|
|||||||
|
|
||||||
The edge module has been deprecated in 2023.9 and removed in 2025.6.
|
The edge module has been deprecated in 2023.9 and removed in 2025.6.
|
||||||
All functionalities of edge have been removed, and having edge configured will result in an error.
|
All functionalities of edge have been removed, and having edge configured will result in an error.
|
||||||
|
|
||||||
|
## Adjustment to dynamic funding rate handling
|
||||||
|
|
||||||
|
With version 2025.12, the handling of dynamic funding rates has been adjusted to also support dynamic funding rates down to 1h funding intervals.
|
||||||
|
As a consequence, the mark and funding rate timeframes have been changed to 1h for every supported futures exchange.
|
||||||
|
|
||||||
|
As the timeframe for both mark and funding_fee candles has changed (usually from 8h to 1h) - already downloaded data will have to be adjusted or partially re-downloaded.
|
||||||
|
You can either re-download everything (`freqtrade download-data [...] --erase` - :warning: can take a long time) - or download the updated data selectively.
|
||||||
|
|
||||||
|
### Strategy
|
||||||
|
|
||||||
|
Most strategies should not need adjustments to continue to work as expected - however, strategies using `@informative("8h", candle_type="funding_rate")` or similar will have to switch the timeframe to 1h.
|
||||||
|
The same is true for `dp.get_pair_dataframe(metadata["pair"], "8h", candle_type="funding_rate")` - which will need to be switched to 1h.
|
||||||
|
|
||||||
|
freqtrade will auto-adjust the timeframe and return `funding_rates` despite the wrongly given timeframe. It'll issue a warning - and may still break your strategy.
|
||||||
|
|
||||||
|
### Selective data re-download
|
||||||
|
|
||||||
|
The script below should serve as an example - you may need to adjust the timeframe and exchange to your needs!
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# Cleanup no longer needed data
|
||||||
|
rm user_data/data/<exchange>/futures/*-mark*
|
||||||
|
rm user_data/data/<exchange>/futures/*-funding_rate*
|
||||||
|
|
||||||
|
# download new data (only required once to fix the mark and funding fee data)
|
||||||
|
freqtrade download-data -t 1h --trading-mode futures --candle-types funding_rate mark [...] --timerange <full timerange you've got other data for>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The result of the above will be that your funding_rates and mark data will have the 1h timeframe.
|
||||||
|
you can verify this with `freqtrade list-data --exchange <yourexchange> --show`.
|
||||||
|
|
||||||
|
!!! Note "Additional arguments"
|
||||||
|
Additional arguments to the above commands may be necessary, like configuration files or explicit user_data if they deviate from the default.
|
||||||
|
|
||||||
|
**Hyperliquid** is a special case now - which will no longer require 1h mark data - but will use regular candles instead (this data never existed and is identical to 1h futures candles). As we don't support download-data for hyperliquid (they don't provide historic data) - there won't be actions necessary for hyperliquid users.
|
||||||
|
|
||||||
|
## Catboost models in freqAI
|
||||||
|
|
||||||
|
CatBoost models have been removed with version 2025.12 and are no longer actively supported.
|
||||||
|
If you have existing bots using CatBoost models, you can still use them in your custom models by copy/pasting them from the git history (as linked below) and installing the Catboost library manually.
|
||||||
|
We do however recommend switching to other supported model libraries like LightGBM or XGBoost for better support and future compatibility.
|
||||||
|
|
||||||
|
* [CatboostRegressor](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostRegressor.py)
|
||||||
|
* [CatboostClassifier](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostClassifier.py)
|
||||||
|
* [CatboostClassifierMultiTarget](https://github.com/freqtrade/freqtrade/blob/c6f3b0081927e161a16b116cc47fb663f7831d30/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py)
|
||||||
|
|||||||
+2
-3
@@ -432,7 +432,6 @@ freqtrade download-data --timerange 20250625-20250801 --config tests/testdata/co
|
|||||||
freqtrade backtesting --config tests/testdata/config.tests.usdt.json -s SampleStrategy --userdir user_data_bttest/ --cache none --timerange 20250701-20250801
|
freqtrade backtesting --config tests/testdata/config.tests.usdt.json -s SampleStrategy --userdir user_data_bttest/ --cache none --timerange 20250701-20250801
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Continuous integration
|
## Continuous integration
|
||||||
|
|
||||||
This documents some decisions taken for the CI Pipeline.
|
This documents some decisions taken for the CI Pipeline.
|
||||||
@@ -464,10 +463,10 @@ git checkout -b new_release <commitid>
|
|||||||
Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these.
|
Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these.
|
||||||
|
|
||||||
* Merge the release branch (stable) into this branch.
|
* Merge the release branch (stable) into this branch.
|
||||||
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
|
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2025.7` for July 2025). Minor versions can be `2025.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
|
||||||
* Commit this part.
|
* Commit this part.
|
||||||
* Push that branch to the remote and create a PR against the **stable branch**.
|
* Push that branch to the remote and create a PR against the **stable branch**.
|
||||||
* Update develop version to next version following the pattern `2019.8-dev`.
|
* Update develop version to next version following the pattern `2025.8-dev`.
|
||||||
|
|
||||||
### Create changelog from git commits
|
### Create changelog from git commits
|
||||||
|
|
||||||
|
|||||||
+32
-1
@@ -319,7 +319,6 @@ 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.
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
||||||
@@ -428,6 +427,38 @@ Your balance and trades will now be used from your vault / subaccount - and no l
|
|||||||
|
|
||||||
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
The Hyperliquid API does not provide historic data beyond the single call to fetch current data, so downloading data is not possible, as the downloaded data would not constitute proper historic data.
|
||||||
|
|
||||||
|
### HIP-3 DEXes
|
||||||
|
|
||||||
|
Hyperliquid supports HIP-3 decentralized exchanges (DEXes), which are independent exchanges built on top of the Hyperliquid infrastructure.
|
||||||
|
These DEXes operate similarly to the main Hyperliquid exchange but are community-created and managed.
|
||||||
|
|
||||||
|
To trade on HIP-3 DEXes with Freqtrade, you need to add them to your configuration using the `hip3_dexes` parameter:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "hyperliquid",
|
||||||
|
"walletAddress": "your_master_wallet_address",
|
||||||
|
"privateKey": "your_api_private_key",
|
||||||
|
"hip3_dexes": ["dex_name_1", "dex_name_2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `"dex_name_1"` and `"dex_name_2"` with the actual names of the HIP-3 DEXes you want to trade on (e.g. `vntl` and `xyz`).
|
||||||
|
|
||||||
|
!!! Warning "Performance and Rate Limit Impact"
|
||||||
|
Each HIP-3 DEX you add significantly impacts bot performance and rate limits.
|
||||||
|
|
||||||
|
* **Additional API Calls**: For each HIP-3 DEX configured, Freqtrade needs to make additional API calls.
|
||||||
|
* **Rate Limit Pressure**: Additional API calls contribute to Hyperliquid's strict rate limits. With multiple DEXes, you may hit rate limits faster, or rather, slow down bot operations due to enforced delays.
|
||||||
|
|
||||||
|
Please only add HIP-3 DEXes that you actively trade on. Monitor your logs for rate limit warnings or signs of slowed operations, and adjust your configuration accordingly.
|
||||||
|
Different HIP-3 DEXes may also use different quote currencies - so make sure to only add DEXes that are compatible with your stake currency to avoid unnecessary delays.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
HIP-3 DEXes share the same wallet and free amount of collateral as your main Hyperliquid account. Trades on different DEXes will affect your overall account balance and margin.
|
||||||
|
|
||||||
|
The pair name for HIP-3 pairs will be slightly different than non HIP-3 pairs. Please use `list-pairs` subcommand to get the correct pair naming for all pairs for the specified dexes.
|
||||||
|
|
||||||
## Bitvavo
|
## Bitvavo
|
||||||
|
|
||||||
If your account is required to use an operatorId, you can set it in the configuration file as follows:
|
If your account is required to use an operatorId, you can set it in the configuration file as follows:
|
||||||
|
|||||||
+8
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Supported Markets
|
## Supported Markets
|
||||||
|
|
||||||
Freqtrade supports spot trading, as well as (isolated) 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-experimental) for an up-to-date list of supported exchanges.
|
||||||
|
|
||||||
### Can my bot open short positions?
|
### Can my bot open short positions?
|
||||||
|
|
||||||
@@ -29,6 +29,13 @@ You can however use the [`adjust_trade_position()` callback](strategy-callbacks.
|
|||||||
|
|
||||||
Backtesting provides an option for this in `--eps` - however this is only there to highlight "hidden" signals, and will not work in live.
|
Backtesting provides an option for this in `--eps` - however this is only there to highlight "hidden" signals, and will not work in live.
|
||||||
|
|
||||||
|
### Does freqtrade support sandbox accounts?
|
||||||
|
|
||||||
|
No, but you can use dry-run mode to simulate trading without risking real funds.
|
||||||
|
|
||||||
|
Sandbox markets are separate, simulated markets - which are not suitable to test your strategy in a realistic environment.
|
||||||
|
These markets usually have different order books, liquidity and trading behaviour (usually with very few participants) - which makes them unsuitable for realistic tests of your strategy.
|
||||||
|
|
||||||
### The bot does not start
|
### The bot does not start
|
||||||
|
|
||||||
Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`.
|
Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`.
|
||||||
|
|||||||
@@ -200,15 +200,15 @@ If this value is set, FreqAI will initially use the predictions from the trainin
|
|||||||
|
|
||||||
## Using different prediction models
|
## Using different prediction models
|
||||||
|
|
||||||
FreqAI has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `CatBoost`, `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`.
|
FreqAI has multiple example prediction model libraries that are ready to be used as is via the flag `--freqaimodel`. These libraries include `LightGBM`, and `XGBoost` regression, classification, and multi-target models, and can be found in `freqai/prediction_models/`.
|
||||||
|
|
||||||
Regression and classification models differ in what targets they predict - a regression model will predict a target of continuous values, for example what price BTC will be at tomorrow, whilst a classifier will predict a target of discrete values, for example if the price of BTC will go up tomorrow or not. This means that you have to specify your targets differently depending on which model type you are using (see details [below](#setting-model-targets)).
|
Regression and classification models differ in what targets they predict - a regression model will predict a target of continuous values, for example what price BTC will be at tomorrow, whilst a classifier will predict a target of discrete values, for example if the price of BTC will go up tomorrow or not. This means that you have to specify your targets differently depending on which model type you are using (see details [below](#setting-model-targets)).
|
||||||
|
|
||||||
All of the aforementioned model libraries implement gradient boosted decision tree algorithms. They all work on the principle of ensemble learning, where predictions from multiple simple learners are combined to get a final prediction that is more stable and generalized. The simple learners in this case are decision trees. Gradient boosting refers to the method of learning, where each simple learner is built in sequence - the subsequent learner is used to improve on the error from the previous learner. If you want to learn more about the different model libraries you can find the information in their respective docs:
|
All of the aforementioned model libraries implement gradient boosted decision tree algorithms. They all work on the principle of ensemble learning, where predictions from multiple simple learners are combined to get a final prediction that is more stable and generalized. The simple learners in this case are decision trees. Gradient boosting refers to the method of learning, where each simple learner is built in sequence - the subsequent learner is used to improve on the error from the previous learner. If you want to learn more about the different model libraries you can find the information in their respective docs:
|
||||||
|
|
||||||
* CatBoost: https://catboost.ai/en/docs/
|
* LightGBM: <https://lightgbm.readthedocs.io/en/v3.3.2/#>
|
||||||
* LightGBM: https://lightgbm.readthedocs.io/en/v3.3.2/#
|
* XGBoost: <https://xgboost.readthedocs.io/en/stable/#>
|
||||||
* XGBoost: https://xgboost.readthedocs.io/en/stable/#
|
* CatBoost: <https://catboost.ai/en/docs/> (No longer actively supported since 2025.12)
|
||||||
|
|
||||||
There are also numerous online articles describing and comparing the algorithms. Some relatively lightweight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model.
|
There are also numerous online articles describing and comparing the algorithms. Some relatively lightweight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model.
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ Make sure to use unique names to avoid overriding built-in models.
|
|||||||
|
|
||||||
#### Regressors
|
#### Regressors
|
||||||
|
|
||||||
If you are using a regressor, you need to specify a target that has continuous values. FreqAI includes a variety of regressors, such as the `CatboostRegressor`via the flag `--freqaimodel CatboostRegressor`. An example of how you could set a regression target for predicting the price 100 candles into the future would be
|
If you are using a regressor, you need to specify a target that has continuous values. FreqAI includes a variety of regressors, such as the `LightGBMRegressor`via the flag `--freqaimodel LightGBMRegressor`. An example of how you could set a regression target for predicting the price 100 candles into the future would be
|
||||||
|
|
||||||
```python
|
```python
|
||||||
df['&s-close_price'] = df['close'].shift(-100)
|
df['&s-close_price'] = df['close'].shift(-100)
|
||||||
@@ -229,7 +229,7 @@ If you want to predict multiple targets, you need to define multiple labels usin
|
|||||||
|
|
||||||
#### Classifiers
|
#### Classifiers
|
||||||
|
|
||||||
If you are using a classifier, you need to specify a target that has discrete values. FreqAI includes a variety of classifiers, such as the `CatboostClassifier` via the flag `--freqaimodel CatboostClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example, if you want to predict if the price 100 candles into the future goes up or down you would set
|
If you are using a classifier, you need to specify a target that has discrete values. FreqAI includes a variety of classifiers, such as the `LightGBMClassifier` via the flag `--freqaimodel LightGBMClassifier`. If you elects to use a classifier, the classes need to be set using strings. For example, if you want to predict if the price 100 candles into the future goes up or down you would set
|
||||||
|
|
||||||
```python
|
```python
|
||||||
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')
|
df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down')
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `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`.
|
||||||
|
|
||||||
|
|
||||||
### Additional parameters
|
### Additional parameters
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
@@ -116,3 +115,4 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `freqai.conv_width` | The width of a neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
| `freqai.conv_width` | The width of a neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||||
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
|
| `freqai.override_exchange_check` | Override the exchange check to force FreqAI to use exchanges that may not have enough historic data. Turn this to True if you know your FreqAI model and strategy do not require historical data. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
|
|||||||
|
|
||||||
More details about feature engineering available:
|
More details about feature engineering available:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the targets
|
:param df: strategy dataframe which will receive the targets
|
||||||
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)).
|
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
FreqAI is, and always will be, a not-for-profit, open source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/latest/freqai/).
|
FreqAI is, and always will be, a not-for-profit, open source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/stable/freqai/).
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - t
|
|||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
//...
|
//...
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": ["https://frequi.freqtrade.io"],
|
"CORS_origins": ["https://frequi.freqtrade.io"],
|
||||||
//...
|
//...
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ The correct configuration for this case is `http://localhost:8080` - the main pa
|
|||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
//...
|
//...
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": ["http://localhost:8080"],
|
"CORS_origins": ["http://localhost:8080"],
|
||||||
//...
|
//...
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
|||||||
|
|
||||||
### Common settings to all Protections
|
### Common settings to all Protections
|
||||||
|
|
||||||
| Parameter| Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
| --------- | ---------- |
|
||||||
| `method` | Protection name to use. <br> **Datatype:** String, selected from [available Protections](#available-protections)
|
| `method` | Protection name to use. <br> **Datatype:** String, selected from [available Protections](#available-protections) |
|
||||||
| `stop_duration_candles` | For how many candles should the lock be set? <br> **Datatype:** Positive integer (in candles)
|
| `stop_duration_candles` | For how many candles should the lock be set? <br> **Datatype:** Positive integer (in candles) |
|
||||||
| `stop_duration` | how many minutes should protections be locked. <br>Cannot be used together with `stop_duration_candles`. <br> **Datatype:** Float (in minutes)
|
| `stop_duration` | how many minutes should protections be locked. <br>Cannot be used together with `stop_duration_candles`. <br> **Datatype:** Float (in minutes) |
|
||||||
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles).
|
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles). |
|
||||||
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes)
|
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes) |
|
||||||
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer
|
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer |
|
||||||
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours)
|
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours) |
|
||||||
|
|
||||||
!!! Note "Durations"
|
!!! Note "Durations"
|
||||||
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
|
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
|
||||||
@@ -69,7 +69,17 @@ def protections(self):
|
|||||||
|
|
||||||
#### MaxDrawdown
|
#### MaxDrawdown
|
||||||
|
|
||||||
`MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover.
|
The `MaxDrawdown` protection evaluates trades that closed within the current `lookback_period` (or `lookback_period_candles`).
|
||||||
|
It supports 2 calculation modes:
|
||||||
|
|
||||||
|
- `calculation_mode: "ratios"` (default): Legacy approximation based on cumulative profit ratios.
|
||||||
|
- `calculation_mode: "equity"`: Standard peak-to-trough drawdown on the account equity curve, using starting balance and cumulative absolute profit.
|
||||||
|
|
||||||
|
With `calculation_mode: "ratios"`, drawdown is derived from cumulative trade profit ratios, not from the account equity curve. This is kept for backward compatibility and can differ from account-level drawdown when position sizing changes over time.
|
||||||
|
|
||||||
|
For new setups, `calculation_mode: "equity"` is recommended. Prefer `calculation_mode: "ratios"` only when you intentionally rely on legacy behavior, especially with fixed stake amount configurations where ratio-based behavior is easier to reason about.
|
||||||
|
|
||||||
|
If the observed drawdown exceeds `max_allowed_drawdown`, trading will stop for `stop_duration` after the last trade - assuming that the bot needs some time to let markets recover.
|
||||||
|
|
||||||
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
||||||
|
|
||||||
@@ -79,6 +89,7 @@ def protections(self):
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"method": "MaxDrawdown",
|
"method": "MaxDrawdown",
|
||||||
|
"calculation_mode": "equity",
|
||||||
"lookback_period_candles": 48,
|
"lookback_period_candles": 48,
|
||||||
"trade_limit": 20,
|
"trade_limit": 20,
|
||||||
"stop_duration_candles": 12,
|
"stop_duration_candles": 12,
|
||||||
@@ -160,6 +171,7 @@ class AwesomeStrategy(IStrategy)
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "MaxDrawdown",
|
"method": "MaxDrawdown",
|
||||||
|
"calculation_mode": "equity",
|
||||||
"lookback_period_candles": 48,
|
"lookback_period_candles": 48,
|
||||||
"trade_limit": 20,
|
"trade_limit": 20,
|
||||||
"stop_duration_candles": 4,
|
"stop_duration_candles": 4,
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
## Highlighted changes
|
# Highlighted changes
|
||||||
|
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
### How to update
|
## How to update
|
||||||
|
|
||||||
As always, you can update your bot using one of the following commands:
|
As always, you can update your bot using one of the following commands:
|
||||||
|
|
||||||
#### docker-compose
|
### docker-compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose pull
|
docker-compose pull
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Installation via setup script
|
### Installation via setup script
|
||||||
|
|
||||||
```
|
``` bash
|
||||||
# Deactivate venv and run
|
# Deactivate venv and run
|
||||||
./setup.sh --update
|
./setup.sh --update
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Plain native installation
|
### Plain native installation
|
||||||
|
|
||||||
```
|
``` bash
|
||||||
git pull
|
git pull
|
||||||
pip install -U -r requirements.txt
|
pip install -U -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|||||||
+6
-2
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
[](https://github.com/freqtrade/freqtrade/actions/workflows/ci.yml)
|
||||||
[](https://doi.org/10.21105/joss.04864)
|
[](https://doi.org/10.21105/joss.04864)
|
||||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[](https://codecov.io/gh/freqtrade/freqtrade)
|
||||||
|
[](https://www.freqtrade.io)
|
||||||
|
[](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 }
|
||||||
@@ -37,6 +39,8 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
|||||||
|
|
||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
|
### Supported Spot Exchanges
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
||||||
- [X] [Bitget](https://www.bitget.com/)
|
- [X] [Bitget](https://www.bitget.com/)
|
||||||
@@ -50,7 +54,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
|||||||
- [X] [MyOKX](https://okx.com/) (OKX EEA)
|
- [X] [MyOKX](https://okx.com/) (OKX EEA)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Supported Futures Exchanges (experimental)
|
### Supported Futures Exchanges
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bitget](https://www.bitget.com/)
|
- [X] [Bitget](https://www.bitget.com/)
|
||||||
|
|||||||
+57
-18
@@ -9,15 +9,25 @@ The freqtrade documentation describes various ways to install freqtrade
|
|||||||
* [Manual Installation](#manual-installation)
|
* [Manual Installation](#manual-installation)
|
||||||
* [Installation with Conda](#installation-with-conda)
|
* [Installation with Conda](#installation-with-conda)
|
||||||
|
|
||||||
Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works.
|
Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly.
|
||||||
|
|
||||||
|
!!! Note "Updating"
|
||||||
|
Keeping freqtrade updated is important to [ensure ongoing compatibility](updating.md#why-update) with exchange API's.
|
||||||
|
Please refer to the [updating guide](updating.md) for details on how to update your installation.
|
||||||
|
|
||||||
|
!!! Note "Windows users"
|
||||||
|
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
|
||||||
|
|
||||||
|
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu/Linux instructions will work.
|
||||||
|
If you really want to install freqtrade natively on Windows, best use the [`./setup.ps1` installation script](#use-setupps1-windows).
|
||||||
|
|
||||||
|
Please also make sure to use the 64bit version of Python, as 32bit versions have severe memory limitations, which can negatively impact your experience with backtesting/hyperopt.
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Information
|
## Information
|
||||||
|
|
||||||
For Windows installation, please use the [windows installation guide](windows_installation.md).
|
The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` (`./setup.ps1` for Windows) script, if it's available for your platform.
|
||||||
|
|
||||||
The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` script, if it's available for your platform.
|
|
||||||
|
|
||||||
!!! Note "Version considerations"
|
!!! Note "Version considerations"
|
||||||
When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests).
|
When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests).
|
||||||
@@ -152,20 +162,9 @@ If you are on Debian, Ubuntu or MacOS, freqtrade provides the script to install
|
|||||||
./setup.sh -i
|
./setup.sh -i
|
||||||
```
|
```
|
||||||
|
|
||||||
### Activate your virtual environment
|
#### Other options of /setup.sh script
|
||||||
|
|
||||||
Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment.
|
You can also update, configure and reset the codebase of your bot with `./setup.sh`
|
||||||
|
|
||||||
```bash
|
|
||||||
# activate virtual environment
|
|
||||||
source ./.venv/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
[You are now ready](#you-are-ready) to run the bot.
|
|
||||||
|
|
||||||
### Other options of /setup.sh script
|
|
||||||
|
|
||||||
You can as well update, configure and reset the codebase of your bot with `./script.sh`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# --update, Command git pull to update.
|
# --update, Command git pull to update.
|
||||||
@@ -194,6 +193,34 @@ This option will pull the last version of your current branch and update your vi
|
|||||||
This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv.
|
This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Activate your virtual environment
|
||||||
|
|
||||||
|
Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# activate virtual environment
|
||||||
|
source ./.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use ./setup.ps1 (Windows)
|
||||||
|
|
||||||
|
The script will ask you a few questions to determine which parts should be installed.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -ExecutionPolicy Bypass
|
||||||
|
cd freqtrade
|
||||||
|
. .\setup.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Activate your virtual environment (Windows)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# activate virtual environment
|
||||||
|
. .\.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
[You are now ready](#you-are-ready) to run the bot.
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
## Manual Installation
|
## Manual Installation
|
||||||
@@ -337,7 +364,7 @@ conda deactivate
|
|||||||
|
|
||||||
Happy trading!
|
Happy trading!
|
||||||
|
|
||||||
-----
|
------
|
||||||
|
|
||||||
## You are ready
|
## You are ready
|
||||||
|
|
||||||
@@ -394,3 +421,15 @@ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10
|
|||||||
```
|
```
|
||||||
|
|
||||||
If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
|
If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
|
||||||
|
|
||||||
|
### Windows Installation error
|
||||||
|
|
||||||
|
```bash
|
||||||
|
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||||
|
|
||||||
|
You can download the Visual C++ build tools from [the Visual Studio website](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
+1
-4
@@ -1,8 +1,5 @@
|
|||||||
# Trading with Leverage
|
# Trading with Leverage
|
||||||
|
|
||||||
!!! Warning "Beta feature"
|
|
||||||
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
|
|
||||||
|
|
||||||
!!! Note "Multiple bots on one account"
|
!!! Note "Multiple bots on one account"
|
||||||
You can't run 2 bots on the same account with leverage. For leveraged / margin trading, freqtrade assumes it's the only user of the account, and all liquidation levels are calculated based on this assumption.
|
You can't run 2 bots on the same account with leverage. For leveraged / margin trading, freqtrade assumes it's the only user of the account, and all liquidation levels are calculated based on this assumption.
|
||||||
|
|
||||||
@@ -55,7 +52,7 @@ Perpetual swaps (also known as Perpetual Futures) are contracts traded at a pric
|
|||||||
In addition to the gains/losses from the change in price of the futures contract, traders also exchange _funding fees_, which are gains/losses worth an amount that is derived from the difference in price between the futures contract and the underlying asset. The difference in price between a futures contract and the underlying asset varies between exchanges.
|
In addition to the gains/losses from the change in price of the futures contract, traders also exchange _funding fees_, which are gains/losses worth an amount that is derived from the difference in price between the futures contract and the underlying asset. The difference in price between a futures contract and the underlying asset varies between exchanges.
|
||||||
|
|
||||||
To trade in futures markets, you'll have to set `trading_mode` to "futures".
|
To trade in futures markets, you'll have to set `trading_mode` to "futures".
|
||||||
You will also have to pick a "margin mode" (explanation below) - with freqtrade currently only supporting isolated margin.
|
You will also have to pick a "margin mode" (explanation below).
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
"trading_mode": "futures",
|
"trading_mode": "futures",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
markdown==3.10
|
markdown==3.10.2
|
||||||
mkdocs==1.6.1
|
mkdocs==1.6.1
|
||||||
mkdocs-material==9.7.0
|
mkdocs-material==9.7.1
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.17.1
|
pymdown-extensions==10.21
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
mike==2.1.3
|
mike==2.1.3
|
||||||
|
|||||||
+3
-3
@@ -17,7 +17,7 @@ Sample configuration:
|
|||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"enable_openapi": false,
|
"enable_openapi": false,
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "Freqtrader",
|
"username": "Freqtrader",
|
||||||
"password": "SuperSecret1!",
|
"password": "SuperSecret1!",
|
||||||
@@ -56,7 +56,7 @@ secrets.token_hex()
|
|||||||
|
|
||||||
!!! Danger "Password selection"
|
!!! Danger "Password selection"
|
||||||
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
||||||
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!).
|
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!). This value should also be 32 characters or longer to be safe.
|
||||||
|
|
||||||
### Configuration with docker
|
### Configuration with docker
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ You would then add that token under `ws_token` in your `api_server` config. Like
|
|||||||
"listen_port": 8080,
|
"listen_port": 8080,
|
||||||
"verbosity": "error",
|
"verbosity": "error",
|
||||||
"enable_openapi": false,
|
"enable_openapi": false,
|
||||||
"jwt_secret_key": "somethingrandom",
|
"jwt_secret_key": "somethingRandomSomethingRandom123",
|
||||||
"CORS_origins": [],
|
"CORS_origins": [],
|
||||||
"username": "Freqtrader",
|
"username": "Freqtrader",
|
||||||
"password": "SuperSecret1!",
|
"password": "SuperSecret1!",
|
||||||
|
|||||||
+6
-1
@@ -31,9 +31,14 @@ The Order-type will be ignored if only one mode is available.
|
|||||||
--8<-- "includes/exchange-features.md"
|
--8<-- "includes/exchange-features.md"
|
||||||
|
|
||||||
!!! Note "Tight stoploss"
|
!!! Note "Tight stoploss"
|
||||||
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
|
Do not set too low/tight stoploss value when using stop loss on exchange!
|
||||||
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
|
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
|
||||||
|
|
||||||
|
!!! Warning "Loose stoploss"
|
||||||
|
Using stoploss on exchange with a very wide stoploss (e.g. -1) may fail to place the stoploss order on exchange due to exchange limitations.
|
||||||
|
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.
|
||||||
|
|
||||||
### 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.
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns the initial stoploss value.
|
When not implemented by a strategy, returns the initial stoploss value.
|
||||||
Only called when use_custom_stoploss is set to True.
|
Only called when use_custom_stoploss is set to True.
|
||||||
@@ -634,7 +634,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
## Custom order price rules
|
## Custom order price rules
|
||||||
|
|
||||||
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
By default, freqtrade use the orderbook to automatically set an order price ([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||||
|
|
||||||
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
||||||
|
|
||||||
@@ -644,7 +644,7 @@ Each of these methods are called right before placing an order on the exchange.
|
|||||||
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Using custom_entry_price, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
|
When using `custom_entry_price()`, the Trade object will be available as soon as the first entry order associated with the trade is created, for the first entry, `trade` parameter value will be `None`.
|
||||||
|
|
||||||
### Custom order entry and exit price example
|
### Custom order entry and exit price example
|
||||||
|
|
||||||
@@ -805,7 +805,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
Timing for this function is critical, so avoid doing heavy computations or
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
network requests in this method.
|
network requests in this method.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
@@ -853,7 +853,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
Timing for this function is critical, so avoid doing heavy computations or
|
Timing for this function is critical, so avoid doing heavy computations or
|
||||||
network requests in this method.
|
network requests in this method.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
@@ -991,7 +991,7 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
This means extra entry or exit orders with additional fees.
|
This means extra entry or exit orders with additional fees.
|
||||||
Only called when `position_adjustment_enable` is set to True.
|
Only called when `position_adjustment_enable` is set to True.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns None
|
When not implemented by a strategy, returns None
|
||||||
|
|
||||||
@@ -1118,7 +1118,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||||
and not timed out on subsequent candles after entry trigger.
|
and not timed out on subsequent candles after entry trigger.
|
||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
|
For full documentation please go to https://www.freqtrade.io/en/stable/strategy-callbacks/
|
||||||
|
|
||||||
When not implemented by a strategy, returns current_order_rate as default.
|
When not implemented by a strategy, returns current_order_rate as default.
|
||||||
If current_order_rate is returned then the existing order is maintained.
|
If current_order_rate is returned then the existing order is maintained.
|
||||||
@@ -1292,6 +1292,23 @@ Currently two types of annotations are supported, `area` and `line`.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Point
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"type": "point", // Type of the annotation, currently only "point" is supported
|
||||||
|
"x": "2024-01-01 15:00:00", // Start date of the point
|
||||||
|
"y": 94000.2, // Price / y axis value
|
||||||
|
"color": "",
|
||||||
|
"z_level": 5, // z-level, higher values are drawn on top of lower values. Positions relative to the Chart elements need to be set in freqUI.
|
||||||
|
"label": "some label",
|
||||||
|
"size": 2, // Optional, line width in pixels. Defaults to 10
|
||||||
|
"shape": "circle", // Optional, can be "circle", "rect", "roundRect", "triangle", "pin", "arrow", "none".
|
||||||
|
"rotate": 0, // Optional, rotation of the shape/symbol in degrees. Defaults to 0
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
The below example will mark the chart with areas for the hours 8 and 15, with a grey color, highlighting the market open and close hours.
|
The below example will mark the chart with areas for the hours 8 and 15, with a grey color, highlighting the market open and close hours.
|
||||||
This is obviously a very basic example.
|
This is obviously a very basic example.
|
||||||
|
|
||||||
@@ -1369,7 +1386,7 @@ Entries will be validated, and won't be passed to the UI if they don't correspon
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif (start_dt.hour % 2) == 0:
|
elif (start_dt.hour % 2) == 0:
|
||||||
price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
|
price = dataframe.loc[dataframe["date"] == start_dt, "close"].mean()
|
||||||
annotations.append(
|
annotations.append(
|
||||||
{
|
{
|
||||||
"type": "area",
|
"type": "area",
|
||||||
|
|||||||
@@ -594,9 +594,9 @@ Features will now expand automatically. As such, the expansion loops, as well as
|
|||||||
More details on how these config defined parameters accelerate feature engineering
|
More details on how these config defined parameters accelerate feature engineering
|
||||||
in the documentation at:
|
in the documentation at:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the features
|
:param df: strategy dataframe which will receive the features
|
||||||
:param period: period of the indicator - usage example:
|
:param period: period of the indicator - usage example:
|
||||||
@@ -657,9 +657,9 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
|||||||
More details on how these config defined parameters accelerate feature engineering
|
More details on how these config defined parameters accelerate feature engineering
|
||||||
in the documentation at:
|
in the documentation at:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
https://www.freqtrade.io/en/stable/freqai-parameter-table/#feature-parameters
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering/#defining-the-features
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the features
|
:param df: strategy dataframe which will receive the features
|
||||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
@@ -690,7 +690,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
|||||||
|
|
||||||
More details about feature engineering available:
|
More details about feature engineering available:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the features
|
:param df: strategy dataframe which will receive the features
|
||||||
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||||
@@ -713,7 +713,7 @@ Targets now get their own, dedicated method.
|
|||||||
|
|
||||||
More details about feature engineering available:
|
More details about feature engineering available:
|
||||||
|
|
||||||
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
https://www.freqtrade.io/en/stable/freqai-feature-engineering
|
||||||
|
|
||||||
:param df: strategy dataframe which will receive the targets
|
:param df: strategy dataframe which will receive the targets
|
||||||
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||||
|
|||||||
+14
-3
@@ -6,6 +6,12 @@ To update your freqtrade installation, please use one of the below methods, corr
|
|||||||
Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release.
|
Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release.
|
||||||
For the develop branch, please follow PR's to avoid being surprised by changes.
|
For the develop branch, please follow PR's to avoid being surprised by changes.
|
||||||
|
|
||||||
|
## Why update?
|
||||||
|
|
||||||
|
Keeping your bot updated not only ensures that you have the latest features and improvements, but is a requirement to keep your bot running smoothly.
|
||||||
|
Freqtrade is heavily dependent on the underlying exchange API's, which change pretty frequently if considered across exchanges.
|
||||||
|
To ensure ongoing compatibility, please make sure to update your bot regularly.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
!!! Note "Legacy installations using the `master` image"
|
!!! Note "Legacy installations using the `master` image"
|
||||||
@@ -38,7 +44,12 @@ pip install -e .
|
|||||||
freqtrade install-ui
|
freqtrade install-ui
|
||||||
```
|
```
|
||||||
|
|
||||||
### Problems updating
|
## Problems updating
|
||||||
|
|
||||||
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib).
|
Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from dependencies which fail to install.
|
||||||
Please refer to the corresponding installation sections (common problems linked below)
|
We try to make sure that heavy dependencies have wheels available for major platforms, but sometimes this is not possible.
|
||||||
|
|
||||||
|
Please refer to the corresponding installation sections (common problem sections linked below).
|
||||||
|
|
||||||
|
[Common installation problems](installation.md#troubleshooting)
|
||||||
|
[Common installation problems - windows](installation.md#windows-installation-error)
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
# Windows installation
|
|
||||||
|
|
||||||
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
|
|
||||||
|
|
||||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
|
||||||
Otherwise, please follow the instructions below.
|
|
||||||
|
|
||||||
All instructions assume that python 3.11+ is installed and available.
|
|
||||||
|
|
||||||
## Clone the git repository
|
|
||||||
|
|
||||||
First of all clone the repository by running:
|
|
||||||
|
|
||||||
``` powershell
|
|
||||||
git clone https://github.com/freqtrade/freqtrade.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, choose your installation method, either automatically via script (recommended) or manually following the corresponding instructions.
|
|
||||||
|
|
||||||
## Install freqtrade automatically
|
|
||||||
|
|
||||||
### Run the installation script
|
|
||||||
|
|
||||||
The script will ask you a few questions to determine which parts should be installed.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Set-ExecutionPolicy -ExecutionPolicy Bypass
|
|
||||||
cd freqtrade
|
|
||||||
. .\setup.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install freqtrade manually
|
|
||||||
|
|
||||||
!!! Note "64bit Python version"
|
|
||||||
Please make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows.
|
|
||||||
32bit python versions are no longer supported under Windows.
|
|
||||||
|
|
||||||
!!! Hint
|
|
||||||
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
|
|
||||||
|
|
||||||
|
|
||||||
### Error during installation on Windows
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
|
||||||
```
|
|
||||||
|
|
||||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
|
||||||
|
|
||||||
You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Freqtrade bot"""
|
"""Freqtrade bot"""
|
||||||
|
|
||||||
__version__ = "2025.11.1"
|
__version__ = "2026.2"
|
||||||
|
|
||||||
if "dev" in __version__:
|
if "dev" in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ This module contains the argument manager class
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from argparse import ArgumentParser, Namespace, _ArgumentGroup
|
from argparse import ArgumentParser, Namespace, _ArgumentGroup
|
||||||
|
from copy import deepcopy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -102,7 +103,13 @@ ARGS_BACKTEST_SHOW = [
|
|||||||
"backtest_breakdown",
|
"backtest_breakdown",
|
||||||
]
|
]
|
||||||
|
|
||||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all", "trading_mode", "dex_exchanges"]
|
ARGS_LIST_EXCHANGES = [
|
||||||
|
"print_one_column",
|
||||||
|
"list_exchanges_all",
|
||||||
|
"trading_mode",
|
||||||
|
"dex_exchanges",
|
||||||
|
"list_exchanges_futures_options",
|
||||||
|
]
|
||||||
|
|
||||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column", "trading_mode"]
|
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column", "trading_mode"]
|
||||||
|
|
||||||
@@ -174,6 +181,7 @@ ARGS_DOWNLOAD_DATA = [
|
|||||||
"dataformat_ohlcv",
|
"dataformat_ohlcv",
|
||||||
"dataformat_trades",
|
"dataformat_trades",
|
||||||
"trading_mode",
|
"trading_mode",
|
||||||
|
"candle_types",
|
||||||
"prepend_data",
|
"prepend_data",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -348,7 +356,11 @@ class Arguments:
|
|||||||
def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
|
def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
|
||||||
for val in optionlist:
|
for val in optionlist:
|
||||||
opt = AVAILABLE_CLI_OPTIONS[val]
|
opt = AVAILABLE_CLI_OPTIONS[val]
|
||||||
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
|
options = deepcopy(opt.kwargs)
|
||||||
|
help_text = options.pop("help", None)
|
||||||
|
if opt.fthelp and isinstance(opt.fthelp, dict) and hasattr(parser, "prog"):
|
||||||
|
help_text = opt.fthelp.get(parser.prog, help_text)
|
||||||
|
parser.add_argument(*opt.cli, dest=val, help=help_text, **options)
|
||||||
|
|
||||||
def _build_subcommands(self) -> None:
|
def _build_subcommands(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Definition of cli arguments used in arguments.py
|
Definition of cli arguments used in arguments.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from argparse import ArgumentTypeError
|
from argparse import SUPPRESS, ArgumentTypeError
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.constants import (
|
from freqtrade.constants import (
|
||||||
@@ -38,8 +38,14 @@ def check_int_nonzero(value: str) -> int:
|
|||||||
|
|
||||||
class Arg:
|
class Arg:
|
||||||
# Optional CLI arguments
|
# Optional CLI arguments
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, fthelp: dict[str, str] | None = None, **kwargs):
|
||||||
|
"""
|
||||||
|
CLI Arguments - used to build subcommand parsers consistently.
|
||||||
|
:param fthelp: dict - fthelp per command - should be "freqtrade <command>": help_text
|
||||||
|
If not provided or not found, 'help' from kwargs is used instead.
|
||||||
|
"""
|
||||||
self.cli = args
|
self.cli = args
|
||||||
|
self.fthelp = fthelp
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
|
||||||
@@ -174,7 +180,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"position_stacking": Arg(
|
"position_stacking": Arg(
|
||||||
"--eps",
|
"--eps",
|
||||||
"--enable-position-stacking",
|
"--enable-position-stacking",
|
||||||
help="Allow buying the same pair multiple times (position stacking).",
|
help=(
|
||||||
|
"Allow buying the same pair multiple times (position stacking). "
|
||||||
|
"Only applicable to backtesting and hyperopt. "
|
||||||
|
"Results archived by this cannot be reproduced in dry/live trading."
|
||||||
|
),
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
),
|
),
|
||||||
@@ -205,9 +215,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"--strategy-list",
|
"--strategy-list",
|
||||||
help="Provide a space-separated list of strategies to backtest. "
|
help="Provide a space-separated list of strategies to backtest. "
|
||||||
"Please note that timeframe needs to be set either in config "
|
"Please note that timeframe needs to be set either in config "
|
||||||
"or via command line. When using this together with `--export trades`, "
|
"or via command line. ",
|
||||||
"the strategy-name is injected into the filename "
|
|
||||||
"(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`",
|
|
||||||
nargs="+",
|
nargs="+",
|
||||||
),
|
),
|
||||||
"backtest_notes": Arg(
|
"backtest_notes": Arg(
|
||||||
@@ -230,6 +238,14 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"exportfilename": Arg(
|
"exportfilename": Arg(
|
||||||
"--backtest-filename",
|
"--backtest-filename",
|
||||||
"--export-filename",
|
"--export-filename",
|
||||||
|
fthelp={
|
||||||
|
"freqtrade backtesting": (
|
||||||
|
"DEPRECATED: This option is deprecated for backtesting and will be removed "
|
||||||
|
"in a future release. "
|
||||||
|
"Using a custom filename for backtest results is no longer supported. "
|
||||||
|
"Use `--backtest-directory` to specify the directory."
|
||||||
|
),
|
||||||
|
},
|
||||||
help="Use this filename for backtest results."
|
help="Use this filename for backtest results."
|
||||||
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
|
"Example: `--backtest-filename=backtest_results_2020-09-27_16-20-48.json`. "
|
||||||
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
|
"Assumes either `user_data/backtest_results/` or `--export-directory` as base directory.",
|
||||||
@@ -378,6 +394,13 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help="Print only DEX exchanges.",
|
help="Print only DEX exchanges.",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
),
|
),
|
||||||
|
"list_exchanges_futures_options": Arg(
|
||||||
|
"--ccxt-show-futures-options-exchanges",
|
||||||
|
help=SUPPRESS,
|
||||||
|
# Show compatibility with ccxt for futures functionality
|
||||||
|
# Doesn't show in help as it's an internal/debug option.
|
||||||
|
action="store_true",
|
||||||
|
),
|
||||||
# List pairs / markets
|
# List pairs / markets
|
||||||
"list_pairs_all": Arg(
|
"list_pairs_all": Arg(
|
||||||
"-a",
|
"-a",
|
||||||
@@ -422,6 +445,14 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
),
|
),
|
||||||
"candle_types": Arg(
|
"candle_types": Arg(
|
||||||
"--candle-types",
|
"--candle-types",
|
||||||
|
fthelp={
|
||||||
|
"freqtrade download-data": (
|
||||||
|
"Select candle type to download. "
|
||||||
|
"Defaults to the necessary candles for the selected trading mode "
|
||||||
|
"(e.g. 'spot' or ('futures', 'funding_rate' and 'mark') for futures)."
|
||||||
|
),
|
||||||
|
"_": "Select candle type to convert. Defaults to all available types.",
|
||||||
|
},
|
||||||
help="Select candle type to convert. Defaults to all available types.",
|
help="Select candle type to convert. Defaults to all available types.",
|
||||||
choices=[c.value for c in CandleType],
|
choices=[c.value for c in CandleType],
|
||||||
nargs="+",
|
nargs="+",
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ def start_list_trades_data(args: dict[str, Any]) -> None:
|
|||||||
end.strftime(DATETIME_PRINT_FORMAT),
|
end.strftime(DATETIME_PRINT_FORMAT),
|
||||||
str(length),
|
str(length),
|
||||||
)
|
)
|
||||||
for pair, start, end, length in sorted(paircombs1, key=lambda x: (x[0]))
|
for pair, start, end, length in sorted(paircombs1, key=lambda x: x[0])
|
||||||
],
|
],
|
||||||
("Pair", "Type", "From", "To", "Trades"),
|
("Pair", "Type", "From", "To", "Trades"),
|
||||||
summary=title,
|
summary=title,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
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 Order, Trade, init_db
|
||||||
|
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.migrations import set_sequence_ids
|
||||||
from freqtrade.persistence.pairlock import PairLock
|
from freqtrade.persistence.pairlock import PairLock
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
|||||||
|
|
||||||
trade_count = 0
|
trade_count = 0
|
||||||
pairlock_count = 0
|
pairlock_count = 0
|
||||||
|
kv_count = 0
|
||||||
|
custom_data_count = 0
|
||||||
for trade in Trade.get_trades():
|
for trade in Trade.get_trades():
|
||||||
trade_count += 1
|
trade_count += 1
|
||||||
make_transient(trade)
|
make_transient(trade)
|
||||||
@@ -41,16 +45,35 @@ def start_convert_db(args: dict[str, Any]) -> None:
|
|||||||
session_target.add(pairlock)
|
session_target.add(pairlock)
|
||||||
session_target.commit()
|
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
|
# Update sequences
|
||||||
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
||||||
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
||||||
max_pairlock_id = session_target.scalar(select(func.max(PairLock.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(
|
set_sequence_ids(
|
||||||
session_target.get_bind(),
|
session_target.get_bind(),
|
||||||
trade_id=max_trade_id,
|
trade_id=(max_trade_id or 0) + 1,
|
||||||
order_id=max_order_id,
|
order_id=(max_order_id or 0) + 1,
|
||||||
pairlock_id=max_pairlock_id,
|
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, and {pairlock_count} Pairlocks.")
|
logger.info(
|
||||||
|
f"Migrated {trade_count} Trades, {pairlock_count} Pairlocks, "
|
||||||
|
f"{kv_count} Key-Value pairs, and {custom_data_count} Custom Data entries."
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import sys
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -38,13 +38,15 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
|||||||
else:
|
else:
|
||||||
available_exchanges = [e for e in available_exchanges if e["valid"] is not False]
|
available_exchanges = [e for e in available_exchanges if e["valid"] is not False]
|
||||||
title = f"Exchanges available for Freqtrade ({len(available_exchanges)} exchanges):"
|
title = f"Exchanges available for Freqtrade ({len(available_exchanges)} exchanges):"
|
||||||
|
show_fut_reasons = args.get("list_exchanges_futures_options", False)
|
||||||
table = Table(title=title)
|
table = Table(title=title)
|
||||||
|
|
||||||
table.add_column("Exchange Name")
|
table.add_column("Exchange Name")
|
||||||
table.add_column("Class Name")
|
table.add_column("Class Name")
|
||||||
table.add_column("Markets")
|
table.add_column("Markets")
|
||||||
table.add_column("Reason")
|
table.add_column("Reason")
|
||||||
|
if show_fut_reasons:
|
||||||
|
table.add_column("Futures Reason")
|
||||||
|
|
||||||
trading_mode = args.get("trading_mode", None)
|
trading_mode = args.get("trading_mode", None)
|
||||||
dex_only = args.get("dex_exchanges", False)
|
dex_only = args.get("dex_exchanges", False)
|
||||||
@@ -78,12 +80,14 @@ def start_list_exchanges(args: dict[str, Any]) -> None:
|
|||||||
if exchange["dex"]:
|
if exchange["dex"]:
|
||||||
trade_modes = Text("DEX: ") + trade_modes
|
trade_modes = Text("DEX: ") + trade_modes
|
||||||
trade_modes.stylize("bold", 0, 3)
|
trade_modes.stylize("bold", 0, 3)
|
||||||
|
futcol = [] if not show_fut_reasons else [exchange["comment_futures"]]
|
||||||
|
|
||||||
table.add_row(
|
table.add_row(
|
||||||
name,
|
name,
|
||||||
classname,
|
classname,
|
||||||
trade_modes,
|
trade_modes,
|
||||||
exchange["comment"],
|
exchange["comment"],
|
||||||
|
*futcol,
|
||||||
style=None if exchange["valid"] else "red",
|
style=None if exchange["valid"] else "red",
|
||||||
)
|
)
|
||||||
# table.add_row(*[exchange[header] for header in headers])
|
# table.add_row(*[exchange[header] for header in headers])
|
||||||
@@ -162,7 +166,14 @@ def start_list_strategies(args: dict[str, Any]) -> None:
|
|||||||
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
strategy_objs = sorted(strategy_objs, key=lambda x: x["name"])
|
||||||
for obj in strategy_objs:
|
for obj in strategy_objs:
|
||||||
if obj["class"]:
|
if obj["class"]:
|
||||||
obj["hyperoptable"] = detect_all_parameters(obj["class"])
|
try:
|
||||||
|
obj["hyperoptable"] = detect_all_parameters(obj["class"])
|
||||||
|
except DependencyException as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Cannot detect hyperoptable parameters for strategy {obj['name']}. Reason: {e}"
|
||||||
|
)
|
||||||
|
obj["hyperoptable"] = {}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
obj["hyperoptable"] = {}
|
obj["hyperoptable"] = {}
|
||||||
|
|
||||||
|
|||||||
@@ -752,6 +752,7 @@ CONF_SCHEMA = {
|
|||||||
"jwt_secret_key": {
|
"jwt_secret_key": {
|
||||||
"description": "Secret key for JWT authentication.",
|
"description": "Secret key for JWT authentication.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"default": "somethingRandomSomethingRandom123",
|
||||||
},
|
},
|
||||||
"CORS_origins": {
|
"CORS_origins": {
|
||||||
"description": "List of allowed CORS origins.",
|
"description": "List of allowed CORS origins.",
|
||||||
@@ -764,7 +765,14 @@ CONF_SCHEMA = {
|
|||||||
"enum": ["error", "info"],
|
"enum": ["error", "info"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": ["enabled", "listen_ip_address", "listen_port", "username", "password"],
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
"listen_ip_address",
|
||||||
|
"listen_port",
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
"jwt_secret_key",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
# end of RPC section
|
# end of RPC section
|
||||||
"db_url": {
|
"db_url": {
|
||||||
|
|||||||
@@ -221,30 +221,30 @@ class Configuration:
|
|||||||
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
config, argname="exportfilename", logstring="Storing backtest results to {} ..."
|
||||||
)
|
)
|
||||||
config["exportfilename"] = Path(config["exportfilename"])
|
config["exportfilename"] = Path(config["exportfilename"])
|
||||||
if config.get("exportdirectory") and Path(config["exportdirectory"]).is_dir():
|
if config.get("exportfilename"):
|
||||||
logger.warning(
|
if Path(config["exportfilename"]).is_dir():
|
||||||
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
|
logger.warning(
|
||||||
"use `--backtest-directory` instead."
|
"DEPRECATED: Using `--export-filename` with directories is deprecated, "
|
||||||
)
|
"use `--backtest-directory` instead."
|
||||||
if config.get("exportdirectory") is None:
|
)
|
||||||
# Fallback - assign export-directory directly.
|
if config.get("exportdirectory") is None:
|
||||||
config["exportdirectory"] = config["exportfilename"]
|
# Fallback - assign export-directory directly.
|
||||||
|
config["exportdirectory"] = config["exportfilename"]
|
||||||
|
elif config.get("runmode") == RunMode.BACKTEST:
|
||||||
|
logger.warning(
|
||||||
|
"DEPRECATED: Using `--export-filename` has no impact when backtesting. "
|
||||||
|
"Please use `--notes` to annotate backtest results and "
|
||||||
|
"`--backtest-directory` to specify the output directory. "
|
||||||
|
)
|
||||||
if not config.get("exportdirectory"):
|
if not config.get("exportdirectory"):
|
||||||
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
|
config["exportdirectory"] = config["user_data_dir"] / "backtest_results"
|
||||||
if not config.get("exportfilename"):
|
|
||||||
config["exportfilename"] = None
|
config["exportfilename"] = config.get("exportfilename", None)
|
||||||
if config.get("exportfilename"):
|
if config.get("exportfilename"):
|
||||||
# ensure exportfilename is a Path object
|
# ensure exportfilename is a Path object
|
||||||
config["exportfilename"] = Path(config["exportfilename"])
|
config["exportfilename"] = Path(config["exportfilename"])
|
||||||
config["exportdirectory"] = Path(config["exportdirectory"])
|
config["exportdirectory"] = Path(config["exportdirectory"])
|
||||||
|
|
||||||
if self.args.get("show_sensitive"):
|
|
||||||
logger.warning(
|
|
||||||
"Sensitive information will be shown in the upcoming output. "
|
|
||||||
"Please make sure to never share this output without redacting "
|
|
||||||
"the information yourself."
|
|
||||||
)
|
|
||||||
|
|
||||||
def _process_optimize_options(self, config: Config) -> None:
|
def _process_optimize_options(self, config: Config) -> None:
|
||||||
# This will override the strategy configuration
|
# This will override the strategy configuration
|
||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
@@ -312,6 +312,13 @@ class Configuration:
|
|||||||
|
|
||||||
self._process_datadir_options(config)
|
self._process_datadir_options(config)
|
||||||
|
|
||||||
|
if self.args.get("show_sensitive"):
|
||||||
|
logger.warning(
|
||||||
|
"Sensitive information will be shown in the upcoming output. "
|
||||||
|
"Please make sure to never share this output without redacting "
|
||||||
|
"the information yourself."
|
||||||
|
)
|
||||||
|
|
||||||
self._args_to_config(
|
self._args_to_config(
|
||||||
config,
|
config,
|
||||||
argname="strategy_list",
|
argname="strategy_list",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def log_config_error_range(path: str, errmsg: str) -> str:
|
|||||||
offsetlist = re.findall(r"(?<=Parse\serror\sat\soffset\s)\d+", errmsg)
|
offsetlist = re.findall(r"(?<=Parse\serror\sat\soffset\s)\d+", errmsg)
|
||||||
if offsetlist:
|
if offsetlist:
|
||||||
offset = int(offsetlist[0])
|
offset = int(offsetlist[0])
|
||||||
text = Path(path).read_text()
|
text = Path(path).read_text(encoding="utf-8")
|
||||||
# Fetch an offset of 80 characters around the error line
|
# Fetch an offset of 80 characters around the error line
|
||||||
subtext = text[offset - min(80, offset) : offset + 80]
|
subtext = text[offset - min(80, offset) : offset + 80]
|
||||||
segments = subtext.split("\n")
|
segments = subtext.split("\n")
|
||||||
|
|||||||
@@ -239,3 +239,6 @@ IntOrInf = float
|
|||||||
|
|
||||||
|
|
||||||
EntryExecuteMode = Literal["initial", "pos_adjust", "replace"]
|
EntryExecuteMode = Literal["initial", "pos_adjust", "replace"]
|
||||||
|
|
||||||
|
# Prefixes for low-priced coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid)
|
||||||
|
PairPrefixes = ["1000", "1000000", "1M", "K"]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from numpy import format_float_positional
|
||||||
from pandas import DataFrame, Series
|
from pandas import DataFrame, Series
|
||||||
|
|
||||||
|
|
||||||
@@ -11,7 +12,10 @@ def get_tick_size_over_time(candles: DataFrame) -> Series:
|
|||||||
# count the number of significant digits for the open and close prices
|
# count the number of significant digits for the open and close prices
|
||||||
for col in ["open", "high", "low", "close"]:
|
for col in ["open", "high", "low", "close"]:
|
||||||
candles[f"{col}_count"] = (
|
candles[f"{col}_count"] = (
|
||||||
candles[col].round(14).apply("{:.15f}".format).str.extract(r"\.(\d*[1-9])")[0].str.len()
|
candles[col]
|
||||||
|
.apply(format_float_positional, precision=14, unique=False, fractional=False, trim="-")
|
||||||
|
.str.extract(r"\.(\d*[1-9])")[0]
|
||||||
|
.str.len()
|
||||||
)
|
)
|
||||||
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
candles["max_count"] = candles[["open_count", "close_count", "high_count", "low_count"]].max(
|
||||||
axis=1
|
axis=1
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ def ohlcv_to_dataframe(
|
|||||||
cols = DEFAULT_DATAFRAME_COLUMNS
|
cols = DEFAULT_DATAFRAME_COLUMNS
|
||||||
df = DataFrame(ohlcv, columns=cols)
|
df = DataFrame(ohlcv, columns=cols)
|
||||||
|
|
||||||
df["date"] = to_datetime(df["date"], unit="ms", utc=True)
|
# Floor date to seconds to account for exchange imprecisions
|
||||||
|
df["date"] = to_datetime(df["date"], unit="ms", utc=True).dt.floor("s")
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -348,6 +348,22 @@ class DataProvider:
|
|||||||
)
|
)
|
||||||
return total_candles
|
return total_candles
|
||||||
|
|
||||||
|
def __fix_funding_rate_timeframe(
|
||||||
|
self, pair: str, timeframe: str | None, candle_type: str
|
||||||
|
) -> str | None:
|
||||||
|
if (
|
||||||
|
candle_type == CandleType.FUNDING_RATE
|
||||||
|
and (ff_tf := self.get_funding_rate_timeframe()) != timeframe
|
||||||
|
):
|
||||||
|
# TODO: does this message make sense? might be pointless as funding fees don't
|
||||||
|
# have a timeframe
|
||||||
|
logger.warning(
|
||||||
|
f"{pair}, {timeframe} requested - funding rate timeframe not matching {ff_tf}."
|
||||||
|
)
|
||||||
|
return ff_tf
|
||||||
|
|
||||||
|
return timeframe
|
||||||
|
|
||||||
def get_pair_dataframe(
|
def get_pair_dataframe(
|
||||||
self, pair: str, timeframe: str | None = None, candle_type: str = ""
|
self, pair: str, timeframe: str | None = None, candle_type: str = ""
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
@@ -361,6 +377,7 @@ class DataProvider:
|
|||||||
:return: Dataframe for this pair
|
:return: Dataframe for this pair
|
||||||
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
"""
|
"""
|
||||||
|
timeframe = self.__fix_funding_rate_timeframe(pair, timeframe, candle_type)
|
||||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
# Get live OHLCV data.
|
# Get live OHLCV data.
|
||||||
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
@@ -620,3 +637,12 @@ class DataProvider:
|
|||||||
except ExchangeError:
|
except ExchangeError:
|
||||||
logger.warning(f"Could not fetch market data for {pair}. Assuming no delisting.")
|
logger.warning(f"Could not fetch market data for {pair}. Assuming no delisting.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_funding_rate_timeframe(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the funding rate timeframe from exchange options
|
||||||
|
:return: Timeframe string
|
||||||
|
"""
|
||||||
|
if self._exchange is None:
|
||||||
|
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||||
|
return self._exchange.get_option("funding_fee_timeframe")
|
||||||
|
|||||||
@@ -70,28 +70,6 @@ class IDataHandler(ABC):
|
|||||||
if match and len(match.groups()) > 1
|
if match and len(match.groups()) > 1
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> list[str]:
|
|
||||||
"""
|
|
||||||
Returns a list of all pairs with ohlcv data available in this datadir
|
|
||||||
for the specified timeframe
|
|
||||||
:param datadir: Directory to search for ohlcv files
|
|
||||||
:param timeframe: Timeframe to search pairs for
|
|
||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
|
||||||
:return: List of Pairs
|
|
||||||
"""
|
|
||||||
candle = ""
|
|
||||||
if candle_type != CandleType.SPOT:
|
|
||||||
datadir = datadir.joinpath("futures")
|
|
||||||
candle = f"-{candle_type}"
|
|
||||||
ext = cls._get_file_extension()
|
|
||||||
_tmp = [
|
|
||||||
re.search(r"^(\S+)(?=\-" + timeframe + candle + f".{ext})", p.name)
|
|
||||||
for p in datadir.glob(f"*{timeframe}{candle}.{ext}")
|
|
||||||
]
|
|
||||||
# Check if regex found something and only return these results
|
|
||||||
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def ohlcv_store(
|
def ohlcv_store(
|
||||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||||
@@ -397,6 +375,9 @@ class IDataHandler(ABC):
|
|||||||
pairdf = self._ohlcv_load(
|
pairdf = self._ohlcv_load(
|
||||||
pair, timeframe, timerange=timerange_startup, candle_type=candle_type
|
pair, timeframe, timerange=timerange_startup, candle_type=candle_type
|
||||||
)
|
)
|
||||||
|
if not pairdf.empty and candle_type == CandleType.FUNDING_RATE:
|
||||||
|
# Funding rate data is sometimes off by a couple of ms - floor to seconds
|
||||||
|
pairdf["date"] = pairdf["date"].dt.floor("s")
|
||||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
||||||
return pairdf
|
return pairdf
|
||||||
else:
|
else:
|
||||||
@@ -508,8 +489,15 @@ class IDataHandler(ABC):
|
|||||||
Applies to bybit and okx, where funding-fee and mark candles have different timeframes.
|
Applies to bybit and okx, where funding-fee and mark candles have different timeframes.
|
||||||
"""
|
"""
|
||||||
paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES)
|
paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES)
|
||||||
|
ff_timeframe_s = timeframe_to_seconds(ff_timeframe)
|
||||||
|
|
||||||
funding_rate_combs = [
|
funding_rate_combs = [
|
||||||
f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe
|
f
|
||||||
|
for f in paircombs
|
||||||
|
if f[2] == CandleType.FUNDING_RATE
|
||||||
|
and f[1] != ff_timeframe
|
||||||
|
# Only allow smaller timeframes to move from smaller to larger timeframes
|
||||||
|
and timeframe_to_seconds(f[1]) < ff_timeframe_s
|
||||||
]
|
]
|
||||||
|
|
||||||
if funding_rate_combs:
|
if funding_rate_combs:
|
||||||
|
|||||||
@@ -308,11 +308,15 @@ def _download_pair_history(
|
|||||||
candle_type=candle_type,
|
candle_type=candle_type,
|
||||||
until_ms=until_ms if until_ms else None,
|
until_ms=until_ms if until_ms else None,
|
||||||
)
|
)
|
||||||
logger.info(f"Downloaded data for {pair} with length {len(new_dataframe)}.")
|
logger.info(
|
||||||
|
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
|
||||||
|
f"{len(new_dataframe)}."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
new_dataframe = pair_candles
|
new_dataframe = pair_candles
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Downloaded data for {pair} with length {len(new_dataframe)}. Parallel Method."
|
f"Downloaded data for {pair}, {timeframe}, {candle_type} with length "
|
||||||
|
f"{len(new_dataframe)}. Parallel Method."
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.empty:
|
if data.empty:
|
||||||
@@ -349,6 +353,7 @@ def _download_pair_history(
|
|||||||
|
|
||||||
def refresh_backtest_ohlcv_data(
|
def refresh_backtest_ohlcv_data(
|
||||||
exchange: Exchange,
|
exchange: Exchange,
|
||||||
|
*,
|
||||||
pairs: list[str],
|
pairs: list[str],
|
||||||
timeframes: list[str],
|
timeframes: list[str],
|
||||||
datadir: Path,
|
datadir: Path,
|
||||||
@@ -359,6 +364,7 @@ def refresh_backtest_ohlcv_data(
|
|||||||
data_format: str | None = None,
|
data_format: str | None = None,
|
||||||
prepend: bool = False,
|
prepend: bool = False,
|
||||||
progress_tracker: CustomProgress | None = None,
|
progress_tracker: CustomProgress | None = None,
|
||||||
|
candle_types: list[CandleType] | None = None,
|
||||||
no_parallel_download: bool = False,
|
no_parallel_download: bool = False,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
@@ -371,10 +377,44 @@ def refresh_backtest_ohlcv_data(
|
|||||||
pairs_not_available = []
|
pairs_not_available = []
|
||||||
fast_candles: dict[PairWithTimeframe, DataFrame] = {}
|
fast_candles: dict[PairWithTimeframe, DataFrame] = {}
|
||||||
data_handler = get_datahandler(datadir, data_format)
|
data_handler = get_datahandler(datadir, data_format)
|
||||||
candle_type = CandleType.get_default(trading_mode)
|
def_candletype = CandleType.SPOT if trading_mode != "futures" else CandleType.FUTURES
|
||||||
|
if trading_mode != "futures":
|
||||||
|
# Ignore user passed candle types for non-futures trading
|
||||||
|
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
|
||||||
|
else:
|
||||||
|
# Filter out SPOT candle type for futures trading
|
||||||
|
candle_types = (
|
||||||
|
[ct for ct in candle_types if ct != CandleType.SPOT] if candle_types else None
|
||||||
|
)
|
||||||
|
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
|
||||||
|
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
|
||||||
|
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
|
||||||
|
|
||||||
|
if candle_types:
|
||||||
|
for ct in candle_types:
|
||||||
|
exchange.verify_candle_type_support(ct)
|
||||||
|
timeframes_with_candletype = [
|
||||||
|
(tf, ct)
|
||||||
|
for ct in candle_types
|
||||||
|
for tf in timeframes
|
||||||
|
if ct != CandleType.FUNDING_RATE
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# Default behavior
|
||||||
|
timeframes_with_candletype = [(tf, def_candletype) for tf in timeframes]
|
||||||
|
timeframes_with_candletype.append((tf_mark, fr_candle_type))
|
||||||
|
if not candle_types or CandleType.FUNDING_RATE in candle_types:
|
||||||
|
# All exchanges need FundingRate for futures trading.
|
||||||
|
# The timeframe is aligned to the mark-price timeframe.
|
||||||
|
timeframes_with_candletype.append((tf_funding_rate, CandleType.FUNDING_RATE))
|
||||||
|
# Deduplicate list ...
|
||||||
|
timeframes_with_candletype = list(dict.fromkeys(timeframes_with_candletype))
|
||||||
|
logger.debug(
|
||||||
|
"Downloading %s.", ", ".join(f'"{tf} {ct}"' for tf, ct in timeframes_with_candletype)
|
||||||
|
)
|
||||||
|
|
||||||
with progress_tracker as progress:
|
with progress_tracker as progress:
|
||||||
tf_length = len(timeframes) if trading_mode != "futures" else len(timeframes) + 2
|
timeframe_task = progress.add_task("Timeframe", total=len(timeframes_with_candletype))
|
||||||
timeframe_task = progress.add_task("Timeframe", total=tf_length)
|
|
||||||
pair_task = progress.add_task("Downloading data...", total=len(pairs))
|
pair_task = progress.add_task("Downloading data...", total=len(pairs))
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
@@ -385,7 +425,7 @@ def refresh_backtest_ohlcv_data(
|
|||||||
pairs_not_available.append(f"{pair}: Pair not available on exchange.")
|
pairs_not_available.append(f"{pair}: Pair not available on exchange.")
|
||||||
logger.info(f"Skipping pair {pair}...")
|
logger.info(f"Skipping pair {pair}...")
|
||||||
continue
|
continue
|
||||||
for timeframe in timeframes:
|
for timeframe, candle_type in timeframes_with_candletype:
|
||||||
# Get fast candles via parallel method on first loop through per timeframe
|
# Get fast candles via parallel method on first loop through per timeframe
|
||||||
# and candle type. Downloads all the pairs in the list and stores them.
|
# and candle type. Downloads all the pairs in the list and stores them.
|
||||||
# Also skips if only 1 pair/timeframe combination is scheduled for download.
|
# Also skips if only 1 pair/timeframe combination is scheduled for download.
|
||||||
@@ -412,7 +452,7 @@ def refresh_backtest_ohlcv_data(
|
|||||||
# get the already downloaded pair candles if they exist
|
# get the already downloaded pair candles if they exist
|
||||||
pair_candles = fast_candles.pop((pair, timeframe, candle_type), None)
|
pair_candles = fast_candles.pop((pair, timeframe, candle_type), None)
|
||||||
|
|
||||||
progress.update(timeframe_task, description=f"Timeframe {timeframe}")
|
progress.update(timeframe_task, description=f"Timeframe {timeframe} {candle_type}")
|
||||||
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
|
logger.debug(f"Downloading pair {pair}, {candle_type}, interval {timeframe}.")
|
||||||
_download_pair_history(
|
_download_pair_history(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
@@ -428,33 +468,6 @@ def refresh_backtest_ohlcv_data(
|
|||||||
pair_candles=pair_candles, # optional pass of dataframe of parallel candles
|
pair_candles=pair_candles, # optional pass of dataframe of parallel candles
|
||||||
)
|
)
|
||||||
progress.update(timeframe_task, advance=1)
|
progress.update(timeframe_task, advance=1)
|
||||||
if trading_mode == "futures":
|
|
||||||
# Predefined candletype (and timeframe) depending on exchange
|
|
||||||
# Downloads what is necessary to backtest based on futures data.
|
|
||||||
tf_mark = exchange.get_option("mark_ohlcv_timeframe")
|
|
||||||
tf_funding_rate = exchange.get_option("funding_fee_timeframe")
|
|
||||||
|
|
||||||
fr_candle_type = CandleType.from_string(exchange.get_option("mark_ohlcv_price"))
|
|
||||||
# All exchanges need FundingRate for futures trading.
|
|
||||||
# The timeframe is aligned to the mark-price timeframe.
|
|
||||||
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
|
|
||||||
for candle_type_f, tf in combs:
|
|
||||||
logger.debug(f"Downloading pair {pair}, {candle_type_f}, interval {tf}.")
|
|
||||||
_download_pair_history(
|
|
||||||
pair=pair,
|
|
||||||
datadir=datadir,
|
|
||||||
exchange=exchange,
|
|
||||||
timerange=timerange,
|
|
||||||
data_handler=data_handler,
|
|
||||||
timeframe=str(tf),
|
|
||||||
new_pairs_days=new_pairs_days,
|
|
||||||
candle_type=candle_type_f,
|
|
||||||
erase=erase,
|
|
||||||
prepend=prepend,
|
|
||||||
)
|
|
||||||
progress.update(
|
|
||||||
timeframe_task, advance=1, description=f"Timeframe {candle_type_f}, {tf}"
|
|
||||||
)
|
|
||||||
|
|
||||||
progress.update(pair_task, advance=1)
|
progress.update(pair_task, advance=1)
|
||||||
progress.update(timeframe_task, description="Timeframe")
|
progress.update(timeframe_task, description="Timeframe")
|
||||||
@@ -800,6 +813,7 @@ def download_data(
|
|||||||
trading_mode=config.get("trading_mode", "spot"),
|
trading_mode=config.get("trading_mode", "spot"),
|
||||||
prepend=config.get("prepend_data", False),
|
prepend=config.get("prepend_data", False),
|
||||||
progress_tracker=progress_tracker,
|
progress_tracker=progress_tracker,
|
||||||
|
candle_types=config.get("candle_types"),
|
||||||
no_parallel_download=config.get("no_parallel_download", False),
|
no_parallel_download=config.get("no_parallel_download", False),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ def combined_dataframes_with_rel_mean(
|
|||||||
df_comb = combine_dataframes_by_column(data, column)
|
df_comb = combine_dataframes_by_column(data, column)
|
||||||
# Trim dataframes to the given timeframe
|
# Trim dataframes to the given timeframe
|
||||||
df_comb = df_comb.iloc[(df_comb.index >= fromdt) & (df_comb.index < todt)]
|
df_comb = df_comb.iloc[(df_comb.index >= fromdt) & (df_comb.index < todt)]
|
||||||
|
rel_mean = df_comb.pct_change().mean(axis=1).fillna(0).cumsum()
|
||||||
df_comb["count"] = df_comb.count(axis=1)
|
df_comb["count"] = df_comb.count(axis=1)
|
||||||
df_comb["mean"] = df_comb.mean(axis=1)
|
df_comb["mean"] = df_comb.mean(axis=1)
|
||||||
df_comb["rel_mean"] = df_comb["mean"].pct_change().fillna(0).cumsum()
|
df_comb["rel_mean"] = rel_mean
|
||||||
return df_comb[["mean", "rel_mean", "count"]]
|
return df_comb[["mean", "rel_mean", "count"]]
|
||||||
|
|
||||||
|
|
||||||
@@ -333,7 +334,10 @@ def calculate_expectancy(trades: pd.DataFrame) -> tuple[float, float]:
|
|||||||
|
|
||||||
|
|
||||||
def calculate_sortino(
|
def calculate_sortino(
|
||||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
trades: pd.DataFrame,
|
||||||
|
min_date: datetime | None,
|
||||||
|
max_date: datetime | None,
|
||||||
|
starting_balance: float,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate sortino
|
Calculate sortino
|
||||||
@@ -361,7 +365,10 @@ def calculate_sortino(
|
|||||||
|
|
||||||
|
|
||||||
def calculate_sharpe(
|
def calculate_sharpe(
|
||||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
trades: pd.DataFrame,
|
||||||
|
min_date: datetime | None,
|
||||||
|
max_date: datetime | None,
|
||||||
|
starting_balance: float,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate sharpe
|
Calculate sharpe
|
||||||
@@ -388,7 +395,10 @@ def calculate_sharpe(
|
|||||||
|
|
||||||
|
|
||||||
def calculate_calmar(
|
def calculate_calmar(
|
||||||
trades: pd.DataFrame, min_date: datetime, max_date: datetime, starting_balance: float
|
trades: pd.DataFrame,
|
||||||
|
min_date: datetime | None,
|
||||||
|
max_date: datetime | None,
|
||||||
|
starting_balance: float,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate calmar
|
Calculate calmar
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class CandleType(str, Enum):
|
class CandleType(StrEnum):
|
||||||
"""Enum to distinguish candle types"""
|
"""Enum to distinguish candle types"""
|
||||||
|
|
||||||
SPOT = "spot"
|
SPOT = "spot"
|
||||||
@@ -14,9 +14,6 @@ class CandleType(str, Enum):
|
|||||||
FUNDING_RATE = "funding_rate"
|
FUNDING_RATE = "funding_rate"
|
||||||
# BORROW_RATE = "borrow_rate" # * unimplemented
|
# BORROW_RATE = "borrow_rate" # * unimplemented
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(value: str) -> "CandleType":
|
def from_string(value: str) -> "CandleType":
|
||||||
if not value:
|
if not value:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class MarginMode(str, Enum):
|
class MarginMode(StrEnum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between
|
Enum to distinguish between
|
||||||
cross margin/futures margin_mode and
|
cross margin/futures margin_mode and
|
||||||
@@ -11,6 +11,3 @@ class MarginMode(str, Enum):
|
|||||||
CROSS = "cross"
|
CROSS = "cross"
|
||||||
ISOLATED = "isolated"
|
ISOLATED = "isolated"
|
||||||
NONE = ""
|
NONE = ""
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.value.lower()}"
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class OrderTypeValues(str, Enum):
|
class OrderTypeValues(StrEnum):
|
||||||
limit = "limit"
|
limit = "limit"
|
||||||
market = "market"
|
market = "market"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class PriceType(str, Enum):
|
class PriceType(StrEnum):
|
||||||
"""Enum to distinguish possible trigger prices for stoplosses"""
|
"""Enum to distinguish possible trigger prices for stoplosses"""
|
||||||
|
|
||||||
LAST = "last"
|
LAST = "last"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class RPCMessageType(str, Enum):
|
class RPCMessageType(StrEnum):
|
||||||
STATUS = "status"
|
STATUS = "status"
|
||||||
WARNING = "warning"
|
WARNING = "warning"
|
||||||
EXCEPTION = "exception"
|
EXCEPTION = "exception"
|
||||||
@@ -25,21 +25,16 @@ class RPCMessageType(str, Enum):
|
|||||||
NEW_CANDLE = "new_candle"
|
NEW_CANDLE = "new_candle"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.value
|
# TODO: do we still need to overwrite __repr__? Impact needs to be looked at in detail
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
# Enum for parsing requests from ws consumers
|
# Enum for parsing requests from ws consumers
|
||||||
class RPCRequestType(str, Enum):
|
class RPCRequestType(StrEnum):
|
||||||
SUBSCRIBE = "subscribe"
|
SUBSCRIBE = "subscribe"
|
||||||
|
|
||||||
WHITELIST = "whitelist"
|
WHITELIST = "whitelist"
|
||||||
ANALYZED_DF = "analyzed_df"
|
ANALYZED_DF = "analyzed_df"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class RunMode(str, Enum):
|
class RunMode(StrEnum):
|
||||||
"""
|
"""
|
||||||
Bot running mode (backtest, hyperopt, ...)
|
Bot running mode (backtest, hyperopt, ...)
|
||||||
can be "live", "dry-run", "backtest", "hyperopt".
|
can be "live", "dry-run", "backtest", "hyperopt".
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class SignalType(Enum):
|
class SignalType(StrEnum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between enter and exit signals
|
Enum to distinguish between enter and exit signals
|
||||||
"""
|
"""
|
||||||
@@ -11,11 +11,8 @@ class SignalType(Enum):
|
|||||||
ENTER_SHORT = "enter_short"
|
ENTER_SHORT = "enter_short"
|
||||||
EXIT_SHORT = "exit_short"
|
EXIT_SHORT = "exit_short"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|
||||||
|
class SignalTagType(StrEnum):
|
||||||
class SignalTagType(Enum):
|
|
||||||
"""
|
"""
|
||||||
Enum for signal columns
|
Enum for signal columns
|
||||||
"""
|
"""
|
||||||
@@ -23,13 +20,7 @@ class SignalTagType(Enum):
|
|||||||
ENTER_TAG = "enter_tag"
|
ENTER_TAG = "enter_tag"
|
||||||
EXIT_TAG = "exit_tag"
|
EXIT_TAG = "exit_tag"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|
||||||
|
class SignalDirection(StrEnum):
|
||||||
class SignalDirection(str, Enum):
|
|
||||||
LONG = "long"
|
LONG = "long"
|
||||||
SHORT = "short"
|
SHORT = "short"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from enum import Enum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class TradingMode(str, Enum):
|
class TradingMode(StrEnum):
|
||||||
"""
|
"""
|
||||||
Enum to distinguish between
|
Enum to distinguish between
|
||||||
spot, margin, futures or any other trading method
|
spot, margin, futures or any other trading method
|
||||||
@@ -10,6 +10,3 @@ class TradingMode(str, Enum):
|
|||||||
SPOT = "spot"
|
SPOT = "spot"
|
||||||
MARGIN = "margin"
|
MARGIN = "margin"
|
||||||
FUTURES = "futures"
|
FUTURES = "futures"
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name.lower()}"
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
|||||||
from freqtrade.exchange.exchange import Exchange
|
from freqtrade.exchange.exchange import Exchange
|
||||||
|
|
||||||
# isort: on
|
# isort: on
|
||||||
from freqtrade.exchange.binance import Binance
|
from freqtrade.exchange.binance import Binance, Binanceus, Binanceusdm
|
||||||
from freqtrade.exchange.bingx import Bingx
|
from freqtrade.exchange.bingx import Bingx
|
||||||
from freqtrade.exchange.bitget import Bitget
|
from freqtrade.exchange.bitget import Bitget
|
||||||
from freqtrade.exchange.bitmart import Bitmart
|
from freqtrade.exchange.bitmart import Bitmart
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from freqtrade.exchange.binance_public_data import (
|
|||||||
download_archive_trades,
|
download_archive_trades,
|
||||||
)
|
)
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas, Tickers
|
from freqtrade.exchange.exchange_types import FtHas, Tickers
|
||||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
|
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs
|
||||||
from freqtrade.misc import deep_merge_dicts, json_load
|
from freqtrade.misc import deep_merge_dicts, json_load
|
||||||
from freqtrade.util import FtTTLCache
|
from freqtrade.util import FtTTLCache
|
||||||
@@ -48,9 +48,12 @@ class Binance(Exchange):
|
|||||||
"has_delisting": True,
|
"has_delisting": True,
|
||||||
}
|
}
|
||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
|
"ohlcv_candle_limit": 499,
|
||||||
"funding_fee_candle_limit": 1000,
|
"funding_fee_candle_limit": 1000,
|
||||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
|
"stoploss_algo_order_info_id": "actualOrderId",
|
||||||
"tickers_have_price": False,
|
"tickers_have_price": False,
|
||||||
"floor_leverage": True,
|
"floor_leverage": True,
|
||||||
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
"fetch_orders_limit_minutes": 7 * 1440, # "fetch_orders" is limited to 7 days
|
||||||
@@ -66,6 +69,7 @@ class Binance(Exchange):
|
|||||||
"BFUSD": "USDT",
|
"BFUSD": "USDT",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
_can_use_data_download_fast = True
|
||||||
|
|
||||||
_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),
|
||||||
@@ -145,20 +149,6 @@ class Binance(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def fetch_stoploss_order(
|
|
||||||
self, order_id: str, pair: str, params: dict | None = None
|
|
||||||
) -> CcxtOrder:
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
params = params or {}
|
|
||||||
params.update({"stop": True})
|
|
||||||
return self.fetch_order(order_id, pair, params)
|
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
params = params or {}
|
|
||||||
params.update({"stop": True})
|
|
||||||
return self.cancel_order(order_id=order_id, pair=pair, params=params)
|
|
||||||
|
|
||||||
def get_historic_ohlcv(
|
def get_historic_ohlcv(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@@ -193,7 +183,8 @@ class Binance(Exchange):
|
|||||||
return DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
return DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self._config["exchange"].get("only_from_ccxt", False)
|
not self._can_use_data_download_fast
|
||||||
|
or self._config["exchange"].get("only_from_ccxt", False)
|
||||||
or
|
or
|
||||||
# only download timeframes with significant improvements,
|
# only download timeframes with significant improvements,
|
||||||
# otherwise fall back to rest API
|
# otherwise fall back to rest API
|
||||||
@@ -417,7 +408,10 @@ class Binance(Exchange):
|
|||||||
) -> tuple[str, list[list]]:
|
) -> tuple[str, list[list]]:
|
||||||
logger.info(f"Fetching trades for {pair} from Binance, {from_id=}, {since=}, {until=}")
|
logger.info(f"Fetching trades for {pair} from Binance, {from_id=}, {since=}, {until=}")
|
||||||
|
|
||||||
if not self._config["exchange"].get("only_from_ccxt", False):
|
if (
|
||||||
|
not self._config["exchange"].get("only_from_ccxt", False)
|
||||||
|
and self._can_use_data_download_fast
|
||||||
|
):
|
||||||
if from_id is None or not since:
|
if from_id is None or not since:
|
||||||
trades = await self._api_async.fetch_trades(
|
trades = await self._api_async.fetch_trades(
|
||||||
pair,
|
pair,
|
||||||
@@ -558,3 +552,28 @@ class Binance(Exchange):
|
|||||||
cache[ft_symbol] = delist_dt
|
cache[ft_symbol] = delist_dt
|
||||||
|
|
||||||
return cache.get(pair, None)
|
return cache.get(pair, None)
|
||||||
|
|
||||||
|
|
||||||
|
class Binanceusdm(Binance):
|
||||||
|
"""Binacne USDM Exchange
|
||||||
|
Same as Binance - only futures trading is supported (via ccxt).
|
||||||
|
|
||||||
|
Not actually necessary, binance should be preferred.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
|
(TradingMode.FUTURES, MarginMode.CROSS),
|
||||||
|
(TradingMode.FUTURES, MarginMode.ISOLATED),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Binanceus(Binance):
|
||||||
|
"""Binance US exchange class.
|
||||||
|
Minimal adjustment to disable futures trading for the US subsidiary of Binance
|
||||||
|
"""
|
||||||
|
|
||||||
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
|
(TradingMode.SPOT, MarginMode.NONE),
|
||||||
|
]
|
||||||
|
# binance vision does not have data for binanceus
|
||||||
|
_can_use_data_download_fast = False
|
||||||
|
|||||||
+41074
-38200
File diff suppressed because it is too large
Load Diff
@@ -31,11 +31,11 @@ class Bitget(Exchange):
|
|||||||
"stop_price_prop": "stopPrice",
|
"stop_price_prop": "stopPrice",
|
||||||
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
"stoploss_blocks_assets": False, # Stoploss orders do not block assets
|
||||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
"ohlcv_candle_limit": 200, # 200 for historical candles, 1000 for recent ones.
|
||||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
}
|
}
|
||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
"mark_ohlcv_timeframe": "4h",
|
|
||||||
"funding_fee_candle_limit": 100,
|
"funding_fee_candle_limit": 100,
|
||||||
"has_delisting": True,
|
"has_delisting": True,
|
||||||
}
|
}
|
||||||
@@ -129,9 +129,6 @@ 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:
|
|
||||||
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def additional_exchange_init(self) -> None:
|
def additional_exchange_init(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ class Bybit(Exchange):
|
|||||||
}
|
}
|
||||||
_ft_has_futures: FtHas = {
|
_ft_has_futures: FtHas = {
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
"mark_ohlcv_timeframe": "4h",
|
|
||||||
"funding_fee_timeframe": "8h",
|
|
||||||
"funding_fee_candle_limit": 200,
|
"funding_fee_candle_limit": 200,
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
|
|||||||
@@ -47,14 +47,16 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool:
|
|||||||
f"{', '.join(available_exchanges())}"
|
f"{', '.join(available_exchanges())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
valid, reason, _ = validate_exchange(exchange)
|
valid, reason, _, _ = validate_exchange(exchange)
|
||||||
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(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}')
|
logger.warning(
|
||||||
|
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(
|
||||||
|
|||||||
@@ -36,17 +36,16 @@ API_RETRY_COUNT = 4
|
|||||||
API_FETCH_ORDER_RETRY_COUNT = 5
|
API_FETCH_ORDER_RETRY_COUNT = 5
|
||||||
|
|
||||||
BAD_EXCHANGES = {
|
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",
|
||||||
"kucoinfutures": "Unsupported futures exchange.",
|
"krakenfutures": "Unsupported futures exchange",
|
||||||
"poloniexfutures": "Unsupported futures exchange.",
|
"kucoinfutures": "Unsupported futures exchange",
|
||||||
"binancecoinm": "Unsupported futures exchange.",
|
"poloniexfutures": "Unsupported futures exchange",
|
||||||
|
"binancecoinm": "Unsupported futures exchange",
|
||||||
}
|
}
|
||||||
|
|
||||||
MAP_EXCHANGE_CHILDCLASS = {
|
MAP_EXCHANGE_CHILDCLASS = {
|
||||||
"binanceus": "binance",
|
|
||||||
"binanceusdm": "binance",
|
|
||||||
"okex": "okx",
|
"okex": "okx",
|
||||||
"gateio": "gate",
|
"gateio": "gate",
|
||||||
"huboi": "htx",
|
"huboi": "htx",
|
||||||
@@ -54,6 +53,8 @@ MAP_EXCHANGE_CHILDCLASS = {
|
|||||||
|
|
||||||
SUPPORTED_EXCHANGES = [
|
SUPPORTED_EXCHANGES = [
|
||||||
"binance",
|
"binance",
|
||||||
|
"binanceus",
|
||||||
|
"binanceusdm",
|
||||||
"bingx",
|
"bingx",
|
||||||
"bitmart",
|
"bitmart",
|
||||||
"bitget",
|
"bitget",
|
||||||
@@ -78,28 +79,35 @@ EXCHANGE_HAS_REQUIRED: dict[str, list[str]] = {
|
|||||||
"fetchOHLCV": [],
|
"fetchOHLCV": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
EXCHANGE_HAS_OPTIONAL = [
|
EXCHANGE_HAS_OPTIONAL: dict[str, list[str]] = {
|
||||||
# Private
|
# Private
|
||||||
"fetchMyTrades", # Trades for order - fee detection
|
"fetchMyTrades": [], # Trades for order - fee detection
|
||||||
"createLimitOrder",
|
"createLimitOrder": [],
|
||||||
"createMarketOrder", # Either OR for orders
|
"createMarketOrder": [], # Either OR for orders
|
||||||
# 'setLeverage', # Margin/Futures trading
|
|
||||||
# 'setMarginMode', # Margin/Futures trading
|
|
||||||
# 'fetchFundingHistory', # Futures trading
|
|
||||||
# Public
|
# Public
|
||||||
"fetchOrderBook",
|
"fetchOrderBook": [],
|
||||||
"fetchL2OrderBook",
|
"fetchL2OrderBook": [],
|
||||||
"fetchTicker", # OR for pricing
|
"fetchTicker": [], # OR for pricing
|
||||||
"fetchTickers", # For volumepairlist?
|
"fetchTickers": [], # For volumepairlist?
|
||||||
"fetchTrades", # Downloading trades data
|
"fetchTrades": [], # Downloading trades data
|
||||||
# 'fetchFundingRateHistory', # Futures trading
|
"fetchOrders": ["fetchOpenOrders", "fetchClosedOrders"], # , # Refinding balance...
|
||||||
# 'fetchPositions', # Futures trading
|
|
||||||
# 'fetchLeverageTiers', # Futures initialization
|
|
||||||
# 'fetchMarketLeverageTiers', # Futures initialization
|
|
||||||
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
|
|
||||||
# ccxt.pro
|
# ccxt.pro
|
||||||
"watchOHLCV",
|
"watchOHLCV": [],
|
||||||
]
|
}
|
||||||
|
|
||||||
|
EXCHANGE_HAS_OPTIONAL_FUTURES: dict[str, list[str]] = {
|
||||||
|
# private
|
||||||
|
"setLeverage": [], # Margin/Futures trading
|
||||||
|
"setMarginMode": [], # Margin/Futures trading
|
||||||
|
"fetchFundingHistory": [], # Futures trading
|
||||||
|
# Public
|
||||||
|
"fetchFundingRateHistory": [], # Futures trading
|
||||||
|
"fetchPositions": [], # Futures trading
|
||||||
|
"fetchLeverageTiers": ["fetchMarketLeverageTiers"], # Futures initialization
|
||||||
|
"fetchMarkOHLCV": [],
|
||||||
|
"fetchIndexOHLCV": [], # Futures additional data
|
||||||
|
"fetchPremiumIndexOHLCV": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def calculate_backoff(retrycount, max_retries):
|
def calculate_backoff(retrycount, max_retries):
|
||||||
|
|||||||
+233
-75
@@ -73,6 +73,7 @@ from freqtrade.exchange.exchange_types import (
|
|||||||
CcxtPosition,
|
CcxtPosition,
|
||||||
FtHas,
|
FtHas,
|
||||||
FundingRate,
|
FundingRate,
|
||||||
|
LeverageTier,
|
||||||
OHLCVResponse,
|
OHLCVResponse,
|
||||||
OrderBook,
|
OrderBook,
|
||||||
Ticker,
|
Ticker,
|
||||||
@@ -104,7 +105,8 @@ from freqtrade.misc import (
|
|||||||
deep_merge_dicts,
|
deep_merge_dicts,
|
||||||
file_dump_json,
|
file_dump_json,
|
||||||
file_load_json,
|
file_load_json,
|
||||||
safe_value_fallback2,
|
safe_value_fallback,
|
||||||
|
safe_value_nested,
|
||||||
)
|
)
|
||||||
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
|
from freqtrade.util import FtTTLCache, PeriodicCache, dt_from_ts, dt_now
|
||||||
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
|
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
|
||||||
@@ -131,6 +133,7 @@ class Exchange:
|
|||||||
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
||||||
"stoploss_order_types": {},
|
"stoploss_order_types": {},
|
||||||
"stoploss_blocks_assets": True, # By default stoploss orders block assets
|
"stoploss_blocks_assets": True, # By default stoploss orders block assets
|
||||||
|
"stoploss_query_requires_stop_flag": False, # Require "stop": True" to fetch stop orders
|
||||||
"order_time_in_force": ["GTC"],
|
"order_time_in_force": ["GTC"],
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||||
@@ -152,8 +155,8 @@ class Exchange:
|
|||||||
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
||||||
"l2_limit_upper": None, # Upper limit for L2 limit
|
"l2_limit_upper": None, # Upper limit for L2 limit
|
||||||
"mark_ohlcv_price": "mark",
|
"mark_ohlcv_price": "mark",
|
||||||
"mark_ohlcv_timeframe": "8h",
|
"mark_ohlcv_timeframe": "1h",
|
||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "1h",
|
||||||
"ccxt_futures_name": "swap",
|
"ccxt_futures_name": "swap",
|
||||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||||
"order_props_in_contracts": ["amount", "filled", "remaining"],
|
"order_props_in_contracts": ["amount", "filled", "remaining"],
|
||||||
@@ -193,29 +196,30 @@ class Exchange:
|
|||||||
self._exchange_ws: ExchangeWS | None = None
|
self._exchange_ws: ExchangeWS | None = None
|
||||||
self._markets: dict = {}
|
self._markets: dict = {}
|
||||||
self._trading_fees: dict[str, Any] = {}
|
self._trading_fees: dict[str, Any] = {}
|
||||||
self._leverage_tiers: dict[str, list[dict]] = {}
|
self._leverage_tiers: dict[str, list[LeverageTier]] = {}
|
||||||
# Lock event loop. This is necessary to avoid race-conditions when using force* commands
|
# Lock event loop. This is necessary to avoid race-conditions when using force* commands
|
||||||
# Due to funding fee fetching.
|
# Due to funding fee fetching.
|
||||||
self._loop_lock = Lock()
|
self._loop_lock = Lock()
|
||||||
self.loop = self._init_async_loop()
|
self.loop = self._init_async_loop()
|
||||||
self._config: Config = {}
|
self._config: Config = config
|
||||||
|
|
||||||
# Leverage properties
|
# Leverage properties
|
||||||
self.trading_mode: TradingMode = TradingMode(
|
self.trading_mode: TradingMode = TradingMode(
|
||||||
config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
|
self._config.get("trading_mode", self._supported_trading_mode_margin_pairs[0][0])
|
||||||
)
|
)
|
||||||
self.margin_mode: MarginMode = MarginMode(
|
self.margin_mode: MarginMode = MarginMode(
|
||||||
MarginMode(config.get("margin_mode"))
|
self._config["margin_mode"]
|
||||||
if config.get("margin_mode")
|
if self._config.get("margin_mode")
|
||||||
else self._supported_trading_mode_margin_pairs[0][1]
|
else self._supported_trading_mode_margin_pairs[0][1]
|
||||||
)
|
)
|
||||||
config["trading_mode"] = self.trading_mode
|
self._config["trading_mode"] = self.trading_mode
|
||||||
config["margin_mode"] = self.margin_mode
|
self._config["margin_mode"] = self.margin_mode
|
||||||
config["candle_type_def"] = CandleType.get_default(self.trading_mode)
|
self._config["candle_type_def"] = CandleType.get_default(self.trading_mode)
|
||||||
self._config.update(config)
|
self.liquidation_buffer = self._config.get("liquidation_buffer", 0.05)
|
||||||
self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
|
|
||||||
|
|
||||||
exchange_conf: ExchangeConfig = exchange_config if exchange_config else config["exchange"]
|
exchange_conf: ExchangeConfig = (
|
||||||
|
exchange_config if exchange_config else self._config["exchange"]
|
||||||
|
)
|
||||||
|
|
||||||
# Deep merge ft_has with default ft_has options
|
# Deep merge ft_has with default ft_has options
|
||||||
# Must be called before ft_has is used.
|
# Must be called before ft_has is used.
|
||||||
@@ -246,14 +250,14 @@ 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] = {}
|
||||||
|
|
||||||
if 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__}")
|
||||||
|
|
||||||
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
|
# Don't remove exchange credentials for dry-run or if always_require_api_keys is set
|
||||||
remove_exchange_credentials(
|
remove_exchange_credentials(
|
||||||
exchange_conf,
|
exchange_conf,
|
||||||
not self._ft_has["always_require_api_keys"] and config.get("dry_run", False),
|
not self._ft_has["always_require_api_keys"] and self._config.get("dry_run", False),
|
||||||
)
|
)
|
||||||
self.log_responses = exchange_conf.get("log_responses", False)
|
self.log_responses = exchange_conf.get("log_responses", False)
|
||||||
|
|
||||||
@@ -294,7 +298,7 @@ class Exchange:
|
|||||||
if validate:
|
if validate:
|
||||||
# Initial markets load
|
# Initial markets load
|
||||||
self.reload_markets(True, load_leverage_tiers=False)
|
self.reload_markets(True, load_leverage_tiers=False)
|
||||||
self.validate_config(config)
|
self.validate_config(self._config)
|
||||||
|
|
||||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||||
self.fill_leverage_tiers()
|
self.fill_leverage_tiers()
|
||||||
@@ -310,10 +314,19 @@ class Exchange:
|
|||||||
if self._exchange_ws:
|
if self._exchange_ws:
|
||||||
self._exchange_ws.cleanup()
|
self._exchange_ws.cleanup()
|
||||||
logger.debug("Exchange object destroyed, closing async loop")
|
logger.debug("Exchange object destroyed, closing async loop")
|
||||||
|
try:
|
||||||
|
generic_loop = asyncio.get_running_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
generic_loop = None
|
||||||
|
loop_running = (getattr(self, "loop", None) and self.loop.is_running()) or (
|
||||||
|
generic_loop is not None and generic_loop.is_running()
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
getattr(self, "_api_async", None)
|
getattr(self, "_api_async", None)
|
||||||
and inspect.iscoroutinefunction(self._api_async.close)
|
and inspect.iscoroutinefunction(self._api_async.close)
|
||||||
and self._api_async.session
|
and self._api_async.session
|
||||||
|
and not loop_running
|
||||||
):
|
):
|
||||||
logger.debug("Closing async ccxt session.")
|
logger.debug("Closing async ccxt session.")
|
||||||
self.loop.run_until_complete(self._api_async.close())
|
self.loop.run_until_complete(self._api_async.close())
|
||||||
@@ -321,6 +334,7 @@ class Exchange:
|
|||||||
self._ws_async
|
self._ws_async
|
||||||
and inspect.iscoroutinefunction(self._ws_async.close)
|
and inspect.iscoroutinefunction(self._ws_async.close)
|
||||||
and self._ws_async.session
|
and self._ws_async.session
|
||||||
|
and not loop_running
|
||||||
):
|
):
|
||||||
logger.debug("Closing ws ccxt session.")
|
logger.debug("Closing ws ccxt session.")
|
||||||
self.loop.run_until_complete(self._ws_async.close())
|
self.loop.run_until_complete(self._ws_async.close())
|
||||||
@@ -478,7 +492,7 @@ class Exchange:
|
|||||||
def _log_exchange_response(self, endpoint: str, response, *, add_info=None) -> None:
|
def _log_exchange_response(self, endpoint: str, response, *, add_info=None) -> None:
|
||||||
"""Log exchange responses"""
|
"""Log exchange responses"""
|
||||||
if self.log_responses:
|
if self.log_responses:
|
||||||
add_info_str = "" if add_info is None else f" {add_info}: "
|
add_info_str = "" if add_info is None else f"{add_info}: "
|
||||||
logger.info(f"API {endpoint}: {add_info_str}{response}")
|
logger.info(f"API {endpoint}: {add_info_str}{response}")
|
||||||
|
|
||||||
def ohlcv_candle_limit(
|
def ohlcv_candle_limit(
|
||||||
@@ -707,7 +721,7 @@ class Exchange:
|
|||||||
self._markets = self._api_async.markets
|
self._markets = self._api_async.markets
|
||||||
self._api.set_markets_from_exchange(self._api_async)
|
self._api.set_markets_from_exchange(self._api_async)
|
||||||
# Assign options array, as it contains some temporary information from the exchange.
|
# Assign options array, as it contains some temporary information from the exchange.
|
||||||
# TODO: investigate with ccxt if it's safe to remove `.options`
|
# ccxt does not implicitly copy options over in set_markets_from_exchange
|
||||||
self._api.options = self._api_async.options
|
self._api.options = self._api_async.options
|
||||||
if self._exchange_ws:
|
if self._exchange_ws:
|
||||||
# Set markets to avoid reloading on websocket api
|
# Set markets to avoid reloading on websocket api
|
||||||
@@ -877,19 +891,20 @@ class Exchange:
|
|||||||
# Only allow 5 calls per pair to somewhat limit the impact
|
# Only allow 5 calls per pair to somewhat limit the impact
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f"This strategy requires {startup_candles} candles to start, "
|
f"This strategy requires {startup_candles} candles to start, "
|
||||||
"which is more than 5x "
|
f"which is more than 5x ({candle_limit * 5 - 1} candles) "
|
||||||
f"the amount of candles {self.name} provides for {timeframe}."
|
f"the amount of candles {self.name} provides for {timeframe}."
|
||||||
)
|
)
|
||||||
elif required_candle_call_count > 1:
|
elif required_candle_call_count > 1:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
f"This strategy requires {startup_candles} candles to start, which is more than "
|
f"This strategy requires {startup_candles} candles to start, "
|
||||||
|
f"which is more than ({candle_limit - 1} candles) "
|
||||||
f"the amount of candles {self.name} provides for {timeframe}."
|
f"the amount of candles {self.name} provides for {timeframe}."
|
||||||
)
|
)
|
||||||
if required_candle_call_count > 1:
|
if required_candle_call_count > 1:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Using {required_candle_call_count} calls to get OHLCV. "
|
f"Using {required_candle_call_count} calls to get OHLCV. "
|
||||||
f"This can result in slower operations for the bot. Please check "
|
f"This can result in slower operations for the bot. Please check "
|
||||||
f"if you really need {startup_candles} candles for your strategy"
|
f"if you really need {startup_candles} candles for your strategy."
|
||||||
)
|
)
|
||||||
return required_candle_call_count
|
return required_candle_call_count
|
||||||
|
|
||||||
@@ -978,12 +993,12 @@ class Exchange:
|
|||||||
swap.linear.fetchOHLCV.limit
|
swap.linear.fetchOHLCV.limit
|
||||||
"""
|
"""
|
||||||
feat = (
|
feat = (
|
||||||
self._api_async.features.get("spot", {})
|
safe_value_nested(self._api_async.features, "spot", {})
|
||||||
if market_type == "spot"
|
if market_type == "spot"
|
||||||
else self._api_async.features.get("swap", {}).get("linear", {})
|
else safe_value_nested(self._api_async.features, "swap.linear", {})
|
||||||
)
|
)
|
||||||
|
|
||||||
return feat.get(endpoint, {}).get(attribute, default)
|
return safe_value_nested(feat, f"{endpoint}.{attribute}", default)
|
||||||
|
|
||||||
def get_precision_amount(self, pair: str) -> float | None:
|
def get_precision_amount(self, pair: str) -> float | None:
|
||||||
"""
|
"""
|
||||||
@@ -1119,6 +1134,7 @@ class Exchange:
|
|||||||
leverage: float,
|
leverage: float,
|
||||||
params: dict | None = None,
|
params: dict | None = None,
|
||||||
stop_loss: bool = False,
|
stop_loss: bool = False,
|
||||||
|
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}_{now.timestamp()}"
|
||||||
@@ -1145,13 +1161,13 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
if stop_loss:
|
if stop_loss:
|
||||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||||
dry_order[self._ft_has["stop_price_prop"]] = dry_order["price"]
|
dry_order[self._ft_has["stop_price_prop"]] = stop_price or dry_order["price"]
|
||||||
# Workaround to avoid filling stoploss orders immediately
|
# Workaround to avoid filling stoploss orders immediately
|
||||||
dry_order["ft_order_type"] = "stoploss"
|
dry_order["ft_order_type"] = "stoploss"
|
||||||
orderbook: OrderBook | None = None
|
orderbook: OrderBook | None = None
|
||||||
if self.exchange_has("fetchL2OrderBook"):
|
if self.exchange_has("fetchL2OrderBook"):
|
||||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
if ordertype == "limit" and orderbook:
|
if not stop_loss and ordertype == "limit" and orderbook:
|
||||||
# Allow a 1% price difference
|
# Allow a 1% price difference
|
||||||
allowed_diff = 0.01
|
allowed_diff = 0.01
|
||||||
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
|
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
|
||||||
@@ -1163,7 +1179,11 @@ class Exchange:
|
|||||||
|
|
||||||
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||||
# Update market order pricing
|
# Update market order pricing
|
||||||
average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
|
slippage = 0.05
|
||||||
|
worst_rate = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
|
||||||
|
average = self.get_dry_market_fill_price(
|
||||||
|
pair, side, amount, rate, worst_rate, orderbook
|
||||||
|
)
|
||||||
dry_order.update(
|
dry_order.update(
|
||||||
{
|
{
|
||||||
"average": average,
|
"average": average,
|
||||||
@@ -1203,7 +1223,13 @@ class Exchange:
|
|||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
def get_dry_market_fill_price(
|
def get_dry_market_fill_price(
|
||||||
self, pair: str, side: str, amount: float, rate: float, orderbook: OrderBook | None
|
self,
|
||||||
|
pair: str,
|
||||||
|
side: str,
|
||||||
|
amount: float,
|
||||||
|
rate: float,
|
||||||
|
worst_rate: float,
|
||||||
|
orderbook: OrderBook | None,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Get the market order fill price based on orderbook interpolation
|
Get the market order fill price based on orderbook interpolation
|
||||||
@@ -1212,8 +1238,6 @@ class Exchange:
|
|||||||
if not orderbook:
|
if not orderbook:
|
||||||
orderbook = self.fetch_l2_order_book(pair, 20)
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
ob_type: OBLiteral = "asks" if side == "buy" else "bids"
|
ob_type: OBLiteral = "asks" if side == "buy" else "bids"
|
||||||
slippage = 0.05
|
|
||||||
max_slippage_val = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
|
|
||||||
|
|
||||||
remaining_amount = amount
|
remaining_amount = amount
|
||||||
filled_value = 0.0
|
filled_value = 0.0
|
||||||
@@ -1237,11 +1261,10 @@ class Exchange:
|
|||||||
forecast_avg_filled_price = max(filled_value, 0) / amount
|
forecast_avg_filled_price = max(filled_value, 0) / amount
|
||||||
# Limit max. slippage to specified value
|
# Limit max. slippage to specified value
|
||||||
if side == "buy":
|
if side == "buy":
|
||||||
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
|
forecast_avg_filled_price = min(forecast_avg_filled_price, worst_rate)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
|
forecast_avg_filled_price = max(forecast_avg_filled_price, worst_rate)
|
||||||
|
|
||||||
return self.price_to_precision(pair, forecast_avg_filled_price)
|
return self.price_to_precision(pair, forecast_avg_filled_price)
|
||||||
|
|
||||||
return rate
|
return rate
|
||||||
@@ -1253,13 +1276,15 @@ class Exchange:
|
|||||||
limit: float,
|
limit: float,
|
||||||
orderbook: OrderBook | None = None,
|
orderbook: OrderBook | None = None,
|
||||||
offset: float = 0.0,
|
offset: float = 0.0,
|
||||||
|
is_stop: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.exchange_has("fetchL2OrderBook"):
|
if not self.exchange_has("fetchL2OrderBook"):
|
||||||
return True
|
# True unless checking a stoploss order
|
||||||
|
return not is_stop
|
||||||
if not orderbook:
|
if not orderbook:
|
||||||
orderbook = self.fetch_l2_order_book(pair, 1)
|
orderbook = self.fetch_l2_order_book(pair, 1)
|
||||||
try:
|
try:
|
||||||
if side == "buy":
|
if (side == "buy" and not is_stop) or (side == "sell" and is_stop):
|
||||||
price = orderbook["asks"][0][0]
|
price = orderbook["asks"][0][0]
|
||||||
if limit * (1 - offset) >= price:
|
if limit * (1 - offset) >= price:
|
||||||
return True
|
return True
|
||||||
@@ -1278,6 +1303,44 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Check dry-run limit order fill and update fee (if it filled).
|
Check dry-run limit order fill and update fee (if it filled).
|
||||||
"""
|
"""
|
||||||
|
if order["status"] != "closed" and order.get("ft_order_type") == "stoploss":
|
||||||
|
# Stoploss branch
|
||||||
|
pair = order["symbol"]
|
||||||
|
if not orderbook and self.exchange_has("fetchL2OrderBook"):
|
||||||
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
|
price = safe_value_fallback(order, self._ft_has["stop_price_prop"], "price")
|
||||||
|
crossed = self._dry_is_price_crossed(
|
||||||
|
pair, order["side"], price, orderbook, is_stop=True
|
||||||
|
)
|
||||||
|
if crossed and immediate:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
"Could not create dry stoploss order. Stoploss would trigger immediately."
|
||||||
|
)
|
||||||
|
|
||||||
|
if crossed:
|
||||||
|
average = self.get_dry_market_fill_price(
|
||||||
|
pair,
|
||||||
|
order["side"],
|
||||||
|
order["amount"],
|
||||||
|
price,
|
||||||
|
worst_rate=order["price"],
|
||||||
|
orderbook=orderbook,
|
||||||
|
)
|
||||||
|
order.update(
|
||||||
|
{
|
||||||
|
"status": "closed",
|
||||||
|
"filled": order["amount"],
|
||||||
|
"remaining": 0,
|
||||||
|
"average": average,
|
||||||
|
"cost": order["amount"] * average,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.add_dry_order_fee(
|
||||||
|
pair,
|
||||||
|
order,
|
||||||
|
"taker" if immediate else "maker",
|
||||||
|
)
|
||||||
|
return order
|
||||||
if (
|
if (
|
||||||
order["status"] != "closed"
|
order["status"] != "closed"
|
||||||
and order["type"] in ["limit"]
|
and order["type"] in ["limit"]
|
||||||
@@ -1362,8 +1425,9 @@ class Exchange:
|
|||||||
amount: float,
|
amount: float,
|
||||||
rate: float,
|
rate: float,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
reduceOnly: bool = False,
|
|
||||||
time_in_force: str = "GTC",
|
time_in_force: str = "GTC",
|
||||||
|
reduceOnly: bool = False,
|
||||||
|
initial_order: bool = True,
|
||||||
) -> CcxtOrder:
|
) -> CcxtOrder:
|
||||||
if self._config["dry_run"]:
|
if self._config["dry_run"]:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
@@ -1380,7 +1444,7 @@ class Exchange:
|
|||||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||||
|
|
||||||
if not reduceOnly:
|
if not reduceOnly:
|
||||||
self._lev_prep(pair, leverage, side)
|
self._lev_prep(pair, leverage, side, accept_fail=not initial_order)
|
||||||
|
|
||||||
order = self._api.create_order(
|
order = self._api.create_order(
|
||||||
pair,
|
pair,
|
||||||
@@ -1517,8 +1581,9 @@ class Exchange:
|
|||||||
ordertype,
|
ordertype,
|
||||||
side,
|
side,
|
||||||
amount,
|
amount,
|
||||||
stop_price_norm,
|
limit_rate or stop_price_norm,
|
||||||
stop_loss=True,
|
stop_loss=True,
|
||||||
|
stop_price=stop_price_norm,
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
)
|
)
|
||||||
return dry_order
|
return dry_order
|
||||||
@@ -1642,7 +1707,24 @@ class Exchange:
|
|||||||
def fetch_stoploss_order(
|
def fetch_stoploss_order(
|
||||||
self, order_id: str, pair: str, params: dict | None = None
|
self, order_id: str, pair: str, params: dict | None = None
|
||||||
) -> CcxtOrder:
|
) -> CcxtOrder:
|
||||||
return self.fetch_order(order_id, pair, params)
|
if self.get_option("stoploss_query_requires_stop_flag"):
|
||||||
|
params = params or {}
|
||||||
|
params["stop"] = True
|
||||||
|
order = self.fetch_order(order_id, pair, params)
|
||||||
|
val = self.get_option("stoploss_algo_order_info_id")
|
||||||
|
if val and order.get("status", "open") == "closed":
|
||||||
|
if new_orderid := order.get("info", {}).get(val):
|
||||||
|
# Fetch real order, which was placed by the algo order.
|
||||||
|
actual_order = self.fetch_order(order_id=new_orderid, pair=pair, params=None)
|
||||||
|
actual_order["id_stop"] = actual_order["id"]
|
||||||
|
actual_order["id"] = order_id
|
||||||
|
actual_order["type"] = "stoploss"
|
||||||
|
actual_order["stopPrice"] = order.get("stopPrice")
|
||||||
|
actual_order["status_stop"] = "triggered"
|
||||||
|
|
||||||
|
return actual_order
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
def fetch_order_or_stoploss_order(
|
def fetch_order_or_stoploss_order(
|
||||||
self, order_id: str, pair: str, stoploss_order: bool = False
|
self, order_id: str, pair: str, stoploss_order: bool = False
|
||||||
@@ -1696,6 +1778,9 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
||||||
|
if self.get_option("stoploss_query_requires_stop_flag"):
|
||||||
|
params = params or {}
|
||||||
|
params["stop"] = True
|
||||||
return self.cancel_order(order_id, pair, params)
|
return self.cancel_order(order_id, pair, params)
|
||||||
|
|
||||||
def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
|
def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
|
||||||
@@ -1760,16 +1845,16 @@ class Exchange:
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_balances(self) -> CcxtBalances:
|
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||||
try:
|
try:
|
||||||
balances = self._api.fetch_balance()
|
balances = self._api.fetch_balance(params or {})
|
||||||
# Remove additional info from ccxt results
|
# Remove additional info from ccxt results
|
||||||
balances.pop("info", None)
|
balances.pop("info", None)
|
||||||
balances.pop("free", None)
|
balances.pop("free", None)
|
||||||
balances.pop("total", None)
|
balances.pop("total", None)
|
||||||
balances.pop("used", None)
|
balances.pop("used", None)
|
||||||
|
|
||||||
self._log_exchange_response("fetch_balance", balances)
|
self._log_exchange_response("fetch_balance", balances, add_info=params)
|
||||||
return balances
|
return balances
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
@@ -1781,7 +1866,9 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_positions(self, pair: str | None = None) -> list[CcxtPosition]:
|
def fetch_positions(
|
||||||
|
self, pair: str | None = None, params: dict | None = None
|
||||||
|
) -> list[CcxtPosition]:
|
||||||
"""
|
"""
|
||||||
Fetch positions from the exchange.
|
Fetch positions from the exchange.
|
||||||
If no pair is given, all positions are returned.
|
If no pair is given, all positions are returned.
|
||||||
@@ -1790,10 +1877,10 @@ class Exchange:
|
|||||||
if self._config["dry_run"] or self.trading_mode != TradingMode.FUTURES:
|
if self._config["dry_run"] or self.trading_mode != TradingMode.FUTURES:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
symbols = []
|
symbols = None
|
||||||
if pair:
|
if pair:
|
||||||
symbols.append(pair)
|
symbols = [pair]
|
||||||
positions: list[CcxtPosition] = self._api.fetch_positions(symbols)
|
positions: list[CcxtPosition] = self._api.fetch_positions(symbols, params=params or {})
|
||||||
self._log_exchange_response("fetch_positions", positions)
|
self._log_exchange_response("fetch_positions", positions)
|
||||||
return positions
|
return positions
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
@@ -1993,12 +2080,13 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return self._config["stake_currency"]
|
return self._config["stake_currency"]
|
||||||
|
|
||||||
def get_conversion_rate(self, coin: str, currency: str) -> float | None:
|
def get_conversion_rate(self, coin: str, currency: str, *, cached=True) -> float | None:
|
||||||
"""
|
"""
|
||||||
Quick and cached way to get conversion rate one currency to the other.
|
Quick and cached way to get conversion rate one currency to the other.
|
||||||
Can then be used as "rate * amount" to convert between currencies.
|
Can then be used as "rate * amount" to convert between currencies.
|
||||||
:param coin: Coin to convert
|
:param coin: Coin to convert
|
||||||
:param currency: Currency to convert to
|
:param currency: Currency to convert to
|
||||||
|
:param cached: Allow cached tickers, default True
|
||||||
:returns: Conversion rate from coin to currency
|
:returns: Conversion rate from coin to currency
|
||||||
:raises: ExchangeErrors
|
:raises: ExchangeErrors
|
||||||
"""
|
"""
|
||||||
@@ -2009,13 +2097,13 @@ class Exchange:
|
|||||||
currency = proxy_currency
|
currency = proxy_currency
|
||||||
if coin == currency:
|
if coin == currency:
|
||||||
return 1.0
|
return 1.0
|
||||||
tickers = self.get_tickers(cached=True)
|
tickers = self.get_tickers(cached=cached)
|
||||||
try:
|
try:
|
||||||
for pair in self.get_valid_pair_combination(coin, currency):
|
for pair in self.get_valid_pair_combination(coin, currency):
|
||||||
ticker: Ticker | None = tickers.get(pair, None)
|
ticker: Ticker | None = tickers.get(pair, None)
|
||||||
if not ticker:
|
if not ticker:
|
||||||
tickers_other: Tickers = self.get_tickers(
|
tickers_other: Tickers = self.get_tickers(
|
||||||
cached=True,
|
cached=cached,
|
||||||
market_type=(
|
market_type=(
|
||||||
TradingMode.SPOT
|
TradingMode.SPOT
|
||||||
if self.trading_mode != TradingMode.SPOT
|
if self.trading_mode != TradingMode.SPOT
|
||||||
@@ -2024,7 +2112,7 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
ticker = tickers_other.get(pair, None)
|
ticker = tickers_other.get(pair, None)
|
||||||
if ticker:
|
if ticker:
|
||||||
rate: float | None = safe_value_fallback2(ticker, ticker, "last", "ask", None)
|
rate: float | None = safe_value_fallback(ticker, "last", "ask", None)
|
||||||
if rate and pair.startswith(currency) and not pair.endswith(currency):
|
if rate and pair.startswith(currency) and not pair.endswith(currency):
|
||||||
rate = 1.0 / rate
|
rate = 1.0 / rate
|
||||||
return rate
|
return rate
|
||||||
@@ -2324,6 +2412,16 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
||||||
|
"""
|
||||||
|
Return order id or id_stop (for conditional orders) based on exchange settings
|
||||||
|
|
||||||
|
:param order: ccxt order dict
|
||||||
|
:return: correct order id
|
||||||
|
"""
|
||||||
|
if self.get_option("stoploss_query_requires_stop_flag") and (
|
||||||
|
order["type"] in ("stoploss", "stop")
|
||||||
|
):
|
||||||
|
return safe_value_fallback(order, "id_stop", "id")
|
||||||
return order["id"]
|
return order["id"]
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
@@ -2483,7 +2581,13 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
logger.debug(f"Downloaded data for {pair} from ccxt with length {len(data)}.")
|
logger.debug(f"Downloaded data for {pair} from ccxt with length {len(data)}.")
|
||||||
return ohlcv_to_dataframe(data, timeframe, pair, fill_missing=False, drop_incomplete=True)
|
# funding_rates are always complete, so never need to be dropped.
|
||||||
|
drop_incomplete = (
|
||||||
|
self._ohlcv_partial_candle if candle_type != CandleType.FUNDING_RATE else False
|
||||||
|
)
|
||||||
|
return ohlcv_to_dataframe(
|
||||||
|
data, timeframe, pair, fill_missing=False, drop_incomplete=drop_incomplete
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(
|
async def _async_get_historic_ohlcv(
|
||||||
self,
|
self,
|
||||||
@@ -2646,24 +2750,25 @@ class Exchange:
|
|||||||
input_coroutines: list[Coroutine[Any, Any, OHLCVResponse]] = []
|
input_coroutines: list[Coroutine[Any, Any, OHLCVResponse]] = []
|
||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
for pair, timeframe, candle_type in set(pair_list):
|
for pair, timeframe, candle_type in set(pair_list):
|
||||||
invalid_funding = (
|
if candle_type == CandleType.FUNDING_RATE and timeframe != (
|
||||||
candle_type == CandleType.FUNDING_RATE
|
ff_tf := self.get_option("funding_fee_timeframe")
|
||||||
and timeframe != self.get_option("funding_fee_timeframe")
|
):
|
||||||
)
|
# TODO: does this message make sense? would docs be better?
|
||||||
|
# if any, this should be cached to avoid log spam!
|
||||||
|
logger.warning(
|
||||||
|
f"Wrong funding rate timeframe {timeframe} for pair {pair}, "
|
||||||
|
f"downloading {ff_tf} instead."
|
||||||
|
)
|
||||||
|
timeframe = ff_tf
|
||||||
invalid_timeframe = timeframe not in self.timeframes and candle_type in (
|
invalid_timeframe = timeframe not in self.timeframes and candle_type in (
|
||||||
CandleType.SPOT,
|
CandleType.SPOT,
|
||||||
CandleType.FUTURES,
|
CandleType.FUTURES,
|
||||||
)
|
)
|
||||||
if invalid_timeframe or invalid_funding:
|
if invalid_timeframe:
|
||||||
timeframes_ = (
|
|
||||||
", ".join(self.timeframes)
|
|
||||||
if candle_type != CandleType.FUNDING_RATE
|
|
||||||
else self.get_option("funding_fee_timeframe")
|
|
||||||
)
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Cannot download ({pair}, {timeframe}, {candle_type}) combination as this "
|
f"Cannot download ({pair}, {timeframe}, {candle_type}) combination as this "
|
||||||
f"timeframe is not available on {self.name}. Available timeframes are "
|
f"timeframe is not available on {self.name}. Available timeframes are "
|
||||||
f"{timeframes_}."
|
f"{', '.join(self.timeframes)}."
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -2700,7 +2805,11 @@ class Exchange:
|
|||||||
has_cache = cache and (pair, timeframe, c_type) in self._klines
|
has_cache = cache and (pair, timeframe, c_type) in self._klines
|
||||||
# in case of existing cache, fill_missing happens after concatenation
|
# in case of existing cache, fill_missing happens after concatenation
|
||||||
ohlcv_df = ohlcv_to_dataframe(
|
ohlcv_df = ohlcv_to_dataframe(
|
||||||
ticks, timeframe, pair=pair, fill_missing=not has_cache, drop_incomplete=drop_incomplete
|
ticks,
|
||||||
|
timeframe,
|
||||||
|
pair=pair,
|
||||||
|
fill_missing=not has_cache and c_type != CandleType.FUNDING_RATE,
|
||||||
|
drop_incomplete=drop_incomplete,
|
||||||
)
|
)
|
||||||
# keeping parsed dataframe in cache
|
# keeping parsed dataframe in cache
|
||||||
if cache:
|
if cache:
|
||||||
@@ -2711,7 +2820,7 @@ class Exchange:
|
|||||||
concat([old, ohlcv_df], axis=0),
|
concat([old, ohlcv_df], axis=0),
|
||||||
timeframe,
|
timeframe,
|
||||||
pair,
|
pair,
|
||||||
fill_missing=True,
|
fill_missing=c_type != CandleType.FUNDING_RATE,
|
||||||
drop_incomplete=False,
|
drop_incomplete=False,
|
||||||
)
|
)
|
||||||
candle_limit = self.ohlcv_candle_limit(timeframe, self._config["candle_type_def"])
|
candle_limit = self.ohlcv_candle_limit(timeframe, self._config["candle_type_def"])
|
||||||
@@ -2804,8 +2913,11 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
pairs_to_download = [p for p in pairs if p not in candles]
|
pairs_to_download = [p for p in pairs if p not in candles]
|
||||||
if pairs_to_download:
|
if pairs_to_download:
|
||||||
candles = self.refresh_latest_ohlcv(pairs_to_download, since_ms=since_ms, cache=False)
|
candles_new = self.refresh_latest_ohlcv(
|
||||||
for c, val in candles.items():
|
pairs_to_download, since_ms=since_ms, cache=False
|
||||||
|
)
|
||||||
|
for c, val in candles_new.items():
|
||||||
|
candles[c] = val
|
||||||
self._expiring_candle_cache[(c[1], since_ms)][c] = val
|
self._expiring_candle_cache[(c[1], since_ms)][c] = val
|
||||||
return candles
|
return candles
|
||||||
|
|
||||||
@@ -2846,9 +2958,10 @@ class Exchange:
|
|||||||
timeframe, candle_type=candle_type, since_ms=since_ms
|
timeframe, candle_type=candle_type, since_ms=since_ms
|
||||||
)
|
)
|
||||||
|
|
||||||
if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES):
|
|
||||||
params.update({"price": candle_type.value})
|
|
||||||
if candle_type != CandleType.FUNDING_RATE:
|
if candle_type != CandleType.FUNDING_RATE:
|
||||||
|
if candle_type and candle_type not in (CandleType.SPOT, CandleType.FUTURES):
|
||||||
|
self.verify_candle_type_support(candle_type)
|
||||||
|
params.update({"price": str(candle_type)})
|
||||||
data = await self._api_async.fetch_ohlcv(
|
data = await self._api_async.fetch_ohlcv(
|
||||||
pair, timeframe=timeframe, since=since_ms, limit=candle_limit, params=params
|
pair, timeframe=timeframe, since=since_ms, limit=candle_limit, params=params
|
||||||
)
|
)
|
||||||
@@ -2913,6 +3026,38 @@ class Exchange:
|
|||||||
data = [[x["timestamp"], x["fundingRate"], 0, 0, 0, 0] for x in data]
|
data = [[x["timestamp"], x["fundingRate"], 0, 0, 0, 0] for x in data]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def check_candle_type_support(self, candle_type: CandleType) -> bool:
|
||||||
|
"""
|
||||||
|
Check that the exchange supports the given candle type.
|
||||||
|
:param candle_type: CandleType to verify
|
||||||
|
:return: True if supported, False otherwise
|
||||||
|
"""
|
||||||
|
if candle_type == CandleType.FUNDING_RATE:
|
||||||
|
if not self.exchange_has("fetchFundingRateHistory"):
|
||||||
|
return False
|
||||||
|
elif candle_type not in (CandleType.SPOT, CandleType.FUTURES):
|
||||||
|
mapping = {
|
||||||
|
CandleType.MARK: "fetchMarkOHLCV",
|
||||||
|
CandleType.INDEX: "fetchIndexOHLCV",
|
||||||
|
CandleType.PREMIUMINDEX: "fetchPremiumIndexOHLCV",
|
||||||
|
CandleType.FUNDING_RATE: "fetchFundingRateHistory",
|
||||||
|
}
|
||||||
|
_method = mapping.get(candle_type, "fetchOHLCV")
|
||||||
|
if not self.exchange_has(_method):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def verify_candle_type_support(self, candle_type: CandleType) -> None:
|
||||||
|
"""
|
||||||
|
Verify that the exchange supports the given candle type.
|
||||||
|
:param candle_type: CandleType to verify
|
||||||
|
:raises OperationalException: if the candle type is not supported
|
||||||
|
"""
|
||||||
|
if not self.check_candle_type_support(candle_type):
|
||||||
|
raise OperationalException(
|
||||||
|
f"Exchange {self._api.name} does not support fetching {candle_type} candles."
|
||||||
|
)
|
||||||
|
|
||||||
# fetch Trade data stuff
|
# fetch Trade data stuff
|
||||||
|
|
||||||
def needed_candle_for_trades_ms(self, timeframe: str, candle_type: CandleType) -> int:
|
def needed_candle_for_trades_ms(self, timeframe: str, candle_type: CandleType) -> int:
|
||||||
@@ -3497,7 +3642,7 @@ class Exchange:
|
|||||||
pair_tiers.append(self.parse_leverage_tier(tier))
|
pair_tiers.append(self.parse_leverage_tier(tier))
|
||||||
self._leverage_tiers[pair] = pair_tiers
|
self._leverage_tiers[pair] = pair_tiers
|
||||||
|
|
||||||
def parse_leverage_tier(self, tier) -> dict:
|
def parse_leverage_tier(self, tier) -> LeverageTier:
|
||||||
info = tier.get("info", {})
|
info = tier.get("info", {})
|
||||||
return {
|
return {
|
||||||
"minNotional": tier["minNotional"],
|
"minNotional": tier["minNotional"],
|
||||||
@@ -3538,7 +3683,11 @@ class Exchange:
|
|||||||
for tier in pair_tiers:
|
for tier in pair_tiers:
|
||||||
# Adjust notional by leverage to do a proper comparison
|
# Adjust notional by leverage to do a proper comparison
|
||||||
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
min_stake = tier["minNotional"] / (prior_max_lev or tier["maxLeverage"])
|
||||||
max_stake = tier["maxNotional"] / tier["maxLeverage"]
|
max_stake = (
|
||||||
|
tier["maxNotional"] / tier["maxLeverage"]
|
||||||
|
if tier["maxNotional"] is not None
|
||||||
|
else float("inf")
|
||||||
|
)
|
||||||
prior_max_lev = tier["maxLeverage"]
|
prior_max_lev = tier["maxLeverage"]
|
||||||
if min_stake <= stake_amount <= max_stake:
|
if min_stake <= stake_amount <= max_stake:
|
||||||
return tier["maxLeverage"]
|
return tier["maxLeverage"]
|
||||||
@@ -3740,10 +3889,11 @@ class Exchange:
|
|||||||
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
|
:param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
|
||||||
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
|
:param futures_funding_rate: Fake funding rate to use if funding_rates are not available
|
||||||
"""
|
"""
|
||||||
|
relevant_cols = ["date", "open_mark", "open_fund"]
|
||||||
if futures_funding_rate is None:
|
if futures_funding_rate is None:
|
||||||
return mark_rates.merge(
|
return mark_rates.merge(
|
||||||
funding_rates, on="date", how="inner", suffixes=["_mark", "_fund"]
|
funding_rates, on="date", how="inner", suffixes=["_mark", "_fund"]
|
||||||
)
|
)[relevant_cols]
|
||||||
else:
|
else:
|
||||||
if len(funding_rates) == 0:
|
if len(funding_rates) == 0:
|
||||||
# No funding rate candles - full fillup with fallback variable
|
# No funding rate candles - full fillup with fallback variable
|
||||||
@@ -3756,15 +3906,23 @@ class Exchange:
|
|||||||
"low": "low_mark",
|
"low": "low_mark",
|
||||||
"volume": "volume_mark",
|
"volume": "volume_mark",
|
||||||
}
|
}
|
||||||
)
|
)[relevant_cols]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Fill up missing funding_rate candles with fallback value
|
# Fill up missing funding_rate candles with fallback value
|
||||||
combined = mark_rates.merge(
|
combined = mark_rates.merge(
|
||||||
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
|
funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
|
||||||
)
|
)
|
||||||
combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
|
# Fill only leading missing funding rates so gaps stay untouched
|
||||||
return combined
|
first_valid_idx = combined["open_fund"].first_valid_index()
|
||||||
|
if first_valid_idx is None:
|
||||||
|
combined["open_fund"] = futures_funding_rate
|
||||||
|
else:
|
||||||
|
is_leading_na = (combined.index <= first_valid_idx) & combined[
|
||||||
|
"open_fund"
|
||||||
|
].isna()
|
||||||
|
combined.loc[is_leading_na, "open_fund"] = futures_funding_rate
|
||||||
|
return combined[relevant_cols].dropna()
|
||||||
|
|
||||||
def calculate_funding_fees(
|
def calculate_funding_fees(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class FtHas(TypedDict, total=False):
|
|||||||
stop_price_type_value_mapping: dict
|
stop_price_type_value_mapping: dict
|
||||||
stoploss_order_types: dict[str, str]
|
stoploss_order_types: dict[str, str]
|
||||||
stoploss_blocks_assets: bool
|
stoploss_blocks_assets: bool
|
||||||
|
stoploss_query_requires_stop_flag: bool
|
||||||
|
stoploss_algo_order_info_id: str
|
||||||
# ohlcv
|
# ohlcv
|
||||||
ohlcv_params: dict
|
ohlcv_params: dict
|
||||||
ohlcv_candle_limit: int
|
ohlcv_candle_limit: int
|
||||||
@@ -113,5 +115,27 @@ class CcxtPosition(TypedDict):
|
|||||||
|
|
||||||
CcxtOrder = dict[str, Any]
|
CcxtOrder = dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class LeverageTier(TypedDict):
|
||||||
|
"""
|
||||||
|
Represents a single leverage tier returned by the exchange.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
minNotional: Minimum notional value (quote currency) for which this tier applies.
|
||||||
|
maxNotional: Maximum notional value (quote currency) for which this tier applies.
|
||||||
|
When ``maxNotional`` is ``None``, the tier is unbounded on the upper side,
|
||||||
|
i.e. there is no maximum notional limit for this tier
|
||||||
|
maintenanceMarginRate: Maintenance margin rate for this tier (fraction, e.g. 0.005 for 0.5%)
|
||||||
|
maxLeverage: Maximum leverage allowed for this tier
|
||||||
|
maintAmt: Optional fixed maintenance margin amount, if provided by the exchange
|
||||||
|
"""
|
||||||
|
|
||||||
|
minNotional: float
|
||||||
|
maxNotional: float | None
|
||||||
|
maintenanceMarginRate: float
|
||||||
|
maxLeverage: float
|
||||||
|
maintAmt: float | None
|
||||||
|
|
||||||
|
|
||||||
# pair, timeframe, candleType, OHLCV, drop last?,
|
# pair, timeframe, candleType, OHLCV, drop last?,
|
||||||
OHLCVResponse = tuple[str, str, CandleType, list, bool]
|
OHLCVResponse = tuple[str, str, CandleType, list, bool]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from ccxt import (
|
|||||||
from freqtrade.exchange.common import (
|
from freqtrade.exchange.common import (
|
||||||
BAD_EXCHANGES,
|
BAD_EXCHANGES,
|
||||||
EXCHANGE_HAS_OPTIONAL,
|
EXCHANGE_HAS_OPTIONAL,
|
||||||
|
EXCHANGE_HAS_OPTIONAL_FUTURES,
|
||||||
EXCHANGE_HAS_REQUIRED,
|
EXCHANGE_HAS_REQUIRED,
|
||||||
MAP_EXCHANGE_CHILDCLASS,
|
MAP_EXCHANGE_CHILDCLASS,
|
||||||
SUPPORTED_EXCHANGES,
|
SUPPORTED_EXCHANGES,
|
||||||
@@ -53,7 +54,22 @@ def available_exchanges(ccxt_module: CcxtModuleType | None = None) -> list[str]:
|
|||||||
return [x for x in exchanges if validate_exchange(x)[0]]
|
return [x for x in exchanges if validate_exchange(x)[0]]
|
||||||
|
|
||||||
|
|
||||||
def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]:
|
def _exchange_has_helper(ex_mod: ccxt.Exchange, required: dict[str, list[str]]) -> list[str]:
|
||||||
|
"""
|
||||||
|
Checks availability of methods (or their replacement)s in ex_mod.has
|
||||||
|
:param ex_mod: ccxt Exchange module
|
||||||
|
:param required: dict of required methods, with possible replacement methods as list
|
||||||
|
:return: list of missing required methods
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
k
|
||||||
|
for k, v in required.items()
|
||||||
|
if ex_mod.has.get(k) is not True
|
||||||
|
and (len(v) == 0 or not (all(ex_mod.has.get(x) for x in v)))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def validate_exchange(exchange: str) -> tuple[bool, str, str, ccxt.Exchange | None]:
|
||||||
"""
|
"""
|
||||||
returns: can_use, reason, exchange_object
|
returns: can_use, reason, exchange_object
|
||||||
with Reason including both missing and missing_opt
|
with Reason including both missing and missing_opt
|
||||||
@@ -64,36 +80,38 @@ def validate_exchange(exchange: str) -> tuple[bool, str, ccxt.Exchange | None]:
|
|||||||
ex_mod = getattr(ccxt.async_support, exchange.lower())()
|
ex_mod = getattr(ccxt.async_support, exchange.lower())()
|
||||||
|
|
||||||
if not ex_mod or not ex_mod.has:
|
if not ex_mod or not ex_mod.has:
|
||||||
return False, "", None
|
return False, "", "", None
|
||||||
|
|
||||||
result = True
|
result = True
|
||||||
reason = ""
|
reasons = []
|
||||||
missing = [
|
reasons_fut = ""
|
||||||
k
|
missing = _exchange_has_helper(ex_mod, EXCHANGE_HAS_REQUIRED)
|
||||||
for k, v in EXCHANGE_HAS_REQUIRED.items()
|
|
||||||
if ex_mod.has.get(k) is not True and not (all(ex_mod.has.get(x) for x in v))
|
|
||||||
]
|
|
||||||
if missing:
|
if missing:
|
||||||
result = False
|
result = False
|
||||||
reason += f"missing: {', '.join(missing)}"
|
reasons.append(f"missing: {', '.join(missing)}")
|
||||||
|
|
||||||
missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
|
missing_opt = _exchange_has_helper(ex_mod, EXCHANGE_HAS_OPTIONAL)
|
||||||
|
|
||||||
|
missing_futures = _exchange_has_helper(ex_mod, EXCHANGE_HAS_OPTIONAL_FUTURES)
|
||||||
|
|
||||||
if exchange.lower() in BAD_EXCHANGES:
|
if exchange.lower() in BAD_EXCHANGES:
|
||||||
result = False
|
result = False
|
||||||
reason = BAD_EXCHANGES.get(exchange.lower(), "")
|
reasons.append(BAD_EXCHANGES.get(exchange.lower(), ""))
|
||||||
|
|
||||||
if missing_opt:
|
if missing_opt:
|
||||||
reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. "
|
reasons.append(f"missing opt: {', '.join(missing_opt)}")
|
||||||
|
|
||||||
return result, reason, ex_mod
|
if missing_futures:
|
||||||
|
reasons_fut = f"missing futures opt: {', '.join(missing_futures)}"
|
||||||
|
|
||||||
|
return result, "; ".join(reasons), reasons_fut, ex_mod
|
||||||
|
|
||||||
|
|
||||||
def _build_exchange_list_entry(
|
def _build_exchange_list_entry(
|
||||||
exchange_name: str, exchangeClasses: dict[str, Any]
|
exchange_name: str, exchangeClasses: dict[str, Any]
|
||||||
) -> ValidExchangesType:
|
) -> ValidExchangesType:
|
||||||
exchange_name = exchange_name.lower()
|
exchange_name = exchange_name.lower()
|
||||||
valid, comment, ex_mod = validate_exchange(exchange_name)
|
valid, comment, comment_fut, ex_mod = validate_exchange(exchange_name)
|
||||||
mapped_exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name).lower()
|
mapped_exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name).lower()
|
||||||
is_alias = getattr(ex_mod, "alias", False)
|
is_alias = getattr(ex_mod, "alias", False)
|
||||||
result: ValidExchangesType = {
|
result: ValidExchangesType = {
|
||||||
@@ -102,6 +120,7 @@ def _build_exchange_list_entry(
|
|||||||
"valid": valid,
|
"valid": valid,
|
||||||
"supported": mapped_exchange_name in SUPPORTED_EXCHANGES and not is_alias,
|
"supported": mapped_exchange_name in SUPPORTED_EXCHANGES and not is_alias,
|
||||||
"comment": comment,
|
"comment": comment,
|
||||||
|
"comment_futures": comment_fut,
|
||||||
"dex": getattr(ex_mod, "dex", False),
|
"dex": getattr(ex_mod, "dex", False),
|
||||||
"is_alias": is_alias,
|
"is_alias": is_alias,
|
||||||
"alias_for": inspect.getmro(ex_mod.__class__)[1]().id
|
"alias_for": inspect.getmro(ex_mod.__class__)[1]().id
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from freqtrade.enums import MarginMode, PriceType, TradingMode
|
|||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
from freqtrade.exchange.exchange_types import FtHas
|
||||||
from freqtrade.misc import safe_value_fallback2
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -30,6 +29,8 @@ class Gate(Exchange):
|
|||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stop_price_param": "stopPrice",
|
"stop_price_param": "stopPrice",
|
||||||
"stop_price_prop": "stopPrice",
|
"stop_price_prop": "stopPrice",
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
|
"stoploss_algo_order_info_id": "fired_order_id",
|
||||||
"l2_limit_upper": 1000,
|
"l2_limit_upper": 1000,
|
||||||
"marketOrderRequiresPrice": True,
|
"marketOrderRequiresPrice": True,
|
||||||
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
|
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
|
||||||
@@ -42,6 +43,7 @@ class Gate(Exchange):
|
|||||||
"stop_price_type_field": "price_type",
|
"stop_price_type_field": "price_type",
|
||||||
"l2_limit_upper": 300,
|
"l2_limit_upper": 300,
|
||||||
"stoploss_blocks_assets": False,
|
"stoploss_blocks_assets": False,
|
||||||
|
"stoploss_algo_order_info_id": "trade_id",
|
||||||
"stop_price_type_value_mapping": {
|
"stop_price_type_value_mapping": {
|
||||||
PriceType.LAST: 0,
|
PriceType.LAST: 0,
|
||||||
PriceType.MARK: 1,
|
PriceType.MARK: 1,
|
||||||
@@ -129,28 +131,3 @@ class Gate(Exchange):
|
|||||||
"rate": pair_fees[takerOrMaker],
|
"rate": pair_fees[takerOrMaker],
|
||||||
}
|
}
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
|
||||||
return safe_value_fallback2(order, order, "id_stop", "id")
|
|
||||||
|
|
||||||
def fetch_stoploss_order(
|
|
||||||
self, order_id: str, pair: str, params: dict | None = None
|
|
||||||
) -> CcxtOrder:
|
|
||||||
order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True})
|
|
||||||
if order.get("status", "open") == "closed":
|
|
||||||
# Places a real order - which we need to fetch explicitly.
|
|
||||||
val = "trade_id" if self.trading_mode == TradingMode.FUTURES else "fired_order_id"
|
|
||||||
|
|
||||||
if new_orderid := order.get("info", {}).get(val):
|
|
||||||
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
|
|
||||||
order1["id_stop"] = order1["id"]
|
|
||||||
order1["id"] = order_id
|
|
||||||
order1["type"] = "stoploss"
|
|
||||||
order1["stopPrice"] = order.get("stopPrice")
|
|
||||||
order1["status_stop"] = "triggered"
|
|
||||||
|
|
||||||
return order1
|
|
||||||
return order
|
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
|
||||||
return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ from typing import Any
|
|||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
from freqtrade.enums.runmode import NON_UTIL_MODES
|
||||||
|
from freqtrade.exceptions import ConfigurationError, ExchangeError, OperationalException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, 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
|
||||||
|
|
||||||
|
|
||||||
@@ -37,9 +38,9 @@ class Hyperliquid(Exchange):
|
|||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_blocks_assets": False,
|
"stoploss_blocks_assets": False,
|
||||||
"stop_price_prop": "stopPrice",
|
"stop_price_prop": "stopPrice",
|
||||||
"funding_fee_timeframe": "1h",
|
|
||||||
"funding_fee_candle_limit": 500,
|
"funding_fee_candle_limit": 500,
|
||||||
"uses_leverage_tiers": False,
|
"uses_leverage_tiers": False,
|
||||||
|
"mark_ohlcv_price": "futures",
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
@@ -57,12 +58,108 @@ class Hyperliquid(Exchange):
|
|||||||
config.update(super()._ccxt_config)
|
config.update(super()._ccxt_config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
def _get_configured_hip3_dexes(self) -> list[str]:
|
||||||
|
"""Get list of configured HIP-3 DEXes."""
|
||||||
|
return self._config.get("exchange", {}).get("hip3_dexes", [])
|
||||||
|
|
||||||
|
def validate_config(self, config: dict) -> None:
|
||||||
|
"""Validate HIP-3 configuration at bot startup."""
|
||||||
|
super().validate_config(config)
|
||||||
|
configured = self._get_configured_hip3_dexes()
|
||||||
|
if not configured or not self.markets:
|
||||||
|
return
|
||||||
|
if self.trading_mode != TradingMode.FUTURES:
|
||||||
|
if configured:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"HIP-3 DEXes are only supported in FUTURES trading mode. "
|
||||||
|
"Please update your configuration!"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if configured and self.margin_mode != MarginMode.ISOLATED:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"HIP-3 DEXes require 'isolated' margin mode. "
|
||||||
|
f"Current margin mode: '{self.margin_mode.value}'. "
|
||||||
|
"Please update your configuration!"
|
||||||
|
)
|
||||||
|
|
||||||
|
available = {
|
||||||
|
m.get("info", {}).get("dex")
|
||||||
|
for m in self.get_markets(
|
||||||
|
quote_currencies=[self._config["stake_currency"]],
|
||||||
|
tradable_only=True,
|
||||||
|
active_only=True,
|
||||||
|
).values()
|
||||||
|
if m.get("info", {}).get("hip3")
|
||||||
|
}
|
||||||
|
available.discard(None)
|
||||||
|
|
||||||
|
invalid = set(configured) - available
|
||||||
|
if invalid:
|
||||||
|
raise ConfigurationError(
|
||||||
|
f"Invalid HIP-3 DEXes configured: {sorted(invalid)}. "
|
||||||
|
f"Available DEXes matching your stake currency ({self._config['stake_currency']}): "
|
||||||
|
f"{sorted(available)}. "
|
||||||
|
f"Check your 'hip3_dexes' configuration!"
|
||||||
|
)
|
||||||
|
|
||||||
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
def market_is_tradable(self, market: dict[str, Any]) -> bool:
|
||||||
|
"""Check if market is tradable, including HIP-3 markets."""
|
||||||
parent_check = super().market_is_tradable(market)
|
parent_check = super().market_is_tradable(market)
|
||||||
|
|
||||||
# Exclude hip3 markets for now - which have the format XYZ:GOOGL/USDT:USDT -
|
market_info = market.get("info", {})
|
||||||
# and XYZ:GOOGL as base
|
if market_info.get("hip3") and self._config["runmode"] in NON_UTIL_MODES:
|
||||||
return parent_check and ":" not in market["base"]
|
configured = self._get_configured_hip3_dexes()
|
||||||
|
if not configured:
|
||||||
|
return False
|
||||||
|
|
||||||
|
market_dex = market_info.get("dex")
|
||||||
|
return parent_check and market_dex in configured
|
||||||
|
|
||||||
|
return parent_check
|
||||||
|
|
||||||
|
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||||
|
"""Fetch balances from default DEX and HIP-3 DEXes needed by tradable pairs.
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
balances = super().get_balances()
|
||||||
|
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():
|
||||||
|
if currency in ["info", "free", "used", "total", "datetime", "timestamp"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if currency not in balances:
|
||||||
|
balances[currency] = amount_info
|
||||||
|
else:
|
||||||
|
balances[currency]["free"] += amount_info["free"]
|
||||||
|
balances[currency]["used"] += amount_info["used"]
|
||||||
|
balances[currency]["total"] += amount_info["total"]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Could not fetch balance for HIP-3 DEX '{dex}': {e}")
|
||||||
|
|
||||||
|
if dexes:
|
||||||
|
self._log_exchange_response("fetch_balance", balances, add_info="combined")
|
||||||
|
return balances
|
||||||
|
|
||||||
|
def fetch_positions(
|
||||||
|
self, pair: str | None = None, params: dict | None = None
|
||||||
|
) -> list[CcxtPosition]:
|
||||||
|
"""Fetch positions from default DEX and HIP-3 DEXes needed by tradable pairs."""
|
||||||
|
positions = super().fetch_positions(pair)
|
||||||
|
dexes = self._get_configured_hip3_dexes()
|
||||||
|
for dex in dexes:
|
||||||
|
try:
|
||||||
|
positions.extend(super().fetch_positions(pair, params={"dex": dex}))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Could not fetch positions from HIP-3 DEX '{dex}': {e}")
|
||||||
|
if dexes:
|
||||||
|
self._log_exchange_response("fetch_positions", positions, add_info="combined")
|
||||||
|
return positions
|
||||||
|
|
||||||
def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
|
def get_max_leverage(self, pair: str, stake_amount: float | None) -> float:
|
||||||
# There are no leverage tiers
|
# There are no leverage tiers
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class Kraken(Exchange):
|
|||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
"trades_pagination_overlap": False,
|
"trades_pagination_overlap": False,
|
||||||
"trades_has_history": True,
|
"trades_has_history": True,
|
||||||
"mark_ohlcv_timeframe": "4h",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: list[tuple[TradingMode, MarginMode]] = [
|
||||||
@@ -71,7 +70,7 @@ class Kraken(Exchange):
|
|||||||
return consolidated
|
return consolidated
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_balances(self) -> CcxtBalances:
|
def get_balances(self, params: dict | None = None) -> CcxtBalances:
|
||||||
if self._config["dry_run"]:
|
if self._config["dry_run"]:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ class Kucoin(Exchange):
|
|||||||
amount: float,
|
amount: float,
|
||||||
rate: float,
|
rate: float,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
reduceOnly: bool = False,
|
|
||||||
time_in_force: str = "GTC",
|
time_in_force: str = "GTC",
|
||||||
|
reduceOnly: bool = False,
|
||||||
|
initial_order: bool = True,
|
||||||
) -> CcxtOrder:
|
) -> CcxtOrder:
|
||||||
res = super().create_order(
|
res = super().create_order(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
@@ -56,6 +57,7 @@ class Kucoin(Exchange):
|
|||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
reduceOnly=reduceOnly,
|
reduceOnly=reduceOnly,
|
||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
|
initial_order=initial_order,
|
||||||
)
|
)
|
||||||
# Kucoin returns only the order-id.
|
# Kucoin returns only the order-id.
|
||||||
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
|
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from freqtrade.exceptions import (
|
|||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
||||||
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
from freqtrade.exchange.exchange_types import CcxtOrder, FtHas
|
||||||
from freqtrade.misc import safe_value_fallback2
|
|
||||||
from freqtrade.util import dt_now, dt_ts
|
from freqtrade.util import dt_now, dt_ts
|
||||||
|
|
||||||
|
|
||||||
@@ -29,10 +28,9 @@ class Okx(Exchange):
|
|||||||
|
|
||||||
_ft_has: FtHas = {
|
_ft_has: FtHas = {
|
||||||
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
|
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
|
||||||
"mark_ohlcv_timeframe": "4h",
|
|
||||||
"funding_fee_timeframe": "8h",
|
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_query_requires_stop_flag": True,
|
||||||
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
|
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
|
||||||
"ws_enabled": True,
|
"ws_enabled": True,
|
||||||
}
|
}
|
||||||
@@ -41,8 +39,8 @@ class Okx(Exchange):
|
|||||||
"stop_price_type_field": "slTriggerPxType",
|
"stop_price_type_field": "slTriggerPxType",
|
||||||
"stop_price_type_value_mapping": {
|
"stop_price_type_value_mapping": {
|
||||||
PriceType.LAST: "last",
|
PriceType.LAST: "last",
|
||||||
PriceType.MARK: "index",
|
PriceType.MARK: "mark",
|
||||||
PriceType.INDEX: "mark",
|
PriceType.INDEX: "index",
|
||||||
},
|
},
|
||||||
"stoploss_blocks_assets": False,
|
"stoploss_blocks_assets": False,
|
||||||
"ws_enabled": True,
|
"ws_enabled": True,
|
||||||
@@ -184,7 +182,10 @@ class Okx(Exchange):
|
|||||||
return float("inf")
|
return float("inf")
|
||||||
|
|
||||||
pair_tiers = self._leverage_tiers[pair]
|
pair_tiers = self._leverage_tiers[pair]
|
||||||
return pair_tiers[-1]["maxNotional"] / leverage
|
last_max_notional = pair_tiers[-1]["maxNotional"]
|
||||||
|
if last_max_notional is None:
|
||||||
|
return float("inf")
|
||||||
|
return last_max_notional / leverage
|
||||||
|
|
||||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict:
|
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> dict:
|
||||||
params = super()._get_stop_params(side, ordertype, stop_price)
|
params = super()._get_stop_params(side, ordertype, stop_price)
|
||||||
@@ -260,21 +261,6 @@ class Okx(Exchange):
|
|||||||
raise OperationalException(e) from 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}).")
|
||||||
|
|
||||||
def get_order_id_conditional(self, order: CcxtOrder) -> str:
|
|
||||||
if order.get("type", "") == "stop":
|
|
||||||
return safe_value_fallback2(order, order, "id_stop", "id")
|
|
||||||
return order["id"]
|
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
|
|
||||||
params1 = {"stop": True}
|
|
||||||
# 'ordType': 'conditional'
|
|
||||||
#
|
|
||||||
return self.cancel_order(
|
|
||||||
order_id=order_id,
|
|
||||||
pair=pair,
|
|
||||||
params=params1,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
|
def _fetch_orders_emulate(self, pair: str, since_ms: int) -> list[CcxtOrder]:
|
||||||
orders = []
|
orders = []
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class BaseClassifierModel(IFreqaiModel):
|
|||||||
"""
|
"""
|
||||||
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
|
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
|
||||||
User *must* inherit from this class and set fit(). See example scripts
|
User *must* inherit from this class and set fit(). See example scripts
|
||||||
such as prediction_models/CatboostClassifier.py for guidance.
|
such as prediction_models/XGBoostClassifier.py for guidance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class BaseRegressionModel(IFreqaiModel):
|
|||||||
"""
|
"""
|
||||||
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
|
Base class for regression type models (e.g. Catboost, LightGBM, XGboost etc.).
|
||||||
User *must* inherit from this class and set fit(). See example scripts
|
User *must* inherit from this class and set fit(). See example scripts
|
||||||
such as prediction_models/CatboostRegressor.py for guidance.
|
such as prediction_models/XGBoostRegressor.py for guidance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
||||||
|
|
||||||
pattern = re.compile(r"sub-train-(\w+)_(\d{10})")
|
pattern = re.compile(r"^sub-train-(.+)_(\d{10})$")
|
||||||
|
|
||||||
delete_dict: dict[str, Any] = {}
|
delete_dict: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,10 @@ class IFreqaiModel(ABC):
|
|||||||
"""
|
"""
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
if not self.train_queue:
|
||||||
|
continue
|
||||||
|
|
||||||
pair = self.train_queue[0]
|
pair = self.train_queue[0]
|
||||||
|
|
||||||
# ensure pair is available in dp
|
# ensure pair is available in dp
|
||||||
@@ -948,7 +952,7 @@ class IFreqaiModel(ABC):
|
|||||||
return dk
|
return dk
|
||||||
|
|
||||||
# Following methods which are overridden by user made prediction models.
|
# Following methods which are overridden by user made prediction models.
|
||||||
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
|
# See freqai/prediction_models/XGBoostRegressor.py for an example.
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
def train(self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
@@ -964,7 +968,7 @@ class IFreqaiModel(ABC):
|
|||||||
def fit(self, data_dictionary: dict[str, Any], dk: FreqaiDataKitchen, **kwargs) -> Any:
|
def fit(self, data_dictionary: dict[str, Any], dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
"""
|
"""
|
||||||
Most regressors use the same function names and arguments e.g. user
|
Most regressors use the same function names and arguments e.g. user
|
||||||
can drop in LGBMRegressor in place of CatBoostRegressor and all data
|
can drop in LGBMRegressor in place of XGBoostRegressor and all data
|
||||||
management will be properly handled by Freqai.
|
management will be properly handled by Freqai.
|
||||||
:param data_dictionary: Dict = the dictionary constructed by DataHandler to hold
|
:param data_dictionary: Dict = the dictionary constructed by DataHandler to hold
|
||||||
all the training and test data/labels.
|
all the training and test data/labels.
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from catboost import CatBoostClassifier, Pool
|
|
||||||
|
|
||||||
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CatboostClassifier(BaseClassifierModel):
|
|
||||||
"""
|
|
||||||
User created prediction model. The class inherits IFreqaiModel, which
|
|
||||||
means it has full access to all Frequency AI functionality. Typically,
|
|
||||||
users would use this to override the common `fit()`, `train()`, or
|
|
||||||
`predict()` methods to add their custom data handling tools or change
|
|
||||||
various aspects of the training that cannot be configured via the
|
|
||||||
top level config.json file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
|
||||||
"""
|
|
||||||
User sets up the training and test data to fit their desired model here
|
|
||||||
:param data_dictionary: the dictionary holding all data for train, test,
|
|
||||||
labels, weights
|
|
||||||
:param dk: The datakitchen object for the current coin/model
|
|
||||||
"""
|
|
||||||
|
|
||||||
train_data = Pool(
|
|
||||||
data=data_dictionary["train_features"],
|
|
||||||
label=data_dictionary["train_labels"],
|
|
||||||
weight=data_dictionary["train_weights"],
|
|
||||||
)
|
|
||||||
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0:
|
|
||||||
test_data = None
|
|
||||||
else:
|
|
||||||
test_data = Pool(
|
|
||||||
data=data_dictionary["test_features"],
|
|
||||||
label=data_dictionary["test_labels"],
|
|
||||||
weight=data_dictionary["test_weights"],
|
|
||||||
)
|
|
||||||
|
|
||||||
cbr = CatBoostClassifier(
|
|
||||||
allow_writing_files=True,
|
|
||||||
loss_function="MultiClass",
|
|
||||||
train_dir=Path(dk.data_path),
|
|
||||||
**self.model_training_parameters,
|
|
||||||
)
|
|
||||||
|
|
||||||
init_model = self.get_init_model(dk.pair)
|
|
||||||
|
|
||||||
cbr.fit(
|
|
||||||
X=train_data,
|
|
||||||
eval_set=test_data,
|
|
||||||
init_model=init_model,
|
|
||||||
)
|
|
||||||
|
|
||||||
return cbr
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from catboost import CatBoostClassifier, Pool
|
|
||||||
|
|
||||||
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
|
||||||
from freqtrade.freqai.base_models.FreqaiMultiOutputClassifier import FreqaiMultiOutputClassifier
|
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CatboostClassifierMultiTarget(BaseClassifierModel):
|
|
||||||
"""
|
|
||||||
User created prediction model. The class inherits IFreqaiModel, which
|
|
||||||
means it has full access to all Frequency AI functionality. Typically,
|
|
||||||
users would use this to override the common `fit()`, `train()`, or
|
|
||||||
`predict()` methods to add their custom data handling tools or change
|
|
||||||
various aspects of the training that cannot be configured via the
|
|
||||||
top level config.json file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
|
||||||
"""
|
|
||||||
User sets up the training and test data to fit their desired model here
|
|
||||||
:param data_dictionary: the dictionary holding all data for train, test,
|
|
||||||
labels, weights
|
|
||||||
:param dk: The datakitchen object for the current coin/model
|
|
||||||
"""
|
|
||||||
|
|
||||||
cbc = CatBoostClassifier(
|
|
||||||
allow_writing_files=True,
|
|
||||||
loss_function="MultiClass",
|
|
||||||
train_dir=Path(dk.data_path),
|
|
||||||
**self.model_training_parameters,
|
|
||||||
)
|
|
||||||
|
|
||||||
X = data_dictionary["train_features"]
|
|
||||||
y = data_dictionary["train_labels"]
|
|
||||||
|
|
||||||
sample_weight = data_dictionary["train_weights"]
|
|
||||||
|
|
||||||
eval_sets = [None] * y.shape[1]
|
|
||||||
|
|
||||||
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) != 0:
|
|
||||||
eval_sets = [None] * data_dictionary["test_labels"].shape[1]
|
|
||||||
|
|
||||||
for i in range(data_dictionary["test_labels"].shape[1]):
|
|
||||||
eval_sets[i] = Pool(
|
|
||||||
data=data_dictionary["test_features"],
|
|
||||||
label=data_dictionary["test_labels"].iloc[:, i],
|
|
||||||
weight=data_dictionary["test_weights"],
|
|
||||||
)
|
|
||||||
|
|
||||||
init_model = self.get_init_model(dk.pair)
|
|
||||||
|
|
||||||
if init_model:
|
|
||||||
init_models = init_model.estimators_
|
|
||||||
else:
|
|
||||||
init_models = [None] * y.shape[1]
|
|
||||||
|
|
||||||
fit_params = []
|
|
||||||
for i in range(len(eval_sets)):
|
|
||||||
fit_params.append(
|
|
||||||
{
|
|
||||||
"eval_set": eval_sets[i],
|
|
||||||
"init_model": init_models[i],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
model = FreqaiMultiOutputClassifier(estimator=cbc)
|
|
||||||
thread_training = self.freqai_info.get("multitarget_parallel_training", False)
|
|
||||||
if thread_training:
|
|
||||||
model.n_jobs = y.shape[1]
|
|
||||||
model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params)
|
|
||||||
|
|
||||||
return model
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from catboost import CatBoostRegressor, Pool
|
|
||||||
|
|
||||||
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CatboostRegressor(BaseRegressionModel):
|
|
||||||
"""
|
|
||||||
User created prediction model. The class inherits IFreqaiModel, which
|
|
||||||
means it has full access to all Frequency AI functionality. Typically,
|
|
||||||
users would use this to override the common `fit()`, `train()`, or
|
|
||||||
`predict()` methods to add their custom data handling tools or change
|
|
||||||
various aspects of the training that cannot be configured via the
|
|
||||||
top level config.json file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
|
||||||
"""
|
|
||||||
User sets up the training and test data to fit their desired model here
|
|
||||||
:param data_dictionary: the dictionary holding all data for train, test,
|
|
||||||
labels, weights
|
|
||||||
:param dk: The datakitchen object for the current coin/model
|
|
||||||
"""
|
|
||||||
|
|
||||||
train_data = Pool(
|
|
||||||
data=data_dictionary["train_features"],
|
|
||||||
label=data_dictionary["train_labels"],
|
|
||||||
weight=data_dictionary["train_weights"],
|
|
||||||
)
|
|
||||||
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0:
|
|
||||||
test_data = None
|
|
||||||
else:
|
|
||||||
test_data = Pool(
|
|
||||||
data=data_dictionary["test_features"],
|
|
||||||
label=data_dictionary["test_labels"],
|
|
||||||
weight=data_dictionary["test_weights"],
|
|
||||||
)
|
|
||||||
|
|
||||||
init_model = self.get_init_model(dk.pair)
|
|
||||||
|
|
||||||
model = CatBoostRegressor(
|
|
||||||
allow_writing_files=True,
|
|
||||||
train_dir=Path(dk.data_path),
|
|
||||||
**self.model_training_parameters,
|
|
||||||
)
|
|
||||||
|
|
||||||
model.fit(
|
|
||||||
X=train_data,
|
|
||||||
eval_set=test_data,
|
|
||||||
init_model=init_model,
|
|
||||||
)
|
|
||||||
|
|
||||||
return model
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from catboost import CatBoostRegressor, Pool
|
|
||||||
|
|
||||||
from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
|
|
||||||
from freqtrade.freqai.base_models.FreqaiMultiOutputRegressor import FreqaiMultiOutputRegressor
|
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CatboostRegressorMultiTarget(BaseRegressionModel):
|
|
||||||
"""
|
|
||||||
User created prediction model. The class inherits IFreqaiModel, which
|
|
||||||
means it has full access to all Frequency AI functionality. Typically,
|
|
||||||
users would use this to override the common `fit()`, `train()`, or
|
|
||||||
`predict()` methods to add their custom data handling tools or change
|
|
||||||
various aspects of the training that cannot be configured via the
|
|
||||||
top level config.json file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
|
||||||
"""
|
|
||||||
User sets up the training and test data to fit their desired model here
|
|
||||||
:param data_dictionary: the dictionary holding all data for train, test,
|
|
||||||
labels, weights
|
|
||||||
:param dk: The datakitchen object for the current coin/model
|
|
||||||
"""
|
|
||||||
|
|
||||||
cbr = CatBoostRegressor(
|
|
||||||
allow_writing_files=True,
|
|
||||||
train_dir=Path(dk.data_path),
|
|
||||||
**self.model_training_parameters,
|
|
||||||
)
|
|
||||||
|
|
||||||
X = data_dictionary["train_features"]
|
|
||||||
y = data_dictionary["train_labels"]
|
|
||||||
|
|
||||||
sample_weight = data_dictionary["train_weights"]
|
|
||||||
|
|
||||||
eval_sets = [None] * y.shape[1]
|
|
||||||
|
|
||||||
if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) != 0:
|
|
||||||
eval_sets = [None] * data_dictionary["test_labels"].shape[1]
|
|
||||||
|
|
||||||
for i in range(data_dictionary["test_labels"].shape[1]):
|
|
||||||
eval_sets[i] = Pool(
|
|
||||||
data=data_dictionary["test_features"],
|
|
||||||
label=data_dictionary["test_labels"].iloc[:, i],
|
|
||||||
weight=data_dictionary["test_weights"],
|
|
||||||
)
|
|
||||||
|
|
||||||
init_model = self.get_init_model(dk.pair)
|
|
||||||
|
|
||||||
if init_model:
|
|
||||||
init_models = init_model.estimators_
|
|
||||||
else:
|
|
||||||
init_models = [None] * y.shape[1]
|
|
||||||
|
|
||||||
fit_params = []
|
|
||||||
for i in range(len(eval_sets)):
|
|
||||||
fit_params.append(
|
|
||||||
{
|
|
||||||
"eval_set": eval_sets[i],
|
|
||||||
"init_model": init_models[i],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
model = FreqaiMultiOutputRegressor(estimator=cbr)
|
|
||||||
thread_training = self.freqai_info.get("multitarget_parallel_training", False)
|
|
||||||
if thread_training:
|
|
||||||
model.n_jobs = y.shape[1]
|
|
||||||
model.fit(X=X, y=y, sample_weight=sample_weight, fit_params=fit_params)
|
|
||||||
|
|
||||||
return model
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user