Compare commits
1235 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43f1537383 | |||
| c9f78afe65 | |||
| a3473f3f60 | |||
| 4eb4275331 | |||
| b9121274fb | |||
| bd266f654e | |||
| c23a045de4 | |||
| 61ee77e07e | |||
| f647fb342b | |||
| d4183b3fcb | |||
| 6b0b62dadf | |||
| 9e9f9b21e5 | |||
| 91e009bf6d | |||
| b5ed693bee | |||
| b8220ee0f7 | |||
| a0336c83c3 | |||
| 6efc62e4cd | |||
| 6292d1af6d | |||
| 9ffdaceef3 | |||
| b2d9b914ea | |||
| 1e10b25e3d | |||
| 5b29e710dc | |||
| e70cafe578 | |||
| 44bdac5e8c | |||
| 85c14578e2 | |||
| 09aaf894c6 | |||
| 795e3e324f | |||
| 39f4fb8797 | |||
| d26aa231fc | |||
| 6d9b8a4a99 | |||
| 2242d544fc | |||
| ad5f0307b7 | |||
| 8fccd98eca | |||
| e71b66f5c2 | |||
| 0e99fe349c | |||
| 96eb109b4e | |||
| ae52ce8cda | |||
| 811198cc3e | |||
| 4e3de64c57 | |||
| 8c866abad8 | |||
| a2d03c4e2c | |||
| 68ab147f57 | |||
| abc82a3fdf | |||
| a87b215d67 | |||
| 914195acf4 | |||
| 96d74063fc | |||
| 5316227219 | |||
| fcb75560c4 | |||
| c4c0371ed3 | |||
| dd1a0156b9 | |||
| 7ecc2f76a2 | |||
| a2cbe5df04 | |||
| 0d4010a0be | |||
| 9d0f488de7 | |||
| 707c6744b9 | |||
| ebfc9a6039 | |||
| 5d0cff2f76 | |||
| f657d06e91 | |||
| b40c45ee42 | |||
| adcf751340 | |||
| 261822147c | |||
| 3ec55885bd | |||
| 9421ca2628 | |||
| 3a4d103bc8 | |||
| 7a2ff60255 | |||
| 915cb5ffbd | |||
| c0713eb77f | |||
| 29fdcdbf56 | |||
| d131dd4050 | |||
| cfae98ae00 | |||
| e4f701fd0d | |||
| 5b66ef4bea | |||
| 7f73e99437 | |||
| 55ce58d79f | |||
| 000f72942a | |||
| aa949153eb | |||
| 5c6f3ea439 | |||
| 261df527d9 | |||
| 6b735bc683 | |||
| 6044bbb6b1 | |||
| 2477ef57f9 | |||
| 1d03e8bc5f | |||
| d3382fbe04 | |||
| 292bd62973 | |||
| c54f28ada8 | |||
| adeab13bdf | |||
| 2ab732480f | |||
| 45ee12e257 | |||
| 63294c4d3a | |||
| bb760a47d5 | |||
| 61ea3d817a | |||
| 7d15c379cb | |||
| c7f7dd1d4b | |||
| 1b714fdb00 | |||
| 9b10287899 | |||
| 2a388e2db3 | |||
| f5b570663a | |||
| 78f9e09a4a | |||
| 3a0e123c67 | |||
| f5c851fa84 | |||
| ef15b7b3d8 | |||
| 901bd74077 | |||
| 28905885e5 | |||
| cebdb421a3 | |||
| 00168425c2 | |||
| 68f67c5ae8 | |||
| c242d89cda | |||
| 66c3eb2820 | |||
| b2a631e93a | |||
| fe36e77412 | |||
| fffb056ad3 | |||
| 0ea47118e1 | |||
| c1ac0f186c | |||
| dd76245393 | |||
| ed6958d05f | |||
| acdd50aada | |||
| 33a0ed67df | |||
| f21a7a25d2 | |||
| 5f01b823da | |||
| b0e7b43958 | |||
| 675ab840f2 | |||
| a225ab71e4 | |||
| 9242dfa355 | |||
| 505f36a95f | |||
| 1ed084557a | |||
| 73e9047cd5 | |||
| 340d2191ff | |||
| 55a1a3afd6 | |||
| ab7a474ab6 | |||
| 8a9b2fc16f | |||
| af8fbad281 | |||
| bbce738523 | |||
| 66a97ff45d | |||
| 7266279768 | |||
| 59d6ae17be | |||
| 1aa9dd02d6 | |||
| 784087384c | |||
| 838fbb76ab | |||
| 7bba034efd | |||
| 106db716f8 | |||
| e76356aff5 | |||
| 0db1869356 | |||
| dc4268b6e7 | |||
| af95d56ceb | |||
| 0d4010c38c | |||
| 90ac387444 | |||
| 18c1eda09b | |||
| 2ec1302c10 | |||
| fad1c19856 | |||
| 3ae3cc63df | |||
| d50e221e62 | |||
| 1552d81f45 | |||
| b2a3fe6879 | |||
| 6541782758 | |||
| ab0f9d78ee | |||
| 23e8932a44 | |||
| 400cbd1836 | |||
| 871f1aabb7 | |||
| 6d7172ac44 | |||
| 49b9b463b4 | |||
| 43213cc6ff | |||
| 6e5a9fe4c9 | |||
| ca7ad8a49b | |||
| 8261c988b9 | |||
| db0645ed1b | |||
| 31d15da49e | |||
| 692fa390c6 | |||
| ad2080ab3e | |||
| 6000e68420 | |||
| 1d36878938 | |||
| b970ddeb66 | |||
| f7179f7c93 | |||
| a00f0ff687 | |||
| 1c1005247e | |||
| 963ff8c620 | |||
| 395bf49198 | |||
| 2ecd63234d | |||
| bd6d4d5d2d | |||
| 1ec1abdc33 | |||
| 800c6223ed | |||
| 3ba1eb6baa | |||
| c60c4b9abb | |||
| 7e023419de | |||
| a74a081e61 | |||
| 6df5cb8878 | |||
| b01aaa4d03 | |||
| 242247be47 | |||
| 172b2587ab | |||
| ffc4d87263 | |||
| 3a7e41e177 | |||
| deeca484d8 | |||
| 1f6a6ae86f | |||
| d9cc45851e | |||
| 6731d6c505 | |||
| 2c0230ba93 | |||
| 35ce88f1e5 | |||
| 55777eba73 | |||
| d02cf8f0b7 | |||
| 2f25206fd5 | |||
| f47db6e9fa | |||
| 45c5b503c0 | |||
| 33c2e754af | |||
| f9d16b5bbb | |||
| f2a65437a6 | |||
| 5a27245fcc | |||
| 3e39453905 | |||
| 7e31cc4100 | |||
| 591a51e4bc | |||
| 081a8b0ed0 | |||
| 9d7c90e9da | |||
| 0336e5275c | |||
| c9533fc9fd | |||
| 3952232214 | |||
| 335f763c80 | |||
| 8b8604b6e2 | |||
| 225ae7fe6a | |||
| 60b666feee | |||
| e0c63e12e4 | |||
| e386d60831 | |||
| b0b0eb66df | |||
| 10604bf49c | |||
| a64cec2bdc | |||
| 75e5f325a9 | |||
| bf7c52a9ee | |||
| 7bd33be8f7 | |||
| 68a37cb71b | |||
| ecd34ce470 | |||
| 4a911bbe90 | |||
| 3bb872a5e7 | |||
| fa40e4e888 | |||
| 4b1cb96446 | |||
| 950eaf230e | |||
| 89c60bbee6 | |||
| 36e1e58dad | |||
| 3bbb7e38ea | |||
| c2beeb4c79 | |||
| e365e913c7 | |||
| efb5cd6545 | |||
| 908a2e817a | |||
| 306dfc4ae8 | |||
| e3ff2ccc97 | |||
| 24804f066c | |||
| 775ea1c8c6 | |||
| 80930d72a6 | |||
| 0adac268ee | |||
| 976cc1ab15 | |||
| 1cc5b6126d | |||
| 0d1d25e868 | |||
| 13974d2508 | |||
| f419d7870d | |||
| fb5fac164d | |||
| a935f1e4de | |||
| f61bf346c0 | |||
| d8a9c9422a | |||
| 12a64c0ffc | |||
| 220f8c6b5f | |||
| e3f983729f | |||
| 8f5fb4e32b | |||
| 75daa44c5a | |||
| 1c2dd884e9 | |||
| 238581ee7a | |||
| 127b0a2e50 | |||
| 3f58c19976 | |||
| 5e32182c72 | |||
| 009db49502 | |||
| 01124292b1 | |||
| 103f27cfd0 | |||
| a31ceb51a0 | |||
| d778beacbb | |||
| 94488c86af | |||
| 0800ed725f | |||
| 2fd5fd8e49 | |||
| 9e2d83f542 | |||
| cb8c91ea8e | |||
| 2893af870e | |||
| af139ffbab | |||
| c26099280f | |||
| fe9f2d005e | |||
| 1d73c7c27d | |||
| d023e75920 | |||
| 063ddd62e6 | |||
| 7f9a6ffc53 | |||
| 7dda3f5803 | |||
| d928792eb4 | |||
| 9dd077d69e | |||
| ceb2631a56 | |||
| a66e8768c9 | |||
| d1eb6d4fed | |||
| 023c155a25 | |||
| 6e395ad7c9 | |||
| 0753f427b1 | |||
| e20d9c8f98 | |||
| fc2a3c9f17 | |||
| 703ec3ccc4 | |||
| 8dd8c24595 | |||
| 76ae539e61 | |||
| 2a9e50a6a9 | |||
| daf564b62f | |||
| 1d9933412a | |||
| 395ac5f6dc | |||
| 491d2cb024 | |||
| 8cf0e4a316 | |||
| 6d3c94a739 | |||
| c6f3a3bbca | |||
| e86980befa | |||
| e29ce218eb | |||
| e88e259033 | |||
| d29a425baa | |||
| b0b036c457 | |||
| d0b5c7d216 | |||
| 25bed7bb87 | |||
| 7287e9da1d | |||
| 0c22710ddd | |||
| f2696c9609 | |||
| 24cab00479 | |||
| 974cf6c365 | |||
| 95b35e452d | |||
| 81633b7c2e | |||
| d14f50f50d | |||
| 531b5727f2 | |||
| 8364fc1bd2 | |||
| c4a0910908 | |||
| 1b228e3705 | |||
| e8fedb685b | |||
| 11c9f96d23 | |||
| 59f9f4d467 | |||
| 1e9fa4c041 | |||
| 6a271317bc | |||
| 1df01a2634 | |||
| c19d6b4e29 | |||
| 459e5e67bd | |||
| b49ff3d5bc | |||
| 2615b0297e | |||
| c06759223e | |||
| 516b49ff50 | |||
| d25e82d095 | |||
| 5608aaca26 | |||
| 7171fd1132 | |||
| c9e6137ad0 | |||
| cf0b37057c | |||
| 69f61ef767 | |||
| 4177afdf8b | |||
| 678c9ae67f | |||
| c475c81841 | |||
| fb56889b43 | |||
| 914d7350fa | |||
| b1367ac46f | |||
| 3d0e1d142f | |||
| 54732b72fd | |||
| e7e6f719e4 | |||
| 5134bf8ec3 | |||
| cb45689c1d | |||
| 14b501a4f7 | |||
| 68ac934929 | |||
| 57eed50acb | |||
| c37b7b77e4 | |||
| da261003df | |||
| 99a4a64052 | |||
| 4690810d5d | |||
| 98db27e8f4 | |||
| d40a631565 | |||
| 5a8f34feac | |||
| 8086d90535 | |||
| 829724c0ec | |||
| 68a8c79c08 | |||
| e99af87b6d | |||
| 3948890c3b | |||
| d1e9e70396 | |||
| f761dc4e1b | |||
| 666b3bf718 | |||
| f7c6828e6a | |||
| 0329c0c3f9 | |||
| 2dddf7fbc7 | |||
| 0ff2c66642 | |||
| c513d1077f | |||
| fc39504cc2 | |||
| 1976a79645 | |||
| 5cc472e2ed | |||
| 17c16fd4cb | |||
| 598478e48d | |||
| 15fdaecd7f | |||
| 43d0c7ff98 | |||
| a64b641fdf | |||
| d320ea052f | |||
| 185ea9c98c | |||
| 2dd3b34136 | |||
| ad4996259e | |||
| c5dc21e80c | |||
| c20b074880 | |||
| 94a6bc608c | |||
| 741834301f | |||
| f937818b80 | |||
| c4f8ff95dd | |||
| dbf1f0897e | |||
| 7a47500b22 | |||
| cb09ef7180 | |||
| ce75a032d0 | |||
| 5dccfab89c | |||
| e836fe58b1 | |||
| 0a05099713 | |||
| 9bc17a9232 | |||
| 4a4be27ebc | |||
| ca1a616b89 | |||
| 818da02f6c | |||
| b545fc5590 | |||
| 1056ff0d18 | |||
| b973b6255c | |||
| f30fc29da0 | |||
| 4a295d1910 | |||
| f1e03a6873 | |||
| 670a584d7e | |||
| 2f9e6c990c | |||
| 66f5f76a6c | |||
| 46f4fd79af | |||
| 5ed072c489 | |||
| 94e190b954 | |||
| 9851d67f07 | |||
| f99bdc4393 | |||
| caf524c685 | |||
| f9124ef5b9 | |||
| c297d99975 | |||
| 6f401a9e15 | |||
| ea566c6eb7 | |||
| dfe8b3e832 | |||
| 3fb5cd3df6 | |||
| 7ff35fea3c | |||
| f049268354 | |||
| 362974b831 | |||
| 14bca509da | |||
| b2ea464250 | |||
| d73e7f292a | |||
| 70e48ca43a | |||
| d34b15d6a9 | |||
| 8dbe6b1c16 | |||
| daabc8ffbe | |||
| 45193127e3 | |||
| ccc59619d1 | |||
| c291d69533 | |||
| 8aec71e27e | |||
| 6c6d2a0f43 | |||
| 8a8cd67988 | |||
| b67bb0fe28 | |||
| d1b600e7b0 | |||
| e58f8e36c4 | |||
| 342d43e6bb | |||
| 7c95848271 | |||
| 864f53bc4b | |||
| f1fcd0760d | |||
| f1730ab51b | |||
| dba21c815a | |||
| c055f82e9a | |||
| 9caa74c796 | |||
| 8620f1178d | |||
| 1370ee498c | |||
| 501a290f81 | |||
| 4ed670f828 | |||
| f6e93114e6 | |||
| 6bc8759321 | |||
| 22efd0bee6 | |||
| 1d40162e9d | |||
| 20d17cbc52 | |||
| a78672c10b | |||
| 6a0a33739b | |||
| b9f142c31e | |||
| 6e814af36d | |||
| f814146093 | |||
| daa9f6cc19 | |||
| a23ae6a979 | |||
| 90ce2ae7e4 | |||
| 3c64c6b034 | |||
| e25f6986d6 | |||
| 95cdb8aa04 | |||
| 4557701daa | |||
| 3b377149e4 | |||
| c0045bad34 | |||
| dcf9bbdaea | |||
| 84d2d5e2a6 | |||
| 0afd5a7385 | |||
| 44bf59668b | |||
| 2131205db6 | |||
| b2b19915e6 | |||
| bba6f8e133 | |||
| a6d2233b95 | |||
| 9857675a5e | |||
| 4ab047dfa7 | |||
| 476ed938f5 | |||
| 40ffac9de0 | |||
| b892d373cd | |||
| c3647e49ad | |||
| 37ed37dc76 | |||
| 5cb688c112 | |||
| 3e394d0612 | |||
| c4c2298686 | |||
| 8564dc10b2 | |||
| 3fb892fcb8 | |||
| 9968348324 | |||
| fa293c54f8 | |||
| 95449ca886 | |||
| 70fa4a53cd | |||
| 467c63ff01 | |||
| b8a9c200fe | |||
| 7c10af65a1 | |||
| e2cd23b1d2 | |||
| 0d408d3d43 | |||
| 2309197771 | |||
| 66fe9abce0 | |||
| 200c18f3e4 | |||
| 351b5f6e65 | |||
| 605cc20a21 | |||
| f73d2a5371 | |||
| 485a074674 | |||
| 865cf5232b | |||
| 95a24c3133 | |||
| 6833059c70 | |||
| 3833dc0b78 | |||
| e0d3c771db | |||
| 5a18ab0784 | |||
| 1d66f82b1d | |||
| 2e765fe6d1 | |||
| 21ea02bbcf | |||
| 2ea0157197 | |||
| 03352f3b62 | |||
| 26eb4f7fe6 | |||
| 7e1f3aa545 | |||
| 3505fbe783 | |||
| 14532e3a56 | |||
| a449f7c78c | |||
| 8854ef8cba | |||
| 526943f29e | |||
| cf770d496b | |||
| bfd9e35e34 | |||
| 299e788891 | |||
| 4c1de4ad56 | |||
| ed57e7d43b | |||
| 818d18d4e0 | |||
| b6aac5079b | |||
| 40450ebecc | |||
| d532da9071 | |||
| df51111c33 | |||
| dd8900a1c6 | |||
| 5404905d28 | |||
| bed51fa790 | |||
| f5a5c2d6b9 | |||
| a102cfdfc9 | |||
| be72670ca2 | |||
| cf2cb94f8d | |||
| fa3a81b022 | |||
| 7ff30c6df8 | |||
| 7751768b2e | |||
| 9c2cdd4fb9 | |||
| 69b9b35a08 | |||
| c2c97d9f78 | |||
| 48d3c8e62e | |||
| ac817b7808 | |||
| 4d4f4bf23e | |||
| c083723698 | |||
| f8d89c46e5 | |||
| 1952e453bb | |||
| 77985fa591 | |||
| a75d891007 | |||
| dae3f72be7 | |||
| f03a99918a | |||
| a655524221 | |||
| 26738370c7 | |||
| fe02f611fb | |||
| 1b10a3a2bf | |||
| 92a060c5b4 | |||
| 096fd1916c | |||
| fb09a16127 | |||
| 6b204c97ed | |||
| 0c4574b3b7 | |||
| d9d9993179 | |||
| 7b494c8333 | |||
| bc9454e0f9 | |||
| 36a0a14a23 | |||
| c137666230 | |||
| 7fed0782d5 | |||
| bd3b70293f | |||
| 30fc24bd8c | |||
| 7e3de178e1 | |||
| 0c9c9fff0e | |||
| b96f6670e3 | |||
| 6e02743256 | |||
| 2b4fa92d09 | |||
| be250230b6 | |||
| 5d33ffc015 | |||
| b48498f27f | |||
| e582d8bacb | |||
| ff40ee655b | |||
| 57deaad806 | |||
| 7779b82277 | |||
| 2bd2058afa | |||
| bf7936b0af | |||
| 8236bbfd48 | |||
| 4dc13ac16a | |||
| eb5423469a | |||
| 43496d7929 | |||
| 92c70b6b90 | |||
| 77897c7d6b | |||
| 531861573a | |||
| c9b904eb0e | |||
| 372f1cb37f | |||
| a3acdd5240 | |||
| e6a125719e | |||
| 78a1551798 | |||
| 6f79d14c9c | |||
| 28d8722fa7 | |||
| 2715b2ccf0 | |||
| 2ea575cb31 | |||
| 1b31c54162 | |||
| e289c10b6c | |||
| 26ed1ca07c | |||
| b1e20bcd1e | |||
| 12a73bc151 | |||
| 19e112f399 | |||
| cfc0410388 | |||
| cccf4f305b | |||
| 4c608e7167 | |||
| cab82e8e60 | |||
| dc7e834911 | |||
| a630799984 | |||
| 916e1bbc7c | |||
| 631cb44f5c | |||
| 367186cc34 | |||
| 92f34f262e | |||
| 5e13b48648 | |||
| 038a111b45 | |||
| 6dfb1a1d14 | |||
| f8330800d1 | |||
| 3ec7c72da1 | |||
| 2ea77b22e0 | |||
| a8b1352021 | |||
| 355fde3bca | |||
| fa7c29fe9f | |||
| 861c577138 | |||
| e062a74e70 | |||
| c330c493d5 | |||
| 8a49d62068 | |||
| a642524928 | |||
| eb96490c99 | |||
| 6282b42741 | |||
| 513df4515b | |||
| 411e21f430 | |||
| f0b5f95fd6 | |||
| 736c396d98 | |||
| 5a7ca35c6b | |||
| 077a947972 | |||
| 8ac3a94358 | |||
| dfbebdea9b | |||
| b795a70102 | |||
| 026b6a39a9 | |||
| 2860e817bd | |||
| 19b78fbc22 | |||
| cde432fef0 | |||
| 7cbc0ce80a | |||
| 8ae44c204e | |||
| ed0e7ead31 | |||
| 3928051baf | |||
| e35c85000e | |||
| 3cabcabcbd | |||
| 85776db692 | |||
| ce81af08d8 | |||
| 5aa6c1dfae | |||
| 4f4dfa2a59 | |||
| 90669e0ba9 | |||
| bc9f6d30c1 | |||
| 4ae2333306 | |||
| 8c63e3dc4f | |||
| b0dddd35ca | |||
| 96ba75179b | |||
| 2589717375 | |||
| bc0816aa66 | |||
| 1743ad7946 | |||
| 9367cbcfd3 | |||
| 43a7b9236b | |||
| 4891174a71 | |||
| 8845f765db | |||
| 7e11bce4f4 | |||
| 8955e09175 | |||
| d13ea71a58 | |||
| b72f61080b | |||
| 75c31cc8cc | |||
| 1b3d9efedd | |||
| 2f8f60373e | |||
| 55781e7f10 | |||
| 72284317c2 | |||
| 80a27bc0db | |||
| 1c9abd9e35 | |||
| c14ac8a205 | |||
| b09fb5826f | |||
| fb1541bdf6 | |||
| 444d18aa39 | |||
| 91ab4abba8 | |||
| 16057da6cc | |||
| d97500581d | |||
| f1e831a7b8 | |||
| 31a396bc25 | |||
| 7cdcd97c26 | |||
| 73b59df77b | |||
| 86aef7cf9d | |||
| 159090c0e7 | |||
| 0cb28f3d82 | |||
| d0d0cbe1d1 | |||
| 02078456fc | |||
| 01dfb1cba8 | |||
| ee205ddc86 | |||
| 298f5685ee | |||
| 486d8a48a0 | |||
| d426077445 | |||
| 9aa455fcd4 | |||
| d9c8b322ce | |||
| 68154a1f52 | |||
| f7c1ee6d3e | |||
| 9c6a49436b | |||
| 75464c22f5 | |||
| cdd44a4005 | |||
| 34313a7af6 | |||
| 4053ee4581 | |||
| 56170dba19 | |||
| 79a2de7a64 | |||
| c0a57d352f | |||
| cbdd86d777 | |||
| 281dd7785e | |||
| ad58bac810 | |||
| 8928d3616a | |||
| e8cffeeffd | |||
| 76d289f0ce | |||
| 245ae99273 | |||
| 70ad7b42b1 | |||
| 0ece73578c | |||
| b317524ed7 | |||
| 8903ba5d89 | |||
| 469166636c | |||
| eabd321281 | |||
| 45c6ae446f | |||
| 952e641213 | |||
| c44b5b1b3a | |||
| fc8625c5c5 | |||
| 150c5510c7 | |||
| 36a005754a | |||
| 479aafc331 | |||
| bdf19f1d66 | |||
| 8cf3e9f91b | |||
| ebebcb886c | |||
| 36c45fd14f | |||
| 0128b63c1c | |||
| e16db814fa | |||
| f81e3d8667 | |||
| b9c7d338b3 | |||
| 4f93106755 | |||
| 02bccd0097 | |||
| 1ba01746a0 | |||
| 83a7d888bc | |||
| eba82360fa | |||
| 3fa23860c0 | |||
| a80afc8f1b | |||
| 97339e14cf | |||
| 443263803c | |||
| 9906e7d646 | |||
| e8f040bfbd | |||
| 97c420b2df | |||
| a4b617e482 | |||
| 7b5e322ef2 | |||
| c06cd38951 | |||
| 0a55753faf | |||
| 6b4d9f97c1 | |||
| 639987cbab | |||
| 56c2aa89bc | |||
| bf4aa91aab | |||
| 500c401b75 | |||
| 81a2cbb4eb | |||
| 0510cf4491 | |||
| 68728409aa | |||
| c00ffcee59 | |||
| 9aec1ddb17 | |||
| d98890f32e | |||
| f659f8e309 | |||
| 54db239175 | |||
| 601c37f862 | |||
| 501e746c52 | |||
| d04146d1b1 | |||
| ea08931ab3 | |||
| ddd1b5c0ff | |||
| e08d8190ae | |||
| fbf7049ac5 | |||
| 2a1a8c0e64 | |||
| 833aaf8e10 | |||
| 566346dd87 | |||
| d0a33d2ee7 | |||
| fab505be1b | |||
| 66c326b789 | |||
| 2f386913ac | |||
| 4f4bfdac4d | |||
| 8b6ea32c4c | |||
| ff497d5c90 | |||
| c05db6742c | |||
| 75f75f3881 | |||
| a4e4310d40 | |||
| dfc3524334 | |||
| a0913588b8 | |||
| c56b344077 | |||
| cb1f971d4b | |||
| 78e64be04e | |||
| 3175121030 | |||
| 8d649988ca | |||
| ec7e7e744b | |||
| 5c754eb4d3 | |||
| 54d8aa7782 | |||
| 4690244673 | |||
| 2de5a59d89 | |||
| 98685f1c98 | |||
| 88e93b4902 | |||
| dcca51985d | |||
| 21f2f67ffa | |||
| c78342b194 | |||
| 29b9be9bd0 | |||
| 4543a1fe02 | |||
| fc7c8cce3c | |||
| 7d1559f319 | |||
| a43502093d | |||
| 47e84ad106 | |||
| 5ade5777e8 | |||
| fb0e824a83 | |||
| a7c7f720c0 | |||
| 224f289ec8 | |||
| d84ece7258 | |||
| 6c5dc7e0a9 | |||
| df20757d21 | |||
| a2ce288241 | |||
| ce3efa8f00 | |||
| c92f28bf6f | |||
| 222ecdecd2 | |||
| 1c11a5f048 | |||
| 903a1dc3e5 | |||
| 6f9a8a089c | |||
| 8bee499328 | |||
| 236499a195 | |||
| 3d91dd8a98 | |||
| 9ccc3e52ec | |||
| f5f151fcc5 | |||
| f455e3327c | |||
| cd9c2c4c23 | |||
| af6fc886f6 | |||
| 7aa56adf15 | |||
| 719faab4b8 | |||
| 9f477aa3c9 | |||
| 61ac36c576 | |||
| 366c148c10 | |||
| bf3f2e4de4 | |||
| 763f4f4a3e | |||
| 4925d8f580 | |||
| b1f88e8861 | |||
| a49f62eecb | |||
| 62c8dd98d5 | |||
| b0a7b64d44 | |||
| d808dd49e8 | |||
| 818d2bf92a | |||
| f98a12c26c | |||
| 477dc50425 | |||
| fab9ff1294 | |||
| 1c91b4427b | |||
| 244662b1a4 | |||
| 186fe5933b | |||
| 8ab35bbaf3 | |||
| 9044052b4e | |||
| 209811d23a | |||
| 764d5507a3 | |||
| 628f6b8b7c | |||
| 0d3de07012 | |||
| db0f449d93 | |||
| 774eacc561 | |||
| e3e4fbd5ba | |||
| b7709126f9 | |||
| 4cfbc55d34 | |||
| 00054dcfde | |||
| 9d6e973e5b | |||
| 6ed337faa3 | |||
| e579ff9532 | |||
| ae361e1d5d | |||
| d03fe1f8ee | |||
| 8865af9104 | |||
| aa54b77702 | |||
| 8073989c98 | |||
| d45599ca3b | |||
| b469addffb | |||
| 47ab285252 | |||
| 95ff59a21c | |||
| 7e08e3a59a | |||
| 4550447409 | |||
| 8f29312c9e | |||
| 5c280d5649 | |||
| b23cea6e59 | |||
| 487469680f | |||
| 8fd13933c3 | |||
| cf70deaf8d | |||
| 3d31eca365 | |||
| d723979c42 | |||
| 366740885a | |||
| 918889a2bd | |||
| fc6d7f012e | |||
| 1947fab3d7 | |||
| cdb97e64ab | |||
| daa59f6248 | |||
| 9c8c30b0e8 | |||
| f3a1177bad | |||
| ad5afd3047 | |||
| 458bfcc89b | |||
| d4122c36ac | |||
| 0e663a5bf8 | |||
| 562efd1841 | |||
| 7baa2b9005 | |||
| 10c5adfa50 | |||
| 44c4729a9d | |||
| dc6af9a1a7 | |||
| 82707be7d0 | |||
| b800f27092 | |||
| 31daf72cc6 | |||
| 22ebf04daa | |||
| 52a091e063 | |||
| d7ea750823 | |||
| b6096efadd | |||
| b927c9dc01 | |||
| 523a58d3d6 | |||
| fbca8e6587 | |||
| a10f78e3ef | |||
| 8987e5f108 | |||
| f584edf809 | |||
| f5848ea891 | |||
| b5c4f9ebe2 | |||
| 0911cd72a2 | |||
| d2a412d2c6 | |||
| cb086f79ff | |||
| d9bff68501 | |||
| 5bfee44bba | |||
| 0012fe36ca | |||
| d186f8f1e1 | |||
| cb17b36981 | |||
| aa283a0447 | |||
| f9fdf1c31b | |||
| 1cf0e7be24 | |||
| 82cb107520 | |||
| b23841fbfe | |||
| 8726a4645d | |||
| 59d2ff3ffa | |||
| 39c651e40c | |||
| a2336f256b | |||
| a76ca771f8 | |||
| f722823b0d | |||
| a3988f56b2 | |||
| 5a467eb969 | |||
| 5f8202e1b5 | |||
| bfc7f48f17 | |||
| 5b2a291109 | |||
| d3a3ddbc61 | |||
| 8a9f2aedbb | |||
| e88a0d5248 | |||
| 2ef11faba7 | |||
| c9eee2944b | |||
| 6f962362f2 | |||
| ba5de0cd00 | |||
| 3081b9402b | |||
| 30fd1e742e | |||
| 4d8e3c25bd | |||
| 1132fa6093 | |||
| 29dfb5c169 | |||
| d10ee0979a | |||
| 0318486bee | |||
| 85e345fc48 | |||
| 1597c3aa89 | |||
| 7d26df01b8 | |||
| c8296ccb2d | |||
| 8d60327d60 | |||
| 04564dc134 | |||
| 6161b858c4 | |||
| 1921a07b89 | |||
| b65ade51be | |||
| dfbb2e2b35 | |||
| 1805db2b07 | |||
| 76fbec0c17 | |||
| 29d337fa02 | |||
| 2c7ae756f5 | |||
| d9dc831772 | |||
| 4241bff32a | |||
| 5dd60eda36 | |||
| 8acdd0b47c | |||
| 125085fbaf | |||
| 7eedcb9c14 | |||
| e6e747bcd8 | |||
| 348a08f1c4 | |||
| b1ac2bf515 | |||
| b710bdaf6c | |||
| 27fa297209 | |||
| 85e64cd121 | |||
| 0d876d7a89 | |||
| 69e5377f3d | |||
| 532ecaf2c8 | |||
| d779d60812 | |||
| c4a80e33ea | |||
| cab1b750b3 | |||
| 9d285e3dc0 | |||
| fff08f737f | |||
| ca789b3282 | |||
| 11eea9b4e1 | |||
| de015a2d7e | |||
| 4cfc7e4427 | |||
| 0a525c6d32 | |||
| ae8c426025 | |||
| 0fe72510d5 | |||
| fecb9db072 | |||
| 30ac648539 | |||
| 25fd4a04d6 | |||
| 9750e9ca4e | |||
| a57b033745 | |||
| 48e16f6aba | |||
| d1d9e25c2e | |||
| 57969f8b01 | |||
| 8484427cf8 | |||
| f4c17be8de | |||
| 0bdd238d7f | |||
| 1bb697e58c | |||
| b072fae507 | |||
| 9fa6bfa655 | |||
| da44b39423 | |||
| d0d6f53dec | |||
| 751b205618 | |||
| d80760d20c | |||
| 5dd919b7ad | |||
| 108a578772 | |||
| 9444bbb6f3 | |||
| 7c0c98a368 | |||
| c1d395a7d8 | |||
| 3f6795962f | |||
| 60e651b481 | |||
| 548db18857 | |||
| aec11618ce | |||
| f0cbb4f949 | |||
| 027e023443 | |||
| 51c15d894b | |||
| b262f0b374 | |||
| a3dee9350f | |||
| d0045673fa | |||
| d92971cca1 | |||
| 87b7513401 | |||
| c03c3a5706 | |||
| 9573974c47 | |||
| 6e9ff5fdd8 | |||
| 022f85095e | |||
| 6a0848a3a9 | |||
| 13376fdad8 | |||
| 5b0c143713 | |||
| 5d0e14b564 | |||
| 38050b5346 | |||
| b1a5776f14 | |||
| 7a7f16b658 | |||
| 684d310ea0 | |||
| 49bfa556bf | |||
| e228733f1a | |||
| 103bd9e2f2 | |||
| ba38a826e9 | |||
| 8103656ae1 | |||
| b980f45b2b | |||
| b4b8dde4fb | |||
| 59d57d3466 | |||
| f0f72fdd33 | |||
| 388dfec50b | |||
| 874413ccc5 | |||
| 4a35d32b6a | |||
| a1166b1077 | |||
| e5c9cde36f | |||
| b5f55c9b14 | |||
| 7c09c01788 | |||
| 0f914cf2bd | |||
| d175ab495b | |||
| f2f4158974 | |||
| 764001a4c2 | |||
| b65cff0adc | |||
| db4f4498dc | |||
| c2c039151c | |||
| 8765e3a4d6 | |||
| f6b3998bbd | |||
| 0691bbaad9 | |||
| 101d9ab87f | |||
| 65a5cf64df | |||
| 608a7c2d38 | |||
| e59eaf33e0 | |||
| 47b66f3220 | |||
| 491f49388c | |||
| bb116456a9 | |||
| 13b1a3e737 | |||
| 98791752a9 | |||
| 0bd9b00132 | |||
| 39a658eac2 | |||
| 3c019e0e16 | |||
| 41e27ba621 | |||
| 3a9d83f86c | |||
| 9d455f58b1 | |||
| 829e10ff87 | |||
| b62830031f | |||
| a553a9923a | |||
| a629d455fb | |||
| feabed30a3 | |||
| 2ca8b0b12e | |||
| bcdf4e0fe8 | |||
| 78e5ec13bb | |||
| 8b51f5f563 | |||
| 756c284ecd | |||
| d1b2e38ae9 | |||
| dd10dec73d | |||
| f822f1795a | |||
| 386915378b | |||
| 2f1c5cf143 | |||
| 3706d28125 | |||
| 0707e70183 | |||
| bebee15d10 | |||
| 5660036f47 | |||
| 262f03bc92 | |||
| 244fd0e731 | |||
| fe6af0ef5d | |||
| fd63f50221 | |||
| 5c13fbb0b8 | |||
| 9a5b090894 | |||
| 5a3f23f00c | |||
| 8b347dfdcf | |||
| deca5479f0 | |||
| 2ea71d466c | |||
| 200f5ac157 | |||
| 9e77effacb | |||
| f5f883202d | |||
| 594757d27d | |||
| fed5d87cfd | |||
| adf5b7f233 | |||
| 1b4c831469 | |||
| 78e7ab92d8 | |||
| 6e45e998ac | |||
| a75e9f193f | |||
| 0899e5cb83 | |||
| 39331b59ed | |||
| 65d1598a90 | |||
| 46b987042b | |||
| 75d1dd2793 | |||
| e5c68661fe | |||
| e482feed7d | |||
| 87fe4108a2 | |||
| 02c831a4e7 | |||
| bcd416c83d | |||
| 1d5608d627 | |||
| 79a14bcbe7 | |||
| 81bc515e5d | |||
| 201522f1b1 | |||
| 44b1005077 | |||
| 48b21d00d2 | |||
| e83eefb71d | |||
| 345a47ede7 | |||
| 03d41bdf46 | |||
| 05f3884722 | |||
| aaa0f49f31 | |||
| 303c628998 | |||
| 8cab2e85be | |||
| a4423778d5 | |||
| 2a7f86bfb4 | |||
| 7add902bc7 | |||
| cc78054b8c | |||
| 533f97f080 | |||
| 5b0bc5bbc5 | |||
| 6f7ab97fc3 | |||
| 27676f4aa2 | |||
| 79dc972e5a | |||
| 66c2e145cb | |||
| d3d7cb1b14 | |||
| e88bb4e05c | |||
| 7e7ae144a9 | |||
| 305eda74e2 | |||
| 84d905a648 | |||
| 32ce819889 | |||
| 26315b6bc2 | |||
| d014e4590e | |||
| c8a4a773ee | |||
| ff3aa7c1a9 | |||
| 84b8cee004 | |||
| 6d9e50d60c | |||
| be352ae014 | |||
| 563742f13c | |||
| 7bcae7b665 | |||
| cb80d7c26f | |||
| 6b829d839b | |||
| bf968a9fd8 | |||
| b4ea37d598 | |||
| 549a0e1c44 | |||
| 2bc9413be1 | |||
| e6766b9b82 | |||
| 75bc5809a9 | |||
| 986bc63e54 | |||
| fd4e27d889 | |||
| 2261cbd92e | |||
| 3033e27466 | |||
| 8927a92eaf | |||
| 5fb539190d | |||
| ade64f25d3 | |||
| 72af1912ca | |||
| 08ca0f7c0f | |||
| bcef00edee | |||
| 06edc5c044 | |||
| 1a74ede126 | |||
| 07c886a2b1 | |||
| b73089deb8 | |||
| 69a63975c1 | |||
| a3cc001f1b | |||
| 97a6fb285f | |||
| feb6accc6c | |||
| 4435c4fd0d | |||
| e55638ed03 | |||
| ed55296d20 | |||
| 3cbe51c3ca | |||
| dc25668468 | |||
| 71ec32ac9e | |||
| 697fad0ac4 | |||
| 0817e1698f | |||
| 61d7129d7c | |||
| df25dbc048 | |||
| a712c5d42c | |||
| e89609dc3a | |||
| 66f7c91357 | |||
| 762dd4f024 | |||
| a51e44eea3 | |||
| 82218d01f4 | |||
| a6356c2821 | |||
| c6f045afa9 | |||
| 126b8dac07 | |||
| 70e9fa6136 | |||
| 3adb3d9b1e | |||
| 6f08b610d6 | |||
| f5359985e8 | |||
| d3443beaf9 | |||
| 854f056eaf | |||
| 5a4e99b413 |
+57
-21
@@ -14,9 +14,10 @@ on:
|
|||||||
- cron: '0 5 * * 4'
|
- cron: '0 5 * * 4'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}"
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
permissions:
|
||||||
|
repository-projects: read
|
||||||
jobs:
|
jobs:
|
||||||
build_linux:
|
build_linux:
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -76,6 +77,17 @@ jobs:
|
|||||||
# Allow failure for coveralls
|
# Allow failure for coveralls
|
||||||
coveralls || true
|
coveralls || true
|
||||||
|
|
||||||
|
- name: Check for repository changes
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "Repository is dirty, changes detected:"
|
||||||
|
git status
|
||||||
|
git diff
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Repository is clean, no changes detected."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Backtesting (multi)
|
- name: Backtesting (multi)
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
@@ -90,14 +102,14 @@ jobs:
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
|
||||||
run: |
|
|
||||||
flake8
|
|
||||||
|
|
||||||
- name: Sort imports (isort)
|
- name: Sort imports (isort)
|
||||||
run: |
|
run: |
|
||||||
isort --check .
|
isort --check .
|
||||||
|
|
||||||
|
- name: Run Ruff
|
||||||
|
run: |
|
||||||
|
ruff check --format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts tests
|
mypy freqtrade scripts tests
|
||||||
@@ -115,7 +127,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest ]
|
os: [ macos-latest ]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -173,6 +185,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pytest --random-order
|
pytest --random-order
|
||||||
|
|
||||||
|
- name: Check for repository changes
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "Repository is dirty, changes detected:"
|
||||||
|
git status
|
||||||
|
git diff
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Repository is clean, no changes detected."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Backtesting
|
- name: Backtesting
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
@@ -186,14 +209,14 @@ jobs:
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
|
||||||
run: |
|
|
||||||
flake8
|
|
||||||
|
|
||||||
- name: Sort imports (isort)
|
- name: Sort imports (isort)
|
||||||
run: |
|
run: |
|
||||||
isort --check .
|
isort --check .
|
||||||
|
|
||||||
|
- name: Run Ruff
|
||||||
|
run: |
|
||||||
|
ruff check --format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts
|
||||||
@@ -212,7 +235,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest ]
|
os: [ windows-latest ]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -236,6 +259,18 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pytest --random-order
|
pytest --random-order
|
||||||
|
|
||||||
|
- name: Check for repository changes
|
||||||
|
run: |
|
||||||
|
if (git status --porcelain) {
|
||||||
|
Write-Host "Repository is dirty, changes detected:"
|
||||||
|
git status
|
||||||
|
git diff
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Repository is clean, no changes detected."
|
||||||
|
}
|
||||||
|
|
||||||
- name: Backtesting
|
- name: Backtesting
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
@@ -248,9 +283,9 @@ jobs:
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
flake8
|
ruff check --format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -301,7 +336,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Documentation build
|
- name: Documentation build
|
||||||
run: |
|
run: |
|
||||||
@@ -321,7 +356,6 @@ jobs:
|
|||||||
build_linux_online:
|
build_linux_online:
|
||||||
# Run pytest with "live" checks
|
# Run pytest with "live" checks
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
# permissions:
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -425,7 +459,7 @@ jobs:
|
|||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@v1.6.4
|
uses: pypa/gh-action-pypi-publish@v1.8.6
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
@@ -433,7 +467,7 @@ jobs:
|
|||||||
repository_url: https://test.pypi.org/legacy/
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.6.4
|
uses: pypa/gh-action-pypi-publish@v1.8.6
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
@@ -466,12 +500,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and test and push docker images
|
- name: Build and test and push docker images
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: freqtradeorg/freqtrade
|
|
||||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
||||||
run: |
|
run: |
|
||||||
build_helpers/publish_docker_multi.sh
|
build_helpers/publish_docker_multi.sh
|
||||||
|
|
||||||
deploy_arm:
|
deploy_arm:
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
needs: [ deploy ]
|
needs: [ deploy ]
|
||||||
# Only run on 64bit machines
|
# Only run on 64bit machines
|
||||||
runs-on: [self-hosted, linux, ARM64]
|
runs-on: [self-hosted, linux, ARM64]
|
||||||
@@ -494,8 +529,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and test and push docker images
|
- name: Build and test and push docker images
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: freqtradeorg/freqtrade
|
|
||||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
||||||
|
GHCR_USERNAME: ${{ github.actor }}
|
||||||
|
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
build_helpers/publish_docker_arm64.sh
|
build_helpers/publish_docker_arm64.sh
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -8,16 +8,17 @@ repos:
|
|||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v0.991"
|
rev: "v1.0.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.0
|
- types-cachetools==5.3.0.5
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.28.11.13
|
- types-requests==2.30.0.0
|
||||||
- types-tabulate==0.9.0.0
|
- types-tabulate==0.9.0.2
|
||||||
- types-python-dateutil==2.8.19.6
|
- types-python-dateutil==2.8.19.13
|
||||||
|
- SQLAlchemy==2.0.15
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
@@ -27,6 +28,12 @@ repos:
|
|||||||
name: isort (python)
|
name: isort (python)
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
|
# Ruff version.
|
||||||
|
rev: 'v0.0.263'
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
+6
-5
@@ -45,16 +45,17 @@ pytest tests/test_<file_name>.py::test_<method_name>
|
|||||||
|
|
||||||
### 2. Test if your code is PEP8 compliant
|
### 2. Test if your code is PEP8 compliant
|
||||||
|
|
||||||
#### Run Flake8
|
#### Run Ruff
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flake8 freqtrade tests scripts
|
ruff .
|
||||||
```
|
```
|
||||||
|
|
||||||
We receive a lot of code that fails the `flake8` checks.
|
We receive a lot of code that fails the `ruff` checks.
|
||||||
To help with that, we encourage you to install the git pre-commit
|
To help with that, we encourage you to install the git pre-commit
|
||||||
hook that will warn you when you try to commit code that fails these checks.
|
hook that will warn you when you try to commit code that fails these checks.
|
||||||
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
|
||||||
|
you can manually run pre-commit with `pre-commit run -a`.
|
||||||
|
|
||||||
##### Additional styles applied
|
##### Additional styles applied
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10.10-slim-bullseye as base
|
FROM python:3.10.11-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
@@ -25,7 +25,7 @@ FROM base as python-deps
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& pip install --upgrade pip
|
&& pip install --upgrade pip wheel
|
||||||
|
|
||||||
# Install TA-lib
|
# Install TA-lib
|
||||||
COPY build_helpers/* /tmp/
|
COPY build_helpers/* /tmp/
|
||||||
|
|||||||
@@ -210,6 +210,6 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
|||||||
- [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/)
|
- [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
- [TA-Lib](https://ta-lib.github.io/ta-lib-python/)
|
||||||
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||||
- [Docker](https://www.docker.com/products/docker) (Recommended)
|
- [Docker](https://www.docker.com/products/docker) (Recommended)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,8 +8,8 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
|||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib \
|
cd ta-lib \
|
||||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
|
&& curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
|
||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
&& curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
|
||||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make
|
&& make
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ python -m pip install --upgrade pip wheel
|
|||||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||||
|
|
||||||
if ($pyv -eq '3.8') {
|
if ($pyv -eq '3.8') {
|
||||||
pip install build_helpers\TA_Lib-0.4.25-cp38-cp38-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.26-cp38-cp38-win_amd64.whl
|
||||||
}
|
}
|
||||||
if ($pyv -eq '3.9') {
|
if ($pyv -eq '3.9') {
|
||||||
pip install build_helpers\TA_Lib-0.4.25-cp39-cp39-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.26-cp39-cp39-win_amd64.whl
|
||||||
}
|
}
|
||||||
if ($pyv -eq '3.10') {
|
if ($pyv -eq '3.10') {
|
||||||
pip install build_helpers\TA_Lib-0.4.25-cp310-cp310-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.26-cp310-cp310-win_amd64.whl
|
||||||
}
|
}
|
||||||
if ($pyv -eq '3.11') {
|
if ($pyv -eq '3.11') {
|
||||||
pip install build_helpers\TA_Lib-0.4.25-cp311-cp311-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.26-cp311-cp311-win_amd64.whl
|
||||||
}
|
}
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ import yaml
|
|||||||
|
|
||||||
pre_commit_file = Path('.pre-commit-config.yaml')
|
pre_commit_file = Path('.pre-commit-config.yaml')
|
||||||
require_dev = Path('requirements-dev.txt')
|
require_dev = Path('requirements-dev.txt')
|
||||||
|
require = Path('requirements.txt')
|
||||||
|
|
||||||
with require_dev.open('r') as rfile:
|
with require_dev.open('r') as rfile:
|
||||||
requirements = rfile.readlines()
|
requirements = rfile.readlines()
|
||||||
|
|
||||||
|
with require.open('r') as rfile:
|
||||||
|
requirements.extend(rfile.readlines())
|
||||||
|
|
||||||
# Extract types only
|
# Extract types only
|
||||||
type_reqs = [r.strip('\n') for r in requirements if r.startswith('types-')]
|
type_reqs = [r.strip('\n') for r in requirements if r.startswith(
|
||||||
|
'types-') or r.startswith('SQLAlchemy')]
|
||||||
|
|
||||||
with pre_commit_file.open('r') as file:
|
with pre_commit_file.open('r') as file:
|
||||||
f = yaml.load(file, Loader=yaml.FullLoader)
|
f = yaml.load(file, Loader=yaml.FullLoader)
|
||||||
|
|||||||
@@ -3,18 +3,22 @@
|
|||||||
# Use BuildKit, otherwise building on ARM fails
|
# Use BuildKit, otherwise building on ARM fails
|
||||||
export DOCKER_BUILDKIT=1
|
export DOCKER_BUILDKIT=1
|
||||||
|
|
||||||
|
IMAGE_NAME=freqtradeorg/freqtrade
|
||||||
|
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
||||||
|
GHCR_IMAGE_NAME=ghcr.io/freqtrade/freqtrade
|
||||||
|
|
||||||
# Replace / with _ to create a valid tag
|
# Replace / with _ to create a valid tag
|
||||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||||
TAG_PLOT=${TAG}_plot
|
TAG_PLOT=${TAG}_plot
|
||||||
TAG_FREQAI=${TAG}_freqai
|
TAG_FREQAI=${TAG}_freqai
|
||||||
TAG_FREQAI_RL=${TAG_FREQAI}rl
|
TAG_FREQAI_RL=${TAG_FREQAI}rl
|
||||||
|
TAG_FREQAI_TORCH=${TAG_FREQAI}torch
|
||||||
TAG_PI="${TAG}_pi"
|
TAG_PI="${TAG}_pi"
|
||||||
|
|
||||||
TAG_ARM=${TAG}_arm
|
TAG_ARM=${TAG}_arm
|
||||||
TAG_PLOT_ARM=${TAG_PLOT}_arm
|
TAG_PLOT_ARM=${TAG_PLOT}_arm
|
||||||
TAG_FREQAI_ARM=${TAG_FREQAI}_arm
|
TAG_FREQAI_ARM=${TAG_FREQAI}_arm
|
||||||
TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm
|
TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm
|
||||||
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
|
||||||
|
|
||||||
echo "Running for ${TAG}"
|
echo "Running for ${TAG}"
|
||||||
|
|
||||||
@@ -38,13 +42,13 @@ if [ $? -ne 0 ]; then
|
|||||||
echo "failed building multiarch images"
|
echo "failed building multiarch images"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
||||||
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
||||||
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
||||||
|
|
||||||
# Tag image for upload and next build step
|
# Tag image for upload and next build step
|
||||||
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
||||||
|
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
|
||||||
|
|
||||||
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||||
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
||||||
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
||||||
@@ -59,7 +63,6 @@ fi
|
|||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
|
||||||
# docker push ${IMAGE_NAME}
|
|
||||||
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
||||||
@@ -82,14 +85,35 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
|
|||||||
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
|
||||||
|
|
||||||
|
# Create special Torch tag - which is identical to the RL tag.
|
||||||
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_TORCH} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
|
||||||
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_TORCH}
|
||||||
|
|
||||||
|
# copy images to ghcr.io
|
||||||
|
|
||||||
|
alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane"
|
||||||
|
mkdir .crane
|
||||||
|
chmod a+rwx .crane
|
||||||
|
|
||||||
|
echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
|
||||||
|
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL}
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_TORCH}
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI}
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT}
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG}
|
||||||
|
|
||||||
# Tag as latest for develop builds
|
# Tag as latest for develop builds
|
||||||
if [ "${TAG}" = "develop" ]; then
|
if [ "${TAG}" = "develop" ]; then
|
||||||
echo 'Tagging image as latest'
|
echo 'Tagging image as latest'
|
||||||
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
||||||
docker manifest push -p ${IMAGE_NAME}:latest
|
docker manifest push -p ${IMAGE_NAME}:latest
|
||||||
|
|
||||||
|
crane copy ${IMAGE_NAME}:latest ${GHCR_IMAGE_NAME}:latest
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
rm -rf .crane
|
||||||
|
|
||||||
# Cleanup old images from arm64 node.
|
# Cleanup old images from arm64 node.
|
||||||
docker image prune -a --force --filter "until=24h"
|
docker image prune -a --force --filter "until=24h"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# The below assumes a correctly setup docker buildx environment
|
# The below assumes a correctly setup docker buildx environment
|
||||||
|
|
||||||
|
IMAGE_NAME=freqtradeorg/freqtrade
|
||||||
|
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
||||||
# Replace / with _ to create a valid tag
|
# Replace / with _ to create a valid tag
|
||||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||||
TAG_PLOT=${TAG}_plot
|
TAG_PLOT=${TAG}_plot
|
||||||
@@ -11,7 +13,6 @@ TAG_PI="${TAG}_pi"
|
|||||||
|
|
||||||
PI_PLATFORM="linux/arm/v7"
|
PI_PLATFORM="linux/arm/v7"
|
||||||
echo "Running for ${TAG}"
|
echo "Running for ${TAG}"
|
||||||
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
|
||||||
CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache
|
CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache
|
||||||
|
|
||||||
# Add commit and commit_message to docker container
|
# Add commit and commit_message to docker container
|
||||||
@@ -57,9 +58,9 @@ fi
|
|||||||
# Tag image for upload and next build step
|
# Tag image for upload and next build step
|
||||||
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
||||||
|
|
||||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
||||||
docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
||||||
|
|
||||||
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
||||||
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
||||||
|
|||||||
BIN
Binary file not shown.
+10
-1
@@ -6,6 +6,15 @@ services:
|
|||||||
# image: freqtradeorg/freqtrade:develop
|
# image: freqtradeorg/freqtrade:develop
|
||||||
# Use plotting image
|
# Use plotting image
|
||||||
# image: freqtradeorg/freqtrade:develop_plot
|
# image: freqtradeorg/freqtrade:develop_plot
|
||||||
|
# # Enable GPU Image and GPU Resources (only relevant for freqAI)
|
||||||
|
# # Make sure to uncomment the whole deploy section
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# reservations:
|
||||||
|
# devices:
|
||||||
|
# - driver: nvidia
|
||||||
|
# count: 1
|
||||||
|
# capabilities: [gpu]
|
||||||
# Build step - only needed when additional dependencies are needed
|
# Build step - only needed when additional dependencies are needed
|
||||||
# build:
|
# build:
|
||||||
# context: .
|
# context: .
|
||||||
@@ -16,7 +25,7 @@ services:
|
|||||||
- "./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/stable/rest-api/ documentation
|
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||||
# before enabling this.
|
# for more information.
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
# Default command used when running `docker compose up`
|
# Default command used when running `docker compose up`
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
freqtrade:
|
||||||
|
image: freqtradeorg/freqtrade:stable_freqaitorch
|
||||||
|
# # Enable GPU Image and GPU Resources
|
||||||
|
# # Make sure to uncomment the whole deploy section
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# reservations:
|
||||||
|
# devices:
|
||||||
|
# - driver: nvidia
|
||||||
|
# count: 1
|
||||||
|
# capabilities: [gpu]
|
||||||
|
|
||||||
|
# Build step - only needed when additional dependencies are needed
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: "./docker/Dockerfile.custom"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: freqtrade
|
||||||
|
volumes:
|
||||||
|
- "./user_data:/freqtrade/user_data"
|
||||||
|
# Expose api on port 8080 (localhost only)
|
||||||
|
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||||
|
# for more information.
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
# Default command used when running `docker compose up`
|
||||||
|
command: >
|
||||||
|
trade
|
||||||
|
--logfile /freqtrade/user_data/logs/freqtrade.log
|
||||||
|
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite
|
||||||
|
--config /freqtrade/user_data/config.json
|
||||||
|
--freqai-model XGBoostClassifier
|
||||||
|
--strategy SampleStrategy
|
||||||
@@ -29,7 +29,7 @@ If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl`
|
|||||||
`user_data/backtest_results` folder.
|
`user_data/backtest_results` folder.
|
||||||
|
|
||||||
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
|
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
|
||||||
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
|
with `--analysis-groups` option provided with space-separated arguments:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5
|
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5
|
||||||
@@ -39,6 +39,7 @@ This command will read from the last backtesting results. The `--analysis-groups
|
|||||||
used to specify the various tabular outputs showing the profit fo each group or trade,
|
used to specify the various tabular outputs showing the profit fo each group or trade,
|
||||||
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
|
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
|
||||||
|
|
||||||
|
* 0: overall winrate and profit summary by enter_tag
|
||||||
* 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
|
||||||
@@ -115,3 +116,38 @@ For example, if your backtest timerange was `20220101-20221231` but you only wan
|
|||||||
```bash
|
```bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
freqtrade backtesting-analysis -c <config.json> --timerange 20220101-20220201
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Printing out rejected signals
|
||||||
|
|
||||||
|
Use the `--rejected-signals` option to print out rejected signals.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade backtesting-analysis -c <config.json> --rejected-signals
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing tables to CSV
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --rejected-signals --analysis-groups 0 1
|
||||||
|
```
|
||||||
|
|
||||||
|
This will write to `user_data/backtest_results`:
|
||||||
|
|
||||||
|
* rejected_signals.csv
|
||||||
|
* group_0.csv
|
||||||
|
* group_1.csv
|
||||||
|
|
||||||
|
To override where the files will be written, also specify the `--analysis-csv-path` option.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade backtesting-analysis -c <config.json> --analysis-to-csv --analysis-csv-path another/data/path/
|
||||||
|
```
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
+8
-7
@@ -274,19 +274,20 @@ A backtesting result will look like that:
|
|||||||
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
||||||
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
||||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||||
========================================================= EXIT REASON STATS ==========================================================
|
|
||||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
|
||||||
|:-------------------|--------:|------:|-------:|--------:|
|
|
||||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
|
||||||
| stop_loss | 166 | 0 | 0 | 166 |
|
|
||||||
| exit_signal | 56 | 36 | 0 | 20 |
|
|
||||||
| force_exit | 2 | 0 | 0 | 2 |
|
|
||||||
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
||||||
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
||||||
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
||||||
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||||
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||||
|
==================== EXIT REASON STATS ====================
|
||||||
|
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||||
|
|:-------------------|--------:|------:|-------:|--------:|
|
||||||
|
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||||
|
| stop_loss | 166 | 0 | 0 | 166 |
|
||||||
|
| exit_signal | 56 | 36 | 0 | 20 |
|
||||||
|
| force_exit | 2 | 0 | 0 | 2 |
|
||||||
|
|
||||||
================== SUMMARY METRICS ==================
|
================== SUMMARY METRICS ==================
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|-----------------------------+---------------------|
|
|-----------------------------+---------------------|
|
||||||
|
|||||||
+4
-1
@@ -12,6 +12,9 @@ This page provides you some basic concepts on how Freqtrade works and operates.
|
|||||||
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
||||||
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
||||||
* **Market order**: Guaranteed to fill, may move price depending on the order size.
|
* **Market order**: Guaranteed to fill, may move price depending on the order size.
|
||||||
|
* **Current Profit**: Currently pending (unrealized) profit for this trade. This is mainly used throughout the bot and UI.
|
||||||
|
* **Realized Profit**: Already realized profit. Only relevant in combination with [partial exits](strategy-callbacks.md#adjust-trade-position) - which also explains the calculation logic for this.
|
||||||
|
* **Total Profit**: Combined realized and unrealized profit. The relative number (%) is calculated against the total investment in this trade.
|
||||||
|
|
||||||
## Fee handling
|
## Fee handling
|
||||||
|
|
||||||
@@ -57,10 +60,10 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
|
|
||||||
* Load historic data for configured pairlist.
|
* Load historic data for configured pairlist.
|
||||||
* Calls `bot_start()` once.
|
* Calls `bot_start()` once.
|
||||||
* Calls `bot_loop_start()` once.
|
|
||||||
* Calculate indicators (calls `populate_indicators()` once per pair).
|
* Calculate indicators (calls `populate_indicators()` once per pair).
|
||||||
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
|
* Calls `bot_loop_start()` strategy callback.
|
||||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
||||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||||
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
||||||
|
|||||||
+11
-11
@@ -138,7 +138,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
||||||
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
||||||
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
||||||
| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float.
|
| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float.
|
||||||
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
||||||
@@ -155,25 +155,25 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
||||||
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
|
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
|
||||||
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to None.*<br> **Datatype:** Float
|
| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates) <br>*Defaults to `None`.*<br> **Datatype:** Float
|
||||||
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
|
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
|
||||||
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
|
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
|
||||||
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
|
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
|
||||||
| | **Unfilled timeout**
|
| | **Unfilled timeout**
|
||||||
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `"minutes"`.* <br> **Datatype:** String
|
||||||
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
||||||
| | **Pricing**
|
| | **Pricing**
|
||||||
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||||
| `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled).
|
| `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled).
|
||||||
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
|
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `true`.*<br> **Datatype:** Boolean
|
||||||
| `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
| `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||||
| `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
|
| `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `"same"`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
|
||||||
| `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled).
|
| `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled).
|
||||||
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
|
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `true`.*<br> **Datatype:** Boolean
|
||||||
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
| `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||||
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
||||||
| | **TODO**
|
| | **TODO**
|
||||||
@@ -199,10 +199,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||||
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
||||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean
|
||||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| | **Plugins**
|
| | **Plugins**
|
||||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
||||||
@@ -213,7 +213,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
|
||||||
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `True`.<br> **Datatype:** boolean
|
| `telegram.reload` | Allow "reload" buttons on telegram messages. <br>*Defaults to `true`.<br> **Datatype:** boolean
|
||||||
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
| `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.<br> **Datatype:** dictionary
|
||||||
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
| `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function. <br> **Datatype:** Boolean
|
||||||
| | **Webhook**
|
| | **Webhook**
|
||||||
|
|||||||
@@ -74,3 +74,8 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re
|
|||||||
* `webhooksell`, `webhookexit` -> `exit`
|
* `webhooksell`, `webhookexit` -> `exit`
|
||||||
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
|
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
|
||||||
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
|
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
|
||||||
|
|
||||||
|
|
||||||
|
## Removal of `populate_any_indicators`
|
||||||
|
|
||||||
|
version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details.
|
||||||
|
|||||||
+5
-5
@@ -24,7 +24,7 @@ This will spin up a local server (usually on port 8000) so you can see if everyt
|
|||||||
To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
||||||
Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`.
|
Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`.
|
||||||
|
|
||||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
This will install all required tools for development, including `pytest`, `ruff`, `mypy`, and `coveralls`.
|
||||||
|
|
||||||
Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing.
|
Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing.
|
||||||
This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine.
|
This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine.
|
||||||
@@ -327,18 +327,18 @@ To check how the new exchange behaves, you can use the following snippet:
|
|||||||
|
|
||||||
``` python
|
``` python
|
||||||
import ccxt
|
import ccxt
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||||
ct = ccxt.binance()
|
ct = ccxt.binance() # Use the exchange you're testing
|
||||||
timeframe = "1d"
|
timeframe = "1d"
|
||||||
pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange!
|
pair = "BTC/USDT" # Make sure to use a pair that exists on that exchange!
|
||||||
raw = ct.fetch_ohlcv(pair, timeframe=timeframe)
|
raw = ct.fetch_ohlcv(pair, timeframe=timeframe)
|
||||||
|
|
||||||
# convert to dataframe
|
# convert to dataframe
|
||||||
df1 = ohlcv_to_dataframe(raw, timeframe, pair=pair, drop_incomplete=False)
|
df1 = ohlcv_to_dataframe(raw, timeframe, pair=pair, drop_incomplete=False)
|
||||||
|
|
||||||
print(df1.tail(1))
|
print(df1.tail(1))
|
||||||
print(datetime.utcnow())
|
print(datetime.now(timezone.utc))
|
||||||
```
|
```
|
||||||
|
|
||||||
``` output
|
``` output
|
||||||
|
|||||||
@@ -142,6 +142,13 @@ To fix this, redefine order types in the strategy to use "limit" instead of "mar
|
|||||||
|
|
||||||
The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy.
|
The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy.
|
||||||
|
|
||||||
|
### I'm trying to start the bot live, but get an API permission error
|
||||||
|
|
||||||
|
Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say.
|
||||||
|
Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console.
|
||||||
|
Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary.
|
||||||
|
Futures will usually have to be enabled specifically.
|
||||||
|
|
||||||
### How do I search the bot logs for something?
|
### How do I search the bot logs for something?
|
||||||
|
|
||||||
By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout.
|
By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout.
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
|
def feature_engineering_expand_all(self, dataframe: DataFrame, period, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This function will automatically expand the defined features on the config defined
|
This function will automatically expand the defined features on the config defined
|
||||||
@@ -77,7 +77,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def feature_engineering_expand_basic(self, dataframe, **kwargs):
|
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This function will automatically expand the defined features on the config defined
|
This function will automatically expand the defined features on the config defined
|
||||||
@@ -101,7 +101,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||||||
dataframe["%-raw_price"] = dataframe["close"]
|
dataframe["%-raw_price"] = dataframe["close"]
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def feature_engineering_standard(self, dataframe, **kwargs):
|
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This optional function will be called once with the dataframe of the base timeframe.
|
This optional function will be called once with the dataframe of the base timeframe.
|
||||||
@@ -122,7 +122,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||||||
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def set_freqai_targets(self, dataframe, **kwargs):
|
def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
Required function to set the targets for the model.
|
Required function to set the targets for the model.
|
||||||
@@ -139,6 +139,7 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||||||
/ dataframe["close"]
|
/ dataframe["close"]
|
||||||
- 1
|
- 1
|
||||||
)
|
)
|
||||||
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`.
|
Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`.
|
||||||
@@ -236,3 +237,181 @@ If you want to predict multiple targets you must specify all labels in the same
|
|||||||
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')
|
||||||
df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down'])
|
df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down'])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## PyTorch Module
|
||||||
|
|
||||||
|
### Quick start
|
||||||
|
|
||||||
|
The easiest way to quickly run a pytorch model is with the following command (for regression task):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note "Installation/docker"
|
||||||
|
The PyTorch module requires large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]?".
|
||||||
|
Users who prefer docker should ensure they use the docker image appended with `_freqaitorch`.
|
||||||
|
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file.
|
||||||
|
This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
#### Model
|
||||||
|
|
||||||
|
You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks.
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
class LogisticRegression(nn.Module):
|
||||||
|
def __init__(self, input_size: int):
|
||||||
|
super().__init__()
|
||||||
|
# Define your layers
|
||||||
|
self.linear = nn.Linear(input_size, 1)
|
||||||
|
self.activation = nn.Sigmoid()
|
||||||
|
|
||||||
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||||
|
# Define the forward pass
|
||||||
|
out = self.linear(x)
|
||||||
|
out = self.activation(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
class MyCoolPyTorchClassifier(BasePyTorchClassifier):
|
||||||
|
"""
|
||||||
|
This is a custom IFreqaiModel showing how a user might setup their own
|
||||||
|
custom Neural Network architecture for their training.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_convertor(self) -> PyTorchDataConvertor:
|
||||||
|
return DefaultPyTorchDataConvertor(target_tensor_type=torch.float)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
config = self.freqai_info.get("model_training_parameters", {})
|
||||||
|
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||||
|
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||||
|
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
class_names = self.get_class_names()
|
||||||
|
self.convert_label_column_to_int(data_dictionary, dk, class_names)
|
||||||
|
n_features = data_dictionary["train_features"].shape[-1]
|
||||||
|
model = LogisticRegression(
|
||||||
|
input_dim=n_features
|
||||||
|
)
|
||||||
|
model.to(self.device)
|
||||||
|
optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate)
|
||||||
|
criterion = torch.nn.CrossEntropyLoss()
|
||||||
|
init_model = self.get_init_model(dk.pair)
|
||||||
|
trainer = PyTorchModelTrainer(
|
||||||
|
model=model,
|
||||||
|
optimizer=optimizer,
|
||||||
|
criterion=criterion,
|
||||||
|
model_meta_data={"class_names": class_names},
|
||||||
|
device=self.device,
|
||||||
|
init_model=init_model,
|
||||||
|
data_convertor=self.data_convertor,
|
||||||
|
**self.trainer_kwargs,
|
||||||
|
)
|
||||||
|
trainer.fit(data_dictionary, self.splits)
|
||||||
|
return trainer
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Trainer
|
||||||
|
|
||||||
|
The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop:
|
||||||
|
Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer.
|
||||||
|
|
||||||
|
In addition, the trainer is responsible for the following:
|
||||||
|
- saving and loading the model
|
||||||
|
- converting the data from `pandas.DataFrame` to `torch.Tensor`.
|
||||||
|
|
||||||
|
#### Integration with Freqai module
|
||||||
|
|
||||||
|
Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy.
|
||||||
|
From top to bottom:
|
||||||
|
|
||||||
|
1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device` attribute used by children classes. Sets `model_type` attribute used by the parent class.
|
||||||
|
2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed.
|
||||||
|
3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Full example
|
||||||
|
|
||||||
|
Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PyTorchMLPRegressor(BasePyTorchRegressor):
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
config = self.freqai_info.get("model_training_parameters", {})
|
||||||
|
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||||
|
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||||
|
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||||
|
|
||||||
|
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
|
n_features = data_dictionary["train_features"].shape[-1]
|
||||||
|
model = PyTorchMLPModel(
|
||||||
|
input_dim=n_features,
|
||||||
|
output_dim=1,
|
||||||
|
**self.model_kwargs
|
||||||
|
)
|
||||||
|
model.to(self.device)
|
||||||
|
optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate)
|
||||||
|
criterion = torch.nn.MSELoss()
|
||||||
|
init_model = self.get_init_model(dk.pair)
|
||||||
|
trainer = PyTorchModelTrainer(
|
||||||
|
model=model,
|
||||||
|
optimizer=optimizer,
|
||||||
|
criterion=criterion,
|
||||||
|
device=self.device,
|
||||||
|
init_model=init_model,
|
||||||
|
target_tensor_type=torch.float,
|
||||||
|
**self.trainer_kwargs,
|
||||||
|
)
|
||||||
|
trainer.fit(data_dictionary)
|
||||||
|
return trainer
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel`, where the former implements the `predict` method that is suitable for our regression task, and the latter implements the train method.
|
||||||
|
|
||||||
|
??? Note "Setting Class Names for Classifiers"
|
||||||
|
When using classifiers, the user must declare the class names (or targets) by overriding the `IFreqaiModel.class_names` attribute. This is achieved by setting `self.freqai.class_names` in the FreqAI strategy inside the `set_freqai_targets` method.
|
||||||
|
|
||||||
|
For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows:
|
||||||
|
```python
|
||||||
|
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
|
||||||
|
self.freqai.class_names = ["down", "up"]
|
||||||
|
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
|
||||||
|
dataframe["close"], 'up', 'down')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py).
|
||||||
|
|
||||||
|
|
||||||
|
#### Improving performance with `torch.compile()`
|
||||||
|
|
||||||
|
Torch provides a `torch.compile()` method that can be used to improve performance for specific GPU hardware. More details can be found [here](https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html). In brief, you simply wrap your `model` in `torch.compile()`:
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
model = PyTorchMLPModel(
|
||||||
|
input_dim=n_features,
|
||||||
|
output_dim=1,
|
||||||
|
**self.model_kwargs
|
||||||
|
)
|
||||||
|
model.to(self.device)
|
||||||
|
model = torch.compile(model)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then proceed to use the model as normal. Keep in mind that doing this will remove eager execution, which means errors and tracebacks will not be informative.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ Low level feature engineering is performed in the user strategy within a set of
|
|||||||
|
|
||||||
| Function | Description |
|
| Function | Description |
|
||||||
|---------------|-------------|
|
|---------------|-------------|
|
||||||
| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||||
| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
||||||
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
||||||
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ Meanwhile, high level feature engineering is handled within `"feature_parameters
|
|||||||
It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy:
|
It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This function will automatically expand the defined features on the config defined
|
This function will automatically expand the defined features on the config defined
|
||||||
@@ -67,7 +67,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs):
|
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This function will automatically expand the defined features on the config defined
|
This function will automatically expand the defined features on the config defined
|
||||||
@@ -96,7 +96,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
|
|||||||
dataframe["%-raw_price"] = dataframe["close"]
|
dataframe["%-raw_price"] = dataframe["close"]
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def feature_engineering_standard(self, dataframe, metadata, **kwargs):
|
def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This optional function will be called once with the dataframe of the base timeframe.
|
This optional function will be called once with the dataframe of the base timeframe.
|
||||||
@@ -122,7 +122,7 @@ It is advisable to start from the template `feature_engineering_*` functions in
|
|||||||
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def set_freqai_targets(self, dataframe, metadata, **kwargs):
|
def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
Required function to set the targets for the model.
|
Required function to set the targets for the model.
|
||||||
@@ -181,15 +181,14 @@ You can ask for each of the defined features to be included also for informative
|
|||||||
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
||||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||||
|
|
||||||
|
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||||
|
|
||||||
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
||||||
|
|
||||||
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
```python
|
||||||
|
def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame:
|
||||||
```py
|
if metadata["tf"] == "1h":
|
||||||
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||||
if metadata["tf"] == "1h":
|
|
||||||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will block `ta.ROC()` from being added to any timeframes other than `"1h"`.
|
This will block `ta.ROC()` from being added to any timeframes other than `"1h"`.
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `purge_old_models` | Number of models to keep on disk (not relevant to backtesting). Default is 2, which means that dry/live runs will keep the latest 2 models on disk. Setting to 0 keeps all models. This parameter also accepts a boolean to maintain backwards compatibility. <br> **Datatype:** Integer. <br> Default: `2`.
|
| `purge_old_models` | Number of models to keep on disk (not relevant to backtesting). Default is 2, which means that dry/live runs will keep the latest 2 models on disk. Setting to 0 keeps all models. This parameter also accepts a boolean to maintain backwards compatibility. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||||
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
||||||
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
||||||
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). Beware that this is currently a naive approach to incremental learning, and it has a high probability of overfitting/getting stuck in local minima while the market moves away from your model. We have the connections here primarily for experimental purposes and so that it is ready for more mature approaches to continual learning in chaotic systems like the crypto market. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
|
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
|
||||||
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
|
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
|
||||||
|
| `activate_tensorboard` | <br> Indicate whether or not to activate tensorboard for the tensorboard enabled modules (currently Reinforcment Learning, XGBoost, Catboost, and PyTorch). Tensorboard needs Torch installed, which means you will need the torch/RL docker image or you need to answer "yes" to the install question about whether or not you wish to install Torch. <br> **Datatype:** Boolean. <br> Default: `True`.
|
||||||
|
|
||||||
### Feature parameters
|
### Feature parameters
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
||||||
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
||||||
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Boolean. <br> Default: `False`.
|
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
|
|
||||||
### Data split parameters
|
### Data split parameters
|
||||||
|
|
||||||
@@ -84,6 +85,29 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `add_state_info` | Tell FreqAI to include state information in the feature set for training and inferencing. The current state variables include trade duration, current profit, trade position. This is only available in dry/live runs, and is automatically switched to false for backtesting. <br> **Datatype:** bool. <br> Default: `False`.
|
| `add_state_info` | Tell FreqAI to include state information in the feature set for training and inferencing. The current state variables include trade duration, current profit, trade position. This is only available in dry/live runs, and is automatically switched to false for backtesting. <br> **Datatype:** bool. <br> Default: `False`.
|
||||||
| `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[<shared layers>, dict(vf=[<non-shared value network layers>], pi=[<non-shared policy network layers>])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each.
|
| `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[<shared layers>, dict(vf=[<non-shared value network layers>], pi=[<non-shared policy network layers>])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each.
|
||||||
| `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting. <br> **Datatype:** bool. <br> Default: `False`.
|
| `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting. <br> **Datatype:** bool. <br> Default: `False`.
|
||||||
|
| `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases) <br> **Datatype:** Boolean. <br> **Default:** `False`
|
||||||
|
| `progress_bar` | Display a progress bar with the current progress, elapsed time and estimated remaining time. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
|
|
||||||
|
### PyTorch parameters
|
||||||
|
|
||||||
|
#### general
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| | **Model training parameters within the `freqai.model_training_parameters` sub dictionary**
|
||||||
|
| `learning_rate` | Learning rate to be passed to the optimizer. <br> **Datatype:** float. <br> Default: `3e-4`.
|
||||||
|
| `model_kwargs` | Parameters to be passed to the model class. <br> **Datatype:** dict. <br> Default: `{}`.
|
||||||
|
| `trainer_kwargs` | Parameters to be passed to the trainer class. <br> **Datatype:** dict. <br> Default: `{}`.
|
||||||
|
|
||||||
|
#### trainer_kwargs
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary**
|
||||||
|
| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. <br> **Datatype:** int. <br> Default: `100`.
|
||||||
|
| `batch_size` | The size of the batches to use during training.. <br> **Datatype:** int. <br> Default: `64`.
|
||||||
|
| `max_n_eval_batches` | The maximum number batches to use for evaluation.. <br> **Datatype:** int, optional. <br> Default: `None`.
|
||||||
|
|
||||||
|
|
||||||
### Additional parameters
|
### Additional parameters
|
||||||
|
|
||||||
@@ -91,5 +115,5 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| | **Extraneous parameters**
|
| | **Extraneous parameters**
|
||||||
| `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 convolutional 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`.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --con
|
|||||||
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column:
|
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def set_freqai_targets(self, dataframe, **kwargs):
|
def set_freqai_targets(self, dataframe, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
Required function to set the targets for the model.
|
Required function to set the targets for the model.
|
||||||
@@ -53,17 +53,19 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
|
|||||||
# For RL, there are no direct targets to set. This is filler (neutral)
|
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||||
# until the agent sends an action.
|
# until the agent sends an action.
|
||||||
dataframe["&-action"] = 0
|
dataframe["&-action"] = 0
|
||||||
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def feature_engineering_standard(self, dataframe, **kwargs):
|
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||||
# The following features are necessary for RL models
|
# The following features are necessary for RL models
|
||||||
dataframe[f"%-raw_close"] = dataframe["close"]
|
dataframe[f"%-raw_close"] = dataframe["close"]
|
||||||
dataframe[f"%-raw_open"] = dataframe["open"]
|
dataframe[f"%-raw_open"] = dataframe["open"]
|
||||||
dataframe[f"%-raw_high"] = dataframe["high"]
|
dataframe[f"%-raw_high"] = dataframe["high"]
|
||||||
dataframe[f"%-raw_low"] = dataframe["low"]
|
dataframe[f"%-raw_low"] = dataframe["low"]
|
||||||
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
||||||
@@ -133,90 +135,104 @@ Parameter details can be found [here](freqai-parameter-table.md), but in general
|
|||||||
|
|
||||||
## Creating a custom reward function
|
## Creating a custom reward function
|
||||||
|
|
||||||
As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but users are encouraged to create their own custom reinforcement learning model class (see below) and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated:
|
!!! danger "Not for production"
|
||||||
|
Warning!
|
||||||
|
The reward function provided with the Freqtrade source code is a showcase of functionality designed to show/test as many possible environment control features as possible. It is also designed to run quickly on small computers. This is a benchmark, it is *not* for live production. Please beware that you will need to create your own custom_reward() function or use a template built by other users outside of the Freqtrade source code.
|
||||||
|
|
||||||
|
As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but this is *not* designed for production. Users *must* create their own custom reinforcement learning model class or use a pre-built one from outside the Freqtrade source code and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated:
|
||||||
|
|
||||||
|
!!! note "Hint"
|
||||||
|
The best reward functions are ones that are continuously differentiable, and well scaled. In other words, adding a single large negative penalty to a rare event is not a good idea, and the neural net will not be able to learn that function. Instead, it is better to add a small negative penalty to a common event. This will help the agent learn faster. Not only this, but you can help improve the continuity of your rewards/penalties by having them scale with severity according to some linear/exponential functions. In other words, you'd slowly scale the penalty as the duration of the trade increases. This is better than a single large penalty occuring at a single point in time.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
|
||||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||||
|
|
||||||
|
|
||||||
class MyCoolRLModel(ReinforcementLearner):
|
class MyCoolRLModel(ReinforcementLearner):
|
||||||
|
"""
|
||||||
|
User created RL prediction model.
|
||||||
|
|
||||||
|
Save this file to `freqtrade/user_data/freqaimodels`
|
||||||
|
|
||||||
|
then use it with:
|
||||||
|
|
||||||
|
freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat
|
||||||
|
|
||||||
|
Here the users can override any of the functions
|
||||||
|
available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this
|
||||||
|
is where the user overrides `MyRLEnv` (see below), to define custom
|
||||||
|
`calculate_reward()` function, or to override any other parts of the environment.
|
||||||
|
|
||||||
|
This class also allows users to override any other part of the IFreqaiModel tree.
|
||||||
|
For example, the user can override `def fit()` or `def train()` or `def predict()`
|
||||||
|
to take fine-tuned control over these processes.
|
||||||
|
|
||||||
|
Another common override may be `def data_cleaning_predict()` where the user can
|
||||||
|
take fine-tuned control over the data handling pipeline.
|
||||||
|
"""
|
||||||
|
class MyRLEnv(Base5ActionRLEnv):
|
||||||
"""
|
"""
|
||||||
User created RL prediction model.
|
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||||
|
Users can override any functions from those parent classes. Here is an example
|
||||||
|
of a user customized `calculate_reward()` function.
|
||||||
|
|
||||||
Save this file to `freqtrade/user_data/freqaimodels`
|
Warning!
|
||||||
|
This is function is a showcase of functionality designed to show as many possible
|
||||||
then use it with:
|
environment control features as possible. It is also designed to run quickly
|
||||||
|
on small computers. This is a benchmark, it is *not* for live production.
|
||||||
freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat
|
|
||||||
|
|
||||||
Here the users can override any of the functions
|
|
||||||
available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this
|
|
||||||
is where the user overrides `MyRLEnv` (see below), to define custom
|
|
||||||
`calculate_reward()` function, or to override any other parts of the environment.
|
|
||||||
|
|
||||||
This class also allows users to override any other part of the IFreqaiModel tree.
|
|
||||||
For example, the user can override `def fit()` or `def train()` or `def predict()`
|
|
||||||
to take fine-tuned control over these processes.
|
|
||||||
|
|
||||||
Another common override may be `def data_cleaning_predict()` where the user can
|
|
||||||
take fine-tuned control over the data handling pipeline.
|
|
||||||
"""
|
"""
|
||||||
class MyRLEnv(Base5ActionRLEnv):
|
def calculate_reward(self, action: int) -> float:
|
||||||
"""
|
# first, penalize if the action is not valid
|
||||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
if not self._is_valid(action):
|
||||||
Users can override any functions from those parent classes. Here is an example
|
return -2
|
||||||
of a user customized `calculate_reward()` function.
|
pnl = self.get_unrealized_profit()
|
||||||
"""
|
|
||||||
def calculate_reward(self, action: int) -> float:
|
|
||||||
# first, penalize if the action is not valid
|
|
||||||
if not self._is_valid(action):
|
|
||||||
return -2
|
|
||||||
pnl = self.get_unrealized_profit()
|
|
||||||
|
|
||||||
factor = 100
|
factor = 100
|
||||||
|
|
||||||
# you can use feature values from dataframe
|
pair = self.pair.replace(':', '')
|
||||||
# Assumes the shifted RSI indicator has been generated in the strategy.
|
|
||||||
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{self.pair}_"
|
|
||||||
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
|
||||||
|
|
||||||
# reward agent for entering trades
|
# you can use feature values from dataframe
|
||||||
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
# Assumes the shifted RSI indicator has been generated in the strategy.
|
||||||
and self._position == Positions.Neutral):
|
rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_"
|
||||||
if rsi_now < 40:
|
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
||||||
factor = 40 / rsi_now
|
|
||||||
else:
|
|
||||||
factor = 1
|
|
||||||
return 25 * factor
|
|
||||||
|
|
||||||
# discourage agent from not entering trades
|
# reward agent for entering trades
|
||||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||||
return -1
|
and self._position == Positions.Neutral):
|
||||||
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
if rsi_now < 40:
|
||||||
trade_duration = self._current_tick - self._last_trade_tick
|
factor = 40 / rsi_now
|
||||||
if trade_duration <= max_trade_duration:
|
else:
|
||||||
factor *= 1.5
|
factor = 1
|
||||||
elif trade_duration > max_trade_duration:
|
return 25 * factor
|
||||||
factor *= 0.5
|
|
||||||
# discourage sitting in position
|
# discourage agent from not entering trades
|
||||||
if self._position in (Positions.Short, Positions.Long) and \
|
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||||
action == Actions.Neutral.value:
|
return -1
|
||||||
return -1 * trade_duration / max_trade_duration
|
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
|
||||||
# close long
|
trade_duration = self._current_tick - self._last_trade_tick
|
||||||
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
if trade_duration <= max_trade_duration:
|
||||||
if pnl > self.profit_aim * self.rr:
|
factor *= 1.5
|
||||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
elif trade_duration > max_trade_duration:
|
||||||
return float(pnl * factor)
|
factor *= 0.5
|
||||||
# close short
|
# discourage sitting in position
|
||||||
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
if self._position in (Positions.Short, Positions.Long) and \
|
||||||
if pnl > self.profit_aim * self.rr:
|
action == Actions.Neutral.value:
|
||||||
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
return -1 * trade_duration / max_trade_duration
|
||||||
return float(pnl * factor)
|
# close long
|
||||||
return 0.
|
if action == Actions.Long_exit.value and self._position == Positions.Long:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(pnl * factor)
|
||||||
|
# close short
|
||||||
|
if action == Actions.Short_exit.value and self._position == Positions.Short:
|
||||||
|
if pnl > self.profit_aim * self.rr:
|
||||||
|
factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2)
|
||||||
|
return float(pnl * factor)
|
||||||
|
return 0.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Tensorboard
|
## Using Tensorboard
|
||||||
|
|
||||||
Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
||||||
|
|
||||||
@@ -229,32 +245,30 @@ where `unique-id` is the `identifier` set in the `freqai` configuration file. Th
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Custom logging
|
||||||
### Custom logging
|
|
||||||
|
|
||||||
FreqAI also provides a built in episodic summary logger called `self.tensorboard_log` for adding custom information to the Tensorboard log. By default, this function is already called once per step inside the environment to record the agent actions. All values accumulated for all steps in a single episode are reported at the conclusion of each episode, followed by a full reset of all metrics to 0 in preparation for the subsequent episode.
|
FreqAI also provides a built in episodic summary logger called `self.tensorboard_log` for adding custom information to the Tensorboard log. By default, this function is already called once per step inside the environment to record the agent actions. All values accumulated for all steps in a single episode are reported at the conclusion of each episode, followed by a full reset of all metrics to 0 in preparation for the subsequent episode.
|
||||||
|
|
||||||
|
|
||||||
`self.tensorboard_log` can also be used anywhere inside the environment, for example, it can be added to the `calculate_reward` function to collect more detailed information about how often various parts of the reward were called:
|
`self.tensorboard_log` can also be used anywhere inside the environment, for example, it can be added to the `calculate_reward` function to collect more detailed information about how often various parts of the reward were called:
|
||||||
|
|
||||||
```py
|
```python
|
||||||
class MyRLEnv(Base5ActionRLEnv):
|
class MyRLEnv(Base5ActionRLEnv):
|
||||||
"""
|
"""
|
||||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
||||||
Users can override any functions from those parent classes. Here is an example
|
Users can override any functions from those parent classes. Here is an example
|
||||||
of a user customized `calculate_reward()` function.
|
of a user customized `calculate_reward()` function.
|
||||||
"""
|
"""
|
||||||
def calculate_reward(self, action: int) -> float:
|
def calculate_reward(self, action: int) -> float:
|
||||||
if not self._is_valid(action):
|
if not self._is_valid(action):
|
||||||
self.tensorboard_log("is_valid")
|
self.tensorboard_log("invalid")
|
||||||
return -2
|
return -2
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter.
|
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)`. In this case the metric values are not incremented.
|
||||||
|
|
||||||
### Choosing a base environment
|
## Choosing a base environment
|
||||||
|
|
||||||
FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
|
FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -128,6 +128,12 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number
|
|||||||
|
|
||||||
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
|
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
|
||||||
|
|
||||||
|
???+ danger "Continual learning enforces a constant parameter space"
|
||||||
|
Since `continual_learning` means that the model parameter space *cannot* change between trainings, `principal_component_analysis` is automatically disabled when `continual_learning` is enabled. Hint: PCA changes the parameter space and the number of features, learn more about PCA [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis).
|
||||||
|
|
||||||
|
???+ danger "Experimental functionality"
|
||||||
|
Beware that this is currently a naive approach to incremental learning, and it has a high probability of overfitting/getting stuck in local minima while the market moves away from your model. We have the mechanics available in FreqAI primarily for experimental purposes and so that it is ready for more mature approaches to continual learning in chaotic systems like the crypto market.
|
||||||
|
|
||||||
## Hyperopt
|
## Hyperopt
|
||||||
|
|
||||||
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):
|
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):
|
||||||
@@ -155,7 +161,14 @@ This specific hyperopt would help you understand the appropriate `DI_values` for
|
|||||||
|
|
||||||
## Using Tensorboard
|
## Using Tensorboard
|
||||||
|
|
||||||
CatBoost models benefit from tracking training metrics via Tensorboard. You can take advantage of the FreqAI integration to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command:
|
!!! note "Availability"
|
||||||
|
FreqAI includes tensorboard for a variety of models, including XGBoost, all PyTorch models, Reinforcement Learning, and Catboost. If you would like to see Tensorboard integrated into another model type, please open an issue on the [Freqtrade GitHub](https://github.com/freqtrade/freqtrade/issues)
|
||||||
|
|
||||||
|
!!! danger "Requirements"
|
||||||
|
Tensorboard logging requires the FreqAI torch installation/docker image.
|
||||||
|
|
||||||
|
|
||||||
|
The easiest way to use tensorboard is to ensure `freqai.activate_tensorboard` is set to `True` (default setting) in your configuration file, run FreqAI, then open a separate shell and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
@@ -165,3 +178,7 @@ tensorboard --logdir user_data/models/unique-id
|
|||||||
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if you wish to view the output in your browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
|
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if you wish to view the output in your browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
!!! note "Deactivate for improved performance"
|
||||||
|
Tensorboard logging can slow down training and should be deactivated for production use.
|
||||||
|
|||||||
+8
-2
@@ -32,7 +32,10 @@ The easiest way to quickly test FreqAI is to run it in dry mode with the followi
|
|||||||
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates
|
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates
|
||||||
```
|
```
|
||||||
|
|
||||||
You will see the boot-up process of automatic data downloading, followed by simultaneous training and trading.
|
You will see the boot-up process of automatic data downloading, followed by simultaneous training and trading.
|
||||||
|
|
||||||
|
!!! danger "Not for production"
|
||||||
|
The example strategy provided with the Freqtrade source code is designed for showcasing/testing a wide variety of FreqAI features. It is also designed to run on small computers so that it can be used as a benchmark between developers and users. It is *not* designed to be run in production.
|
||||||
|
|
||||||
An example strategy, prediction model, and config to use as a starting points can be found in
|
An example strategy, prediction model, and config to use as a starting points can be found in
|
||||||
`freqtrade/templates/FreqaiExampleStrategy.py`, `freqtrade/freqai/prediction_models/LightGBMRegressor.py`, and
|
`freqtrade/templates/FreqaiExampleStrategy.py`, `freqtrade/freqai/prediction_models/LightGBMRegressor.py`, and
|
||||||
@@ -69,12 +72,15 @@ pip install -r requirements-freqai.txt
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since it does not provide wheels for this platform.
|
Catboost will not be installed on low-powered arm devices (raspberry), since it does not provide wheels for this platform.
|
||||||
|
|
||||||
### Usage with docker
|
### Usage with docker
|
||||||
|
|
||||||
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
||||||
|
|
||||||
|
!!! note "docker-compose-freqai.yml"
|
||||||
|
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||||
|
|
||||||
### FreqAI position in open-source machine learning landscape
|
### FreqAI position in open-source machine learning landscape
|
||||||
|
|
||||||
Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data.
|
Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data.
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ The below example assumes a timeframe of 1 hour:
|
|||||||
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
|
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
|
||||||
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
|
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
|
||||||
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
|
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
|
||||||
* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
* Locks all pairs that had 2 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
||||||
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
|
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
|||||||
+10
-19
@@ -30,12 +30,6 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
|||||||
!!! Warning "Up-to-date clock"
|
!!! Warning "Up-to-date clock"
|
||||||
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
||||||
|
|
||||||
!!! Error "Running setup.py install for gym did not run successfully."
|
|
||||||
If you get an error related with gym we suggest you to downgrade setuptools it to version 65.5.0 you can do it with the following command:
|
|
||||||
```bash
|
|
||||||
pip install setuptools==65.5.0
|
|
||||||
```
|
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -52,7 +46,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
|||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
|
||||||
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions [below](#install-ta-lib))
|
* [TA-Lib](https://ta-lib.github.io/ta-lib-python/) (install instructions [below](#install-ta-lib))
|
||||||
|
|
||||||
### Install code
|
### Install code
|
||||||
|
|
||||||
@@ -210,7 +204,7 @@ sudo ./build_helpers/install_ta-lib.sh
|
|||||||
|
|
||||||
##### TA-Lib manual installation
|
##### TA-Lib manual installation
|
||||||
|
|
||||||
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
[Official installation guide](https://ta-lib.github.io/ta-lib-python/install.html)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||||
@@ -242,6 +236,7 @@ source .env/bin/activate
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
python3 -m pip install -e .
|
python3 -m pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -290,10 +285,8 @@ cd freqtrade
|
|||||||
|
|
||||||
#### Freqtrade install: Conda Environment
|
#### Freqtrade install: Conda Environment
|
||||||
|
|
||||||
Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conda env create -n freqtrade-conda -f environment.yml
|
conda create --name freqtrade python=3.10
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note "Creating Conda Environment"
|
!!! Note "Creating Conda Environment"
|
||||||
@@ -302,12 +295,9 @@ conda env create -n freqtrade-conda -f environment.yml
|
|||||||
```bash
|
```bash
|
||||||
# choose your own packages
|
# choose your own packages
|
||||||
conda env create -n [name of the environment] [python version] [packages]
|
conda env create -n [name of the environment] [python version] [packages]
|
||||||
|
|
||||||
# point to file with packages
|
|
||||||
conda env create -n [name of the environment] -f [file]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Enter/exit freqtrade-conda environment
|
#### Enter/exit freqtrade environment
|
||||||
|
|
||||||
To check available environments, type
|
To check available environments, type
|
||||||
|
|
||||||
@@ -319,7 +309,7 @@ Enter installed environment
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# enter conda environment
|
# enter conda environment
|
||||||
conda activate freqtrade-conda
|
conda activate freqtrade
|
||||||
|
|
||||||
# exit conda environment - don't do it now
|
# exit conda environment - don't do it now
|
||||||
conda deactivate
|
conda deactivate
|
||||||
@@ -329,6 +319,7 @@ Install last python dependencies with pip
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
python3 -m pip install -e .
|
python3 -m pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -336,7 +327,7 @@ Patch conda libta-lib (Linux only)
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ensure that the environment is active!
|
# Ensure that the environment is active!
|
||||||
conda activate freqtrade-conda
|
conda activate freqtrade
|
||||||
|
|
||||||
cd build_helpers
|
cd build_helpers
|
||||||
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
|
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
|
||||||
@@ -355,8 +346,8 @@ conda env list
|
|||||||
# activate base environment
|
# activate base environment
|
||||||
conda activate
|
conda activate
|
||||||
|
|
||||||
# activate freqtrade-conda environment
|
# activate freqtrade environment
|
||||||
conda activate freqtrade-conda
|
conda activate freqtrade
|
||||||
|
|
||||||
#deactivate any conda environments
|
#deactivate any conda environments
|
||||||
conda deactivate
|
conda deactivate
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
|
|||||||
| `producers` | **Required.** List of producers <br> **Datatype:** Array.
|
| `producers` | **Required.** List of producers <br> **Datatype:** Array.
|
||||||
| `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.<br> **Datatype:** string
|
| `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.<br> **Datatype:** string
|
||||||
| `producers.host` | **Required.** The hostname or IP address from your producer.<br> **Datatype:** string
|
| `producers.host` | **Required.** The hostname or IP address from your producer.<br> **Datatype:** string
|
||||||
| `producers.port` | **Required.** The port matching the above host.<br> **Datatype:** string
|
| `producers.port` | **Required.** The port matching the above host.<br>*Defaults to `8080`.*<br> **Datatype:** Integer
|
||||||
| `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.<br> **Datatype:** string
|
| `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.<br> **Datatype:** string
|
||||||
| `producers.ws_token` | **Required.** `ws_token` as configured on the producer.<br> **Datatype:** string
|
| `producers.ws_token` | **Required.** `ws_token` as configured on the producer.<br> **Datatype:** string
|
||||||
| | **Optional settings**
|
| | **Optional settings**
|
||||||
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
|
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `wait_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `ping_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `false`.*<br> **Datatype:** Boolean.
|
||||||
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
|
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
|
||||||
|
|
||||||
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
|
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.3.7
|
markdown==3.3.7
|
||||||
mkdocs==1.4.2
|
mkdocs==1.4.3
|
||||||
mkdocs-material==9.0.13
|
mkdocs-material==9.1.14
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==9.9.2
|
pymdown-extensions==10.0.1
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
|||||||
+3
-4
@@ -9,9 +9,6 @@ This same command can also be used to update freqUI, should there be a new relea
|
|||||||
|
|
||||||
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
|
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
|
||||||
|
|
||||||
!!! info "Alpha release"
|
|
||||||
FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose).
|
|
||||||
|
|
||||||
!!! Note "developers"
|
!!! Note "developers"
|
||||||
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
|
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
|
||||||
|
|
||||||
@@ -137,7 +134,9 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||||||
| `reload_config` | Reloads the configuration file.
|
| `reload_config` | Reloads the configuration file.
|
||||||
| `trades` | List last trades. Limited to 500 trades per call.
|
| `trades` | List last trades. Limited to 500 trades per call.
|
||||||
| `trade/<tradeid>` | Get specific trade.
|
| `trade/<tradeid>` | Get specific trade.
|
||||||
| `delete_trade <trade_id>` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
| `trade/<tradeid>` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||||
|
| `trade/<tradeid>/open-order` | DELETE - Cancel open order for this trade.
|
||||||
|
| `trade/<tradeid>/reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.
|
||||||
| `show_config` | Shows part of the current configuration with relevant settings to operation.
|
| `show_config` | Shows part of the current configuration with relevant settings to operation.
|
||||||
| `logs` | Shows last log messages.
|
| `logs` | Shows last log messages.
|
||||||
| `status` | Lists all open trades.
|
| `status` | Lists all open trades.
|
||||||
|
|||||||
+16
-9
@@ -23,10 +23,22 @@ These modes can be configured with these values:
|
|||||||
'stoploss_on_exchange_limit_ratio': 0.99
|
'stoploss_on_exchange_limit_ratio': 0.99
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
Stoploss on exchange is only supported for the following exchanges, and not all exchanges support both stop-limit and stop-market.
|
||||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gate (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
|
The Order-type will be ignored if only one mode is available.
|
||||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
|
||||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
| Exchange | stop-loss type |
|
||||||
|
|----------|-------------|
|
||||||
|
| Binance | limit |
|
||||||
|
| Binance Futures | market, limit |
|
||||||
|
| Huobi | limit |
|
||||||
|
| kraken | market, limit |
|
||||||
|
| Gate | limit |
|
||||||
|
| Okx | limit |
|
||||||
|
| Kucoin | stop-limit, stop-market|
|
||||||
|
|
||||||
|
!!! Note "Tight stoploss"
|
||||||
|
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
|
||||||
|
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
|
||||||
|
|
||||||
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
||||||
|
|
||||||
@@ -197,11 +209,6 @@ You can also keep a static stoploss until the offset is reached, and then trail
|
|||||||
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
|
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
|
||||||
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
|
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
|
||||||
|
|
||||||
``` python
|
|
||||||
trailing_stop_positive_offset = 0.011
|
|
||||||
trailing_only_offset_is_reached = True
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration (offset is buy-price + 3%):
|
Configuration (offset is buy-price + 3%):
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
# Advanced Strategies
|
# Advanced Strategies
|
||||||
|
|
||||||
This page explains some advanced concepts available for strategies.
|
This page explains some advanced concepts available for strategies.
|
||||||
If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first.
|
If you're just getting started, please familiarize yourself with the [Freqtrade basics](bot-basics.md) and methods described in [Strategy Customization](strategy-customization.md) first.
|
||||||
|
|
||||||
[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs.
|
The call sequence of the methods described here is covered under [bot execution logic](bot-basics.md#bot-execution-logic). Those docs are also helpful in deciding which method is most suitable for your customisation needs.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
All callback methods described below should only be implemented in a strategy if they are actually used.
|
Callback methods should *only* be implemented if a strategy uses them.
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
|
Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
|
||||||
|
|
||||||
## Storing information
|
## Storing information
|
||||||
|
|
||||||
Storing information can be accomplished by creating a new dictionary within the strategy class.
|
Storing information can be accomplished by creating a new dictionary within the strategy class.
|
||||||
|
|
||||||
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
|
The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
@@ -227,8 +227,8 @@ for val in self.buy_ema_short.range:
|
|||||||
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
# Append columns to existing dataframe
|
# Combine all dataframes, and reassign the original dataframe column
|
||||||
merged_frame = pd.concat(frames, axis=1)
|
dataframe = pd.concat(frames, axis=1)
|
||||||
```
|
```
|
||||||
|
|
||||||
Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant.
|
Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
# Assign this to the class by using self.*
|
# Assign this to the class by using self.*
|
||||||
# can then be used by populate_* methods
|
# can then be used by populate_* methods
|
||||||
self.cust_remote_data = requests.get('https://some_remote_source.example.com')
|
self.custom_remote_data = requests.get('https://some_remote_source.example.com')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -51,7 +51,8 @@ During hyperopt, this runs only once at startup.
|
|||||||
|
|
||||||
## Bot loop start
|
## Bot loop start
|
||||||
|
|
||||||
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5
|
||||||
|
seconds, unless configured differently) or once per candle in backtest/hyperopt mode.
|
||||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
@@ -61,11 +62,12 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
def bot_loop_start(self, **kwargs) -> None:
|
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Called at the start of the bot iteration (one loop).
|
Called at the start of the bot iteration (one loop).
|
||||||
Might be used to perform pair-independent tasks
|
Might be used to perform pair-independent tasks
|
||||||
(e.g. gather some remote resource for comparison)
|
(e.g. gather some remote resource for comparison)
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
"""
|
"""
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
@@ -316,11 +318,11 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
# evaluate highest to lowest, so that highest possible stop is used
|
# evaluate highest to lowest, so that highest possible stop is used
|
||||||
if current_profit > 0.40:
|
if current_profit > 0.40:
|
||||||
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
elif current_profit > 0.25:
|
elif current_profit > 0.25:
|
||||||
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
elif current_profit > 0.20:
|
elif current_profit > 0.20:
|
||||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
return 1
|
return 1
|
||||||
@@ -350,7 +352,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
# Convert absolute price to percentage relative to current_rate
|
# Convert absolute price to percentage relative to current_rate
|
||||||
if stoploss_price < current_rate:
|
if stoploss_price < current_rate:
|
||||||
return (stoploss_price / current_rate) - 1
|
return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -881,7 +881,7 @@ All columns of the informative dataframe will be available on the returning data
|
|||||||
|
|
||||||
### *stoploss_from_open()*
|
### *stoploss_from_open()*
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price.
|
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the entry point instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point.
|
||||||
|
|
||||||
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
|
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
|
||||||
|
|
||||||
@@ -889,6 +889,8 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
|||||||
|
|
||||||
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
|
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
|
||||||
|
|
||||||
|
This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%).
|
||||||
|
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
|
||||||
@@ -907,7 +909,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
|||||||
|
|
||||||
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||||
if current_profit > 0.10:
|
if current_profit > 0.10:
|
||||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -954,12 +956,14 @@ In some situations it may be confusing to deal with stops relative to current ra
|
|||||||
|
|
||||||
## Additional data (Wallets)
|
## Additional data (Wallets)
|
||||||
|
|
||||||
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
The strategy provides access to the `wallets` object. This contains the current balances on the exchange.
|
||||||
|
|
||||||
!!! Note
|
!!! Note "Backtesting / Hyperopt"
|
||||||
Wallets is not available during backtesting / hyperopt.
|
Wallets behaves differently depending on the function it's called.
|
||||||
|
Within `populate_*()` methods, it'll return the full wallet as configured.
|
||||||
|
Within [callbacks](strategy-callbacks.md), you'll get the wallet state corresponding to the actual simulated wallet at that point in the simulation process.
|
||||||
|
|
||||||
Please always check if `Wallets` is available to avoid failures during backtesting.
|
Please always check if `wallets` is available to avoid failures during backtesting.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
if self.wallets:
|
if self.wallets:
|
||||||
@@ -1036,11 +1040,10 @@ from datetime import timedelta, datetime, timezone
|
|||||||
|
|
||||||
# Within populate indicators (or populate_buy):
|
# Within populate indicators (or populate_buy):
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
# fetch closed trades for the last 2 days
|
# fetch closed trades for the last 2 days
|
||||||
trades = Trade.get_trades([Trade.pair == metadata['pair'],
|
trades = Trade.get_trades_proxy(
|
||||||
Trade.open_date > datetime.utcnow() - timedelta(days=2),
|
pair=metadata['pair'], is_open=False,
|
||||||
Trade.is_open.is_(False),
|
open_date=datetime.now(timezone.utc) - timedelta(days=2))
|
||||||
]).all()
|
|
||||||
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy
|
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy
|
||||||
sumprofit = sum(trade.close_profit for trade in trades)
|
sumprofit = sum(trade.close_profit for trade in trades)
|
||||||
if sumprofit < 0:
|
if sumprofit < 0:
|
||||||
|
|||||||
@@ -578,7 +578,7 @@ def populate_any_indicators(
|
|||||||
Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed.
|
Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed.
|
||||||
|
|
||||||
``` python linenums="1"
|
``` python linenums="1"
|
||||||
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
|
def feature_engineering_expand_all(self, dataframe, period, **kwargs) -> DataFrame::
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This function will automatically expand the defined features on the config defined
|
This function will automatically expand the defined features on the config defined
|
||||||
@@ -638,7 +638,7 @@ Features will now expand automatically. As such, the expansion loops, as well as
|
|||||||
Basic features. Make sure to remove the `{pair}` part from your features.
|
Basic features. Make sure to remove the `{pair}` part from your features.
|
||||||
|
|
||||||
``` python linenums="1"
|
``` python linenums="1"
|
||||||
def feature_engineering_expand_basic(self, dataframe, **kwargs):
|
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame::
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This function will automatically expand the defined features on the config defined
|
This function will automatically expand the defined features on the config defined
|
||||||
@@ -673,7 +673,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
|||||||
### FreqAI - feature engineering standard
|
### FreqAI - feature engineering standard
|
||||||
|
|
||||||
``` python linenums="1"
|
``` python linenums="1"
|
||||||
def feature_engineering_standard(self, dataframe, **kwargs):
|
def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
This optional function will be called once with the dataframe of the base timeframe.
|
This optional function will be called once with the dataframe of the base timeframe.
|
||||||
@@ -704,7 +704,7 @@ Basic features. Make sure to remove the `{pair}` part from your features.
|
|||||||
Targets now get their own, dedicated method.
|
Targets now get their own, dedicated method.
|
||||||
|
|
||||||
``` python linenums="1"
|
``` python linenums="1"
|
||||||
def set_freqai_targets(self, dataframe, **kwargs):
|
def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
*Only functional with FreqAI enabled strategies*
|
*Only functional with FreqAI enabled strategies*
|
||||||
Required function to set the targets for the model.
|
Required function to set the targets for the model.
|
||||||
|
|||||||
+32
-4
@@ -152,7 +152,7 @@ You can create your own keyboard in `config.json`:
|
|||||||
!!! Note "Supported Commands"
|
!!! Note "Supported Commands"
|
||||||
Only the following commands are allowed. Command arguments are not supported!
|
Only the following commands are allowed. Command arguments are not supported!
|
||||||
|
|
||||||
`/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`
|
`/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||||
|
|
||||||
## Telegram commands
|
## Telegram commands
|
||||||
|
|
||||||
@@ -179,6 +179,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/count` | Displays number of trades used and available
|
| `/count` | Displays number of trades used and available
|
||||||
| `/locks` | Show currently locked pairs.
|
| `/locks` | Show currently locked pairs.
|
||||||
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
|
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
|
||||||
|
| `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. If no direction is provided, the currently set direction will be displayed.
|
||||||
| **Modify Trade states** |
|
| **Modify Trade states** |
|
||||||
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||||
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
||||||
@@ -186,11 +187,13 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
|
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
|
||||||
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
|
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
|
||||||
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||||
|
| `/reload_trade <trade_id>` | Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange.
|
||||||
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
|
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
|
||||||
| **Metrics** |
|
| **Metrics** |
|
||||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||||
| `/performance` | Show performance of each finished trade grouped by pair
|
| `/performance` | Show performance of each finished trade grouped by pair
|
||||||
| `/balance` | Show account balance per currency
|
| `/balance` | Show bot managed balance per currency
|
||||||
|
| `/balance full` | Show account balance per currency
|
||||||
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
||||||
| `/weekly <n>` | Shows profit or loss per week, over the last n weeks (n defaults to 8)
|
| `/weekly <n>` | Shows profit or loss per week, over the last n weeks (n defaults to 8)
|
||||||
| `/monthly <n>` | Shows profit or loss per month, over the last n months (n defaults to 6)
|
| `/monthly <n>` | Shows profit or loss per month, over the last n months (n defaults to 6)
|
||||||
@@ -201,7 +204,6 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||||
|
|
||||||
|
|
||||||
## Telegram commands in action
|
## Telegram commands in action
|
||||||
|
|
||||||
Below, example of Telegram message you will receive for each command.
|
Below, example of Telegram message you will receive for each command.
|
||||||
@@ -242,7 +244,7 @@ Enter Tag is configurable via Strategy.
|
|||||||
> **Enter Tag:** Awesome Long Signal
|
> **Enter Tag:** Awesome Long Signal
|
||||||
> **Open Rate:** `0.00007489`
|
> **Open Rate:** `0.00007489`
|
||||||
> **Current Rate:** `0.00007489`
|
> **Current Rate:** `0.00007489`
|
||||||
> **Current Profit:** `12.95%`
|
> **Unrealized Profit:** `12.95%`
|
||||||
> **Stoploss:** `0.00007389 (-0.02%)`
|
> **Stoploss:** `0.00007389 (-0.02%)`
|
||||||
|
|
||||||
### /status table
|
### /status table
|
||||||
@@ -278,6 +280,7 @@ Return a summary of your profit/loss and performance.
|
|||||||
> ∙ `33.095 EUR`
|
> ∙ `33.095 EUR`
|
||||||
>
|
>
|
||||||
> **Total Trade Count:** `138`
|
> **Total Trade Count:** `138`
|
||||||
|
> **Bot started:** `2022-07-11 18:40:44`
|
||||||
> **First Trade opened:** `3 days ago`
|
> **First Trade opened:** `3 days ago`
|
||||||
> **Latest Trade opened:** `2 minutes ago`
|
> **Latest Trade opened:** `2 minutes ago`
|
||||||
> **Avg. Duration:** `2:33:45`
|
> **Avg. Duration:** `2:33:45`
|
||||||
@@ -291,6 +294,7 @@ The relative profit of `15.2 Σ%` is be based on the starting capital - so in th
|
|||||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||||
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
||||||
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||||
|
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
|
||||||
|
|
||||||
### /forceexit <trade_id>
|
### /forceexit <trade_id>
|
||||||
|
|
||||||
@@ -416,3 +420,27 @@ ARDR/ETH 0.366667 0.143059 -0.01
|
|||||||
### /version
|
### /version
|
||||||
|
|
||||||
> **Version:** `0.14.3`
|
> **Version:** `0.14.3`
|
||||||
|
|
||||||
|
### /marketdir
|
||||||
|
|
||||||
|
If a market direction is provided the command updates the user managed variable that represents the current market direction.
|
||||||
|
This variable is not set to any valid market direction on bot startup and must be set by the user. The example below is for `/marketdir long`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Successfully updated marketdirection from none to long.
|
||||||
|
```
|
||||||
|
|
||||||
|
If no market direction is provided the command outputs the currently set market directions. The example below is for `/marketdir`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Currently set marketdirection: even
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the market direction in your strategy via `self.market_direction`.
|
||||||
|
|
||||||
|
!!! Warning "Bot restarts"
|
||||||
|
Please note that the market direction is not persisted, and will be reset after a bot restart/reload.
|
||||||
|
|
||||||
|
!!! Danger "Backtesting"
|
||||||
|
As this value/variable is intended to be changed manually in dry/live trading.
|
||||||
|
Strategies using `market_direction` will probably not produce reliable, reproducible results (changes to this variable will not be reflected for backtesting). Use at your own risk.
|
||||||
|
|||||||
+61
-6
@@ -723,6 +723,9 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V]
|
|||||||
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
||||||
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
||||||
[--timerange YYYYMMDD-[YYYYMMDD]]
|
[--timerange YYYYMMDD-[YYYYMMDD]]
|
||||||
|
[--rejected]
|
||||||
|
[--analysis-to-csv]
|
||||||
|
[--analysis-csv-path PATH]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@@ -736,19 +739,27 @@ optional arguments:
|
|||||||
pair and enter_tag, 4: by pair, enter_ and exit_tag
|
pair and enter_tag, 4: by pair, enter_ and exit_tag
|
||||||
(this can get quite large)
|
(this can get quite large)
|
||||||
--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]
|
--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]
|
||||||
Comma separated list of entry signals to analyse.
|
Space separated list of entry signals to analyse.
|
||||||
Default: all. e.g. 'entry_tag_a,entry_tag_b'
|
Default: all. e.g. 'entry_tag_a entry_tag_b'
|
||||||
--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]
|
--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]
|
||||||
Comma separated list of exit signals to analyse.
|
Space separated list of exit signals to analyse.
|
||||||
Default: all. e.g.
|
Default: all. e.g.
|
||||||
'exit_tag_a,roi,stop_loss,trailing_stop_loss'
|
'exit_tag_a roi stop_loss trailing_stop_loss'
|
||||||
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
||||||
Comma separated list of indicators to analyse. e.g.
|
Space separated list of indicators to analyse. e.g.
|
||||||
'close,rsi,bb_lowerband,profit_abs'
|
'close rsi bb_lowerband profit_abs'
|
||||||
--timerange YYYYMMDD-[YYYYMMDD]
|
--timerange YYYYMMDD-[YYYYMMDD]
|
||||||
Timerange to filter trades for analysis,
|
Timerange to filter trades for analysis,
|
||||||
start inclusive, end exclusive. e.g.
|
start inclusive, end exclusive. e.g.
|
||||||
20220101-20220201
|
20220101-20220201
|
||||||
|
--rejected
|
||||||
|
Print out rejected trades table
|
||||||
|
--analysis-to-csv
|
||||||
|
Write out tables to individual CSVs, by default to
|
||||||
|
'user_data/backtest_results' unless '--analysis-csv-path' is given.
|
||||||
|
--analysis-csv-path [PATH]
|
||||||
|
Optional path where individual CSVs will be written. If not used,
|
||||||
|
CSVs will be written to 'user_data/backtest_results'.
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
@@ -955,3 +966,47 @@ Print trades with id 2 and 3 as json
|
|||||||
``` bash
|
``` bash
|
||||||
freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json
|
freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Strategy-Updater
|
||||||
|
|
||||||
|
Updates listed strategies or all strategies within the strategies folder to be v3 compliant.
|
||||||
|
If the command runs without --strategy-list then all strategies inside the strategies folder will be converted.
|
||||||
|
Your original strategy will remain available in the `user_data/strategies_orig_updater/` directory.
|
||||||
|
|
||||||
|
!!! Warning "Conversion results"
|
||||||
|
Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion.
|
||||||
|
We also recommend to run a python formatter (e.g. `black`) to format results in a sane manner.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade strategy-updater [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
|
Provide a space-separated list of strategies to
|
||||||
|
backtest. Please note that timeframe needs to be set
|
||||||
|
either in config or via command line. When using this
|
||||||
|
together with `--export trades`, the strategy-name is
|
||||||
|
injected into the filename (so `backtest-data.json`
|
||||||
|
becomes `backtest-data-SampleStrategy.json`
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE, --log-file FILE
|
||||||
|
Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
Specify configuration file (default:
|
||||||
|
`userdir/config.json` or `config.json` whichever
|
||||||
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH, --data-dir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ git clone https://github.com/freqtrade/freqtrade.git
|
|||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.25-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows.
|
||||||
|
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
||||||
|
|
||||||
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows.
|
|
||||||
Other versions must be downloaded from the above link.
|
Other versions must be downloaded from the above link.
|
||||||
|
|
||||||
``` powershell
|
``` powershell
|
||||||
@@ -45,8 +45,6 @@ freqtrade
|
|||||||
The above installation script assumes you're using powershell on a 64bit windows.
|
The above installation script assumes you're using powershell on a 64bit windows.
|
||||||
Commands for the legacy CMD windows console may differ.
|
Commands for the legacy CMD windows console may differ.
|
||||||
|
|
||||||
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
|
||||||
|
|
||||||
### Error during installation on Windows
|
### Error during installation on Windows
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
name: freqtrade
|
|
||||||
channels:
|
|
||||||
- conda-forge
|
|
||||||
# - defaults
|
|
||||||
dependencies:
|
|
||||||
# 1/4 req main
|
|
||||||
- python>=3.8,<=3.10
|
|
||||||
- numpy
|
|
||||||
- pandas
|
|
||||||
- pip
|
|
||||||
|
|
||||||
- py-find-1st
|
|
||||||
- aiohttp
|
|
||||||
- SQLAlchemy
|
|
||||||
- python-telegram-bot<20.0.0
|
|
||||||
- arrow
|
|
||||||
- cachetools
|
|
||||||
- requests
|
|
||||||
- urllib3
|
|
||||||
- jsonschema
|
|
||||||
- TA-Lib
|
|
||||||
- tabulate
|
|
||||||
- jinja2
|
|
||||||
- blosc
|
|
||||||
- sdnotify
|
|
||||||
- fastapi
|
|
||||||
- uvicorn
|
|
||||||
- pyjwt
|
|
||||||
- aiofiles
|
|
||||||
- psutil
|
|
||||||
- colorama
|
|
||||||
- questionary
|
|
||||||
- prompt-toolkit
|
|
||||||
- schedule
|
|
||||||
- python-dateutil
|
|
||||||
- joblib
|
|
||||||
- pyarrow
|
|
||||||
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# 2/4 req dev
|
|
||||||
|
|
||||||
- coveralls
|
|
||||||
- flake8
|
|
||||||
- mypy
|
|
||||||
- pytest
|
|
||||||
- pytest-asyncio
|
|
||||||
- pytest-cov
|
|
||||||
- pytest-mock
|
|
||||||
- isort
|
|
||||||
- nbconvert
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# 3/4 req hyperopt
|
|
||||||
|
|
||||||
- scipy
|
|
||||||
- scikit-learn<1.2.0
|
|
||||||
- filelock
|
|
||||||
- scikit-optimize
|
|
||||||
- progressbar2
|
|
||||||
# ============================
|
|
||||||
# 4/4 req plot
|
|
||||||
|
|
||||||
- plotly
|
|
||||||
- jupyter
|
|
||||||
|
|
||||||
- pip:
|
|
||||||
- pycoingecko
|
|
||||||
# - py_find_1st
|
|
||||||
- tables
|
|
||||||
- pytest-random-order
|
|
||||||
- ccxt
|
|
||||||
- flake8-tidy-imports
|
|
||||||
- -e .
|
|
||||||
# - python-rapidjso
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2023.2'
|
__version__ = '2023.5'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ from freqtrade.commands.optimize_commands import (start_backtesting, start_backt
|
|||||||
start_edge, start_hyperopt)
|
start_edge, start_hyperopt)
|
||||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||||
|
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
||||||
from freqtrade.commands.trade_commands import start_trading
|
from freqtrade.commands.trade_commands import start_trading
|
||||||
from freqtrade.commands.webserver_commands import start_webserver
|
from freqtrade.commands.webserver_commands import start_webserver
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
|
|||||||
|
|
||||||
if (not Path(signals_file).exists()):
|
if (not Path(signals_file).exists()):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
(f"Cannot find latest backtest signals file: {signals_file}."
|
f"Cannot find latest backtest signals file: {signals_file}."
|
||||||
"Run backtesting with `--export signals`.")
|
"Run backtesting with `--export signals`."
|
||||||
)
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_coloriz
|
|||||||
|
|
||||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
||||||
|
|
||||||
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"]
|
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"]
|
||||||
|
|
||||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||||
|
|
||||||
@@ -106,15 +106,19 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
|||||||
"disableparamexport", "backtest_breakdown"]
|
"disableparamexport", "backtest_breakdown"]
|
||||||
|
|
||||||
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
||||||
"exit_reason_list", "indicator_list", "timerange"]
|
"exit_reason_list", "indicator_list", "timerange",
|
||||||
|
"analysis_rejected", "analysis_to_csv", "analysis_csv_path"]
|
||||||
|
|
||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
||||||
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter",
|
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv",
|
||||||
|
"strategy-updater"]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
|
|
||||||
|
ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||||
|
|
||||||
|
|
||||||
class Arguments:
|
class Arguments:
|
||||||
"""
|
"""
|
||||||
@@ -198,8 +202,8 @@ class Arguments:
|
|||||||
start_list_freqAI_models, start_list_markets,
|
start_list_freqAI_models, start_list_markets,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
start_new_config, start_new_strategy, start_plot_dataframe,
|
||||||
start_plot_profit, start_show_trades, start_test_pairlist,
|
start_plot_profit, start_show_trades, start_strategy_update,
|
||||||
start_trading, start_webserver)
|
start_test_pairlist, start_trading, start_webserver)
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='command',
|
subparsers = self.parser.add_subparsers(dest='command',
|
||||||
# Use custom message when no subhandler is added
|
# Use custom message when no subhandler is added
|
||||||
@@ -440,3 +444,11 @@ class Arguments:
|
|||||||
parents=[_common_parser])
|
parents=[_common_parser])
|
||||||
webserver_cmd.set_defaults(func=start_webserver)
|
webserver_cmd.set_defaults(func=start_webserver)
|
||||||
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
|
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
|
||||||
|
|
||||||
|
# Add strategy_updater subcommand
|
||||||
|
strategy_updater_cmd = subparsers.add_parser('strategy-updater',
|
||||||
|
help='updates outdated strategy'
|
||||||
|
'files to the current version',
|
||||||
|
parents=[_common_parser])
|
||||||
|
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
||||||
|
self._build_args(optionlist=ARGS_STRATEGY_UTILS, parser=strategy_updater_cmd)
|
||||||
|
|||||||
@@ -636,30 +636,45 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"4: by pair, enter_ and exit_tag (this can get quite large), "
|
"4: by pair, enter_ and exit_tag (this can get quite large), "
|
||||||
"5: by exit_tag"),
|
"5: by exit_tag"),
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default=['0', '1', '2'],
|
default=[],
|
||||||
choices=['0', '1', '2', '3', '4', '5'],
|
choices=['0', '1', '2', '3', '4', '5'],
|
||||||
),
|
),
|
||||||
"enter_reason_list": Arg(
|
"enter_reason_list": Arg(
|
||||||
"--enter-reason-list",
|
"--enter-reason-list",
|
||||||
help=("Comma separated list of entry signals to analyse. Default: all. "
|
help=("Space separated list of entry signals to analyse. Default: all. "
|
||||||
"e.g. 'entry_tag_a,entry_tag_b'"),
|
"e.g. 'entry_tag_a entry_tag_b'"),
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default=['all'],
|
default=['all'],
|
||||||
),
|
),
|
||||||
"exit_reason_list": Arg(
|
"exit_reason_list": Arg(
|
||||||
"--exit-reason-list",
|
"--exit-reason-list",
|
||||||
help=("Comma separated list of exit signals to analyse. Default: all. "
|
help=("Space separated list of exit signals to analyse. Default: all. "
|
||||||
"e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"),
|
"e.g. 'exit_tag_a roi stop_loss trailing_stop_loss'"),
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default=['all'],
|
default=['all'],
|
||||||
),
|
),
|
||||||
"indicator_list": Arg(
|
"indicator_list": Arg(
|
||||||
"--indicator-list",
|
"--indicator-list",
|
||||||
help=("Comma separated list of indicators to analyse. "
|
help=("Space separated list of indicators to analyse. "
|
||||||
"e.g. 'close,rsi,bb_lowerband,profit_abs'"),
|
"e.g. 'close rsi bb_lowerband profit_abs'"),
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default=[],
|
default=[],
|
||||||
),
|
),
|
||||||
|
"analysis_rejected": Arg(
|
||||||
|
'--rejected-signals',
|
||||||
|
help='Analyse rejected signals',
|
||||||
|
action='store_true',
|
||||||
|
),
|
||||||
|
"analysis_to_csv": Arg(
|
||||||
|
'--analysis-to-csv',
|
||||||
|
help='Save selected analysis tables to individual CSVs',
|
||||||
|
action='store_true',
|
||||||
|
),
|
||||||
|
"analysis_csv_path": Arg(
|
||||||
|
'--analysis-csv-path',
|
||||||
|
help=("Specify a path to save the analysis CSVs "
|
||||||
|
"if --analysis-to-csv is enabled. Default: user_data/basktesting_results/"),
|
||||||
|
),
|
||||||
"freqaimodel": Arg(
|
"freqaimodel": Arg(
|
||||||
'--freqaimodel',
|
'--freqaimodel',
|
||||||
help='Specify a custom freqaimodels.',
|
help='Specify a custom freqaimodels.',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
|
||||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
||||||
refresh_backtest_trades_data)
|
refresh_backtest_trades_data)
|
||||||
@@ -20,15 +20,24 @@ from freqtrade.util.binance_mig import migrate_binance_futures_data
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _data_download_sanity(config: Config) -> None:
|
||||||
|
if 'days' in config and 'timerange' in config:
|
||||||
|
raise OperationalException("--days and --timerange are mutually exclusive. "
|
||||||
|
"You can only specify one or the other.")
|
||||||
|
|
||||||
|
if 'pairs' not in config:
|
||||||
|
raise OperationalException(
|
||||||
|
"Downloading data requires a list of pairs. "
|
||||||
|
"Please check the documentation on how to configure this.")
|
||||||
|
|
||||||
|
|
||||||
def start_download_data(args: Dict[str, Any]) -> None:
|
def start_download_data(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Download data (former download_backtest_data.py script)
|
Download data (former download_backtest_data.py script)
|
||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
if 'days' in config and 'timerange' in config:
|
_data_download_sanity(config)
|
||||||
raise OperationalException("--days and --timerange are mutually exclusive. "
|
|
||||||
"You can only specify one or the other.")
|
|
||||||
timerange = TimeRange()
|
timerange = TimeRange()
|
||||||
if 'days' in config:
|
if 'days' in config:
|
||||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||||
@@ -40,15 +49,10 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
config['stake_currency'] = ''
|
config['stake_currency'] = ''
|
||||||
|
|
||||||
if 'pairs' not in config:
|
|
||||||
raise OperationalException(
|
|
||||||
"Downloading data requires a list of pairs. "
|
|
||||||
"Please check the documentation on how to configure this.")
|
|
||||||
|
|
||||||
pairs_not_available: List[str] = []
|
pairs_not_available: List[str] = []
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
|
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
|
||||||
or config.get('include_inactive')]
|
or config.get('include_inactive')]
|
||||||
|
|
||||||
@@ -121,7 +125,7 @@ def start_convert_trades(args: Dict[str, Any]) -> None:
|
|||||||
"Please check the documentation on how to configure this.")
|
"Please check the documentation on how to configure this.")
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
# Manual validations of relevant settings
|
# Manual validations of relevant settings
|
||||||
if not config['exchange'].get('skip_pair_validation', False):
|
if not config['exchange'].get('skip_pair_validation', False):
|
||||||
exchange.validate_pairs(config['pairs'])
|
exchange.validate_pairs(config['pairs'])
|
||||||
@@ -200,11 +204,14 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
|||||||
pair, timeframe, candle_type,
|
pair, timeframe, candle_type,
|
||||||
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
||||||
) for pair, timeframe, candle_type in paircombs]
|
) for pair, timeframe, candle_type in paircombs]
|
||||||
|
|
||||||
print(tabulate([
|
print(tabulate([
|
||||||
(pair, timeframe, candle_type,
|
(pair, timeframe, candle_type,
|
||||||
start.strftime(DATETIME_PRINT_FORMAT),
|
start.strftime(DATETIME_PRINT_FORMAT),
|
||||||
end.strftime(DATETIME_PRINT_FORMAT))
|
end.strftime(DATETIME_PRINT_FORMAT))
|
||||||
for pair, timeframe, candle_type, start, end in paircombs1
|
for pair, timeframe, candle_type, start, end in sorted(
|
||||||
|
paircombs1,
|
||||||
|
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
|
||||||
],
|
],
|
||||||
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
||||||
tablefmt='psql', stralign='right'))
|
tablefmt='psql', stralign='right'))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
@@ -20,7 +20,7 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
init_db(config['db_url'])
|
init_db(config['db_url'])
|
||||||
session_target = Trade._session
|
session_target = Trade.session
|
||||||
init_db(config['db_url_from'])
|
init_db(config['db_url_from'])
|
||||||
logger.info("Starting db migration.")
|
logger.info("Starting db migration.")
|
||||||
|
|
||||||
@@ -36,16 +36,16 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
session_target.commit()
|
session_target.commit()
|
||||||
|
|
||||||
for pairlock in PairLock.query:
|
for pairlock in PairLock.get_all_locks():
|
||||||
pairlock_count += 1
|
pairlock_count += 1
|
||||||
make_transient(pairlock)
|
make_transient(pairlock)
|
||||||
session_target.add(pairlock)
|
session_target.add(pairlock)
|
||||||
session_target.commit()
|
session_target.commit()
|
||||||
|
|
||||||
# Update sequences
|
# Update sequences
|
||||||
max_trade_id = session_target.query(func.max(Trade.id)).scalar()
|
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
||||||
max_order_id = session_target.query(func.max(Order.id)).scalar()
|
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
||||||
max_pairlock_id = session_target.query(func.max(PairLock.id)).scalar()
|
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
||||||
|
|
||||||
set_sequence_ids(session_target.get_bind(),
|
set_sequence_ids(session_target.get_bind(),
|
||||||
trade_id=max_trade_id,
|
trade_id=max_trade_id,
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
|
|||||||
config['timeframe'] = None
|
config['timeframe'] = None
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
|
||||||
if args['print_one_column']:
|
if args['print_one_column']:
|
||||||
print('\n'.join(exchange.timeframes))
|
print('\n'.join(exchange.timeframes))
|
||||||
@@ -133,7 +133,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
|
||||||
# By default only active pairs/markets are to be shown
|
# By default only active pairs/markets are to be shown
|
||||||
active_only = not args.get('list_pairs_all', False)
|
active_only = not args.get('list_pairs_all', False)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
|
|||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
|
||||||
quote_currencies = args.get('quote_currencies')
|
quote_currencies = args.get('quote_currencies')
|
||||||
if not quote_currencies:
|
if not quote_currencies:
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
|
from freqtrade.enums import RunMode
|
||||||
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.strategy.strategyupdater import StrategyUpdater
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def start_strategy_update(args: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Start the strategy updating script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if sys.version_info == (3, 8): # pragma: no cover
|
||||||
|
sys.exit("Freqtrade strategy updater requires Python version >= 3.9")
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
|
config, enum_failed=False, recursive=config.get('recursive_strategy_search', False))
|
||||||
|
|
||||||
|
filtered_strategy_objs = []
|
||||||
|
if args['strategy_list']:
|
||||||
|
filtered_strategy_objs = [
|
||||||
|
strategy_obj for strategy_obj in strategy_objs
|
||||||
|
if strategy_obj['name'] in args['strategy_list']
|
||||||
|
]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Use all available entries.
|
||||||
|
filtered_strategy_objs = strategy_objs
|
||||||
|
|
||||||
|
processed_locations = set()
|
||||||
|
for strategy_obj in filtered_strategy_objs:
|
||||||
|
if strategy_obj['location'] not in processed_locations:
|
||||||
|
processed_locations.add(strategy_obj['location'])
|
||||||
|
start_conversion(strategy_obj, config)
|
||||||
|
|
||||||
|
|
||||||
|
def start_conversion(strategy_obj, config):
|
||||||
|
print(f"Conversion of {Path(strategy_obj['location']).name} started.")
|
||||||
|
instance_strategy_updater = StrategyUpdater()
|
||||||
|
start = time.perf_counter()
|
||||||
|
instance_strategy_updater.start(config, strategy_obj)
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"Conversion of {Path(strategy_obj['location']).name} took {elapsed:.1f} seconds.")
|
||||||
@@ -27,10 +27,7 @@ def _extend_validator(validator_class):
|
|||||||
if 'default' in subschema:
|
if 'default' in subschema:
|
||||||
instance.setdefault(prop, subschema['default'])
|
instance.setdefault(prop, subschema['default'])
|
||||||
|
|
||||||
for error in validate_properties(
|
yield from validate_properties(validator, properties, instance, schema)
|
||||||
validator, properties, instance, schema,
|
|
||||||
):
|
|
||||||
yield error
|
|
||||||
|
|
||||||
return validators.extend(
|
return validators.extend(
|
||||||
validator_class, {'properties': set_defaults}
|
validator_class, {'properties': set_defaults}
|
||||||
@@ -177,7 +174,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]):
|
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]):
|
||||||
if (pl.get('method') == 'StaticPairList'
|
if (isinstance(pl, dict) and pl.get('method') == 'StaticPairList'
|
||||||
and not conf.get('exchange', {}).get('pair_whitelist')):
|
and not conf.get('exchange', {}).get('pair_whitelist')):
|
||||||
raise OperationalException("StaticPairList requires pair_whitelist to be set.")
|
raise OperationalException("StaticPairList requires pair_whitelist to be set.")
|
||||||
|
|
||||||
|
|||||||
@@ -465,6 +465,15 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='timerange',
|
self._args_to_config(config, argname='timerange',
|
||||||
logstring='Filter trades by timerange: {}')
|
logstring='Filter trades by timerange: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='analysis_rejected',
|
||||||
|
logstring='Analyse rejected signals: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='analysis_to_csv',
|
||||||
|
logstring='Store analysis tables to CSV: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='analysis_csv_path',
|
||||||
|
logstring='Path to store analysis CSVs: {}')
|
||||||
|
|
||||||
def _process_runmode(self, config: Config) -> None:
|
def _process_runmode(self, config: Config) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='dry_run',
|
self._args_to_config(config, argname='dry_run',
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Read config from stdin if requested in the options
|
# Read config from stdin if requested in the options
|
||||||
with open(path) if path != '-' else sys.stdin as file:
|
with Path(path).open() if path != '-' else sys.stdin as file:
|
||||||
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import re
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import arrow
|
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
@@ -116,7 +114,7 @@ class TimeRange:
|
|||||||
:param text: value from --timerange
|
:param text: value from --timerange
|
||||||
:return: Start and End range period
|
:return: Start and End range period
|
||||||
"""
|
"""
|
||||||
if text is None:
|
if not text:
|
||||||
return TimeRange(None, None, 0, 0)
|
return TimeRange(None, None, 0, 0)
|
||||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||||
(r'^(\d{8})-$', ('date', None)),
|
(r'^(\d{8})-$', ('date', None)),
|
||||||
@@ -139,7 +137,8 @@ class TimeRange:
|
|||||||
if stype[0]:
|
if stype[0]:
|
||||||
starts = rvals[index]
|
starts = rvals[index]
|
||||||
if stype[0] == 'date' and len(starts) == 8:
|
if stype[0] == 'date' and len(starts) == 8:
|
||||||
start = arrow.get(starts, 'YYYYMMDD').int_timestamp
|
start = int(datetime.strptime(starts, '%Y%m%d').replace(
|
||||||
|
tzinfo=timezone.utc).timestamp())
|
||||||
elif len(starts) == 13:
|
elif len(starts) == 13:
|
||||||
start = int(starts) // 1000
|
start = int(starts) // 1000
|
||||||
else:
|
else:
|
||||||
@@ -148,7 +147,8 @@ class TimeRange:
|
|||||||
if stype[1]:
|
if stype[1]:
|
||||||
stops = rvals[index]
|
stops = rvals[index]
|
||||||
if stype[1] == 'date' and len(stops) == 8:
|
if stype[1] == 'date' and len(stops) == 8:
|
||||||
stop = arrow.get(stops, 'YYYYMMDD').int_timestamp
|
stop = int(datetime.strptime(stops, '%Y%m%d').replace(
|
||||||
|
tzinfo=timezone.utc).timestamp())
|
||||||
elif len(stops) == 13:
|
elif len(stops) == 13:
|
||||||
stop = int(stops) // 1000
|
stop = int(stops) // 1000
|
||||||
else:
|
else:
|
||||||
|
|||||||
+10
-4
@@ -36,9 +36,10 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
|
|||||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
||||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5']
|
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet']
|
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
|
||||||
|
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
|
||||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||||
BACKTEST_CACHE_DEFAULT = 'day'
|
BACKTEST_CACHE_DEFAULT = 'day'
|
||||||
@@ -63,6 +64,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels'
|
|||||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||||
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||||
FULL_DATAFRAME_THRESHOLD = 100
|
FULL_DATAFRAME_THRESHOLD = 100
|
||||||
|
CUSTOM_TAG_MAX_LENGTH = 255
|
||||||
|
|
||||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||||
|
|
||||||
@@ -588,6 +590,7 @@ CONF_SCHEMA = {
|
|||||||
"rl_config": {
|
"rl_config": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"drop_ohlc_from_features": {"type": "boolean", "default": False},
|
||||||
"train_cycles": {"type": "integer"},
|
"train_cycles": {"type": "integer"},
|
||||||
"max_trade_duration_candles": {"type": "integer"},
|
"max_trade_duration_candles": {"type": "integer"},
|
||||||
"add_state_info": {"type": "boolean", "default": False},
|
"add_state_info": {"type": "boolean", "default": False},
|
||||||
@@ -596,7 +599,8 @@ CONF_SCHEMA = {
|
|||||||
"model_type": {"type": "string", "default": "PPO"},
|
"model_type": {"type": "string", "default": "PPO"},
|
||||||
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
||||||
"net_arch": {"type": "array", "default": [128, 128]},
|
"net_arch": {"type": "array", "default": [128, 128]},
|
||||||
"randomize_startinng_position": {"type": "boolean", "default": False},
|
"randomize_starting_position": {"type": "boolean", "default": False},
|
||||||
|
"progress_bar": {"type": "boolean", "default": True},
|
||||||
"model_reward_parameters": {
|
"model_reward_parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -686,4 +690,6 @@ BidAsk = Literal['bid', 'ask']
|
|||||||
OBLiteral = Literal['asks', 'bids']
|
OBLiteral = Literal['asks', 'bids']
|
||||||
|
|
||||||
Config = Dict[str, Any]
|
Config = Dict[str, Any]
|
||||||
|
# Exchange part of the configuration.
|
||||||
|
ExchangeConfig = Dict[str, Any]
|
||||||
IntOrInf = float
|
IntOrInf = float
|
||||||
|
|||||||
@@ -246,14 +246,8 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
|||||||
"""
|
"""
|
||||||
Compatibility support for older backtest data.
|
Compatibility support for older backtest data.
|
||||||
"""
|
"""
|
||||||
df['open_date'] = pd.to_datetime(df['open_date'],
|
df['open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
||||||
utc=True,
|
df['close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
df['close_date'] = pd.to_datetime(df['close_date'],
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
# Compatibility support for pre short Columns
|
# Compatibility support for pre short Columns
|
||||||
if 'is_short' not in df.columns:
|
if 'is_short' not in df.columns:
|
||||||
df['is_short'] = False
|
df['is_short'] = False
|
||||||
@@ -346,7 +340,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
|||||||
return df_final[df_final['open_trades'] > max_open_trades]
|
return df_final[df_final['open_trades'] > max_open_trades]
|
||||||
|
|
||||||
|
|
||||||
def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
|
def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Convert list of Trade objects to pandas Dataframe
|
Convert list of Trade objects to pandas Dataframe
|
||||||
:param trades: List of trade objects
|
:param trades: List of trade objects
|
||||||
@@ -373,7 +367,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
|
|||||||
filters = []
|
filters = []
|
||||||
if strategy:
|
if strategy:
|
||||||
filters.append(Trade.strategy == strategy)
|
filters.append(Trade.strategy == strategy)
|
||||||
trades = trade_list_to_dataframe(Trade.get_trades(filters).all())
|
trades = trade_list_to_dataframe(list(Trade.get_trades(filters).all()))
|
||||||
|
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
|
|||||||
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, infer_datetime_format=True)
|
df['date'] = to_datetime(df['date'], unit='ms', utc=True)
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange, timeframe_to_seconds
|
|||||||
from freqtrade.exchange.types import OrderBook
|
from freqtrade.exchange.types import OrderBook
|
||||||
from freqtrade.misc import append_candles_to_dataframe
|
from freqtrade.misc import append_candles_to_dataframe
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
|
from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg
|
||||||
from freqtrade.util import PeriodicCache
|
from freqtrade.util import PeriodicCache
|
||||||
|
|
||||||
|
|
||||||
@@ -118,8 +119,7 @@ class DataProvider:
|
|||||||
:param new_candle: This is a new candle
|
:param new_candle: This is a new candle
|
||||||
"""
|
"""
|
||||||
if self.__rpc:
|
if self.__rpc:
|
||||||
self.__rpc.send_msg(
|
msg: RPCAnalyzedDFMsg = {
|
||||||
{
|
|
||||||
'type': RPCMessageType.ANALYZED_DF,
|
'type': RPCMessageType.ANALYZED_DF,
|
||||||
'data': {
|
'data': {
|
||||||
'key': pair_key,
|
'key': pair_key,
|
||||||
@@ -127,7 +127,7 @@ class DataProvider:
|
|||||||
'la': datetime.now(timezone.utc)
|
'la': datetime.now(timezone.utc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
self.__rpc.send_msg(msg)
|
||||||
if new_candle:
|
if new_candle:
|
||||||
self.__rpc.send_msg({
|
self.__rpc.send_msg({
|
||||||
'type': RPCMessageType.NEW_CANDLE,
|
'type': RPCMessageType.NEW_CANDLE,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import joblib
|
import joblib
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -15,22 +16,31 @@ from freqtrade.exceptions import OperationalException
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _load_signal_candles(backtest_dir: Path):
|
def _load_backtest_analysis_data(backtest_dir: Path, name: str):
|
||||||
if backtest_dir.is_dir():
|
if backtest_dir.is_dir():
|
||||||
scpf = Path(backtest_dir,
|
scpf = Path(backtest_dir,
|
||||||
Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl"
|
Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
|
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scp = open(scpf, "rb")
|
with scpf.open("rb") as scp:
|
||||||
signal_candles = joblib.load(scp)
|
loaded_data = joblib.load(scp)
|
||||||
logger.info(f"Loaded signal candles: {str(scpf)}")
|
logger.info(f"Loaded {name} candles: {str(scpf)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Cannot load signal candles from pickled results: ", e)
|
logger.error(f"Cannot load {name} data from pickled results: ", e)
|
||||||
|
return None
|
||||||
|
|
||||||
return signal_candles
|
return loaded_data
|
||||||
|
|
||||||
|
|
||||||
|
def _load_rejected_signals(backtest_dir: Path):
|
||||||
|
return _load_backtest_analysis_data(backtest_dir, "rejected")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_signal_candles(backtest_dir: Path):
|
||||||
|
return _load_backtest_analysis_data(backtest_dir, "signals")
|
||||||
|
|
||||||
|
|
||||||
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
|
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
|
||||||
@@ -43,9 +53,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
|||||||
for pair in pairlist:
|
for pair in pairlist:
|
||||||
if pair in signal_candles[strategy_name]:
|
if pair in signal_candles[strategy_name]:
|
||||||
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
||||||
pair,
|
pair, trades, signal_candles[strategy_name][pair])
|
||||||
trades,
|
|
||||||
signal_candles[strategy_name][pair])
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
||||||
|
|
||||||
@@ -85,7 +93,7 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
def _do_group_table_output(bigdf, glist):
|
def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ):
|
||||||
for g in glist:
|
for g in glist:
|
||||||
# 0: summary wins/losses grouped by enter tag
|
# 0: summary wins/losses grouped by enter tag
|
||||||
if g == "0":
|
if g == "0":
|
||||||
@@ -116,7 +124,8 @@ def _do_group_table_output(bigdf, glist):
|
|||||||
|
|
||||||
sortcols = ['total_num_buys']
|
sortcols = ['total_num_buys']
|
||||||
|
|
||||||
_print_table(new, sortcols, show_index=True)
|
_print_table(new, sortcols, show_index=True, name="Group 0:",
|
||||||
|
to_csv=to_csv, csv_path=csv_path)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
||||||
@@ -154,11 +163,24 @@ def _do_group_table_output(bigdf, glist):
|
|||||||
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
|
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
|
||||||
new['total_profit_pct'] = new['total_profit_pct'] * 100
|
new['total_profit_pct'] = new['total_profit_pct'] * 100
|
||||||
|
|
||||||
_print_table(new, sortcols)
|
_print_table(new, sortcols, name=f"Group {g}:",
|
||||||
|
to_csv=to_csv, csv_path=csv_path)
|
||||||
else:
|
else:
|
||||||
logger.warning("Invalid group mask specified.")
|
logger.warning("Invalid group mask specified.")
|
||||||
|
|
||||||
|
|
||||||
|
def _do_rejected_signals_output(rejected_signals_df: pd.DataFrame,
|
||||||
|
to_csv: bool = False, csv_path=None) -> None:
|
||||||
|
cols = ['pair', 'date', 'enter_tag']
|
||||||
|
sortcols = ['date', 'pair', 'enter_tag']
|
||||||
|
_print_table(rejected_signals_df[cols],
|
||||||
|
sortcols,
|
||||||
|
show_index=False,
|
||||||
|
name="Rejected Signals:",
|
||||||
|
to_csv=to_csv,
|
||||||
|
csv_path=csv_path)
|
||||||
|
|
||||||
|
|
||||||
def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'):
|
def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'):
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == 'date':
|
||||||
@@ -192,38 +214,64 @@ def prepare_results(analysed_trades, stratname,
|
|||||||
return res_df
|
return res_df
|
||||||
|
|
||||||
|
|
||||||
def print_results(res_df, analysis_groups, indicator_list):
|
def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_list: List[str],
|
||||||
|
csv_path: Path, rejected_signals=None, to_csv=False):
|
||||||
if res_df.shape[0] > 0:
|
if res_df.shape[0] > 0:
|
||||||
if analysis_groups:
|
if analysis_groups:
|
||||||
_do_group_table_output(res_df, analysis_groups)
|
_do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path)
|
||||||
|
|
||||||
|
if rejected_signals is not None:
|
||||||
|
if rejected_signals.empty:
|
||||||
|
print("There were no rejected signals.")
|
||||||
|
else:
|
||||||
|
_do_rejected_signals_output(rejected_signals, to_csv=to_csv, csv_path=csv_path)
|
||||||
|
|
||||||
|
# NB this can be large for big dataframes!
|
||||||
if "all" in indicator_list:
|
if "all" in indicator_list:
|
||||||
print(res_df)
|
_print_table(res_df,
|
||||||
elif indicator_list is not None:
|
show_index=False,
|
||||||
|
name="Indicators:",
|
||||||
|
to_csv=to_csv,
|
||||||
|
csv_path=csv_path)
|
||||||
|
elif indicator_list is not None and indicator_list:
|
||||||
available_inds = []
|
available_inds = []
|
||||||
for ind in indicator_list:
|
for ind in indicator_list:
|
||||||
if ind in res_df:
|
if ind in res_df:
|
||||||
available_inds.append(ind)
|
available_inds.append(ind)
|
||||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||||
_print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False)
|
_print_table(res_df[ilist],
|
||||||
|
sortcols=['exit_reason'],
|
||||||
|
show_index=False,
|
||||||
|
name="Indicators:",
|
||||||
|
to_csv=to_csv,
|
||||||
|
csv_path=csv_path)
|
||||||
else:
|
else:
|
||||||
print("\\No trades to show")
|
print("\\No trades to show")
|
||||||
|
|
||||||
|
|
||||||
def _print_table(df, sortcols=None, show_index=False):
|
def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None,
|
||||||
|
to_csv=False, csv_path: Path):
|
||||||
if (sortcols is not None):
|
if (sortcols is not None):
|
||||||
data = df.sort_values(sortcols)
|
data = df.sort_values(sortcols)
|
||||||
else:
|
else:
|
||||||
data = df
|
data = df
|
||||||
|
|
||||||
print(
|
if to_csv:
|
||||||
tabulate(
|
safe_name = Path(csv_path, name.lower().replace(" ", "_").replace(":", "") + ".csv")
|
||||||
data,
|
data.to_csv(safe_name)
|
||||||
headers='keys',
|
print(f"Saved {name} to {safe_name}")
|
||||||
tablefmt='psql',
|
else:
|
||||||
showindex=show_index
|
if name is not None:
|
||||||
|
print(name)
|
||||||
|
|
||||||
|
print(
|
||||||
|
tabulate(
|
||||||
|
data,
|
||||||
|
headers='keys',
|
||||||
|
tablefmt='psql',
|
||||||
|
showindex=show_index
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def process_entry_exit_reasons(config: Config):
|
def process_entry_exit_reasons(config: Config):
|
||||||
@@ -232,6 +280,11 @@ def process_entry_exit_reasons(config: Config):
|
|||||||
enter_reason_list = config.get('enter_reason_list', ["all"])
|
enter_reason_list = config.get('enter_reason_list', ["all"])
|
||||||
exit_reason_list = config.get('exit_reason_list', ["all"])
|
exit_reason_list = config.get('exit_reason_list', ["all"])
|
||||||
indicator_list = config.get('indicator_list', [])
|
indicator_list = config.get('indicator_list', [])
|
||||||
|
do_rejected = config.get('analysis_rejected', False)
|
||||||
|
to_csv = config.get('analysis_to_csv', False)
|
||||||
|
csv_path = Path(config.get('analysis_csv_path', config['exportfilename']))
|
||||||
|
if to_csv and not csv_path.is_dir():
|
||||||
|
raise OperationalException(f"Specified directory {csv_path} does not exist.")
|
||||||
|
|
||||||
timerange = TimeRange.parse_timerange(None if config.get(
|
timerange = TimeRange.parse_timerange(None if config.get(
|
||||||
'timerange') is None else str(config.get('timerange')))
|
'timerange') is None else str(config.get('timerange')))
|
||||||
@@ -241,8 +294,16 @@ def process_entry_exit_reasons(config: Config):
|
|||||||
for strategy_name, results in backtest_stats['strategy'].items():
|
for strategy_name, results in backtest_stats['strategy'].items():
|
||||||
trades = load_backtest_data(config['exportfilename'], strategy_name)
|
trades = load_backtest_data(config['exportfilename'], strategy_name)
|
||||||
|
|
||||||
if not trades.empty:
|
if trades is not None and not trades.empty:
|
||||||
signal_candles = _load_signal_candles(config['exportfilename'])
|
signal_candles = _load_signal_candles(config['exportfilename'])
|
||||||
|
|
||||||
|
rej_df = None
|
||||||
|
if do_rejected:
|
||||||
|
rejected_signals_dict = _load_rejected_signals(config['exportfilename'])
|
||||||
|
rej_df = prepare_results(rejected_signals_dict, strategy_name,
|
||||||
|
enter_reason_list, exit_reason_list,
|
||||||
|
timerange=timerange)
|
||||||
|
|
||||||
analysed_trades_dict = _process_candles_and_indicators(
|
analysed_trades_dict = _process_candles_and_indicators(
|
||||||
config['exchange']['pair_whitelist'], strategy_name,
|
config['exchange']['pair_whitelist'], strategy_name,
|
||||||
trades, signal_candles)
|
trades, signal_candles)
|
||||||
@@ -253,7 +314,10 @@ def process_entry_exit_reasons(config: Config):
|
|||||||
|
|
||||||
print_results(res_df,
|
print_results(res_df,
|
||||||
analysis_groups,
|
analysis_groups,
|
||||||
indicator_list)
|
indicator_list,
|
||||||
|
rejected_signals=rej_df,
|
||||||
|
to_csv=to_csv,
|
||||||
|
csv_path=csv_path)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from pandas import DataFrame, read_feather, to_datetime
|
from pandas import DataFrame, read_feather, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -63,10 +63,7 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
pairdata.columns = self._columns
|
pairdata.columns = self._columns
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
pairdata['date'] = to_datetime(pairdata['date'],
|
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
@@ -92,12 +89,11 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
:param data: List of Lists containing trade data,
|
:param data: List of Lists containing trade data,
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
self.create_dir_if_needed(filename)
|
||||||
|
|
||||||
raise NotImplementedError()
|
tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
|
||||||
# array = pa.array(data)
|
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
|
||||||
# array
|
|
||||||
# feather.write_feather(data, filename)
|
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: TradeList):
|
||||||
"""
|
"""
|
||||||
@@ -116,14 +112,13 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
if not filename.exists():
|
||||||
# tradesdata = misc.file_load_json(filename)
|
return []
|
||||||
|
|
||||||
# if not tradesdata:
|
tradesdata = read_feather(filename)
|
||||||
# return []
|
|
||||||
|
|
||||||
# return tradesdata
|
return tradesdata.values.tolist()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
|
||||||
from pandas import DataFrame, concat
|
from pandas import DataFrame, concat
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
@@ -236,8 +235,8 @@ def _download_pair_history(pair: str, *,
|
|||||||
new_data = exchange.get_historic_ohlcv(pair=pair,
|
new_data = exchange.get_historic_ohlcv(pair=pair,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
since_ms=since_ms if since_ms else
|
since_ms=since_ms if since_ms else
|
||||||
arrow.utcnow().shift(
|
int((datetime.now() - timedelta(days=new_pairs_days)
|
||||||
days=-new_pairs_days).int_timestamp * 1000,
|
).timestamp()) * 1000,
|
||||||
is_new_pair=data.empty,
|
is_new_pair=data.empty,
|
||||||
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
|
||||||
@@ -349,7 +348,7 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
trades = []
|
trades = []
|
||||||
|
|
||||||
if not since:
|
if not since:
|
||||||
since = arrow.utcnow().shift(days=-new_pairs_days).int_timestamp * 1000
|
since = int((datetime.now() - timedelta(days=-new_pairs_days)).timestamp()) * 1000
|
||||||
|
|
||||||
from_id = trades[-1][1] if trades else None
|
from_id = trades[-1][1] if trades else None
|
||||||
if trades and since < trades[-1][0]:
|
if trades and since < trades[-1][0]:
|
||||||
|
|||||||
@@ -75,10 +75,7 @@ class JsonDataHandler(IDataHandler):
|
|||||||
return DataFrame(columns=self._columns)
|
return DataFrame(columns=self._columns)
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
pairdata['date'] = to_datetime(pairdata['date'],
|
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
pairdata.columns = self._columns
|
pairdata.columns = self._columns
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
pairdata['date'] = to_datetime(pairdata['date'],
|
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Any, Dict, List, NamedTuple
|
from typing import Any, Dict, List, NamedTuple
|
||||||
|
|
||||||
import arrow
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import utils_find_1st as utf1st
|
import utils_find_1st as utf1st
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
from freqtrade.util import dt_now
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -79,8 +80,8 @@ class Edge:
|
|||||||
self._stoploss_range_step
|
self._stoploss_range_step
|
||||||
)
|
)
|
||||||
|
|
||||||
self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift(
|
self._timerange: TimeRange = TimeRange.parse_timerange(
|
||||||
days=-1 * self._since_number_of_days).format('YYYYMMDD'))
|
f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-")
|
||||||
if config.get('fee'):
|
if config.get('fee'):
|
||||||
self.fee = config['fee']
|
self.fee = config['fee']
|
||||||
else:
|
else:
|
||||||
@@ -97,7 +98,7 @@ class Edge:
|
|||||||
heartbeat = self.edge_config.get('process_throttle_secs')
|
heartbeat = self.edge_config.get('process_throttle_secs')
|
||||||
|
|
||||||
if (self._last_updated > 0) and (
|
if (self._last_updated > 0) and (
|
||||||
self._last_updated + heartbeat > arrow.utcnow().int_timestamp):
|
self._last_updated + heartbeat > int(dt_now().timestamp())):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
data: Dict[str, Any] = {}
|
data: Dict[str, Any] = {}
|
||||||
@@ -189,7 +190,7 @@ class Edge:
|
|||||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||||
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||||
self._cached_pairs = self._process_expectancy(trades_df)
|
self._cached_pairs = self._process_expectancy(trades_df)
|
||||||
self._last_updated = arrow.utcnow().int_timestamp
|
self._last_updated = int(dt_now().timestamp())
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from freqtrade.enums.exitchecktuple import ExitCheckTuple
|
|||||||
from freqtrade.enums.exittype import ExitType
|
from freqtrade.enums.exittype import ExitType
|
||||||
from freqtrade.enums.hyperoptstate import HyperoptState
|
from freqtrade.enums.hyperoptstate import HyperoptState
|
||||||
from freqtrade.enums.marginmode import MarginMode
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
|
from freqtrade.enums.marketstatetype import MarketDirection
|
||||||
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||||
from freqtrade.enums.pricetype import PriceType
|
from freqtrade.enums.pricetype import PriceType
|
||||||
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ 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:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ExitType(Enum):
|
|||||||
EMERGENCY_EXIT = "emergency_exit"
|
EMERGENCY_EXIT = "emergency_exit"
|
||||||
CUSTOM_EXIT = "custom_exit"
|
CUSTOM_EXIT = "custom_exit"
|
||||||
PARTIAL_EXIT = "partial_exit"
|
PARTIAL_EXIT = "partial_exit"
|
||||||
|
SOLD_ON_EXCHANGE = "sold_on_exchange"
|
||||||
NONE = ""
|
NONE = ""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class MarketDirection(Enum):
|
||||||
|
"""
|
||||||
|
Enum for various market directions.
|
||||||
|
"""
|
||||||
|
LONG = "long"
|
||||||
|
SHORT = "short"
|
||||||
|
EVEN = "even"
|
||||||
|
NONE = "none"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# convert to string
|
||||||
|
return self.value
|
||||||
@@ -4,6 +4,7 @@ from enum import Enum
|
|||||||
class RPCMessageType(str, Enum):
|
class RPCMessageType(str, Enum):
|
||||||
STATUS = 'status'
|
STATUS = 'status'
|
||||||
WARNING = 'warning'
|
WARNING = 'warning'
|
||||||
|
EXCEPTION = 'exception'
|
||||||
STARTUP = 'startup'
|
STARTUP = 'startup'
|
||||||
|
|
||||||
ENTRY = 'entry'
|
ENTRY = 'entry'
|
||||||
@@ -37,5 +38,8 @@ class RPCRequestType(str, Enum):
|
|||||||
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)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ 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(Enum):
|
class SignalTagType(Enum):
|
||||||
"""
|
"""
|
||||||
@@ -18,7 +21,13 @@ 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(str, Enum):
|
class SignalDirection(str, Enum):
|
||||||
LONG = 'long'
|
LONG = 'long'
|
||||||
SHORT = 'short'
|
SHORT = 'short'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name.lower()}"
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
# isort: off
|
# isort: off
|
||||||
from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS
|
from freqtrade.exchange.common import remove_exchange_credentials, 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
|
||||||
from freqtrade.exchange.bitpanda import Bitpanda
|
from freqtrade.exchange.bitpanda import Bitpanda
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
|
from freqtrade.exchange.bitvavo import Bitvavo
|
||||||
from freqtrade.exchange.bybit import Bybit
|
from freqtrade.exchange.bybit import Bybit
|
||||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||||
from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts,
|
from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
|
||||||
amount_to_precision, available_exchanges,
|
amount_to_contracts, amount_to_precision,
|
||||||
ccxt_exchanges, contracts_to_amount,
|
available_exchanges, ccxt_exchanges,
|
||||||
date_minus_candles, is_exchange_known_ccxt,
|
contracts_to_amount, date_minus_candles,
|
||||||
market_is_active, price_to_precision,
|
is_exchange_known_ccxt, market_is_active,
|
||||||
timeframe_to_minutes, timeframe_to_msecs,
|
price_to_precision, timeframe_to_minutes,
|
||||||
timeframe_to_next_date, timeframe_to_prev_date,
|
timeframe_to_msecs, timeframe_to_next_date,
|
||||||
timeframe_to_seconds, validate_exchange,
|
timeframe_to_prev_date, timeframe_to_seconds,
|
||||||
validate_exchanges)
|
validate_exchange, validate_exchanges)
|
||||||
from freqtrade.exchange.gate import Gate
|
from freqtrade.exchange.gate import Gate
|
||||||
from freqtrade.exchange.hitbtc import Hitbtc
|
from freqtrade.exchange.hitbtc import Hitbtc
|
||||||
from freqtrade.exchange.huobi import Huobi
|
from freqtrade.exchange.huobi import Huobi
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
""" Binance exchange subclass """
|
""" Binance exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||||
@@ -23,7 +22,7 @@ class Binance(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||||
"order_time_in_force": ['GTC', 'FOK', 'IOC'],
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "fromId",
|
"trades_pagination_arg": "fromId",
|
||||||
@@ -31,6 +30,7 @@ class Binance(Exchange):
|
|||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
|
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
||||||
"tickers_have_price": False,
|
"tickers_have_price": False,
|
||||||
"floor_leverage": True,
|
"floor_leverage": True,
|
||||||
"stop_price_type_field": "workingType",
|
"stop_price_type_field": "workingType",
|
||||||
@@ -65,7 +65,7 @@ class Binance(Exchange):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
|
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
|
||||||
position_side = self._api.fapiPrivateGetPositionsideDual()
|
position_side = self._api.fapiPrivateGetPositionSideDual()
|
||||||
self._log_exchange_response('position_side_setting', position_side)
|
self._log_exchange_response('position_side_setting', position_side)
|
||||||
assets_margin = self._api.fapiPrivateGetMultiAssetsMargin()
|
assets_margin = self._api.fapiPrivateGetMultiAssetsMargin()
|
||||||
self._log_exchange_response('multi_asset_margin', assets_margin)
|
self._log_exchange_response('multi_asset_margin', assets_margin)
|
||||||
@@ -104,8 +104,9 @@ class Binance(Exchange):
|
|||||||
if x and x[3] and x[3][0] and x[3][0][0] > since_ms:
|
if x and x[3] and x[3][0] and x[3][0][0] > since_ms:
|
||||||
# Set starting date to first available candle.
|
# Set starting date to first available candle.
|
||||||
since_ms = x[3][0][0]
|
since_ms = x[3][0][0]
|
||||||
logger.info(f"Candle-data for {pair} available starting with "
|
logger.info(
|
||||||
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
f"Candle-data for {pair} available starting with "
|
||||||
|
f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}.")
|
||||||
|
|
||||||
return await super()._async_get_historic_ohlcv(
|
return await super()._async_get_historic_ohlcv(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
@@ -195,7 +196,7 @@ class Binance(Exchange):
|
|||||||
leverage_tiers_path = (
|
leverage_tiers_path = (
|
||||||
Path(__file__).parent / 'binance_leverage_tiers.json'
|
Path(__file__).parent / 'binance_leverage_tiers.json'
|
||||||
)
|
)
|
||||||
with open(leverage_tiers_path) as json_file:
|
with leverage_tiers_path.open() as json_file:
|
||||||
return json_load(json_file)
|
return json_load(json_file)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
"""Kucoin exchange subclass."""
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Bitvavo(Exchange):
|
||||||
|
"""Bitvavo exchange class.
|
||||||
|
|
||||||
|
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||||
|
|
||||||
|
Please note that this exchange is not included in the list of exchanges
|
||||||
|
officially supported by the Freqtrade development team. So some features
|
||||||
|
may still not work as expected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ft_has: Dict = {
|
||||||
|
"ohlcv_candle_limit": 1440,
|
||||||
|
}
|
||||||
@@ -27,11 +27,10 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 200,
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"ohlcv_candle_limit": 200,
|
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
"mark_ohlcv_timeframe": "4h",
|
"mark_ohlcv_timeframe": "4h",
|
||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
@@ -115,7 +114,7 @@ class Bybit(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 _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||||
if self.trading_mode != TradingMode.SPOT:
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
params = {'leverage': leverage}
|
params = {'leverage': leverage}
|
||||||
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
|
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import time
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Callable, Optional, TypeVar, cast, overload
|
from typing import Any, Callable, Optional, TypeVar, cast, overload
|
||||||
|
|
||||||
|
from freqtrade.constants import ExchangeConfig
|
||||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
|
|
||||||
@@ -84,20 +85,22 @@ EXCHANGE_HAS_OPTIONAL = [
|
|||||||
# 'fetchPositions', # Futures trading
|
# 'fetchPositions', # Futures trading
|
||||||
# 'fetchLeverageTiers', # Futures initialization
|
# 'fetchLeverageTiers', # Futures initialization
|
||||||
# 'fetchMarketLeverageTiers', # Futures initialization
|
# 'fetchMarketLeverageTiers', # Futures initialization
|
||||||
|
# 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance...
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def remove_credentials(config) -> None:
|
def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Removes exchange keys from the configuration and specifies dry-run
|
Removes exchange keys from the configuration and specifies dry-run
|
||||||
Used for backtesting / hyperopt / edge and utils.
|
Used for backtesting / hyperopt / edge and utils.
|
||||||
Modifies the input dict!
|
Modifies the input dict!
|
||||||
"""
|
"""
|
||||||
if config.get('dry_run', False):
|
if dry_run:
|
||||||
config['exchange']['key'] = ''
|
exchange_config['key'] = ''
|
||||||
config['exchange']['secret'] = ''
|
exchange_config['apiKey'] = ''
|
||||||
config['exchange']['password'] = ''
|
exchange_config['secret'] = ''
|
||||||
config['exchange']['uid'] = ''
|
exchange_config['password'] = ''
|
||||||
|
exchange_config['uid'] = ''
|
||||||
|
|
||||||
|
|
||||||
def calculate_backoff(retrycount, max_retries):
|
def calculate_backoff(retrycount, max_retries):
|
||||||
|
|||||||
+193
-123
@@ -11,7 +11,6 @@ from math import floor
|
|||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
|
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
import arrow
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import ccxt.async_support as ccxt_async
|
import ccxt.async_support as ccxt_async
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
@@ -20,27 +19,30 @@ from dateutil import parser
|
|||||||
from pandas import DataFrame, concat
|
from pandas import DataFrame, concat
|
||||||
|
|
||||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
|
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
|
||||||
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
|
BuySell, Config, EntryExit, ExchangeConfig,
|
||||||
OBLiteral, PairWithTimeframe)
|
ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe)
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
||||||
from freqtrade.enums.pricetype import PriceType
|
from freqtrade.enums.pricetype import PriceType
|
||||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, OperationalException, PricingError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_exchange_credentials,
|
||||||
retrier_async)
|
retrier, retrier_async)
|
||||||
from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision,
|
from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType,
|
||||||
amount_to_contracts, amount_to_precision,
|
amount_to_contract_precision, amount_to_contracts,
|
||||||
contracts_to_amount, date_minus_candles,
|
amount_to_precision, contracts_to_amount,
|
||||||
is_exchange_known_ccxt, market_is_active,
|
date_minus_candles, is_exchange_known_ccxt,
|
||||||
price_to_precision, timeframe_to_minutes,
|
market_is_active, price_to_precision,
|
||||||
timeframe_to_msecs, timeframe_to_next_date,
|
timeframe_to_minutes, timeframe_to_msecs,
|
||||||
timeframe_to_prev_date, timeframe_to_seconds)
|
timeframe_to_next_date, timeframe_to_prev_date,
|
||||||
|
timeframe_to_seconds)
|
||||||
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
|
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
|
||||||
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
||||||
safe_value_fallback2)
|
safe_value_fallback2)
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
|
from freqtrade.util import dt_from_ts, dt_now
|
||||||
|
from freqtrade.util.datetime_helpers import dt_humanize, dt_ts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -59,8 +61,8 @@ class Exchange:
|
|||||||
# or by specifying them in the configuration.
|
# or by specifying them in the configuration.
|
||||||
_ft_has_default: Dict = {
|
_ft_has_default: Dict = {
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
"order_time_in_force": ["GTC"],
|
"order_time_in_force": ["GTC"],
|
||||||
"time_in_force_parameter": "timeInForce",
|
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_candle_limit": 500,
|
"ohlcv_candle_limit": 500,
|
||||||
"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
|
||||||
@@ -69,6 +71,7 @@ class Exchange:
|
|||||||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||||
"tickers_have_quoteVolume": True,
|
"tickers_have_quoteVolume": True,
|
||||||
|
"tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers
|
||||||
"tickers_have_price": True,
|
"tickers_have_price": True,
|
||||||
"trades_pagination": "time", # Possible are "time" or "id"
|
"trades_pagination": "time", # Possible are "time" or "id"
|
||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
@@ -80,6 +83,8 @@ class Exchange:
|
|||||||
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
||||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||||
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||||
|
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
|
||||||
|
"marketOrderRequiresPrice": False,
|
||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
_ft_has_futures: Dict = {}
|
_ft_has_futures: Dict = {}
|
||||||
@@ -88,8 +93,8 @@ class Exchange:
|
|||||||
# TradingMode.SPOT always supported and not required in this list
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, config: Config, validate: bool = True,
|
def __init__(self, config: Config, *, exchange_config: Optional[ExchangeConfig] = None,
|
||||||
load_leverage_tiers: bool = False) -> None:
|
validate: bool = True, load_leverage_tiers: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
it does basic validation whether the specified exchange and pairs are valid.
|
it does basic validation whether the specified exchange and pairs are valid.
|
||||||
@@ -103,8 +108,7 @@ class Exchange:
|
|||||||
# 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 = asyncio.new_event_loop()
|
self.loop = self._init_async_loop()
|
||||||
asyncio.set_event_loop(self.loop)
|
|
||||||
self._config: Config = {}
|
self._config: Config = {}
|
||||||
|
|
||||||
self._config.update(config)
|
self._config.update(config)
|
||||||
@@ -128,13 +132,13 @@ 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] = {}
|
||||||
remove_credentials(config)
|
|
||||||
|
|
||||||
if config['dry_run']:
|
if 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__}")
|
||||||
exchange_config = config['exchange']
|
exchange_conf: Dict[str, Any] = exchange_config if exchange_config else config['exchange']
|
||||||
self.log_responses = exchange_config.get('log_responses', False)
|
remove_exchange_credentials(exchange_conf, config.get('dry_run', False))
|
||||||
|
self.log_responses = exchange_conf.get('log_responses', False)
|
||||||
|
|
||||||
# Leverage properties
|
# Leverage properties
|
||||||
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
||||||
@@ -149,8 +153,8 @@ class Exchange:
|
|||||||
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
|
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
|
||||||
if exchange_config.get('_ft_has_params'):
|
if exchange_conf.get('_ft_has_params'):
|
||||||
self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'),
|
self._ft_has = deep_merge_dicts(exchange_conf.get('_ft_has_params'),
|
||||||
self._ft_has)
|
self._ft_has)
|
||||||
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
|
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
|
||||||
|
|
||||||
@@ -162,18 +166,18 @@ class Exchange:
|
|||||||
|
|
||||||
# Initialize ccxt objects
|
# Initialize ccxt objects
|
||||||
ccxt_config = self._ccxt_config
|
ccxt_config = self._ccxt_config
|
||||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config)
|
ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}), ccxt_config)
|
||||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config)
|
ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_sync_config', {}), ccxt_config)
|
||||||
|
|
||||||
self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config)
|
self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config)
|
||||||
|
|
||||||
ccxt_async_config = self._ccxt_config
|
ccxt_async_config = self._ccxt_config
|
||||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
|
ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}),
|
||||||
ccxt_async_config)
|
ccxt_async_config)
|
||||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
|
ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_async_config', {}),
|
||||||
ccxt_async_config)
|
ccxt_async_config)
|
||||||
self._api_async = self._init_ccxt(
|
self._api_async = self._init_ccxt(
|
||||||
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
||||||
|
|
||||||
logger.info(f'Using Exchange "{self.name}"')
|
logger.info(f'Using Exchange "{self.name}"')
|
||||||
self.required_candle_call_count = 1
|
self.required_candle_call_count = 1
|
||||||
@@ -186,7 +190,7 @@ class Exchange:
|
|||||||
self._startup_candle_count, config.get('timeframe', ''))
|
self._startup_candle_count, config.get('timeframe', ''))
|
||||||
|
|
||||||
# Converts the interval provided in minutes in config to seconds
|
# Converts the interval provided in minutes in config to seconds
|
||||||
self.markets_refresh_interval: int = exchange_config.get(
|
self.markets_refresh_interval: int = exchange_conf.get(
|
||||||
"markets_refresh_interval", 60) * 60
|
"markets_refresh_interval", 60) * 60
|
||||||
|
|
||||||
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
|
||||||
@@ -205,6 +209,13 @@ class Exchange:
|
|||||||
and self._api_async.session):
|
and self._api_async.session):
|
||||||
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())
|
||||||
|
if self.loop and not self.loop.is_closed():
|
||||||
|
self.loop.close()
|
||||||
|
|
||||||
|
def _init_async_loop(self) -> asyncio.AbstractEventLoop:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
return loop
|
||||||
|
|
||||||
def validate_config(self, config):
|
def validate_config(self, config):
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
@@ -480,7 +491,7 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
self._markets = self._api.load_markets(params={})
|
self._markets = self._api.load_markets(params={})
|
||||||
self._load_async_markets()
|
self._load_async_markets()
|
||||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
self._last_markets_refresh = dt_ts()
|
||||||
if self._ft_has['needs_trading_fees']:
|
if self._ft_has['needs_trading_fees']:
|
||||||
self._trading_fees = self.fetch_trading_fees()
|
self._trading_fees = self.fetch_trading_fees()
|
||||||
|
|
||||||
@@ -491,15 +502,14 @@ class Exchange:
|
|||||||
"""Reload markets both sync and async if refresh interval has passed """
|
"""Reload markets both sync and async if refresh interval has passed """
|
||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded
|
||||||
if (self._last_markets_refresh > 0) and (
|
if (self._last_markets_refresh > 0) and (
|
||||||
self._last_markets_refresh + self.markets_refresh_interval
|
self._last_markets_refresh + self.markets_refresh_interval > dt_ts()):
|
||||||
> arrow.utcnow().int_timestamp):
|
|
||||||
return None
|
return None
|
||||||
logger.debug("Performing scheduled market reload..")
|
logger.debug("Performing scheduled market reload..")
|
||||||
try:
|
try:
|
||||||
self._markets = self._api.load_markets(reload=True, params={})
|
self._markets = self._api.load_markets(reload=True, params={})
|
||||||
# Also reload async markets to avoid issues with newly listed pairs
|
# Also reload async markets to avoid issues with newly listed pairs
|
||||||
self._load_async_markets(reload=True)
|
self._load_async_markets(reload=True)
|
||||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
self._last_markets_refresh = dt_ts()
|
||||||
self.fill_leverage_tiers()
|
self.fill_leverage_tiers()
|
||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception("Could not reload markets.")
|
logger.exception("Could not reload markets.")
|
||||||
@@ -730,12 +740,14 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
|
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
|
||||||
|
|
||||||
def price_to_precision(self, pair: str, price: float) -> float:
|
def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
|
||||||
"""
|
"""
|
||||||
Returns the price rounded up to the precision the Exchange accepts.
|
Returns the price rounded to the precision the Exchange accepts.
|
||||||
Rounds up
|
The default price_rounding_mode in conf is ROUND.
|
||||||
|
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||||
"""
|
"""
|
||||||
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
|
return price_to_precision(price, self.get_precision_price(pair),
|
||||||
|
self.precisionMode, rounding_mode=rounding_mode)
|
||||||
|
|
||||||
def price_get_one_pip(self, pair: str, price: float) -> float:
|
def price_get_one_pip(self, pair: str, price: float) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -758,12 +770,12 @@ class Exchange:
|
|||||||
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
|
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
|
||||||
|
|
||||||
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
|
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
|
||||||
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
|
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage)
|
||||||
if max_stake_amount is None:
|
if max_stake_amount is None:
|
||||||
# * Should never be executed
|
# * Should never be executed
|
||||||
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
|
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
|
||||||
'never set max_stake_amount to None')
|
'never set max_stake_amount to None')
|
||||||
return max_stake_amount / leverage
|
return max_stake_amount
|
||||||
|
|
||||||
def _get_stake_amount_limit(
|
def _get_stake_amount_limit(
|
||||||
self,
|
self,
|
||||||
@@ -781,43 +793,41 @@ class Exchange:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(f"Can't get market information for symbol {pair}")
|
raise ValueError(f"Can't get market information for symbol {pair}")
|
||||||
|
|
||||||
|
if isMin:
|
||||||
|
# reserve some percent defined in config (5% default) + stoploss
|
||||||
|
margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent',
|
||||||
|
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||||
|
stoploss_reserve = (
|
||||||
|
margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||||
|
)
|
||||||
|
# it should not be more than 50%
|
||||||
|
stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
|
||||||
|
else:
|
||||||
|
margin_reserve = 1.0
|
||||||
|
stoploss_reserve = 1.0
|
||||||
|
|
||||||
stake_limits = []
|
stake_limits = []
|
||||||
limits = market['limits']
|
limits = market['limits']
|
||||||
if (limits['cost'][limit] is not None):
|
if (limits['cost'][limit] is not None):
|
||||||
stake_limits.append(
|
stake_limits.append(
|
||||||
self._contracts_to_amount(
|
self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve
|
||||||
pair,
|
|
||||||
limits['cost'][limit]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (limits['amount'][limit] is not None):
|
if (limits['amount'][limit] is not None):
|
||||||
stake_limits.append(
|
stake_limits.append(
|
||||||
self._contracts_to_amount(
|
self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve
|
||||||
pair,
|
|
||||||
limits['amount'][limit] * price
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not stake_limits:
|
if not stake_limits:
|
||||||
return None if isMin else float('inf')
|
return None if isMin else float('inf')
|
||||||
|
|
||||||
# reserve some percent defined in config (5% default) + stoploss
|
|
||||||
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
|
|
||||||
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
|
||||||
amount_reserve_percent = (
|
|
||||||
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
|
||||||
)
|
|
||||||
# it should not be more than 50%
|
|
||||||
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
|
|
||||||
|
|
||||||
# The value returned should satisfy both limits: for amount (base currency) and
|
# The value returned should satisfy both limits: for amount (base currency) and
|
||||||
# for cost (quote, stake currency), so max() is used here.
|
# for cost (quote, stake currency), so max() is used here.
|
||||||
# See also #2575 at github.
|
# See also #2575 at github.
|
||||||
return self._get_stake_amount_considering_leverage(
|
return self._get_stake_amount_considering_leverage(
|
||||||
max(stake_limits) * amount_reserve_percent,
|
max(stake_limits) if isMin else min(stake_limits),
|
||||||
leverage or 1.0
|
leverage or 1.0
|
||||||
) if isMin else min(stake_limits)
|
)
|
||||||
|
|
||||||
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
|
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -833,7 +843,8 @@ class Exchange:
|
|||||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
rate: float, leverage: float, params: Dict = {},
|
rate: float, leverage: float, params: Dict = {},
|
||||||
stop_loss: bool = False) -> Dict[str, Any]:
|
stop_loss: bool = False) -> Dict[str, Any]:
|
||||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
now = dt_now()
|
||||||
|
order_id = f'dry_run_{side}_{now.timestamp()}'
|
||||||
# Rounding here must respect to contract sizes
|
# Rounding here must respect to contract sizes
|
||||||
_amount = self._contracts_to_amount(
|
_amount = self._contracts_to_amount(
|
||||||
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
|
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
|
||||||
@@ -848,8 +859,8 @@ class Exchange:
|
|||||||
'side': side,
|
'side': side,
|
||||||
'filled': 0,
|
'filled': 0,
|
||||||
'remaining': _amount,
|
'remaining': _amount,
|
||||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
'datetime': now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
'timestamp': dt_ts(now),
|
||||||
'status': "open",
|
'status': "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
'info': {},
|
'info': {},
|
||||||
@@ -857,7 +868,7 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
if stop_loss:
|
if stop_loss:
|
||||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||||
dry_order["stopPrice"] = dry_order["price"]
|
dry_order[self._ft_has['stop_price_param']] = dry_order["price"]
|
||||||
# Workaround to avoid filling stoploss orders immediately
|
# Workaround to avoid filling stoploss orders immediately
|
||||||
dry_order["ft_order_type"] = "stoploss"
|
dry_order["ft_order_type"] = "stoploss"
|
||||||
orderbook: Optional[OrderBook] = None
|
orderbook: Optional[OrderBook] = None
|
||||||
@@ -880,7 +891,7 @@ class Exchange:
|
|||||||
'filled': _amount,
|
'filled': _amount,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'status': "closed",
|
'status': "closed",
|
||||||
'cost': (dry_order['amount'] * average) / leverage
|
'cost': (dry_order['amount'] * average)
|
||||||
})
|
})
|
||||||
# market orders will always incurr taker fees
|
# market orders will always incurr taker fees
|
||||||
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
|
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
|
||||||
@@ -1009,7 +1020,7 @@ class Exchange:
|
|||||||
from freqtrade.persistence import Order
|
from freqtrade.persistence import Order
|
||||||
order = Order.order_by_id(order_id)
|
order = Order.order_by_id(order_id)
|
||||||
if order:
|
if order:
|
||||||
ccxt_order = order.to_ccxt_object()
|
ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_param'])
|
||||||
self._dry_run_open_orders[order_id] = ccxt_order
|
self._dry_run_open_orders[order_id] = ccxt_order
|
||||||
return ccxt_order
|
return ccxt_order
|
||||||
# Gracefully handle errors with dry-run orders.
|
# Gracefully handle errors with dry-run orders.
|
||||||
@@ -1018,10 +1029,10 @@ class Exchange:
|
|||||||
|
|
||||||
# Order handling
|
# Order handling
|
||||||
|
|
||||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||||
if self.trading_mode != TradingMode.SPOT:
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
self.set_margin_mode(pair, self.margin_mode)
|
self.set_margin_mode(pair, self.margin_mode, accept_fail)
|
||||||
self._set_leverage(leverage, pair)
|
self._set_leverage(leverage, pair, accept_fail)
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
@@ -1033,12 +1044,18 @@ class Exchange:
|
|||||||
) -> Dict:
|
) -> Dict:
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
if time_in_force != 'GTC' and ordertype != 'market':
|
if time_in_force != 'GTC' and ordertype != 'market':
|
||||||
param = self._ft_has.get('time_in_force_parameter', '')
|
params.update({'timeInForce': time_in_force.upper()})
|
||||||
params.update({param: time_in_force.upper()})
|
|
||||||
if reduceOnly:
|
if reduceOnly:
|
||||||
params.update({'reduceOnly': True})
|
params.update({'reduceOnly': True})
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def _order_needs_price(self, ordertype: str) -> bool:
|
||||||
|
return (
|
||||||
|
ordertype != 'market'
|
||||||
|
or self._api.options.get("createMarketBuyOrderRequiresPrice", False)
|
||||||
|
or self._ft_has.get('marketOrderRequiresPrice', False)
|
||||||
|
)
|
||||||
|
|
||||||
def create_order(
|
def create_order(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -1061,8 +1078,7 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||||
needs_price = (ordertype != 'market'
|
needs_price = self._order_needs_price(ordertype)
|
||||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
|
||||||
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:
|
||||||
@@ -1086,7 +1102,7 @@ class Exchange:
|
|||||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise ExchangeError(
|
raise InvalidOrderException(
|
||||||
f'Could not create {ordertype} {side} order on market {pair}. '
|
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to {side} amount {amount} at rate {rate}. '
|
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
@@ -1105,11 +1121,11 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
if not self._ft_has.get('stoploss_on_exchange'):
|
if not self._ft_has.get('stoploss_on_exchange'):
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
price_param = self._ft_has['stop_price_param']
|
||||||
return (
|
return (
|
||||||
order.get('stopPrice', None) is None
|
order.get(price_param, None) is None
|
||||||
or ((side == "sell" and stop_loss > float(order['stopPrice'])) or
|
or ((side == "sell" and stop_loss > float(order[price_param])) or
|
||||||
(side == "buy" and stop_loss < float(order['stopPrice'])))
|
(side == "buy" and stop_loss < float(order[price_param])))
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
||||||
@@ -1136,14 +1152,21 @@ class Exchange:
|
|||||||
"sell" else (stop_price >= limit_rate))
|
"sell" else (stop_price >= limit_rate))
|
||||||
# Ensure rate is less than stop price
|
# Ensure rate is less than stop price
|
||||||
if bad_stop_price:
|
if bad_stop_price:
|
||||||
raise OperationalException(
|
# This can for example happen if the stop / liquidation price is set to 0
|
||||||
'In stoploss limit order, stop price should be more than limit price')
|
# Which is possible if a market-order closes right away.
|
||||||
|
# The InvalidOrderException will bubble up to exit_positions, where it will be
|
||||||
|
# handled gracefully.
|
||||||
|
raise InvalidOrderException(
|
||||||
|
"In stoploss limit order, stop price should be more than limit price. "
|
||||||
|
f"Stop price: {stop_price}, Limit price: {limit_rate}, "
|
||||||
|
f"Limit Price pct: {limit_price_pct}"
|
||||||
|
)
|
||||||
return limit_rate
|
return limit_rate
|
||||||
|
|
||||||
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 = self._params.copy()
|
params = self._params.copy()
|
||||||
# Verify if stopPrice works for your exchange!
|
# Verify if stopPrice works for your exchange, else configure stop_price_param
|
||||||
params.update({'stopPrice': stop_price})
|
params.update({self._ft_has['stop_price_param']: stop_price})
|
||||||
return params
|
return params
|
||||||
|
|
||||||
@retrier(retries=0)
|
@retrier(retries=0)
|
||||||
@@ -1169,12 +1192,12 @@ class Exchange:
|
|||||||
|
|
||||||
user_order_type = order_types.get('stoploss', 'market')
|
user_order_type = order_types.get('stoploss', 'market')
|
||||||
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
||||||
|
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||||
stop_price_norm = self.price_to_precision(pair, stop_price)
|
stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||||
limit_rate = None
|
limit_rate = None
|
||||||
if user_order_type == 'limit':
|
if user_order_type == 'limit':
|
||||||
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
||||||
limit_rate = self.price_to_precision(pair, limit_rate)
|
limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
@@ -1200,7 +1223,7 @@ class Exchange:
|
|||||||
|
|
||||||
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||||
|
|
||||||
self._lev_prep(pair, leverage, side)
|
self._lev_prep(pair, leverage, side, accept_fail=True)
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||||
amount=amount, price=limit_rate, params=params)
|
amount=amount, price=limit_rate, params=params)
|
||||||
self._log_exchange_response('create_stoploss_order', order)
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
@@ -1410,6 +1433,47 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier(retries=0)
|
||||||
|
def fetch_orders(self, pair: str, since: datetime) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Fetch all orders for a pair "since"
|
||||||
|
:param pair: Pair for the query
|
||||||
|
:param since: Starting time for the query
|
||||||
|
"""
|
||||||
|
if self._config['dry_run']:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def fetch_orders_emulate() -> List[Dict]:
|
||||||
|
orders = []
|
||||||
|
if self.exchange_has('fetchClosedOrders'):
|
||||||
|
orders = self._api.fetch_closed_orders(pair, since=since_ms)
|
||||||
|
if self.exchange_has('fetchOpenOrders'):
|
||||||
|
orders_open = self._api.fetch_open_orders(pair, since=since_ms)
|
||||||
|
orders.extend(orders_open)
|
||||||
|
return orders
|
||||||
|
|
||||||
|
try:
|
||||||
|
since_ms = int((since.timestamp() - 10) * 1000)
|
||||||
|
if self.exchange_has('fetchOrders'):
|
||||||
|
try:
|
||||||
|
orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms)
|
||||||
|
except ccxt.NotSupported:
|
||||||
|
# Some exchanges don't support fetchOrders
|
||||||
|
# attempt to fetch open and closed orders separately
|
||||||
|
orders = fetch_orders_emulate()
|
||||||
|
else:
|
||||||
|
orders = fetch_orders_emulate()
|
||||||
|
self._log_exchange_response('fetch_orders', orders)
|
||||||
|
orders = [self._order_contracts_to_amount(o) for o in orders]
|
||||||
|
return orders
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not fetch positions due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_trading_fees(self) -> Dict[str, Any]:
|
def fetch_trading_fees(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -1867,11 +1931,11 @@ class Exchange:
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
"one_call: %s msecs (%s)",
|
"one_call: %s msecs (%s)",
|
||||||
one_call,
|
one_call,
|
||||||
arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True)
|
dt_humanize(dt_now() - timedelta(milliseconds=one_call), only_distance=True)
|
||||||
)
|
)
|
||||||
input_coroutines = [self._async_get_candle_history(
|
input_coroutines = [self._async_get_candle_history(
|
||||||
pair, timeframe, candle_type, since) for since in
|
pair, timeframe, candle_type, since) for since in
|
||||||
range(since_ms, until_ms or (arrow.utcnow().int_timestamp * 1000), one_call)]
|
range(since_ms, until_ms or dt_ts(), one_call)]
|
||||||
|
|
||||||
data: List = []
|
data: List = []
|
||||||
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||||
@@ -1961,7 +2025,8 @@ class Exchange:
|
|||||||
cache: bool, drop_incomplete: bool) -> DataFrame:
|
cache: bool, drop_incomplete: bool) -> DataFrame:
|
||||||
# keeping last candle time as last refreshed time of the pair
|
# keeping last candle time as last refreshed time of the pair
|
||||||
if ticks and cache:
|
if ticks and cache:
|
||||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000
|
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
|
||||||
|
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] // 1000
|
||||||
# keeping parsed dataframe in cache
|
# keeping parsed dataframe in cache
|
||||||
ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||||
drop_incomplete=drop_incomplete)
|
drop_incomplete=drop_incomplete)
|
||||||
@@ -2034,7 +2099,9 @@ class Exchange:
|
|||||||
# Timeframe in seconds
|
# Timeframe in seconds
|
||||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||||
plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
|
plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
|
||||||
return plr < arrow.utcnow().int_timestamp
|
# current,active candle open date
|
||||||
|
now = int(timeframe_to_prev_date(timeframe).timestamp())
|
||||||
|
return plr < now
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
async def _async_get_candle_history(
|
async def _async_get_candle_history(
|
||||||
@@ -2051,7 +2118,7 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Fetch OHLCV asynchronously
|
# Fetch OHLCV asynchronously
|
||||||
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
s = '(' + dt_from_ts(since_ms).isoformat() + ') ' if since_ms is not None else ''
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Fetching pair %s, %s, interval %s, since %s %s...",
|
"Fetching pair %s, %s, interval %s, since %s %s...",
|
||||||
pair, candle_type, timeframe, since_ms, s
|
pair, candle_type, timeframe, since_ms, s
|
||||||
@@ -2141,7 +2208,7 @@ class Exchange:
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
"Fetching trades for pair %s, since %s %s...",
|
"Fetching trades for pair %s, since %s %s...",
|
||||||
pair, since,
|
pair, since,
|
||||||
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
|
'(' + dt_from_ts(since).isoformat() + ') ' if since is not None else ''
|
||||||
)
|
)
|
||||||
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
|
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
|
||||||
trades = self._trades_contracts_to_amount(trades)
|
trades = self._trades_contracts_to_amount(trades)
|
||||||
@@ -2350,12 +2417,12 @@ class Exchange:
|
|||||||
# Must fetch the leverage tiers for each market separately
|
# Must fetch the leverage tiers for each market separately
|
||||||
# * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets
|
# * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets
|
||||||
markets = self.markets
|
markets = self.markets
|
||||||
symbols = []
|
|
||||||
|
|
||||||
for symbol, market in markets.items():
|
symbols = [
|
||||||
|
symbol for symbol, market in markets.items()
|
||||||
if (self.market_is_future(market)
|
if (self.market_is_future(market)
|
||||||
and market['quote'] == self._config['stake_currency']):
|
and market['quote'] == self._config['stake_currency'])
|
||||||
symbols.append(symbol)
|
]
|
||||||
|
|
||||||
tiers: Dict[str, List[Dict]] = {}
|
tiers: Dict[str, List[Dict]] = {}
|
||||||
|
|
||||||
@@ -2375,25 +2442,26 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
logger.info("Using cached leverage_tiers.")
|
logger.info("Using cached leverage_tiers.")
|
||||||
|
|
||||||
async def gather_results():
|
async def gather_results(input_coro):
|
||||||
return await asyncio.gather(*input_coro, return_exceptions=True)
|
return await asyncio.gather(*input_coro, return_exceptions=True)
|
||||||
|
|
||||||
for input_coro in chunks(coros, 100):
|
for input_coro in chunks(coros, 100):
|
||||||
|
|
||||||
with self._loop_lock:
|
with self._loop_lock:
|
||||||
results = self.loop.run_until_complete(gather_results())
|
results = self.loop.run_until_complete(gather_results(input_coro))
|
||||||
|
|
||||||
for symbol, res in results:
|
for res in results:
|
||||||
tiers[symbol] = res
|
if isinstance(res, Exception):
|
||||||
|
logger.warning(f"Leverage tier exception: {repr(res)}")
|
||||||
|
continue
|
||||||
|
symbol, tier = res
|
||||||
|
tiers[symbol] = tier
|
||||||
if len(coros) > 0:
|
if len(coros) > 0:
|
||||||
self.cache_leverage_tiers(tiers, self._config['stake_currency'])
|
self.cache_leverage_tiers(tiers, self._config['stake_currency'])
|
||||||
logger.info(f"Done initializing {len(symbols)} markets.")
|
logger.info(f"Done initializing {len(symbols)} markets.")
|
||||||
|
|
||||||
return tiers
|
return tiers
|
||||||
else:
|
return {}
|
||||||
return {}
|
|
||||||
else:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None:
|
def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None:
|
||||||
|
|
||||||
@@ -2409,14 +2477,17 @@ class Exchange:
|
|||||||
def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
|
def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
|
||||||
filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
|
filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
|
||||||
if filename.is_file():
|
if filename.is_file():
|
||||||
tiers = file_load_json(filename)
|
try:
|
||||||
updated = tiers.get('updated')
|
tiers = file_load_json(filename)
|
||||||
if updated:
|
updated = tiers.get('updated')
|
||||||
updated_dt = parser.parse(updated)
|
if updated:
|
||||||
if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
|
updated_dt = parser.parse(updated)
|
||||||
logger.info("Cached leverage tiers are outdated. Will update.")
|
if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
|
||||||
return None
|
logger.info("Cached leverage tiers are outdated. Will update.")
|
||||||
return tiers['data']
|
return None
|
||||||
|
return tiers['data']
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error loading cached leverage tiers. Refreshing.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def fill_leverage_tiers(self) -> None:
|
def fill_leverage_tiers(self) -> None:
|
||||||
@@ -2522,7 +2593,6 @@ class Exchange:
|
|||||||
self,
|
self,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
pair: Optional[str] = None,
|
pair: Optional[str] = None,
|
||||||
trading_mode: Optional[TradingMode] = None,
|
|
||||||
accept_fail: bool = False,
|
accept_fail: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -2540,7 +2610,7 @@ class Exchange:
|
|||||||
self._log_exchange_response('set_leverage', res)
|
self._log_exchange_response('set_leverage', res)
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except ccxt.BadRequest as e:
|
except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
|
||||||
if not accept_fail:
|
if not accept_fail:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
@@ -2751,10 +2821,10 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
|
f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
|
||||||
|
|
||||||
isolated_liq = None
|
liquidation_price = None
|
||||||
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
|
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
|
||||||
|
|
||||||
isolated_liq = self.dry_run_liquidation_price(
|
liquidation_price = self.dry_run_liquidation_price(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
open_rate=open_rate,
|
open_rate=open_rate,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
@@ -2769,16 +2839,16 @@ class Exchange:
|
|||||||
positions = self.fetch_positions(pair)
|
positions = self.fetch_positions(pair)
|
||||||
if len(positions) > 0:
|
if len(positions) > 0:
|
||||||
pos = positions[0]
|
pos = positions[0]
|
||||||
isolated_liq = pos['liquidationPrice']
|
liquidation_price = pos['liquidationPrice']
|
||||||
|
|
||||||
if isolated_liq is not None:
|
if liquidation_price is not None:
|
||||||
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
|
buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
|
||||||
isolated_liq = (
|
liquidation_price_buffer = (
|
||||||
isolated_liq - buffer_amount
|
liquidation_price - buffer_amount
|
||||||
if is_short else
|
if is_short else
|
||||||
isolated_liq + buffer_amount
|
liquidation_price + buffer_amount
|
||||||
)
|
)
|
||||||
return isolated_liq
|
return max(liquidation_price_buffer, 0.0)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -2872,8 +2942,8 @@ class Exchange:
|
|||||||
if nominal_value >= tier['minNotional']:
|
if nominal_value >= tier['minNotional']:
|
||||||
return (tier['maintenanceMarginRate'], tier['maintAmt'])
|
return (tier['maintenanceMarginRate'], tier['maintAmt'])
|
||||||
|
|
||||||
raise OperationalException("nominal value can not be lower than 0")
|
raise ExchangeError("nominal value can not be lower than 0")
|
||||||
# The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it
|
# The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it
|
||||||
# describes the min amt for a tier, and the lowest tier will always go down to 0
|
# describes the min amt for a tier, and the lowest tier will always go down to 0
|
||||||
else:
|
else:
|
||||||
raise OperationalException(f"Cannot get maintenance ratio using {self.name}")
|
raise ExchangeError(f"Cannot get maintenance ratio using {self.name}")
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
Exchange support utils
|
Exchange support utils
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import ceil
|
from math import ceil, floor
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
|
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
|
||||||
|
TRUNCATE, decimal_to_precision)
|
||||||
|
|
||||||
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
|
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts
|
||||||
|
|
||||||
|
|
||||||
CcxtModuleType = Any
|
CcxtModuleType = Any
|
||||||
@@ -98,9 +100,8 @@ def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> d
|
|||||||
if not date:
|
if not date:
|
||||||
date = datetime.now(timezone.utc)
|
date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
|
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000
|
||||||
ROUND_DOWN) // 1000
|
return dt_from_ts(new_timestamp)
|
||||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
||||||
@@ -112,9 +113,8 @@ def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> d
|
|||||||
"""
|
"""
|
||||||
if not date:
|
if not date:
|
||||||
date = datetime.now(timezone.utc)
|
date = datetime.now(timezone.utc)
|
||||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
|
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000
|
||||||
ROUND_UP) // 1000
|
return dt_from_ts(new_timestamp)
|
||||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
|
||||||
|
|
||||||
|
|
||||||
def date_minus_candles(
|
def date_minus_candles(
|
||||||
@@ -219,35 +219,51 @@ def amount_to_contract_precision(
|
|||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
def price_to_precision(price: float, price_precision: Optional[float],
|
def price_to_precision(
|
||||||
precisionMode: Optional[int]) -> float:
|
price: float,
|
||||||
|
price_precision: Optional[float],
|
||||||
|
precisionMode: Optional[int],
|
||||||
|
*,
|
||||||
|
rounding_mode: int = ROUND,
|
||||||
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Returns the price rounded up to the precision the Exchange accepts.
|
Returns the price rounded to the precision the Exchange accepts.
|
||||||
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
||||||
which does not support rounding up
|
which does not support rounding up.
|
||||||
|
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||||
|
|
||||||
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||||
align with amount_to_precision().
|
align with amount_to_precision().
|
||||||
!!! Rounds up
|
|
||||||
:param price: price to convert
|
:param price: price to convert
|
||||||
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
||||||
:param precisionMode: precision mode to use. Should be used from precisionMode
|
:param precisionMode: precision mode to use. Should be used from precisionMode
|
||||||
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
||||||
|
:param rounding_mode: rounding mode to use. Defaults to ROUND
|
||||||
:return: price rounded up to the precision the Exchange accepts
|
:return: price rounded up to the precision the Exchange accepts
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if price_precision is not None and precisionMode is not None:
|
if price_precision is not None and precisionMode is not None:
|
||||||
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
|
||||||
# precision=price_precision,
|
|
||||||
# counting_mode=self.precisionMode,
|
|
||||||
# ))
|
|
||||||
if precisionMode == TICK_SIZE:
|
if precisionMode == TICK_SIZE:
|
||||||
|
if rounding_mode == ROUND:
|
||||||
|
ticks = price / price_precision
|
||||||
|
rounded_ticks = round(ticks)
|
||||||
|
return rounded_ticks * price_precision
|
||||||
precision = FtPrecise(price_precision)
|
precision = FtPrecise(price_precision)
|
||||||
price_str = FtPrecise(price)
|
price_str = FtPrecise(price)
|
||||||
missing = price_str % precision
|
missing = price_str % precision
|
||||||
if not missing == FtPrecise("0"):
|
if not missing == FtPrecise("0"):
|
||||||
price = round(float(str(price_str - missing + precision)), 14)
|
return round(float(str(price_str - missing + precision)), 14)
|
||||||
else:
|
return price
|
||||||
symbol_prec = price_precision
|
elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
|
||||||
big_price = price * pow(10, symbol_prec)
|
ndigits = round(price_precision)
|
||||||
price = ceil(big_price) / pow(10, symbol_prec)
|
if rounding_mode == ROUND:
|
||||||
|
return round(price, ndigits)
|
||||||
|
ticks = price * (10**ndigits)
|
||||||
|
if rounding_mode == ROUND_UP:
|
||||||
|
return ceil(ticks) / (10**ndigits)
|
||||||
|
if rounding_mode == TRUNCATE:
|
||||||
|
return int(ticks) / (10**ndigits)
|
||||||
|
if rounding_mode == ROUND_DOWN:
|
||||||
|
return floor(ticks) / (10**ndigits)
|
||||||
|
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
|
||||||
|
raise ValueError(f"Unknown precisionMode {precisionMode}")
|
||||||
return price
|
return price
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.misc import safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback2
|
||||||
|
|
||||||
@@ -28,10 +27,13 @@ class Gate(Exchange):
|
|||||||
"order_time_in_force": ['GTC', 'IOC'],
|
"order_time_in_force": ['GTC', 'IOC'],
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"marketOrderRequiresPrice": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"needs_trading_fees": True,
|
"needs_trading_fees": True,
|
||||||
|
"marketOrderRequiresPrice": False,
|
||||||
|
"tickers_have_bid_ask": False,
|
||||||
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
||||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||||
"stop_price_type_field": "price_type",
|
"stop_price_type_field": "price_type",
|
||||||
@@ -49,14 +51,6 @@ class Gate(Exchange):
|
|||||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
|
||||||
|
|
||||||
if self.trading_mode != TradingMode.FUTURES:
|
|
||||||
if any(v == 'market' for k, v in order_types.items()):
|
|
||||||
raise OperationalException(
|
|
||||||
f'Exchange {self.name} does not support market orders.')
|
|
||||||
super().validate_stop_ordertypes(order_types)
|
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
side: BuySell,
|
side: BuySell,
|
||||||
@@ -74,8 +68,7 @@ class Gate(Exchange):
|
|||||||
)
|
)
|
||||||
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
|
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
|
||||||
params['type'] = 'market'
|
params['type'] = 'market'
|
||||||
param = self._ft_has.get('time_in_force_parameter', '')
|
params.update({'timeInForce': 'IOC'})
|
||||||
params.update({param: 'IOC'})
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali
|
|||||||
OperationalException, TemporaryError)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
|
from freqtrade.exchange.exchange_utils import ROUND_DOWN, ROUND_UP
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
|
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@ class Kraken(Exchange):
|
|||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
params.update({'reduceOnly': True})
|
params.update({'reduceOnly': True})
|
||||||
|
|
||||||
|
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||||
if order_types.get('stoploss', 'market') == 'limit':
|
if order_types.get('stoploss', 'market') == 'limit':
|
||||||
ordertype = "stop-loss-limit"
|
ordertype = "stop-loss-limit"
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
@@ -116,11 +118,11 @@ class Kraken(Exchange):
|
|||||||
limit_rate = stop_price * limit_price_pct
|
limit_rate = stop_price * limit_price_pct
|
||||||
else:
|
else:
|
||||||
limit_rate = stop_price * (2 - limit_price_pct)
|
limit_rate = stop_price * (2 - limit_price_pct)
|
||||||
params['price2'] = self.price_to_precision(pair, limit_rate)
|
params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||||
else:
|
else:
|
||||||
ordertype = "stop-loss"
|
ordertype = "stop-loss"
|
||||||
|
|
||||||
stop_price = self.price_to_precision(pair, stop_price)
|
stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
@@ -158,7 +160,6 @@ class Kraken(Exchange):
|
|||||||
self,
|
self,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
pair: Optional[str] = None,
|
pair: Optional[str] = None,
|
||||||
trading_mode: Optional[TradingMode] = None,
|
|
||||||
accept_fail: bool = False,
|
accept_fail: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class Kucoin(Exchange):
|
|||||||
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
|
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
|
||||||
# Since we rely on status heavily, we must set it to 'open' here.
|
# Since we rely on status heavily, we must set it to 'open' here.
|
||||||
# ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553)
|
# ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553)
|
||||||
res['type'] = ordertype
|
if not self._config['dry_run']:
|
||||||
res['status'] = 'open'
|
res['type'] = ordertype
|
||||||
|
res['status'] = 'open'
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||||
from freqtrade.enums.pricetype import PriceType
|
from freqtrade.enums.pricetype import PriceType
|
||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
|
||||||
|
TemporaryError)
|
||||||
from freqtrade.exchange import Exchange, date_minus_candles
|
from freqtrade.exchange import Exchange, date_minus_candles
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
|
from freqtrade.misc import safe_value_fallback2
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -24,11 +26,14 @@ class Okx(Exchange):
|
|||||||
"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",
|
"mark_ohlcv_timeframe": "4h",
|
||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stop_price_param": "stopLossPrice",
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"tickers_have_quoteVolume": False,
|
"tickers_have_quoteVolume": False,
|
||||||
"fee_cost_in_contracts": True,
|
"fee_cost_in_contracts": True,
|
||||||
"stop_price_type_field": "tpTriggerPxType",
|
"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: "index",
|
||||||
@@ -121,10 +126,9 @@ class Okx(Exchange):
|
|||||||
return params
|
return params
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||||
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
|
||||||
try:
|
try:
|
||||||
# TODO-lev: Test me properly (check mgnMode passed)
|
|
||||||
res = self._api.set_leverage(
|
res = self._api.set_leverage(
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
symbol=pair,
|
symbol=pair,
|
||||||
@@ -157,3 +161,66 @@ class Okx(Exchange):
|
|||||||
|
|
||||||
pair_tiers = self._leverage_tiers[pair]
|
pair_tiers = self._leverage_tiers[pair]
|
||||||
return pair_tiers[-1]['maxNotional'] / leverage
|
return pair_tiers[-1]['maxNotional'] / leverage
|
||||||
|
|
||||||
|
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
params = super()._get_stop_params(side, ordertype, stop_price)
|
||||||
|
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
|
||||||
|
params['tdMode'] = self.margin_mode.value
|
||||||
|
params['posSide'] = self._get_posSide(side, True)
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict:
|
||||||
|
if (
|
||||||
|
order['status'] == 'closed'
|
||||||
|
and (real_order_id := order.get('info', {}).get('ordId')) is not None
|
||||||
|
):
|
||||||
|
# Once a order triggered, we fetch the regular followup order.
|
||||||
|
order_reg = self.fetch_order(real_order_id, pair)
|
||||||
|
self._log_exchange_response('fetch_stoploss_order1', order_reg)
|
||||||
|
order_reg['id_stop'] = order_reg['id']
|
||||||
|
order_reg['id'] = order_id
|
||||||
|
order_reg['type'] = 'stoploss'
|
||||||
|
order_reg['status_stop'] = 'triggered'
|
||||||
|
return order_reg
|
||||||
|
order['type'] = 'stoploss'
|
||||||
|
return order
|
||||||
|
|
||||||
|
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
|
if self._config['dry_run']:
|
||||||
|
return self.fetch_dry_run_order(order_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
params1 = {'stop': True}
|
||||||
|
order_reg = self._api.fetch_order(order_id, pair, params=params1)
|
||||||
|
self._log_exchange_response('fetch_stoploss_order', order_reg)
|
||||||
|
return self._convert_stop_order(pair, order_id, order_reg)
|
||||||
|
except ccxt.OrderNotFound:
|
||||||
|
pass
|
||||||
|
params2 = {'stop': True, 'ordType': 'conditional'}
|
||||||
|
for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders,
|
||||||
|
self._api.fetch_canceled_orders):
|
||||||
|
try:
|
||||||
|
orders = method(pair, params=params2)
|
||||||
|
orders_f = [order for order in orders if order['id'] == order_id]
|
||||||
|
if orders_f:
|
||||||
|
order = orders_f[0]
|
||||||
|
return self._convert_stop_order(pair, order_id, order)
|
||||||
|
except ccxt.BaseError:
|
||||||
|
pass
|
||||||
|
raise RetryableOrderError(
|
||||||
|
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
|
||||||
|
|
||||||
|
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||||
|
if order['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 = {}) -> Dict:
|
||||||
|
params1 = {'stop': True}
|
||||||
|
# 'ordType': 'conditional'
|
||||||
|
#
|
||||||
|
return self.cancel_order(
|
||||||
|
order_id=order_id,
|
||||||
|
pair=pair,
|
||||||
|
params=params1,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from gym import spaces
|
from gymnasium import spaces
|
||||||
|
|
||||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
|||||||
self._update_unrealized_total_profit()
|
self._update_unrealized_total_profit()
|
||||||
step_reward = self.calculate_reward(action)
|
step_reward = self.calculate_reward(action)
|
||||||
self.total_reward += step_reward
|
self.total_reward += step_reward
|
||||||
self.tensorboard_log(self.actions._member_names_[action])
|
self.tensorboard_log(self.actions._member_names_[action], category="actions")
|
||||||
|
|
||||||
trade_type = None
|
trade_type = None
|
||||||
if self.is_tradesignal(action):
|
if self.is_tradesignal(action):
|
||||||
@@ -66,7 +66,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
|||||||
elif action == Actions.Sell.value and not self.can_short:
|
elif action == Actions.Sell.value and not self.can_short:
|
||||||
self._update_total_profit()
|
self._update_total_profit()
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
trade_type = "neutral"
|
trade_type = "exit"
|
||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
else:
|
else:
|
||||||
print("case not defined")
|
print("case not defined")
|
||||||
@@ -74,7 +74,7 @@ class Base3ActionRLEnv(BaseEnvironment):
|
|||||||
if trade_type is not None:
|
if trade_type is not None:
|
||||||
self.trade_history.append(
|
self.trade_history.append(
|
||||||
{'price': self.current_price(), 'index': self._current_tick,
|
{'price': self.current_price(), 'index': self._current_tick,
|
||||||
'type': trade_type})
|
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||||
|
|
||||||
if (self._total_profit < self.max_drawdown or
|
if (self._total_profit < self.max_drawdown or
|
||||||
self._total_unrealized_profit < self.max_drawdown):
|
self._total_unrealized_profit < self.max_drawdown):
|
||||||
@@ -94,9 +94,12 @@ class Base3ActionRLEnv(BaseEnvironment):
|
|||||||
|
|
||||||
observation = self._get_observation()
|
observation = self._get_observation()
|
||||||
|
|
||||||
|
# user can play with time if they want
|
||||||
|
truncated = False
|
||||||
|
|
||||||
self._update_history(info)
|
self._update_history(info)
|
||||||
|
|
||||||
return observation, step_reward, self._done, info
|
return observation, step_reward, self._done, truncated, info
|
||||||
|
|
||||||
def is_tradesignal(self, action: int) -> bool:
|
def is_tradesignal(self, action: int) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from gym import spaces
|
from gymnasium import spaces
|
||||||
|
|
||||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||||
|
|
||||||
@@ -48,20 +48,10 @@ class Base4ActionRLEnv(BaseEnvironment):
|
|||||||
self._update_unrealized_total_profit()
|
self._update_unrealized_total_profit()
|
||||||
step_reward = self.calculate_reward(action)
|
step_reward = self.calculate_reward(action)
|
||||||
self.total_reward += step_reward
|
self.total_reward += step_reward
|
||||||
self.tensorboard_log(self.actions._member_names_[action])
|
self.tensorboard_log(self.actions._member_names_[action], category="actions")
|
||||||
|
|
||||||
trade_type = None
|
trade_type = None
|
||||||
if self.is_tradesignal(action):
|
if self.is_tradesignal(action):
|
||||||
"""
|
|
||||||
Action: Neutral, position: Long -> Close Long
|
|
||||||
Action: Neutral, position: Short -> Close Short
|
|
||||||
|
|
||||||
Action: Long, position: Neutral -> Open Long
|
|
||||||
Action: Long, position: Short -> Close Short and Open Long
|
|
||||||
|
|
||||||
Action: Short, position: Neutral -> Open Short
|
|
||||||
Action: Short, position: Long -> Close Long and Open Short
|
|
||||||
"""
|
|
||||||
|
|
||||||
if action == Actions.Neutral.value:
|
if action == Actions.Neutral.value:
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
@@ -69,16 +59,16 @@ class Base4ActionRLEnv(BaseEnvironment):
|
|||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
elif action == Actions.Long_enter.value:
|
elif action == Actions.Long_enter.value:
|
||||||
self._position = Positions.Long
|
self._position = Positions.Long
|
||||||
trade_type = "long"
|
trade_type = "enter_long"
|
||||||
self._last_trade_tick = self._current_tick
|
self._last_trade_tick = self._current_tick
|
||||||
elif action == Actions.Short_enter.value:
|
elif action == Actions.Short_enter.value:
|
||||||
self._position = Positions.Short
|
self._position = Positions.Short
|
||||||
trade_type = "short"
|
trade_type = "enter_short"
|
||||||
self._last_trade_tick = self._current_tick
|
self._last_trade_tick = self._current_tick
|
||||||
elif action == Actions.Exit.value:
|
elif action == Actions.Exit.value:
|
||||||
self._update_total_profit()
|
self._update_total_profit()
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
trade_type = "neutral"
|
trade_type = "exit"
|
||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
else:
|
else:
|
||||||
print("case not defined")
|
print("case not defined")
|
||||||
@@ -86,7 +76,7 @@ class Base4ActionRLEnv(BaseEnvironment):
|
|||||||
if trade_type is not None:
|
if trade_type is not None:
|
||||||
self.trade_history.append(
|
self.trade_history.append(
|
||||||
{'price': self.current_price(), 'index': self._current_tick,
|
{'price': self.current_price(), 'index': self._current_tick,
|
||||||
'type': trade_type})
|
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||||
|
|
||||||
if (self._total_profit < self.max_drawdown or
|
if (self._total_profit < self.max_drawdown or
|
||||||
self._total_unrealized_profit < self.max_drawdown):
|
self._total_unrealized_profit < self.max_drawdown):
|
||||||
@@ -106,9 +96,12 @@ class Base4ActionRLEnv(BaseEnvironment):
|
|||||||
|
|
||||||
observation = self._get_observation()
|
observation = self._get_observation()
|
||||||
|
|
||||||
|
# user can play with time if they want
|
||||||
|
truncated = False
|
||||||
|
|
||||||
self._update_history(info)
|
self._update_history(info)
|
||||||
|
|
||||||
return observation, step_reward, self._done, info
|
return observation, step_reward, self._done, truncated, info
|
||||||
|
|
||||||
def is_tradesignal(self, action: int) -> bool:
|
def is_tradesignal(self, action: int) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from gym import spaces
|
from gymnasium import spaces
|
||||||
|
|
||||||
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
|
||||||
|
|
||||||
@@ -49,20 +49,10 @@ class Base5ActionRLEnv(BaseEnvironment):
|
|||||||
self._update_unrealized_total_profit()
|
self._update_unrealized_total_profit()
|
||||||
step_reward = self.calculate_reward(action)
|
step_reward = self.calculate_reward(action)
|
||||||
self.total_reward += step_reward
|
self.total_reward += step_reward
|
||||||
self.tensorboard_log(self.actions._member_names_[action])
|
self.tensorboard_log(self.actions._member_names_[action], category="actions")
|
||||||
|
|
||||||
trade_type = None
|
trade_type = None
|
||||||
if self.is_tradesignal(action):
|
if self.is_tradesignal(action):
|
||||||
"""
|
|
||||||
Action: Neutral, position: Long -> Close Long
|
|
||||||
Action: Neutral, position: Short -> Close Short
|
|
||||||
|
|
||||||
Action: Long, position: Neutral -> Open Long
|
|
||||||
Action: Long, position: Short -> Close Short and Open Long
|
|
||||||
|
|
||||||
Action: Short, position: Neutral -> Open Short
|
|
||||||
Action: Short, position: Long -> Close Long and Open Short
|
|
||||||
"""
|
|
||||||
|
|
||||||
if action == Actions.Neutral.value:
|
if action == Actions.Neutral.value:
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
@@ -70,21 +60,21 @@ class Base5ActionRLEnv(BaseEnvironment):
|
|||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
elif action == Actions.Long_enter.value:
|
elif action == Actions.Long_enter.value:
|
||||||
self._position = Positions.Long
|
self._position = Positions.Long
|
||||||
trade_type = "long"
|
trade_type = "enter_long"
|
||||||
self._last_trade_tick = self._current_tick
|
self._last_trade_tick = self._current_tick
|
||||||
elif action == Actions.Short_enter.value:
|
elif action == Actions.Short_enter.value:
|
||||||
self._position = Positions.Short
|
self._position = Positions.Short
|
||||||
trade_type = "short"
|
trade_type = "enter_short"
|
||||||
self._last_trade_tick = self._current_tick
|
self._last_trade_tick = self._current_tick
|
||||||
elif action == Actions.Long_exit.value:
|
elif action == Actions.Long_exit.value:
|
||||||
self._update_total_profit()
|
self._update_total_profit()
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
trade_type = "neutral"
|
trade_type = "exit_long"
|
||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
elif action == Actions.Short_exit.value:
|
elif action == Actions.Short_exit.value:
|
||||||
self._update_total_profit()
|
self._update_total_profit()
|
||||||
self._position = Positions.Neutral
|
self._position = Positions.Neutral
|
||||||
trade_type = "neutral"
|
trade_type = "exit_short"
|
||||||
self._last_trade_tick = None
|
self._last_trade_tick = None
|
||||||
else:
|
else:
|
||||||
print("case not defined")
|
print("case not defined")
|
||||||
@@ -92,7 +82,7 @@ class Base5ActionRLEnv(BaseEnvironment):
|
|||||||
if trade_type is not None:
|
if trade_type is not None:
|
||||||
self.trade_history.append(
|
self.trade_history.append(
|
||||||
{'price': self.current_price(), 'index': self._current_tick,
|
{'price': self.current_price(), 'index': self._current_tick,
|
||||||
'type': trade_type})
|
'type': trade_type, 'profit': self.get_unrealized_profit()})
|
||||||
|
|
||||||
if (self._total_profit < self.max_drawdown or
|
if (self._total_profit < self.max_drawdown or
|
||||||
self._total_unrealized_profit < self.max_drawdown):
|
self._total_unrealized_profit < self.max_drawdown):
|
||||||
@@ -111,10 +101,12 @@ class Base5ActionRLEnv(BaseEnvironment):
|
|||||||
)
|
)
|
||||||
|
|
||||||
observation = self._get_observation()
|
observation = self._get_observation()
|
||||||
|
# user can play with time if they want
|
||||||
|
truncated = False
|
||||||
|
|
||||||
self._update_history(info)
|
self._update_history(info)
|
||||||
|
|
||||||
return observation, step_reward, self._done, info
|
return observation, step_reward, self._done, truncated, info
|
||||||
|
|
||||||
def is_tradesignal(self, action: int) -> bool:
|
def is_tradesignal(self, action: int) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ from abc import abstractmethod
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Type, Union
|
from typing import Optional, Type, Union
|
||||||
|
|
||||||
import gym
|
import gymnasium as gym
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from gym import spaces
|
from gymnasium import spaces
|
||||||
from gym.utils import seeding
|
from gymnasium.utils import seeding
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
|
||||||
@@ -127,6 +127,14 @@ class BaseEnvironment(gym.Env):
|
|||||||
self.history: dict = {}
|
self.history: dict = {}
|
||||||
self.trade_history: list = []
|
self.trade_history: list = []
|
||||||
|
|
||||||
|
def get_attr(self, attr: str):
|
||||||
|
"""
|
||||||
|
Returns the attribute of the environment
|
||||||
|
:param attr: attribute to return
|
||||||
|
:return: attribute
|
||||||
|
"""
|
||||||
|
return getattr(self, attr)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set_action_space(self):
|
def set_action_space(self):
|
||||||
"""
|
"""
|
||||||
@@ -137,7 +145,8 @@ class BaseEnvironment(gym.Env):
|
|||||||
self.np_random, seed = seeding.np_random(seed)
|
self.np_random, seed = seeding.np_random(seed)
|
||||||
return [seed]
|
return [seed]
|
||||||
|
|
||||||
def tensorboard_log(self, metric: str, value: Union[int, float] = 1, inc: bool = True):
|
def tensorboard_log(self, metric: str, value: Optional[Union[int, float]] = None,
|
||||||
|
inc: Optional[bool] = None, category: str = "custom"):
|
||||||
"""
|
"""
|
||||||
Function builds the tensorboard_metrics dictionary
|
Function builds the tensorboard_metrics dictionary
|
||||||
to be parsed by the TensorboardCallback. This
|
to be parsed by the TensorboardCallback. This
|
||||||
@@ -149,22 +158,29 @@ class BaseEnvironment(gym.Env):
|
|||||||
|
|
||||||
def calculate_reward(self, action: int) -> float:
|
def calculate_reward(self, action: int) -> float:
|
||||||
if not self._is_valid(action):
|
if not self._is_valid(action):
|
||||||
self.tensorboard_log("is_valid")
|
self.tensorboard_log("invalid")
|
||||||
return -2
|
return -2
|
||||||
|
|
||||||
:param metric: metric to be tracked and incremented
|
:param metric: metric to be tracked and incremented
|
||||||
:param value: value to increment `metric` by
|
:param value: `metric` value
|
||||||
:param inc: sets whether the `value` is incremented or not
|
:param inc: (deprecated) sets whether the `value` is incremented or not
|
||||||
|
:param category: `metric` category
|
||||||
"""
|
"""
|
||||||
if not inc or metric not in self.tensorboard_metrics:
|
increment = True if value is None else False
|
||||||
self.tensorboard_metrics[metric] = value
|
value = 1 if increment else value
|
||||||
|
|
||||||
|
if category not in self.tensorboard_metrics:
|
||||||
|
self.tensorboard_metrics[category] = {}
|
||||||
|
|
||||||
|
if not increment or metric not in self.tensorboard_metrics[category]:
|
||||||
|
self.tensorboard_metrics[category][metric] = value
|
||||||
else:
|
else:
|
||||||
self.tensorboard_metrics[metric] += value
|
self.tensorboard_metrics[category][metric] += value
|
||||||
|
|
||||||
def reset_tensorboard_log(self):
|
def reset_tensorboard_log(self):
|
||||||
self.tensorboard_metrics = {}
|
self.tensorboard_metrics = {}
|
||||||
|
|
||||||
def reset(self):
|
def reset(self, seed=None):
|
||||||
"""
|
"""
|
||||||
Reset is called at the beginning of every episode
|
Reset is called at the beginning of every episode
|
||||||
"""
|
"""
|
||||||
@@ -195,7 +211,7 @@ class BaseEnvironment(gym.Env):
|
|||||||
self.close_trade_profit = []
|
self.close_trade_profit = []
|
||||||
self._total_unrealized_profit = 1
|
self._total_unrealized_profit = 1
|
||||||
|
|
||||||
return self._get_observation()
|
return self._get_observation(), self.history
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def step(self, action: int):
|
def step(self, action: int):
|
||||||
@@ -290,6 +306,12 @@ class BaseEnvironment(gym.Env):
|
|||||||
"""
|
"""
|
||||||
An example reward function. This is the one function that users will likely
|
An example reward function. This is the one function that users will likely
|
||||||
wish to inject their own creativity into.
|
wish to inject their own creativity into.
|
||||||
|
|
||||||
|
Warning!
|
||||||
|
This is function is a showcase of functionality designed to show as many possible
|
||||||
|
environment control features as possible. It is also designed to run quickly
|
||||||
|
on small computers. This is a benchmark, it is *not* for live production.
|
||||||
|
|
||||||
:param action: int = The action made by the agent for the current candle.
|
:param action: int = The action made by the agent for the current candle.
|
||||||
:return:
|
:return:
|
||||||
float = the reward to give to the agent for current step (used for optimization
|
float = the reward to give to the agent for current step (used for optimization
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
|
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
import gym
|
import gymnasium as gym
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -16,14 +16,14 @@ from pandas import DataFrame
|
|||||||
from stable_baselines3.common.callbacks import EvalCallback
|
from stable_baselines3.common.callbacks import EvalCallback
|
||||||
from stable_baselines3.common.monitor import Monitor
|
from stable_baselines3.common.monitor import Monitor
|
||||||
from stable_baselines3.common.utils import set_random_seed
|
from stable_baselines3.common.utils import set_random_seed
|
||||||
from stable_baselines3.common.vec_env import SubprocVecEnv
|
from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
||||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv
|
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv
|
||||||
from freqtrade.freqai.RL.BaseEnvironment import BaseActions, Positions
|
from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment, Positions
|
||||||
from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback
|
from freqtrade.freqai.tensorboard.TensorboardCallback import TensorboardCallback
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
'cpu_count', 1), max(int(self.max_system_threads / 2), 1))
|
'cpu_count', 1), max(int(self.max_system_threads / 2), 1))
|
||||||
th.set_num_threads(self.max_threads)
|
th.set_num_threads(self.max_threads)
|
||||||
self.reward_params = self.freqai_info['rl_config']['model_reward_parameters']
|
self.reward_params = self.freqai_info['rl_config']['model_reward_parameters']
|
||||||
self.train_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env()
|
self.train_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env()
|
||||||
self.eval_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env()
|
self.eval_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env()
|
||||||
self.eval_callback: Optional[EvalCallback] = None
|
self.eval_callback: Optional[EvalCallback] = None
|
||||||
self.model_type = self.freqai_info['rl_config']['model_type']
|
self.model_type = self.freqai_info['rl_config']['model_type']
|
||||||
self.rl_config = self.freqai_info['rl_config']
|
self.rl_config = self.freqai_info['rl_config']
|
||||||
@@ -114,6 +114,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
|
|
||||||
# normalize all data based on train_dataset only
|
# normalize all data based on train_dataset only
|
||||||
prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk)
|
prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk)
|
||||||
|
|
||||||
data_dictionary = dk.normalize_data(data_dictionary)
|
data_dictionary = dk.normalize_data(data_dictionary)
|
||||||
|
|
||||||
# data cleaning/analysis
|
# data cleaning/analysis
|
||||||
@@ -148,12 +149,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
|
|
||||||
env_info = self.pack_env_dict(dk.pair)
|
env_info = self.pack_env_dict(dk.pair)
|
||||||
|
|
||||||
self.train_env = self.MyRLEnv(df=train_df,
|
self.train_env = self.MyRLEnv(df=train_df, prices=prices_train, **env_info)
|
||||||
prices=prices_train,
|
self.eval_env = Monitor(self.MyRLEnv(df=test_df, prices=prices_test, **env_info))
|
||||||
**env_info)
|
|
||||||
self.eval_env = Monitor(self.MyRLEnv(df=test_df,
|
|
||||||
prices=prices_test,
|
|
||||||
**env_info))
|
|
||||||
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
|
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
|
||||||
render=False, eval_freq=len(train_df),
|
render=False, eval_freq=len(train_df),
|
||||||
best_model_save_path=str(dk.data_path))
|
best_model_save_path=str(dk.data_path))
|
||||||
@@ -238,6 +235,9 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
filtered_dataframe, _ = dk.filter_features(
|
filtered_dataframe, _ = dk.filter_features(
|
||||||
unfiltered_df, dk.training_features_list, training_filter=False
|
unfiltered_df, dk.training_features_list, training_filter=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
filtered_dataframe = self.drop_ohlc_from_df(filtered_dataframe, dk)
|
||||||
|
|
||||||
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
|
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
|
||||||
dk.data_dictionary["prediction_features"] = filtered_dataframe
|
dk.data_dictionary["prediction_features"] = filtered_dataframe
|
||||||
|
|
||||||
@@ -285,7 +285,6 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
train_df = data_dictionary["train_features"]
|
train_df = data_dictionary["train_features"]
|
||||||
test_df = data_dictionary["test_features"]
|
test_df = data_dictionary["test_features"]
|
||||||
|
|
||||||
# %-raw_volume_gen_shift-2_ETH/USDT_1h
|
|
||||||
# price data for model training and evaluation
|
# price data for model training and evaluation
|
||||||
tf = self.config['timeframe']
|
tf = self.config['timeframe']
|
||||||
rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low',
|
rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low',
|
||||||
@@ -318,8 +317,24 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
prices_test.rename(columns=rename_dict, inplace=True)
|
prices_test.rename(columns=rename_dict, inplace=True)
|
||||||
prices_test.reset_index(drop=True)
|
prices_test.reset_index(drop=True)
|
||||||
|
|
||||||
|
train_df = self.drop_ohlc_from_df(train_df, dk)
|
||||||
|
test_df = self.drop_ohlc_from_df(test_df, dk)
|
||||||
|
|
||||||
return prices_train, prices_test
|
return prices_train, prices_test
|
||||||
|
|
||||||
|
def drop_ohlc_from_df(self, df: DataFrame, dk: FreqaiDataKitchen):
|
||||||
|
"""
|
||||||
|
Given a dataframe, drop the ohlc data
|
||||||
|
"""
|
||||||
|
drop_list = ['%-raw_open', '%-raw_low', '%-raw_high', '%-raw_close']
|
||||||
|
|
||||||
|
if self.rl_config["drop_ohlc_from_features"]:
|
||||||
|
df.drop(drop_list, axis=1, inplace=True)
|
||||||
|
feature_list = dk.training_features_list
|
||||||
|
dk.training_features_list = [e for e in feature_list if e not in drop_list]
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any:
|
def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any:
|
||||||
"""
|
"""
|
||||||
Can be used by user if they are trying to limit_ram_usage *and*
|
Can be used by user if they are trying to limit_ram_usage *and*
|
||||||
@@ -356,6 +371,12 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
"""
|
"""
|
||||||
An example reward function. This is the one function that users will likely
|
An example reward function. This is the one function that users will likely
|
||||||
wish to inject their own creativity into.
|
wish to inject their own creativity into.
|
||||||
|
|
||||||
|
Warning!
|
||||||
|
This is function is a showcase of functionality designed to show as many possible
|
||||||
|
environment control features as possible. It is also designed to run quickly
|
||||||
|
on small computers. This is a benchmark, it is *not* for live production.
|
||||||
|
|
||||||
:param action: int = The action made by the agent for the current candle.
|
:param action: int = The action made by the agent for the current candle.
|
||||||
:return:
|
:return:
|
||||||
float = the reward to give to the agent for current step (used for optimization
|
float = the reward to give to the agent for current step (used for optimization
|
||||||
@@ -416,9 +437,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
|||||||
return 0.
|
return 0.
|
||||||
|
|
||||||
|
|
||||||
def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int,
|
def make_env(MyRLEnv: Type[BaseEnvironment], env_id: str, rank: int,
|
||||||
seed: int, train_df: DataFrame, price: DataFrame,
|
seed: int, train_df: DataFrame, price: DataFrame,
|
||||||
monitor: bool = False,
|
|
||||||
env_info: Dict[str, Any] = {}) -> Callable:
|
env_info: Dict[str, Any] = {}) -> Callable:
|
||||||
"""
|
"""
|
||||||
Utility function for multiprocessed env.
|
Utility function for multiprocessed env.
|
||||||
@@ -435,8 +455,7 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int,
|
|||||||
|
|
||||||
env = MyRLEnv(df=train_df, prices=price, id=env_id, seed=seed + rank,
|
env = MyRLEnv(df=train_df, prices=price, id=env_id, seed=seed + rank,
|
||||||
**env_info)
|
**env_info)
|
||||||
if monitor:
|
|
||||||
env = Monitor(env)
|
|
||||||
return env
|
return env
|
||||||
set_random_seed(seed)
|
set_random_seed(seed)
|
||||||
return _init
|
return _init
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
import pandas as pd
|
||||||
|
import torch
|
||||||
|
from pandas import DataFrame
|
||||||
|
from torch.nn import functional as F
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePyTorchClassifier(BasePyTorchModel):
|
||||||
|
"""
|
||||||
|
A PyTorch implementation of a classifier.
|
||||||
|
User must implement fit method
|
||||||
|
|
||||||
|
Important!
|
||||||
|
|
||||||
|
- User must declare the target class names in the strategy,
|
||||||
|
under IStrategy.set_freqai_targets method.
|
||||||
|
|
||||||
|
for example, in your strategy:
|
||||||
|
```
|
||||||
|
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||||
|
self.freqai.class_names = ["down", "up"]
|
||||||
|
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
|
||||||
|
dataframe["close"], 'up', 'down')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
"""
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.class_name_to_index = None
|
||||||
|
self.index_to_class_name = None
|
||||||
|
|
||||||
|
def predict(
|
||||||
|
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||||
|
"""
|
||||||
|
Filter the prediction features data and predict with it.
|
||||||
|
:param dk: dk: The datakitchen object
|
||||||
|
:param unfiltered_df: Full dataframe for the current backtest period.
|
||||||
|
:return:
|
||||||
|
:pred_df: dataframe containing the predictions
|
||||||
|
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||||
|
data (NaNs) or felt uncertain about data (PCA and DI index)
|
||||||
|
:raises ValueError: if 'class_names' doesn't exist in model meta_data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class_names = self.model.model_meta_data.get("class_names", None)
|
||||||
|
if not class_names:
|
||||||
|
raise ValueError(
|
||||||
|
"Missing class names. "
|
||||||
|
"self.model.model_meta_data['class_names'] is None."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.class_name_to_index:
|
||||||
|
self.init_class_names_to_index_mapping(class_names)
|
||||||
|
|
||||||
|
dk.find_features(unfiltered_df)
|
||||||
|
filtered_df, _ = dk.filter_features(
|
||||||
|
unfiltered_df, dk.training_features_list, training_filter=False
|
||||||
|
)
|
||||||
|
filtered_df = dk.normalize_data_from_metadata(filtered_df)
|
||||||
|
dk.data_dictionary["prediction_features"] = filtered_df
|
||||||
|
self.data_cleaning_predict(dk)
|
||||||
|
x = self.data_convertor.convert_x(
|
||||||
|
dk.data_dictionary["prediction_features"],
|
||||||
|
device=self.device
|
||||||
|
)
|
||||||
|
self.model.model.eval()
|
||||||
|
logits = self.model.model(x)
|
||||||
|
probs = F.softmax(logits, dim=-1)
|
||||||
|
predicted_classes = torch.argmax(probs, dim=-1)
|
||||||
|
predicted_classes_str = self.decode_class_names(predicted_classes)
|
||||||
|
# used .tolist to convert probs into an iterable, in this way Tensors
|
||||||
|
# are automatically moved to the CPU first if necessary.
|
||||||
|
pred_df_prob = DataFrame(probs.detach().tolist(), columns=class_names)
|
||||||
|
pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]])
|
||||||
|
pred_df = pd.concat([pred_df, pred_df_prob], axis=1)
|
||||||
|
return (pred_df, dk.do_predict)
|
||||||
|
|
||||||
|
def encode_class_names(
|
||||||
|
self,
|
||||||
|
data_dictionary: Dict[str, pd.DataFrame],
|
||||||
|
dk: FreqaiDataKitchen,
|
||||||
|
class_names: List[str],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
encode class name, str -> int
|
||||||
|
assuming first column of *_labels data frame to be the target column
|
||||||
|
containing the class names
|
||||||
|
"""
|
||||||
|
|
||||||
|
target_column_name = dk.label_list[0]
|
||||||
|
for split in self.splits:
|
||||||
|
label_df = data_dictionary[f"{split}_labels"]
|
||||||
|
self.assert_valid_class_names(label_df[target_column_name], class_names)
|
||||||
|
label_df[target_column_name] = list(
|
||||||
|
map(lambda x: self.class_name_to_index[x], label_df[target_column_name])
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assert_valid_class_names(
|
||||||
|
target_column: pd.Series,
|
||||||
|
class_names: List[str]
|
||||||
|
):
|
||||||
|
non_defined_labels = set(target_column) - set(class_names)
|
||||||
|
if len(non_defined_labels) != 0:
|
||||||
|
raise OperationalException(
|
||||||
|
f"Found non defined labels: {non_defined_labels}, ",
|
||||||
|
f"expecting labels: {class_names}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def decode_class_names(self, class_ints: torch.Tensor) -> List[str]:
|
||||||
|
"""
|
||||||
|
decode class name, int -> str
|
||||||
|
"""
|
||||||
|
|
||||||
|
return list(map(lambda x: self.index_to_class_name[x.item()], class_ints))
|
||||||
|
|
||||||
|
def init_class_names_to_index_mapping(self, class_names):
|
||||||
|
self.class_name_to_index = {s: i for i, s in enumerate(class_names)}
|
||||||
|
self.index_to_class_name = {i: s for i, s in enumerate(class_names)}
|
||||||
|
logger.info(f"encoded class name to index: {self.class_name_to_index}")
|
||||||
|
|
||||||
|
def convert_label_column_to_int(
|
||||||
|
self,
|
||||||
|
data_dictionary: Dict[str, pd.DataFrame],
|
||||||
|
dk: FreqaiDataKitchen,
|
||||||
|
class_names: List[str]
|
||||||
|
):
|
||||||
|
self.init_class_names_to_index_mapping(class_names)
|
||||||
|
self.encode_class_names(data_dictionary, dk, class_names)
|
||||||
|
|
||||||
|
def get_class_names(self) -> List[str]:
|
||||||
|
if not self.class_names:
|
||||||
|
raise ValueError(
|
||||||
|
"self.class_names is empty, "
|
||||||
|
"set self.freqai.class_names = ['class a', 'class b', 'class c'] "
|
||||||
|
"inside IStrategy.set_freqai_targets method."
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.class_names
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import logging
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from time import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
from freqtrade.freqai.freqai_interface import IFreqaiModel
|
||||||
|
from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePyTorchModel(IFreqaiModel, ABC):
|
||||||
|
"""
|
||||||
|
Base class for PyTorch type models.
|
||||||
|
User *must* inherit from this class and set fit() and predict() and
|
||||||
|
data_convertor property.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(config=kwargs["config"])
|
||||||
|
self.dd.model_type = "pytorch"
|
||||||
|
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
test_size = self.freqai_info.get('data_split_parameters', {}).get('test_size')
|
||||||
|
self.splits = ["train", "test"] if test_size != 0 else ["train"]
|
||||||
|
self.window_size = self.freqai_info.get("conv_width", 1)
|
||||||
|
|
||||||
|
def train(
|
||||||
|
self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Filter the training data and train a model to it. Train makes heavy use of the datakitchen
|
||||||
|
for storing, saving, loading, and analyzing the data.
|
||||||
|
:param unfiltered_df: Full dataframe for the current training period
|
||||||
|
:return:
|
||||||
|
:model: Trained model which can be used to inference (self.predict)
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"-------------------- Starting training {pair} --------------------")
|
||||||
|
|
||||||
|
start_time = time()
|
||||||
|
|
||||||
|
features_filtered, labels_filtered = dk.filter_features(
|
||||||
|
unfiltered_df,
|
||||||
|
dk.training_features_list,
|
||||||
|
dk.label_list,
|
||||||
|
training_filter=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# split data into train/test data.
|
||||||
|
data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered)
|
||||||
|
if not self.freqai_info.get("fit_live_predictions", 0) or not self.live:
|
||||||
|
dk.fit_labels()
|
||||||
|
# normalize all data based on train_dataset only
|
||||||
|
data_dictionary = dk.normalize_data(data_dictionary)
|
||||||
|
|
||||||
|
# optional additional data cleaning/analysis
|
||||||
|
self.data_cleaning_train(dk)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Training model on {len(dk.data_dictionary['train_features'].columns)} features"
|
||||||
|
)
|
||||||
|
logger.info(f"Training model on {len(data_dictionary['train_features'])} data points")
|
||||||
|
|
||||||
|
model = self.fit(data_dictionary, dk)
|
||||||
|
end_time = time()
|
||||||
|
|
||||||
|
logger.info(f"-------------------- Done training {pair} "
|
||||||
|
f"({end_time - start_time:.2f} secs) --------------------")
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def data_convertor(self) -> PyTorchDataConvertor:
|
||||||
|
"""
|
||||||
|
a class responsible for converting `*_features` & `*_labels` pandas dataframes
|
||||||
|
to pytorch tensors.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Abstract property")
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePyTorchRegressor(BasePyTorchModel):
|
||||||
|
"""
|
||||||
|
A PyTorch implementation of a regressor.
|
||||||
|
User must implement fit method
|
||||||
|
"""
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def predict(
|
||||||
|
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||||
|
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||||
|
"""
|
||||||
|
Filter the prediction features data and predict with it.
|
||||||
|
:param unfiltered_df: Full dataframe for the current backtest period.
|
||||||
|
:return:
|
||||||
|
:pred_df: dataframe containing the predictions
|
||||||
|
:do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove
|
||||||
|
data (NaNs) or felt uncertain about data (PCA and DI index)
|
||||||
|
"""
|
||||||
|
|
||||||
|
dk.find_features(unfiltered_df)
|
||||||
|
filtered_df, _ = dk.filter_features(
|
||||||
|
unfiltered_df, dk.training_features_list, training_filter=False
|
||||||
|
)
|
||||||
|
filtered_df = dk.normalize_data_from_metadata(filtered_df)
|
||||||
|
dk.data_dictionary["prediction_features"] = filtered_df
|
||||||
|
|
||||||
|
self.data_cleaning_predict(dk)
|
||||||
|
x = self.data_convertor.convert_x(
|
||||||
|
dk.data_dictionary["prediction_features"],
|
||||||
|
device=self.device
|
||||||
|
)
|
||||||
|
self.model.model.eval()
|
||||||
|
y = self.model.model(x)
|
||||||
|
pred_df = DataFrame(y.detach().tolist(), columns=[dk.label_list[0]])
|
||||||
|
pred_df = dk.denormalize_labels_from_metadata(pred_df)
|
||||||
|
return (pred_df, dk.do_predict)
|
||||||
@@ -126,7 +126,7 @@ class FreqaiDataDrawer:
|
|||||||
"""
|
"""
|
||||||
exists = self.global_metadata_path.is_file()
|
exists = self.global_metadata_path.is_file()
|
||||||
if exists:
|
if exists:
|
||||||
with open(self.global_metadata_path, "r") as fp:
|
with self.global_metadata_path.open("r") as fp:
|
||||||
metatada_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
metatada_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||||
return metatada_dict
|
return metatada_dict
|
||||||
return {}
|
return {}
|
||||||
@@ -139,7 +139,7 @@ class FreqaiDataDrawer:
|
|||||||
"""
|
"""
|
||||||
exists = self.pair_dictionary_path.is_file()
|
exists = self.pair_dictionary_path.is_file()
|
||||||
if exists:
|
if exists:
|
||||||
with open(self.pair_dictionary_path, "r") as fp:
|
with self.pair_dictionary_path.open("r") as fp:
|
||||||
self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||||
else:
|
else:
|
||||||
logger.info("Could not find existing datadrawer, starting from scratch")
|
logger.info("Could not find existing datadrawer, starting from scratch")
|
||||||
@@ -152,7 +152,7 @@ class FreqaiDataDrawer:
|
|||||||
if self.freqai_info.get('write_metrics_to_disk', False):
|
if self.freqai_info.get('write_metrics_to_disk', False):
|
||||||
exists = self.metric_tracker_path.is_file()
|
exists = self.metric_tracker_path.is_file()
|
||||||
if exists:
|
if exists:
|
||||||
with open(self.metric_tracker_path, "r") as fp:
|
with self.metric_tracker_path.open("r") as fp:
|
||||||
self.metric_tracker = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
self.metric_tracker = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||||
logger.info("Loading existing metric tracker from disk.")
|
logger.info("Loading existing metric tracker from disk.")
|
||||||
else:
|
else:
|
||||||
@@ -166,7 +166,7 @@ class FreqaiDataDrawer:
|
|||||||
exists = self.historic_predictions_path.is_file()
|
exists = self.historic_predictions_path.is_file()
|
||||||
if exists:
|
if exists:
|
||||||
try:
|
try:
|
||||||
with open(self.historic_predictions_path, "rb") as fp:
|
with self.historic_predictions_path.open("rb") as fp:
|
||||||
self.historic_predictions = cloudpickle.load(fp)
|
self.historic_predictions = cloudpickle.load(fp)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Found existing historic predictions at {self.full_path}, but beware "
|
f"Found existing historic predictions at {self.full_path}, but beware "
|
||||||
@@ -176,7 +176,7 @@ class FreqaiDataDrawer:
|
|||||||
except EOFError:
|
except EOFError:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Historical prediction file was corrupted. Trying to load backup file.')
|
'Historical prediction file was corrupted. Trying to load backup file.')
|
||||||
with open(self.historic_predictions_bkp_path, "rb") as fp:
|
with self.historic_predictions_bkp_path.open("rb") as fp:
|
||||||
self.historic_predictions = cloudpickle.load(fp)
|
self.historic_predictions = cloudpickle.load(fp)
|
||||||
logger.warning('FreqAI successfully loaded the backup historical predictions file.')
|
logger.warning('FreqAI successfully loaded the backup historical predictions file.')
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ class FreqaiDataDrawer:
|
|||||||
"""
|
"""
|
||||||
Save historic predictions pickle to disk
|
Save historic predictions pickle to disk
|
||||||
"""
|
"""
|
||||||
with open(self.historic_predictions_path, "wb") as fp:
|
with self.historic_predictions_path.open("wb") as fp:
|
||||||
cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL)
|
cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL)
|
||||||
|
|
||||||
# create a backup
|
# create a backup
|
||||||
@@ -200,16 +200,16 @@ class FreqaiDataDrawer:
|
|||||||
Save metric tracker of all pair metrics collected.
|
Save metric tracker of all pair metrics collected.
|
||||||
"""
|
"""
|
||||||
with self.save_lock:
|
with self.save_lock:
|
||||||
with open(self.metric_tracker_path, 'w') as fp:
|
with self.metric_tracker_path.open('w') as fp:
|
||||||
rapidjson.dump(self.metric_tracker, fp, default=self.np_encoder,
|
rapidjson.dump(self.metric_tracker, fp, default=self.np_encoder,
|
||||||
number_mode=rapidjson.NM_NATIVE)
|
number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
def save_drawer_to_disk(self):
|
def save_drawer_to_disk(self) -> None:
|
||||||
"""
|
"""
|
||||||
Save data drawer full of all pair model metadata in present model folder.
|
Save data drawer full of all pair model metadata in present model folder.
|
||||||
"""
|
"""
|
||||||
with self.save_lock:
|
with self.save_lock:
|
||||||
with open(self.pair_dictionary_path, 'w') as fp:
|
with self.pair_dictionary_path.open('w') as fp:
|
||||||
rapidjson.dump(self.pair_dict, fp, default=self.np_encoder,
|
rapidjson.dump(self.pair_dict, fp, default=self.np_encoder,
|
||||||
number_mode=rapidjson.NM_NATIVE)
|
number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ class FreqaiDataDrawer:
|
|||||||
Save global metadata json to disk
|
Save global metadata json to disk
|
||||||
"""
|
"""
|
||||||
with self.save_lock:
|
with self.save_lock:
|
||||||
with open(self.global_metadata_path, 'w') as fp:
|
with self.global_metadata_path.open('w') as fp:
|
||||||
rapidjson.dump(metadata, fp, default=self.np_encoder,
|
rapidjson.dump(metadata, fp, default=self.np_encoder,
|
||||||
number_mode=rapidjson.NM_NATIVE)
|
number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
@@ -424,7 +424,7 @@ class FreqaiDataDrawer:
|
|||||||
dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns)
|
dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns)
|
||||||
dk.data["label_list"] = dk.label_list
|
dk.data["label_list"] = dk.label_list
|
||||||
|
|
||||||
with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp:
|
with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp:
|
||||||
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
|
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -446,7 +446,7 @@ class FreqaiDataDrawer:
|
|||||||
dump(model, save_path / f"{dk.model_filename}_model.joblib")
|
dump(model, save_path / f"{dk.model_filename}_model.joblib")
|
||||||
elif self.model_type == 'keras':
|
elif self.model_type == 'keras':
|
||||||
model.save(save_path / f"{dk.model_filename}_model.h5")
|
model.save(save_path / f"{dk.model_filename}_model.h5")
|
||||||
elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type:
|
elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]:
|
||||||
model.save(save_path / f"{dk.model_filename}_model.zip")
|
model.save(save_path / f"{dk.model_filename}_model.zip")
|
||||||
|
|
||||||
if dk.svm_model is not None:
|
if dk.svm_model is not None:
|
||||||
@@ -457,7 +457,7 @@ class FreqaiDataDrawer:
|
|||||||
dk.data["training_features_list"] = dk.training_features_list
|
dk.data["training_features_list"] = dk.training_features_list
|
||||||
dk.data["label_list"] = dk.label_list
|
dk.data["label_list"] = dk.label_list
|
||||||
# store the metadata
|
# store the metadata
|
||||||
with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp:
|
with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp:
|
||||||
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
|
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
# save the train data to file so we can check preds for area of applicability later
|
# save the train data to file so we can check preds for area of applicability later
|
||||||
@@ -471,7 +471,7 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
if self.freqai_info["feature_parameters"].get("principal_component_analysis"):
|
if self.freqai_info["feature_parameters"].get("principal_component_analysis"):
|
||||||
cloudpickle.dump(
|
cloudpickle.dump(
|
||||||
dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb")
|
dk.pca, (dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("wb")
|
||||||
)
|
)
|
||||||
|
|
||||||
self.model_dictionary[coin] = model
|
self.model_dictionary[coin] = model
|
||||||
@@ -491,12 +491,12 @@ class FreqaiDataDrawer:
|
|||||||
Load only metadata into datakitchen to increase performance during
|
Load only metadata into datakitchen to increase performance during
|
||||||
presaved backtesting (prediction file loading).
|
presaved backtesting (prediction file loading).
|
||||||
"""
|
"""
|
||||||
with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp:
|
with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp:
|
||||||
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||||
dk.training_features_list = dk.data["training_features_list"]
|
dk.training_features_list = dk.data["training_features_list"]
|
||||||
dk.label_list = dk.data["label_list"]
|
dk.label_list = dk.data["label_list"]
|
||||||
|
|
||||||
def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any:
|
def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: # noqa: C901
|
||||||
"""
|
"""
|
||||||
loads all data required to make a prediction on a sub-train time range
|
loads all data required to make a prediction on a sub-train time range
|
||||||
:returns:
|
:returns:
|
||||||
@@ -514,7 +514,7 @@ class FreqaiDataDrawer:
|
|||||||
dk.data = self.meta_data_dictionary[coin]["meta_data"]
|
dk.data = self.meta_data_dictionary[coin]["meta_data"]
|
||||||
dk.data_dictionary["train_features"] = self.meta_data_dictionary[coin]["train_df"]
|
dk.data_dictionary["train_features"] = self.meta_data_dictionary[coin]["train_df"]
|
||||||
else:
|
else:
|
||||||
with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp:
|
with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp:
|
||||||
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
dk.data_dictionary["train_features"] = pd.read_pickle(
|
dk.data_dictionary["train_features"] = pd.read_pickle(
|
||||||
@@ -537,6 +537,11 @@ class FreqaiDataDrawer:
|
|||||||
self.model_type, self.freqai_info['rl_config']['model_type'])
|
self.model_type, self.freqai_info['rl_config']['model_type'])
|
||||||
MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type'])
|
MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type'])
|
||||||
model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model")
|
model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model")
|
||||||
|
elif self.model_type == 'pytorch':
|
||||||
|
import torch
|
||||||
|
zip = torch.load(dk.data_path / f"{dk.model_filename}_model.zip")
|
||||||
|
model = zip["pytrainer"]
|
||||||
|
model = model.load_from_checkpoint(zip)
|
||||||
|
|
||||||
if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file():
|
if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file():
|
||||||
dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib")
|
dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib")
|
||||||
@@ -552,7 +557,7 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
|
if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
|
||||||
dk.pca = cloudpickle.load(
|
dk.pca = cloudpickle.load(
|
||||||
open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb")
|
(dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("rb")
|
||||||
)
|
)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
@@ -570,12 +575,12 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
for pair in dk.all_pairs:
|
for pair in dk.all_pairs:
|
||||||
for tf in feat_params.get("include_timeframes"):
|
for tf in feat_params.get("include_timeframes"):
|
||||||
|
hist_df = history_data[pair][tf]
|
||||||
# check if newest candle is already appended
|
# check if newest candle is already appended
|
||||||
df_dp = strategy.dp.get_pair_dataframe(pair, tf)
|
df_dp = strategy.dp.get_pair_dataframe(pair, tf)
|
||||||
if len(df_dp.index) == 0:
|
if len(df_dp.index) == 0:
|
||||||
continue
|
continue
|
||||||
if str(history_data[pair][tf].iloc[-1]["date"]) == str(
|
if str(hist_df.iloc[-1]["date"]) == str(
|
||||||
df_dp.iloc[-1:]["date"].iloc[-1]
|
df_dp.iloc[-1:]["date"].iloc[-1]
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
@@ -583,21 +588,30 @@ class FreqaiDataDrawer:
|
|||||||
try:
|
try:
|
||||||
index = (
|
index = (
|
||||||
df_dp.loc[
|
df_dp.loc[
|
||||||
df_dp["date"] == history_data[pair][tf].iloc[-1]["date"]
|
df_dp["date"] == hist_df.iloc[-1]["date"]
|
||||||
].index[0]
|
].index[0]
|
||||||
+ 1
|
+ 1
|
||||||
)
|
)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.warning(
|
if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]:
|
||||||
f"Unable to update pair history for {pair}. "
|
raise OperationalException("In memory historical data is older than "
|
||||||
"If this does not resolve itself after 1 additional candle, "
|
f"oldest DataProvider candle for {pair} on "
|
||||||
"please report the error to #freqai discord channel"
|
f"timeframe {tf}")
|
||||||
)
|
else:
|
||||||
return
|
index = -1
|
||||||
|
logger.warning(
|
||||||
|
f"No common dates in historical data and dataprovider for {pair}. "
|
||||||
|
f"Appending latest dataprovider candle to historical data "
|
||||||
|
"but please be aware that there is likely a gap in the historical "
|
||||||
|
"data. \n"
|
||||||
|
f"Historical data ends at {hist_df.iloc[-1]['date']} "
|
||||||
|
f"while dataprovider starts at {df_dp['date'].iloc[0]} and"
|
||||||
|
f"ends at {df_dp['date'].iloc[0]}."
|
||||||
|
)
|
||||||
|
|
||||||
history_data[pair][tf] = pd.concat(
|
history_data[pair][tf] = pd.concat(
|
||||||
[
|
[
|
||||||
history_data[pair][tf],
|
hist_df,
|
||||||
df_dp.iloc[index:],
|
df_dp.iloc[index:],
|
||||||
],
|
],
|
||||||
ignore_index=True,
|
ignore_index=True,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user