Compare commits
996 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3b6256f8a | |||
| f37c4e5935 | |||
| 4342aa3bfd | |||
| 43188ca447 | |||
| c35b9f8a3a | |||
| 7af524ff90 | |||
| 02a7f6398d | |||
| 18723d2b6d | |||
| 608a408c75 | |||
| e5d75c4454 | |||
| effe2b5367 | |||
| 2f37385eb4 | |||
| 0fa7f9e47f | |||
| 81c8e6b943 | |||
| b3843f0d53 | |||
| 84913f2ea8 | |||
| 1450d9e9cd | |||
| 4394fd1346 | |||
| 3a6393e2d1 | |||
| 990f9e03f8 | |||
| 0b549fc7f8 | |||
| 5890665931 | |||
| e03e0e838b | |||
| 99b11c088b | |||
| f42fd25800 | |||
| a20fe8cd09 | |||
| c398504f23 | |||
| dd3fbfcfda | |||
| f0562c391c | |||
| 027ce4337d | |||
| fd21658523 | |||
| 523864601a | |||
| 8f0dbc6be1 | |||
| 8d72ee358c | |||
| 0077f3c9a5 | |||
| 6aa4de4d29 | |||
| 817aaa164c | |||
| ea9c51570f | |||
| 2fe6fe14aa | |||
| 85dd371ee3 | |||
| 0fc3c675b6 | |||
| 55fe379e13 | |||
| 96a8263c57 | |||
| 79ecca3e40 | |||
| 267f2e352e | |||
| 1ae3b1e622 | |||
| 34ac2dc9ae | |||
| c15f811602 | |||
| efe332a395 | |||
| 8fd2dcd257 | |||
| 48ea43f954 | |||
| 6b78dac6f0 | |||
| 0a40a345fe | |||
| 656b32814b | |||
| c9c44a4710 | |||
| 5167f6936d | |||
| fdf88a8019 | |||
| 087c59cfbf | |||
| 83480d90f1 | |||
| 67abfcf4d4 | |||
| 00e4c51741 | |||
| a1b93dc915 | |||
| 40011eb9a3 | |||
| f1d002a735 | |||
| 991a279b23 | |||
| 5b879df2b0 | |||
| d191138009 | |||
| a8ee0cc4c8 | |||
| 85658bb637 | |||
| b4fb133702 | |||
| b7537b6ade | |||
| 633992da52 | |||
| 0be5ca7503 | |||
| e310e4abbb | |||
| 1b132ee21d | |||
| 7f0b6250d0 | |||
| 3b5c2ea0d7 | |||
| c6f4c82f44 | |||
| 5bc0dcfb22 | |||
| 06630a5991 | |||
| 5f97374752 | |||
| 3dd29437d5 | |||
| 4e18c35727 | |||
| 0f1e7ed652 | |||
| fbb91ca331 | |||
| 713d9fdc8f | |||
| 3681e3e754 | |||
| 32d77f808e | |||
| d58b941bf1 | |||
| 34df710265 | |||
| f324f8775f | |||
| 00f8f05268 | |||
| 3d5620a422 | |||
| e704c2def4 | |||
| d534f2014a | |||
| 612ec38e57 | |||
| ac5b12cfd2 | |||
| f9b6830b78 | |||
| 8a64f0b884 | |||
| 15da4aa9bd | |||
| c167575098 | |||
| b56c663bea | |||
| 501a9a8c98 | |||
| c333c9c5a1 | |||
| 11dd349c2b | |||
| d355f011df | |||
| ef80772fc3 | |||
| ff95adb2eb | |||
| 5b1cda9236 | |||
| 32a4aa0ca8 | |||
| 50864c731b | |||
| 5bfda534b2 | |||
| 22a08c7c8d | |||
| 73b30cf3fa | |||
| 6ce1123136 | |||
| 8cbdcbd50d | |||
| 42ea4821f4 | |||
| b7ab7dd25e | |||
| c2d6ab2fa5 | |||
| 6069670ce2 | |||
| c248bb27e9 | |||
| f3fd1f010c | |||
| a4d078efd1 | |||
| cfdf21942d | |||
| e18fbbd972 | |||
| 78ca939ae6 | |||
| eccc1498af | |||
| 0a6a0a8b88 | |||
| 6cfc2b2bdf | |||
| 2d5f3c150b | |||
| 3ccb612cd9 | |||
| dc5c1e1300 | |||
| 91ceb5c19a | |||
| d073692cae | |||
| e1b8742b1f | |||
| d866bb6f01 | |||
| 2737de1fdd | |||
| a41b72ca19 | |||
| ea1b3c38f8 | |||
| 31fb06fa80 | |||
| 291478a974 | |||
| 29d2fc2e1b | |||
| 59cc607761 | |||
| c79502cb4b | |||
| f124f2daaf | |||
| 026f989625 | |||
| 9a2ed35030 | |||
| d412a8fc1a | |||
| fbed3fad78 | |||
| 393df83a91 | |||
| e967dfa145 | |||
| 4aab57ce62 | |||
| 5841c65430 | |||
| 338ff66268 | |||
| 629a6be269 | |||
| 4896e04c41 | |||
| a53adb95e1 | |||
| ab82f5cc69 | |||
| 6fc1884e8e | |||
| cd392fbbdf | |||
| 893e2cbbc1 | |||
| bcebb4d804 | |||
| cd81897c84 | |||
| cb9fa734d1 | |||
| 3f27c2792a | |||
| 883f508544 | |||
| 3caf964c46 | |||
| 7fcbe9788d | |||
| 225c94c0f5 | |||
| 6a5847a157 | |||
| a9287359a3 | |||
| 3741cbf3a9 | |||
| 962417bdea | |||
| 34c1838566 | |||
| 3b7288edaa | |||
| e28b2f44d3 | |||
| 72c41ec2c5 | |||
| 5be32cd801 | |||
| 6640c9e3d9 | |||
| 37dacdff0d | |||
| 671ce25a28 | |||
| dbc74f0d5e | |||
| f33e89e164 | |||
| 4b320b0f2f | |||
| 7a141abeca | |||
| a8228f0eac | |||
| ad079f858d | |||
| 0b3126a0fe | |||
| 4686d22020 | |||
| 2304f852ff | |||
| 68d2d24f2a | |||
| 67f608fb5f | |||
| 1b70e9b074 | |||
| 7c7ddb3b19 | |||
| 69e9ab90df | |||
| 28b5552231 | |||
| b27f99dbc3 | |||
| 6121c7ed86 | |||
| 394c260eb2 | |||
| 582f51ccdd | |||
| 74bb1a29b6 | |||
| ddb42879ef | |||
| 53b24ea69b | |||
| 80071dd2d8 | |||
| 545096cc47 | |||
| 9f682b5829 | |||
| 209e7033cd | |||
| 7f69a06819 | |||
| 20b2f25067 | |||
| 079330ac64 | |||
| 88524db290 | |||
| e1ad87a565 | |||
| 65009373ee | |||
| 7f84996c1e | |||
| 90df6bcd54 | |||
| 3237cad8c8 | |||
| 2a11597ad3 | |||
| 59ec3fd860 | |||
| 685fef1ee8 | |||
| 690a3599d0 | |||
| e4977d4cbd | |||
| 7b78e66e7d | |||
| 8726d63923 | |||
| 5e9a929e9e | |||
| c3ce11c222 | |||
| 0ab54b88cf | |||
| 934e72656a | |||
| b950128c4d | |||
| c1982ab836 | |||
| 86a9968bdd | |||
| 2bceb35b79 | |||
| e7eb1c132d | |||
| fdc573f2f7 | |||
| 02d124b85b | |||
| 3eade3e8db | |||
| 6cce455835 | |||
| f028bdf342 | |||
| 983764ad0a | |||
| a12f368796 | |||
| c9dd99a4b5 | |||
| 67d3dca49c | |||
| d5d3188b99 | |||
| 50287ce556 | |||
| 3c1bbd9610 | |||
| ec1b2e1da5 | |||
| 73970d27bf | |||
| 0224c50c9e | |||
| b0fdf6ac52 | |||
| 682b462d82 | |||
| 22307913d8 | |||
| b9786b979a | |||
| abda629a72 | |||
| a4fca5b1fe | |||
| 903062d479 | |||
| b6a71f2d2a | |||
| 7a6888dfd3 | |||
| f3da2c6fd8 | |||
| ce63eb30a2 | |||
| b50a532284 | |||
| a9380f0d53 | |||
| 316812f427 | |||
| fa368a9a4a | |||
| 9030e1041f | |||
| 2c3cef5dee | |||
| e722d81456 | |||
| 10dbf47836 | |||
| a2160e4503 | |||
| 842727202f | |||
| 3526ba1a9b | |||
| c07f2caa4a | |||
| 21e1f3d910 | |||
| c2d265e4b7 | |||
| f0e82592c3 | |||
| 501f629c05 | |||
| 687ad05047 | |||
| 3852757250 | |||
| 256471dacd | |||
| ead7bd8c2e | |||
| 80dbb0dd09 | |||
| 0552df2fe1 | |||
| e4b843405d | |||
| d2802ad499 | |||
| a9906f0788 | |||
| b17c0cd4a2 | |||
| 0575e222b1 | |||
| 1c5a699aef | |||
| 292ef85d96 | |||
| b3c9be1cd0 | |||
| 13e329f176 | |||
| b9f4a23548 | |||
| 9746d38060 | |||
| ae68d02038 | |||
| 9896ddf71e | |||
| 0d4fcad285 | |||
| 3e7d5bbae8 | |||
| 4d0ee014de | |||
| ea8edbd23b | |||
| f6de7d952a | |||
| 3a87522070 | |||
| 7ccddb5d58 | |||
| e9c04debfb | |||
| 5c8c53cff8 | |||
| 79e15591ef | |||
| b62661d8cd | |||
| e5e3002d45 | |||
| e664527da6 | |||
| 063b55d41a | |||
| ce7061c934 | |||
| 846b7734e1 | |||
| c72d18270e | |||
| 02836368fb | |||
| d282027858 | |||
| d78a5798e3 | |||
| 16958eaef2 | |||
| 2c5cca4be6 | |||
| d329ad28c2 | |||
| 2de86af51e | |||
| 97780ee5bf | |||
| e162f98cf5 | |||
| c6967b11be | |||
| 1e5d353cf7 | |||
| 17a538b5ba | |||
| fc746214f6 | |||
| 58432c1cfa | |||
| 69119764bf | |||
| a20e641bcd | |||
| ad68eff8f4 | |||
| 68552a61bd | |||
| b7419305d4 | |||
| 8cc2c5010a | |||
| 872154a6fc | |||
| c8ee8e82f9 | |||
| 5cbcf72501 | |||
| 35876cf8ee | |||
| ac60871ff9 | |||
| 866e34a983 | |||
| 7107971820 | |||
| 3484590640 | |||
| 971b7d074c | |||
| ec89aad10b | |||
| fc13a99b33 | |||
| 03fb204408 | |||
| 0a96aa69b9 | |||
| ddbfce01b5 | |||
| 12e75e849d | |||
| 916a847639 | |||
| 4769b50709 | |||
| 18dd9cf803 | |||
| def204448a | |||
| 63e652619f | |||
| 3cb4b10656 | |||
| 23c3757c7a | |||
| 39b2a096ab | |||
| b20aee7653 | |||
| 4d7c4352a7 | |||
| a5c914951f | |||
| 9ab08d513a | |||
| 87ce7dc02c | |||
| 3c7c4bc02f | |||
| 0484b891f1 | |||
| 71689d9d6f | |||
| 881247a823 | |||
| d26154f994 | |||
| 0e14dd6190 | |||
| 3676514ba6 | |||
| 9ec23a0292 | |||
| 1272a15c35 | |||
| a3a175e332 | |||
| f7c7990aff | |||
| 71b777ef34 | |||
| c4cd547451 | |||
| b4e8122fd3 | |||
| 3f44f56f6f | |||
| cd28244cf1 | |||
| b1393f1bd4 | |||
| 34c5c68a91 | |||
| c9a018cf0e | |||
| 94363061ae | |||
| 4f2d7b858f | |||
| 663b1e11f8 | |||
| 2d6a49013f | |||
| 0333abcbdc | |||
| 0440b3ef75 | |||
| d9ed8c6560 | |||
| 2f2467256c | |||
| 26f0fe2383 | |||
| 5e3da30ab9 | |||
| 651d2c06b9 | |||
| b371593084 | |||
| 3096cddbcf | |||
| 5bbe6aacfd | |||
| fe4bf271b4 | |||
| 0d3d1e6d9a | |||
| 57e01329ce | |||
| 99359a7f85 | |||
| 5f0932f5ed | |||
| fece88e7cd | |||
| c8eb2523ac | |||
| 964b439119 | |||
| ee4f4f2d0d | |||
| 7c215688c0 | |||
| 087ca2bd4a | |||
| 6193df36a3 | |||
| 893b43766f | |||
| a70ca6f612 | |||
| e5c783d2db | |||
| e1b652d890 | |||
| 4e98b5f710 | |||
| 901b3f6fe7 | |||
| d0a9d927dd | |||
| 16eb95c71d | |||
| 054ed0a7b6 | |||
| 82853e37a1 | |||
| 81c6336e11 | |||
| 6404bd8b2b | |||
| 9a9a6eaa63 | |||
| 67cdab6ee9 | |||
| 546ea430c9 | |||
| 4cc4f0ffa2 | |||
| 58470ba9d0 | |||
| c580e8783f | |||
| bc1ad3acbd | |||
| d2e328b8bd | |||
| 93e9c4fa65 | |||
| 8d231dbf31 | |||
| b178cc5f31 | |||
| 6ee0f16e4f | |||
| 1d701967b7 | |||
| d6b43c5eed | |||
| 3670699ea2 | |||
| 9b11091fc7 | |||
| 58dfffe87a | |||
| e573409925 | |||
| e3fda16d16 | |||
| 9e2e60e7ad | |||
| bb2024f789 | |||
| e96f4f0e53 | |||
| 71d3572c27 | |||
| 6218d732b1 | |||
| 62667d123f | |||
| cfd5af67ca | |||
| 47a952e41c | |||
| c9f43587ec | |||
| e79a58e1c4 | |||
| c62e52c759 | |||
| 5ab61d9da4 | |||
| 55efaec83d | |||
| c5c2c8c185 | |||
| 755ce3a858 | |||
| 105cd99395 | |||
| db7799d2fb | |||
| 30f94ef5b7 | |||
| 6c259ddca1 | |||
| 0f82ea8767 | |||
| eac5d53a64 | |||
| 4b43ccae11 | |||
| 8a2763d275 | |||
| 4b3230689c | |||
| aa86c49640 | |||
| 1adc8836dd | |||
| 92993c0c4c | |||
| 9b2c116d9b | |||
| 2ad85a6957 | |||
| a0e92f5a47 | |||
| 17f0697c43 | |||
| 199e3f91f2 | |||
| cc14cf23b1 | |||
| 327f750ead | |||
| a304080c08 | |||
| 020d4b5b4d | |||
| 78cb1e608f | |||
| b84a2fcac7 | |||
| c0119d7baa | |||
| 0bd513012a | |||
| 3f6cd9ee51 | |||
| 8964c138f1 | |||
| ef23f0fcba | |||
| 074343f0f1 | |||
| 966eb59fd3 | |||
| 778bd7b3b0 | |||
| b4cd46d6db | |||
| 1d73b57e8c | |||
| 476703bf76 | |||
| 9f1599b7f0 | |||
| e541c95c46 | |||
| cd09d3567f | |||
| 4c25e9d21a | |||
| dd08c134b4 | |||
| aa8bb27cf0 | |||
| d428d48393 | |||
| 3bdfa85237 | |||
| 95a824fbe7 | |||
| bb85efd6ad | |||
| 7321a14c35 | |||
| 2745a5d334 | |||
| bf8b3e831d | |||
| d123d3bb82 | |||
| b14873400d | |||
| d9b95a4c27 | |||
| 71178ff1d2 | |||
| d3d9f3281e | |||
| 3b4b833dd0 | |||
| fcc87399cd | |||
| 654f8a7573 | |||
| 2fdb9663d7 | |||
| 3739a10735 | |||
| 9a425af76f | |||
| 3c48208b31 | |||
| 68db8d0201 | |||
| 2207773678 | |||
| cbe6d52a73 | |||
| b7fc0190f7 | |||
| 4061eaf888 | |||
| ef042ae5ec | |||
| 6ee792069d | |||
| 46c81d7018 | |||
| caac77c90b | |||
| 90332128b1 | |||
| af05e3d747 | |||
| 59a287106d | |||
| 8b5e6bf9be | |||
| d9882978d5 | |||
| 04f2b7bad6 | |||
| 8597709b2e | |||
| 2fea75e312 | |||
| 511e4d16d5 | |||
| e0f780f10e | |||
| 44689d058b | |||
| 1431b2b44a | |||
| 106bda51d4 | |||
| 90e4eb59b2 | |||
| 701f6fc050 | |||
| 0235db48a8 | |||
| e9e7bf3caf | |||
| 4464b02719 | |||
| 4ed9ffbf31 | |||
| c017366086 | |||
| 34bce4d251 | |||
| 6754072bca | |||
| 15771043f7 | |||
| b417a0297b | |||
| 5daafaabc2 | |||
| 08db2f05aa | |||
| 0654186400 | |||
| d85518ccb2 | |||
| c34cb9eb12 | |||
| cb01a46089 | |||
| 0bcbb28d51 | |||
| e2a47ee95a | |||
| 36836ea803 | |||
| 19d9adaf9c | |||
| cf078f8093 | |||
| 5ddca4e9f9 | |||
| 6db66b1e58 | |||
| 0a903e45f2 | |||
| d0a2b9403e | |||
| a18c85ec64 | |||
| 0914b8b5f4 | |||
| cb2a871538 | |||
| ef877b5fcc | |||
| 2738f3e437 | |||
| 79f765a930 | |||
| 696f8b308a | |||
| 4a2473e7e9 | |||
| a0caa7c5a2 | |||
| 388a0c605c | |||
| 55e4d08fd8 | |||
| 7c00a4ed13 | |||
| 793cc1acc0 | |||
| 33d910ccea | |||
| 8a44337c30 | |||
| 35871d7b4b | |||
| 786d4e03fc | |||
| 5e5b56e445 | |||
| 384ebc5b38 | |||
| 8567af8d28 | |||
| 88e818926a | |||
| 4bcb4fdd82 | |||
| 0b3a6aa9de | |||
| 12ea1cde80 | |||
| df73b8288c | |||
| a50291cd90 | |||
| be8ea685de | |||
| d050beb627 | |||
| 1380cadf15 | |||
| 72eeb6561c | |||
| eeb460e55c | |||
| e8d0b01991 | |||
| 94020a664b | |||
| 8c5194d5e2 | |||
| 67e81c9018 | |||
| 25ebdb4cb3 | |||
| 1657fd1f0f | |||
| 63e702a3bf | |||
| e9f21d0209 | |||
| a044649eef | |||
| 48317c2307 | |||
| d598c91de3 | |||
| 5003c2af21 | |||
| 6963a92332 | |||
| e2863e1620 | |||
| 869756d87d | |||
| 203890844b | |||
| ba76b87952 | |||
| fbab18b081 | |||
| f7178ecd6f | |||
| 59cbac7147 | |||
| fbc70a60f7 | |||
| b45365d385 | |||
| 9990780e6b | |||
| 65df2f30c1 | |||
| e7aa706f73 | |||
| 3619247123 | |||
| 047dac7b7d | |||
| 7646849405 | |||
| 10ae0ed9c1 | |||
| e060e729df | |||
| d81bbba4c2 | |||
| d52936fd42 | |||
| 21a5abf25d | |||
| c3980a2746 | |||
| 2cadbd1229 | |||
| 0f57291aa7 | |||
| 0e9169e1ec | |||
| 8680b5faa0 | |||
| 03149cfa42 | |||
| 4eb4732de7 | |||
| 3a5b22c397 | |||
| c3a5d4b4e4 | |||
| b97ff3a784 | |||
| 806af7d796 | |||
| 3c25737f38 | |||
| bf98851dc1 | |||
| 202b72fd8e | |||
| 6f60117319 | |||
| 7070ec30e4 | |||
| bf77019d1c | |||
| b776e5a8e0 | |||
| ca208305a0 | |||
| 972c5c327c | |||
| 80edfd1531 | |||
| 58cd455642 | |||
| caf59d25e8 | |||
| 7df32a34a0 | |||
| 48097f4a7d | |||
| ca3f349d84 | |||
| 0815bb6244 | |||
| 835cb54876 | |||
| 08c2765633 | |||
| f9202c3f6b | |||
| eb186e22d6 | |||
| cbc323875b | |||
| 736b583ff4 | |||
| 757ec53877 | |||
| 7814927dd1 | |||
| 5f8ff373ab | |||
| f286cb0a90 | |||
| 59bd1a1f1f | |||
| e52f6ca6c0 | |||
| e6663c0304 | |||
| ced92300e0 | |||
| f9d061b648 | |||
| e6661a33f9 | |||
| d2afe281bf | |||
| c237aa00b8 | |||
| 23efa4c9c3 | |||
| 8b028734c2 | |||
| 51feeddc5c | |||
| af3e42f59d | |||
| 34d58eb7d5 | |||
| ca0a04fba5 | |||
| 84b3f9924a | |||
| 50b264c6f8 | |||
| 88e8a9fe2b | |||
| d65aef72e8 | |||
| 8273c0c2cd | |||
| c7624b1ed6 | |||
| 54536a1c8e | |||
| 7daa06a207 | |||
| da647735b6 | |||
| 7d0ecfde93 | |||
| 2ef716e94c | |||
| de68850d28 | |||
| 59cefda283 | |||
| babcd5887f | |||
| c9a5e5a98c | |||
| 51dc5811c2 | |||
| 05e36f7b21 | |||
| 0738cae4a0 | |||
| da3c42bbbc | |||
| e728e3bc75 | |||
| 315783441d | |||
| 62f1421cfb | |||
| 52f12b704b | |||
| ee5051f788 | |||
| b30b1227e3 | |||
| dd2f8db392 | |||
| 5a76d81529 | |||
| b9851c78ad | |||
| 777cf2ae7a | |||
| 3729b3d793 | |||
| 39fc78205d | |||
| 46a67e8d4f | |||
| 69017805c0 | |||
| bcec611973 | |||
| fbb11a394a | |||
| 5cb916977e | |||
| 7d4c9bc0cf | |||
| ea2beb46e7 | |||
| 216ee8117a | |||
| 9c1210b34e | |||
| 921e034266 | |||
| 6f358b16c0 | |||
| 264ab2c471 | |||
| a9d6f0023a | |||
| 31af1d5dc7 | |||
| 588bbb45b5 | |||
| 278c999f1f | |||
| 825ae15109 | |||
| 30cacc7cca | |||
| 7ed037bad7 | |||
| 77b00b2a12 | |||
| 773bc386f7 | |||
| 90dacaca09 | |||
| 3689ad16b5 | |||
| ab9954350e | |||
| 533f11d9eb | |||
| 551033a7c3 | |||
| e003930e18 | |||
| 2efef72ec2 | |||
| eb40fc698d | |||
| 751db2be78 | |||
| 6ea353447e | |||
| 5a3839320d | |||
| 555f4b51e1 | |||
| be82248e01 | |||
| 7bed7801cc | |||
| eb409de916 | |||
| 8ce39a6d75 | |||
| 72dc65cb6a | |||
| c94c667fb1 | |||
| 2a0175f629 | |||
| cd68173440 | |||
| c920e3a031 | |||
| 83afc4ac7e | |||
| 19f62649ea | |||
| e4186cde9e | |||
| 61ed6aee62 | |||
| 051b6e9458 | |||
| 44a24685ac | |||
| 1cde980c08 | |||
| 05ef4f9b6f | |||
| 53f00f248e | |||
| a9cd282500 | |||
| f142abfb76 | |||
| c98c6c38dc | |||
| 26aa14dbfc | |||
| 04cd931cc8 | |||
| b8a6330c3f | |||
| 949c3c660b | |||
| 9297a90d7f | |||
| b19f17fdfa | |||
| bbdc6c0f5c | |||
| 528c65c194 | |||
| a932df7320 | |||
| 2d9b05a3ab | |||
| 7cf1d25dc6 | |||
| 19c955d505 | |||
| bd243da47e | |||
| bf24c3b279 | |||
| e9c72acff2 | |||
| 09fd22c2f8 | |||
| 45721ed62e | |||
| 5182045cda | |||
| 51ab8fecfc | |||
| b36907133c | |||
| b6d899344e | |||
| 095f9fc1ab | |||
| 0ab9eb7b05 | |||
| d0c582e137 | |||
| d3d4464983 | |||
| 1d0934074c | |||
| 9382fb3be8 | |||
| 223af95da7 | |||
| 8b68b55de4 | |||
| 99592d1a12 | |||
| b69f5aaac3 | |||
| 0c1fefe6e7 | |||
| a049ff9371 | |||
| 0c51e2637f | |||
| 7492b75e1c | |||
| 9a73f7a7b8 | |||
| 7813d1acd6 | |||
| 24dfd9b93b | |||
| 7d65b3e1b4 | |||
| 2c20464983 | |||
| e97d9013d5 | |||
| cf43427be5 | |||
| 7ae41be975 | |||
| 1e18b35f22 | |||
| bb78285661 | |||
| b47ba8cfd3 | |||
| 1dccdf0594 | |||
| 5da48d8ffc | |||
| 4f4b7c5625 | |||
| 9dcf236387 | |||
| 97bc5804fd | |||
| beab966851 | |||
| 00a7e8f9a2 | |||
| 59dc526683 | |||
| aef7f9dc03 | |||
| abc6a08397 | |||
| eb4da8a6f6 | |||
| 494fd734bc | |||
| 9174f60b1d | |||
| c2905a4522 | |||
| adf03e2ee3 | |||
| fe5f085ec1 | |||
| 748ac50d83 | |||
| d2069c1729 | |||
| 38cf50830a | |||
| bf96a43efb | |||
| 22b4555926 | |||
| 7fb3632e7b | |||
| e5f0fe288a | |||
| 58aaaffe2b | |||
| c62aefbfde | |||
| 16dfde22fc | |||
| bbbc8a760c | |||
| 9b45bb2e24 | |||
| e69c21c9f5 | |||
| 72933859cf | |||
| 0a5cee6a73 | |||
| a80c979482 | |||
| 4a4b695b94 | |||
| a8c246ce0d | |||
| c6e979ddda | |||
| 1a78346cf1 | |||
| e6d0d53e47 | |||
| 68680bb2c6 | |||
| 7de415a6a4 | |||
| f46fa440f0 | |||
| 56177d7e92 | |||
| 41584317e4 | |||
| 3264e2c31d | |||
| fadc7a2051 | |||
| cebc55687b | |||
| 5940279f7b | |||
| 35f6333b67 | |||
| 262b044969 | |||
| a571df784c | |||
| 078aa8327b | |||
| 10945edbe6 | |||
| 5d9b69cbfe | |||
| 89117dd53f | |||
| 4af9654696 | |||
| 1e0ce7d47a | |||
| 27cd54b42c | |||
| ec64a182f9 | |||
| dd30df7057 | |||
| 4c6e33aa82 | |||
| 8cb934849a | |||
| c627aee096 | |||
| 9d552d45c2 | |||
| 1e1b8dbe53 | |||
| 2d9d8dc976 | |||
| ba674fc796 | |||
| 58550515ad | |||
| 27bae60b68 | |||
| 646dd63faf | |||
| 8d2b389e27 | |||
| fa174a392a | |||
| 042e35e8d3 | |||
| b76513ce33 | |||
| 368bfcf476 | |||
| fee3c598d0 | |||
| 69264cc164 | |||
| e81929bc55 | |||
| 0843b19b6c | |||
| 813b472c6c | |||
| 2f079711ec | |||
| 97e9d2dc42 | |||
| 7344f20803 | |||
| bfe04464b4 | |||
| 2225f5661b | |||
| 9e77c93a54 | |||
| b65fa98cee | |||
| b57821b273 | |||
| db7f9598b0 | |||
| e547da10de | |||
| 5a0c15f377 | |||
| 0f5b69b4f2 | |||
| 86d4497aaf | |||
| 4b80961c65 | |||
| 785cc9df85 | |||
| 1bfecb0313 | |||
| b88a561eb5 | |||
| 4bf026605e | |||
| db6933856f | |||
| 01107ffa59 | |||
| 74f678a26f | |||
| bf93648198 | |||
| 51dccaeb4c | |||
| ed4252af53 | |||
| 2fde902c82 | |||
| 4ea75af2d9 | |||
| 1bba0cf7fc | |||
| b1fd0c73c7 | |||
| 29eb6d938b | |||
| 19620470bd | |||
| 7a69b01b9b | |||
| bc531cf846 | |||
| 14908e52e2 | |||
| a86bf8d6d7 | |||
| 3662ca3ca6 | |||
| 5fae03f7be | |||
| f7f0f3bebb | |||
| ccbab6ee6f | |||
| ef12dafea8 | |||
| 08a65b50aa | |||
| d28d6cbdb4 | |||
| 41a6709ece | |||
| 756f5d5408 | |||
| 422b9c8fbc | |||
| 40ec2c4921 | |||
| 5cf7ad85a0 | |||
| f02c9f0435 | |||
| c59b5e7781 | |||
| e525c6694b | |||
| 897c14e2e5 | |||
| 6c00bf423b | |||
| 094984eb2f | |||
| 081411fba4 | |||
| 178c0972ff | |||
| 910e317a45 | |||
| 1ef5adbb0e | |||
| f5db856a75 | |||
| b2ad40253f | |||
| 1e0d622d80 | |||
| 1d941249ed | |||
| 5bfd3fdc33 | |||
| 753b807a45 | |||
| 04409602a6 | |||
| b2796b850b | |||
| 8c633aab19 | |||
| f14b4133c3 | |||
| 8f2e4aa4fd | |||
| 3c2e043e82 | |||
| 609ed2d7b3 | |||
| 183166b3fb | |||
| 12dc91719e | |||
| cd071c3bdc | |||
| 0618ecce47 | |||
| 13438ad8de | |||
| 60c2b1d6ca | |||
| eaf4ffcbaf | |||
| 79e59bdda0 | |||
| b409ab5ead | |||
| 790c7036a1 | |||
| 73902f507a | |||
| 1a80f6bc4e | |||
| 5feb7fe654 | |||
| 980960bc6a | |||
| 52c614733f | |||
| e913d12c71 | |||
| 5dc24232ca | |||
| ef2115d667 | |||
| 74476eae43 | |||
| 84335d58b3 | |||
| f175fc6e9b | |||
| 9edd7e6bc8 | |||
| 7478c56ca5 | |||
| 1f36f4802a | |||
| 4d920d7866 | |||
| 19fb3f9b54 | |||
| fafc102573 | |||
| 3868f9c698 | |||
| 0204f666b3 | |||
| f32a13bfb0 | |||
| 4809c9f07a | |||
| 19af144b33 | |||
| 4db7962048 | |||
| 7e6f2cba53 | |||
| c6199e5ca6 | |||
| f7ff1dc610 | |||
| 8ad6eb9896 | |||
| 74709461e3 | |||
| 9e6cc5ebbd | |||
| a2d8f92e05 | |||
| a7d90e2a25 | |||
| 83b37e2f78 | |||
| 488629096b | |||
| a3f167f6df | |||
| 1e8814b43e | |||
| 2e430519e3 | |||
| 2164b02c66 |
+115
-78
@@ -25,26 +25,25 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
||||||
python-version: ["3.9", "3.10", "3.11"]
|
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Cache_dependencies
|
- name: Cache_dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ~/dependencies/
|
path: ~/dependencies/
|
||||||
key: ${{ runner.os }}-dependencies
|
key: ${{ runner.os }}-dependencies
|
||||||
|
|
||||||
- name: pip cache (linux)
|
- name: pip cache (linux)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
if: runner.os == 'Linux'
|
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
@@ -55,7 +54,6 @@ jobs:
|
|||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
@@ -90,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Backtesting (multi)
|
- name: Backtesting (multi)
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade new-strategy -s AwesomeStrategy
|
freqtrade new-strategy -s AwesomeStrategy
|
||||||
freqtrade new-strategy -s AwesomeStrategyMin --template minimal
|
freqtrade new-strategy -s AwesomeStrategyMin --template minimal
|
||||||
@@ -98,7 +96,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Hyperopt
|
- name: Hyperopt
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
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
|
||||||
|
|
||||||
@@ -108,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --format=github .
|
ruff check --output-format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -122,35 +120,34 @@ jobs:
|
|||||||
details: Freqtrade CI failed on ${{ matrix.os }}
|
details: Freqtrade CI failed on ${{ matrix.os }}
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
build_macos:
|
build-macos:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest ]
|
os: [ "macos-latest", "macos-13" ]
|
||||||
python-version: ["3.9", "3.10", "3.11"]
|
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Cache_dependencies
|
- name: Cache_dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ~/dependencies/
|
path: ~/dependencies/
|
||||||
key: ${{ runner.os }}-dependencies
|
key: ${{ matrix.os }}-dependencies
|
||||||
|
|
||||||
- name: pip cache (macOS)
|
- name: pip cache (macOS)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
if: runner.os == 'macOS'
|
|
||||||
with:
|
with:
|
||||||
path: ~/Library/Caches/pip
|
path: ~/Library/Caches/pip
|
||||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
|
|
||||||
- name: TA binary *nix
|
- name: TA binary *nix
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
@@ -158,7 +155,6 @@ jobs:
|
|||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
- name: Installation - macOS
|
- name: Installation - macOS
|
||||||
if: runner.os == 'macOS'
|
|
||||||
run: |
|
run: |
|
||||||
# brew update
|
# brew update
|
||||||
# TODO: Should be the brew upgrade
|
# TODO: Should be the brew upgrade
|
||||||
@@ -166,16 +162,21 @@ jobs:
|
|||||||
# https://github.com/actions/runner-images/issues/6817
|
# https://github.com/actions/runner-images/issues/6817
|
||||||
rm /usr/local/bin/2to3 || true
|
rm /usr/local/bin/2to3 || true
|
||||||
rm /usr/local/bin/2to3-3.11 || true
|
rm /usr/local/bin/2to3-3.11 || true
|
||||||
|
rm /usr/local/bin/2to3-3.12 || true
|
||||||
rm /usr/local/bin/idle3 || true
|
rm /usr/local/bin/idle3 || true
|
||||||
rm /usr/local/bin/idle3.11 || true
|
rm /usr/local/bin/idle3.11 || true
|
||||||
|
rm /usr/local/bin/idle3.12 || true
|
||||||
rm /usr/local/bin/pydoc3 || true
|
rm /usr/local/bin/pydoc3 || true
|
||||||
rm /usr/local/bin/pydoc3.11 || true
|
rm /usr/local/bin/pydoc3.11 || true
|
||||||
|
rm /usr/local/bin/pydoc3.12 || true
|
||||||
rm /usr/local/bin/python3 || true
|
rm /usr/local/bin/python3 || true
|
||||||
rm /usr/local/bin/python3.11 || true
|
rm /usr/local/bin/python3.11 || true
|
||||||
|
rm /usr/local/bin/python3.12 || true
|
||||||
rm /usr/local/bin/python3-config || true
|
rm /usr/local/bin/python3-config || true
|
||||||
rm /usr/local/bin/python3.11-config || true
|
rm /usr/local/bin/python3.11-config || true
|
||||||
|
rm /usr/local/bin/python3.12-config || true
|
||||||
|
|
||||||
brew install hdf5 c-blosc
|
brew install hdf5 c-blosc libomp
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||||
@@ -200,14 +201,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Backtesting
|
- name: Backtesting
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade new-strategy -s AwesomeStrategyAdv --template advanced
|
freqtrade new-strategy -s AwesomeStrategyAdv --template advanced
|
||||||
freqtrade backtesting --datadir tests/testdata --strategy AwesomeStrategyAdv
|
freqtrade backtesting --datadir tests/testdata --strategy AwesomeStrategyAdv
|
||||||
|
|
||||||
- name: Hyperopt
|
- name: Hyperopt
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
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
|
||||||
|
|
||||||
@@ -217,7 +218,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --format=github .
|
ruff check --output-format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -231,24 +232,24 @@ jobs:
|
|||||||
details: Test Succeeded!
|
details: Test Succeeded!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
build_windows:
|
build-windows:
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest ]
|
os: [ windows-latest ]
|
||||||
python-version: ["3.9", "3.10", "3.11"]
|
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Pip cache (Windows)
|
- name: Pip cache (Windows)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~\AppData\Local\pip\Cache
|
path: ~\AppData\Local\pip\Cache
|
||||||
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
@@ -275,19 +276,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Backtesting
|
- name: Backtesting
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||||
|
|
||||||
- name: Hyperopt
|
- name: Hyperopt
|
||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp tests/testdata/config.tests.json config.json
|
||||||
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: Run Ruff
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
ruff check --format=github .
|
ruff check --output-format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@@ -301,13 +302,13 @@ jobs:
|
|||||||
details: Test Failed
|
details: Test Failed
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
mypy_version_check:
|
mypy-version-check:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
@@ -321,12 +322,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
- uses: pre-commit/action@v3.0.0
|
- uses: pre-commit/action@v3.0.0
|
||||||
|
|
||||||
docs_check:
|
docs-check:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -336,7 +337,7 @@ jobs:
|
|||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
@@ -362,20 +363,19 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Cache_dependencies
|
- name: Cache_dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: ~/dependencies/
|
path: ~/dependencies/
|
||||||
key: ${{ runner.os }}-dependencies
|
key: ${{ runner.os }}-dependencies
|
||||||
|
|
||||||
- name: pip cache (linux)
|
- name: pip cache (linux)
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
if: runner.os == 'Linux'
|
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||||
@@ -386,7 +386,6 @@ jobs:
|
|||||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||||
|
|
||||||
- name: Installation - *nix
|
- name: Installation - *nix
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
@@ -399,17 +398,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI_WEB_PROXY: http://152.67.78.211:13128
|
CI_WEB_PROXY: http://152.67.78.211:13128
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
pytest --random-order --longrun --durations 20 -n auto --dist loadscope
|
||||||
|
|
||||||
|
|
||||||
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||||
notify-complete:
|
notify-complete:
|
||||||
needs: [
|
needs: [
|
||||||
build_linux,
|
build_linux,
|
||||||
build_macos,
|
build-macos,
|
||||||
build_windows,
|
build-windows,
|
||||||
docs_check,
|
docs-check,
|
||||||
mypy_version_check,
|
mypy-version-check,
|
||||||
pre-commit,
|
pre-commit,
|
||||||
build_linux_online
|
build_linux_online
|
||||||
]
|
]
|
||||||
@@ -436,8 +435,63 @@ jobs:
|
|||||||
details: Test Completed!
|
details: Test Completed!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
deploy:
|
build:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check, pre-commit ]
|
name: "Build"
|
||||||
|
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Build distribution
|
||||||
|
run: |
|
||||||
|
pip install -U build
|
||||||
|
python -m build --sdist --wheel
|
||||||
|
|
||||||
|
- name: Upload artifacts 📦
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: freqtrade-build
|
||||||
|
path: |
|
||||||
|
dist
|
||||||
|
retention-days: 10
|
||||||
|
|
||||||
|
deploy-pypi:
|
||||||
|
name: "Deploy to PyPI"
|
||||||
|
needs: [ build ]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: (github.event_name == 'release')
|
||||||
|
environment:
|
||||||
|
name: release
|
||||||
|
url: https://pypi.org/p/freqtrade
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download artifact 📦
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: freqtrade-build
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Publish to PyPI (Test)
|
||||||
|
uses: pypa/gh-action-pypi-publish@v1.8.11
|
||||||
|
with:
|
||||||
|
repository-url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@v1.8.11
|
||||||
|
|
||||||
|
|
||||||
|
deploy-docker:
|
||||||
|
needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
@@ -446,34 +500,15 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Extract branch name
|
- name: Extract branch name
|
||||||
shell: bash
|
id: extract-branch
|
||||||
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})"
|
|
||||||
id: extract_branch
|
|
||||||
|
|
||||||
- name: Build distribution
|
|
||||||
run: |
|
run: |
|
||||||
pip install -U setuptools wheel
|
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||||
python setup.py sdist bdist_wheel
|
echo "branch=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
|
||||||
if: (github.event_name == 'release')
|
|
||||||
with:
|
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.pypi_test_password }}
|
|
||||||
repository_url: https://test.pypi.org/legacy/
|
|
||||||
|
|
||||||
- name: Publish to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
|
||||||
if: (github.event_name == 'release')
|
|
||||||
with:
|
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.pypi_password }}
|
|
||||||
|
|
||||||
- name: Dockerhub login
|
- name: Dockerhub login
|
||||||
env:
|
env:
|
||||||
@@ -502,14 +537,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and test and push docker images
|
- name: Build and test and push docker images
|
||||||
env:
|
env:
|
||||||
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:
|
||||||
|
name: "Deploy Docker"
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
needs: [ deploy ]
|
needs: [ deploy-docker ]
|
||||||
# Only run on 64bit machines
|
# Only run on 64bit machines
|
||||||
runs-on: [self-hosted, linux, ARM64]
|
runs-on: [self-hosted, linux, ARM64]
|
||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
@@ -518,9 +554,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Extract branch name
|
- name: Extract branch name
|
||||||
shell: bash
|
id: extract-branch
|
||||||
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})"
|
run: |
|
||||||
id: extract_branch
|
echo "GITHUB_REF='${GITHUB_REF}'"
|
||||||
|
echo "branch=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Dockerhub login
|
- name: Dockerhub login
|
||||||
env:
|
env:
|
||||||
@@ -531,7 +568,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and test and push docker images
|
- name: Build and test and push docker images
|
||||||
env:
|
env:
|
||||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
BRANCH_NAME: ${{ steps.extract-branch.outputs.branch }}
|
||||||
GHCR_USERNAME: ${{ github.actor }}
|
GHCR_USERNAME: ${{ github.actor }}
|
||||||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Docker Hub Description
|
- name: Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@v3
|
uses: peter-evans/dockerhub-description@v4
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Pre-commit auto-update
|
||||||
|
|
||||||
|
on:
|
||||||
|
# every day at midnight
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * 2"
|
||||||
|
# on demand
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-update:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
|
||||||
|
- name: Install pre-commit
|
||||||
|
run: pip install pre-commit
|
||||||
|
|
||||||
|
- name: Run auto-update
|
||||||
|
run: pre-commit autoupdate
|
||||||
|
|
||||||
|
- name: Run pre-commit
|
||||||
|
run: pre-commit run --all-files
|
||||||
|
|
||||||
|
- uses: peter-evans/create-pull-request@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||||
|
add-paths: .pre-commit-config.yaml
|
||||||
|
labels: |
|
||||||
|
Tech maintenance
|
||||||
|
branch: update/pre-commit-hooks
|
||||||
|
title: Update pre-commit hooks
|
||||||
|
commit-message: "chore: update pre-commit hooks"
|
||||||
|
committer: Freqtrade Bot <noreply@github.com>
|
||||||
|
body: Update versions of pre-commit hooks to latest version.
|
||||||
|
delete-branch: true
|
||||||
@@ -111,7 +111,6 @@ target/
|
|||||||
#exceptions
|
#exceptions
|
||||||
!*.gitkeep
|
!*.gitkeep
|
||||||
!config_examples/config_binance.example.json
|
!config_examples/config_binance.example.json
|
||||||
!config_examples/config_bittrex.example.json
|
|
||||||
!config_examples/config_full.example.json
|
!config_examples/config_full.example.json
|
||||||
!config_examples/config_kraken.example.json
|
!config_examples/config_kraken.example.json
|
||||||
!config_examples/config_freqai.example.json
|
!config_examples/config_freqai.example.json
|
||||||
|
|||||||
+11
-10
@@ -2,27 +2,28 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/pycqa/flake8
|
||||||
rev: "6.0.0"
|
rev: "7.0.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
|
additional_dependencies: [Flake8-pyproject]
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.5.1"
|
rev: "v1.8.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.6
|
- types-cachetools==5.3.0.7
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.31.0.4
|
- types-requests==2.31.0.20240125
|
||||||
- types-tabulate==0.9.0.3
|
- types-tabulate==0.9.0.20240106
|
||||||
- types-python-dateutil==2.8.19.14
|
- types-python-dateutil==2.8.19.20240106
|
||||||
- SQLAlchemy==2.0.21
|
- SQLAlchemy==2.0.25
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: "5.12.0"
|
rev: "5.13.2"
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
@@ -30,12 +31,12 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.0.270'
|
rev: 'v0.1.14'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- 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.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: |
|
exclude: |
|
||||||
|
|||||||
+1
-1
@@ -125,7 +125,7 @@ Exceptions:
|
|||||||
|
|
||||||
Contributors may be given commit privileges. Preference will be given to those with:
|
Contributors may be given commit privileges. Preference will be given to those with:
|
||||||
|
|
||||||
1. Past contributions to Freqtrade and other related open-source projects. Contributions to Freqtrade include both code (both accepted and pending) and friendly participation in the issue tracker and Pull request reviews. Quantity and quality are considered.
|
1. Past contributions to Freqtrade and other related open-source projects. Contributions to Freqtrade include both code (both accepted and pending) and friendly participation in the issue tracker and Pull request reviews. Both quantity and quality are considered.
|
||||||
1. A coding style that the other core committers find simple, minimal, and clean.
|
1. A coding style that the other core committers find simple, minimal, and clean.
|
||||||
1. Access to resources for cross-platform development and testing.
|
1. Access to resources for cross-platform development and testing.
|
||||||
1. Time to devote to the project regularly.
|
1. Time to devote to the project regularly.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11.5-slim-bullseye as base
|
FROM python:3.11.7-slim-bookworm as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ recursive-include freqtrade/templates/ *.j2 *.ipynb
|
|||||||
include freqtrade/exchange/binance_leverage_tiers.json
|
include freqtrade/exchange/binance_leverage_tiers.json
|
||||||
include freqtrade/rpc/api_server/ui/fallback_file.html
|
include freqtrade/rpc/api_server/ui/fallback_file.html
|
||||||
include freqtrade/rpc/api_server/ui/favicon.ico
|
include freqtrade/rpc/api_server/ui/favicon.ico
|
||||||
|
|
||||||
|
prune tests
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bitmart](https://bitmart.com/)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [X] [Huobi](http://huobi.com/)
|
- [X] [HTX](https://www.htx.com/) (Former Huobi)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -54,7 +54,7 @@ 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
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
docker run --rm -v $(pwd)/tests/testdata/config.tests.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "failed running backtest"
|
echo "failed running backtest"
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
|||||||
docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
docker run --rm -v $(pwd)/tests/testdata/config.tests.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "failed running backtest"
|
echo "failed running backtest"
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"max_open_trades": 3,
|
"max_open_trades": 3,
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "USDT",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"tradable_balance_ratio": 0.99,
|
"tradable_balance_ratio": 0.99,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
@@ -36,21 +36,21 @@
|
|||||||
"ccxt_async_config": {
|
"ccxt_async_config": {
|
||||||
},
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ALGO/BTC",
|
"ALGO/USDT",
|
||||||
"ATOM/BTC",
|
"ATOM/USDT",
|
||||||
"BAT/BTC",
|
"BAT/USDT",
|
||||||
"BCH/BTC",
|
"BCH/USDT",
|
||||||
"BRD/BTC",
|
"BRD/USDT",
|
||||||
"EOS/BTC",
|
"EOS/USDT",
|
||||||
"ETH/BTC",
|
"ETH/USDT",
|
||||||
"IOTA/BTC",
|
"IOTA/USDT",
|
||||||
"LINK/BTC",
|
"LINK/USDT",
|
||||||
"LTC/BTC",
|
"LTC/USDT",
|
||||||
"NEO/BTC",
|
"NEO/USDT",
|
||||||
"NXS/BTC",
|
"NXS/USDT",
|
||||||
"XMR/BTC",
|
"XMR/USDT",
|
||||||
"XRP/BTC",
|
"XRP/USDT",
|
||||||
"XTZ/BTC"
|
"XTZ/USDT"
|
||||||
],
|
],
|
||||||
"pair_blacklist": [
|
"pair_blacklist": [
|
||||||
"BNB/.*"
|
"BNB/.*"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
"train_period_days": 15,
|
"train_period_days": 15,
|
||||||
"backtest_period_days": 7,
|
"backtest_period_days": 7,
|
||||||
"live_retrain_hours": 0,
|
"live_retrain_hours": 0,
|
||||||
"identifier": "uniqe-id",
|
"identifier": "unique-id",
|
||||||
"feature_parameters": {
|
"feature_parameters": {
|
||||||
"include_timeframes": [
|
"include_timeframes": [
|
||||||
"3m",
|
"3m",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.9.16-slim-bullseye as base
|
FROM python:3.11.7-slim-bookworm as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
@@ -11,12 +11,13 @@ ENV FT_APP_ENV="docker"
|
|||||||
# Prepare environment
|
# Prepare environment
|
||||||
RUN mkdir /freqtrade \
|
RUN mkdir /freqtrade \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-dev libutf8proc-dev libsnappy-dev \
|
&& apt-get -y install sudo libatlas3-base libopenblas-dev curl sqlite3 libhdf5-dev libutf8proc-dev libsnappy-dev \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
&& useradd -u 1000 -G sudo -U -m ftuser \
|
||||||
&& chown ftuser:ftuser /freqtrade \
|
&& chown ftuser:ftuser /freqtrade \
|
||||||
# Allow sudoers
|
# Allow sudoers
|
||||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
|
||||||
|
&& pip install --upgrade pip
|
||||||
|
|
||||||
WORKDIR /freqtrade
|
WORKDIR /freqtrade
|
||||||
|
|
||||||
@@ -25,20 +26,16 @@ FROM base as python-deps
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& pip install --upgrade pip \
|
|
||||||
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
||||||
|
|
||||||
# Install TA-lib
|
# Install TA-lib
|
||||||
COPY build_helpers/* /tmp/
|
COPY build_helpers/* /tmp/
|
||||||
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
COPY --chown=ftuser:ftuser requirements.txt /freqtrade/
|
||||||
USER ftuser
|
USER ftuser
|
||||||
RUN pip install --user --no-cache-dir numpy==1.25.2 \
|
RUN pip install --user --no-cache-dir numpy \
|
||||||
&& pip install --user /tmp/pyarrow-*.whl \
|
&& pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib==0.4.28 \
|
||||||
&& pip install --user --no-build-isolation TA-Lib==0.4.28 \
|
|
||||||
&& pip install --user --no-cache-dir -r requirements.txt
|
&& pip install --user --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy dependencies to runtime-image
|
# Copy dependencies to runtime-image
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM freqtradeorg/freqtrade:develop_plot
|
FROM freqtradeorg/freqtrade:develop_plot
|
||||||
|
|
||||||
|
|
||||||
# Pin jupyter-client to avoid tornado version conflict
|
# Pin prompt-toolkit to avoid questionary version conflict
|
||||||
RUN pip install jupyterlab jupyter-client==7.3.4 --user --no-cache-dir
|
RUN pip install jupyterlab "prompt-toolkit<=3.0.36" jupyter-client --user --no-cache-dir
|
||||||
|
|
||||||
# Empty the ENTRYPOINT to allow all commands
|
# Empty the ENTRYPOINT to allow all commands
|
||||||
ENTRYPOINT []
|
ENTRYPOINT []
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ services:
|
|||||||
context: ..
|
context: ..
|
||||||
dockerfile: docker/Dockerfile.jupyter
|
dockerfile: docker/Dockerfile.jupyter
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: freqtrade
|
# container_name: freqtrade
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8888:8888"
|
- "127.0.0.1:8888:8888"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+6
-6
@@ -31,9 +31,9 @@ optional arguments:
|
|||||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--data-format-ohlcv {json,jsongz,hdf5}
|
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `json`).
|
(default: `feather`).
|
||||||
--max-open-trades INT
|
--max-open-trades INT
|
||||||
Override the value of the `max_open_trades`
|
Override the value of the `max_open_trades`
|
||||||
configuration setting.
|
configuration setting.
|
||||||
@@ -170,11 +170,11 @@ freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000
|
|||||||
|
|
||||||
Using a different on-disk historical candle (OHLCV) data source
|
Using a different on-disk historical candle (OHLCV) data source
|
||||||
|
|
||||||
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
|
Assume you downloaded the history data from the Binance exchange and kept it in the `user_data/data/binance-20180101` directory.
|
||||||
You can then use this data for backtesting as follows:
|
You can then use this data for backtesting as follows:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101
|
freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/binance-20180101
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -618,13 +618,13 @@ To compare multiple strategies, a list of Strategies can be provided to backtest
|
|||||||
This is limited to 1 timeframe value per run. However, data is only loaded once from disk so if you have multiple
|
This is limited to 1 timeframe value per run. However, data is only loaded once from disk so if you have multiple
|
||||||
strategies you'd like to compare, this will give a nice runtime boost.
|
strategies you'd like to compare, this will give a nice runtime boost.
|
||||||
|
|
||||||
All listed Strategies need to be in the same directory.
|
All listed Strategies need to be in the same directory, unless also `--recursive-strategy-search` is specified, where sub-directories within the strategy directory are also considered.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting --timerange 20180401-20180410 --timeframe 5m --strategy-list Strategy001 Strategy002 --export trades
|
freqtrade backtesting --timerange 20180401-20180410 --timeframe 5m --strategy-list Strategy001 Strategy002 --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
This will save the results to `user_data/backtest_results/backtest-result-<datetime>.json`, including results for both `Strategy001` and `Strategy002`.
|
||||||
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
|
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
|
||||||
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
|
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
|
||||||
|
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ For example, if you have 10 ETH available in your wallet on the exchange and `tr
|
|||||||
To fully utilize compounding profits when using multiple bots on the same exchange account, you'll want to limit each bot to a certain starting balance.
|
To fully utilize compounding profits when using multiple bots on the same exchange account, you'll want to limit each bot to a certain starting balance.
|
||||||
This can be accomplished by setting `available_capital` to the desired starting balance.
|
This can be accomplished by setting `available_capital` to the desired starting balance.
|
||||||
|
|
||||||
Assuming your account has 10.000 USDT and you want to run 2 different strategies on this exchange.
|
Assuming your account has 10000 USDT and you want to run 2 different strategies on this exchange.
|
||||||
You'd set `available_capital=5000` - granting each bot an initial capital of 5000 USDT.
|
You'd set `available_capital=5000` - granting each bot an initial capital of 5000 USDT.
|
||||||
The bot will then split this starting balance equally into `max_open_trades` buckets.
|
The bot will then split this starting balance equally into `max_open_trades` buckets.
|
||||||
Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot.
|
Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot.
|
||||||
@@ -572,9 +572,11 @@ In addition to fiat currencies, a range of crypto currencies is supported.
|
|||||||
The valid values are:
|
The valid values are:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
"BTC", "ETH", "XRP", "LTC", "BCH", "BNB"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Removing `fiat_display_currency` completely from the configuration will skip initializing coingecko, and will not show any FIAT currency conversion. This has no importance for the correct functioning of the bot.
|
||||||
|
|
||||||
## Using Dry-run mode
|
## Using Dry-run mode
|
||||||
|
|
||||||
We recommend starting the bot in the Dry-run mode to see how your bot will
|
We recommend starting the bot in the Dry-run mode to see how your bot will
|
||||||
@@ -594,7 +596,7 @@ creating trades on the exchange.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "binance",
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
...
|
...
|
||||||
@@ -644,7 +646,7 @@ API Keys are usually only required for live trading (trading for real money, bot
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "binance",
|
||||||
"key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b",
|
"key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b",
|
||||||
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
||||||
//"password": "", // Optional, not needed by all exchanges)
|
//"password": "", // Optional, not needed by all exchanges)
|
||||||
|
|||||||
+5
-4
@@ -318,6 +318,7 @@ Additional tests / steps to complete:
|
|||||||
* Check if balance shows correctly (*)
|
* Check if balance shows correctly (*)
|
||||||
* Create market order (*)
|
* Create market order (*)
|
||||||
* Create limit order (*)
|
* Create limit order (*)
|
||||||
|
* Cancel order (*)
|
||||||
* Complete trade (enter + exit) (*)
|
* Complete trade (enter + exit) (*)
|
||||||
* Compare result calculation between exchange and bot
|
* Compare result calculation between exchange and bot
|
||||||
* Ensure fees are applied correctly (check the database against the exchange)
|
* Ensure fees are applied correctly (check the database against the exchange)
|
||||||
@@ -418,6 +419,9 @@ This part of the documentation is aimed at maintainers, and shows how to create
|
|||||||
|
|
||||||
### Create release branch
|
### Create release branch
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Make sure that the `stable` branch is up-to-date!
|
||||||
|
|
||||||
First, pick a commit that's about one week old (to not include latest additions to releases).
|
First, pick a commit that's about one week old (to not include latest additions to releases).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
@@ -430,14 +434,11 @@ Determine if crucial bugfixes have been made between this commit and the current
|
|||||||
* Merge the release branch (stable) into this branch.
|
* Merge the release branch (stable) into this branch.
|
||||||
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
|
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
|
||||||
* Commit this part.
|
* Commit this part.
|
||||||
* push that branch to the remote and create a PR against the stable branch.
|
* Push that branch to the remote and create a PR against the **stable branch**.
|
||||||
* Update develop version to next version following the pattern `2019.8-dev`.
|
* Update develop version to next version following the pattern `2019.8-dev`.
|
||||||
|
|
||||||
### Create changelog from git commits
|
### Create changelog from git commits
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Make sure that the `stable` branch is up-to-date!
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
# Needs to be done before merging / pulling that branch.
|
# Needs to be done before merging / pulling that branch.
|
||||||
git log --oneline --no-decorate --no-merges stable..new_release
|
git log --oneline --no-decorate --no-merges stable..new_release
|
||||||
|
|||||||
+59
-44
@@ -127,6 +127,8 @@ Freqtrade will not attempt to change these settings.
|
|||||||
|
|
||||||
## Kraken
|
## Kraken
|
||||||
|
|
||||||
|
Kraken supports [time_in_force](configuration.md#understand-order_time_in_force) with settings "GTC" (good till cancelled), "IOC" (immediate-or-cancel) and "PO" (Post only) settings.
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||||
@@ -136,6 +138,43 @@ Freqtrade will not attempt to change these settings.
|
|||||||
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
||||||
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
||||||
|
|
||||||
|
To speed up downloading, you can download the [trades zip files](https://support.kraken.com/hc/en-us/articles/360047543791-Downloadable-historical-market-data-time-and-sales-) kraken provides.
|
||||||
|
These are usually updated once per quarter. Freqtrade expects these files to be placed in `user_data/data/kraken/trades_csv`.
|
||||||
|
|
||||||
|
A structure as follows can make sense if using incremental files, with the "full" history in one directory, and incremental files in different directories.
|
||||||
|
The assumption for this mode is that the data is downloaded and unzipped keeping filenames as they are.
|
||||||
|
Duplicate content will be ignored (based on timestamp) - though the assumption is that there is no gap in the data.
|
||||||
|
|
||||||
|
This means, if your "full" history ends in Q4 2022 - then both incremental updates Q1 2023 and Q2 2023 are available.
|
||||||
|
Not having this will lead to incomplete data, and therefore invalid results while using the data.
|
||||||
|
|
||||||
|
```
|
||||||
|
└── trades_csv
|
||||||
|
├── Kraken_full_history
|
||||||
|
│ ├── BCHEUR.csv
|
||||||
|
│ └── XBTEUR.csv
|
||||||
|
├── Kraken_Trading_History_Q1_2023
|
||||||
|
│ ├── BCHEUR.csv
|
||||||
|
│ └── XBTEUR.csv
|
||||||
|
└── Kraken_Trading_History_Q2_2023
|
||||||
|
├── BCHEUR.csv
|
||||||
|
└── XBTEUR.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
You can convert these files into freqtrade files:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade convert-trade-data --exchange kraken --format-from kraken_csv --format-to feather
|
||||||
|
# Convert trade data to different ohlcv timeframes
|
||||||
|
freqtrade trades-to-ohlcv -p BTC/EUR BCH/EUR --exchange kraken -t 1m 5m 15m 1h
|
||||||
|
```
|
||||||
|
|
||||||
|
The converted data also makes downloading data possible, and will start the download after the latest loaded trade.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade download-data --exchange kraken --dl-trades -p BTC/EUR BCH/EUR
|
||||||
|
```
|
||||||
|
|
||||||
!!! Warning "Downloading data from kraken"
|
!!! Warning "Downloading data from kraken"
|
||||||
Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine.
|
Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine.
|
||||||
It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient.
|
It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient.
|
||||||
@@ -144,48 +183,6 @@ To download data for the Kraken exchange, using `--dl-trades` is mandatory, othe
|
|||||||
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate.
|
Please pay attention that rateLimit configuration entry holds delay in milliseconds between requests, NOT requests\sec rate.
|
||||||
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
|
So, in order to mitigate Kraken API "Rate limit exceeded" exception, this configuration should be increased, NOT decreased.
|
||||||
|
|
||||||
## Bittrex
|
|
||||||
|
|
||||||
### Order types
|
|
||||||
|
|
||||||
Bittrex does not support market orders. If you have a message at the bot startup about this, you should change order type values set in your configuration and/or in the strategy from `"market"` to `"limit"`. See some more details on this [here in the FAQ](faq.md#im-getting-the-exchange-bittrex-does-not-support-market-orders-message-and-cannot-run-my-strategy).
|
|
||||||
|
|
||||||
Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment.
|
|
||||||
Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected.
|
|
||||||
|
|
||||||
### Volume pairlist
|
|
||||||
|
|
||||||
Bittrex does not support the direct usage of VolumePairList. This can however be worked around by using the advanced mode with `lookback_days: 1` (or more), which will emulate 24h volume.
|
|
||||||
|
|
||||||
Read more in the [pairlist documentation](plugins.md#volumepairlist-advanced-mode).
|
|
||||||
|
|
||||||
### Restricted markets
|
|
||||||
|
|
||||||
Bittrex split its exchange into US and International versions.
|
|
||||||
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
|
|
||||||
|
|
||||||
If you have restricted pairs in your whitelist, you'll get a warning message in the log on Freqtrade startup for each restricted pair.
|
|
||||||
|
|
||||||
The warning message will look similar to the following:
|
|
||||||
|
|
||||||
``` output
|
|
||||||
[...] Message: bittrex {"success":false,"message":"RESTRICTED_MARKET","result":null,"explanation":null}"
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're an "International" customer on the Bittrex exchange, then this warning will probably not impact you.
|
|
||||||
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your whitelist.
|
|
||||||
|
|
||||||
You can get a list of restricted markets by using the following snippet:
|
|
||||||
|
|
||||||
``` python
|
|
||||||
import ccxt
|
|
||||||
ct = ccxt.bittrex()
|
|
||||||
lm = ct.load_markets()
|
|
||||||
|
|
||||||
res = [p for p, x in lm.items() if 'US' in x['info']['prohibitedIn']]
|
|
||||||
print(res)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kucoin
|
## Kucoin
|
||||||
|
|
||||||
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
@@ -211,10 +208,10 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force)
|
|||||||
For Kucoin, it is suggested to add `"KCS/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `KCS` on the account or unless you're willing to disable using `KCS` for fees.
|
For Kucoin, it is suggested to add `"KCS/<STAKE>"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `KCS` on the account or unless you're willing to disable using `KCS` for fees.
|
||||||
Kucoin accounts may use `KCS` for fees, and if a trade happens to be on `KCS`, further trades may consume this position and make the initial `KCS` trade unsellable as the expected amount is not there anymore.
|
Kucoin accounts may use `KCS` for fees, and if a trade happens to be on `KCS`, further trades may consume this position and make the initial `KCS` trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
## Huobi
|
## HTX (formerly Huobi)
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
HTX supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
|
|
||||||
## OKX (former OKEX)
|
## OKX (former OKEX)
|
||||||
|
|
||||||
@@ -265,6 +262,24 @@ We do strongly recommend to limit all API keys to the IP you're going to use it
|
|||||||
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||||
|
|
||||||
|
## Bitmart
|
||||||
|
|
||||||
|
Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret.
|
||||||
|
It's therefore required to pass the UID as well.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"exchange": {
|
||||||
|
"name": "bitmart",
|
||||||
|
"uid": "your_bitmart_api_key_memo",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"password": "your_exchange_api_key_password",
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning "Necessary Verification"
|
||||||
|
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
|
||||||
|
|
||||||
## All exchanges
|
## All exchanges
|
||||||
|
|
||||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||||
|
|||||||
+1
-7
@@ -128,15 +128,9 @@ This warning can point to one of the below problems:
|
|||||||
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
||||||
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
||||||
|
|
||||||
### I'm getting the "RESTRICTED_MARKET" message in the log
|
|
||||||
|
|
||||||
Currently known to happen for US Bittrex users.
|
|
||||||
|
|
||||||
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
|
|
||||||
|
|
||||||
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
||||||
|
|
||||||
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex and Gate.io).
|
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Gate.io).
|
||||||
|
|
||||||
To fix this, redefine order types in the strategy to use "limit" instead of "market":
|
To fix this, redefine order types in the strategy to use "limit" instead of "market":
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ Below are the values you can expect to include/use inside a typical strategy dat
|
|||||||
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
||||||
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
||||||
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
||||||
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`. <br> **Datatype:** Depends on the output of the model.
|
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%` (see details below). <br> **Datatype:** Depends on the feature created by the user.
|
||||||
|
| `df['%%*']` | Any dataframe column prepended with `%%` in `feature_engineering_*()` is treated as a training feature, just the same as the above `%` prepend. However, in this case, the features are returned back to the strategy for FreqUI/plot-dataframe plotting and monitoring in Dry/Live/Backtesting <br> **Datatype:** Depends on the feature created by the user. Please note that features created in `feature_engineering_expand()` will have automatic FreqAI naming schemas depending on the expansions that you configured (i.e. `include_timeframes`, `include_corr_pairlist`, `indicators_periods_candles`, `include_shifted_candles`). So if you want to plot `%%-rsi` from `feature_engineering_expand_all()`, the final naming scheme for your plotting config would be: `%%-rsi-period_10_ETH/USDT:USDT_1h` for the `rsi` feature with `period=10`, `timeframe=1h`, and `pair=ETH/USDT:USDT` (the `:USDT` is added if you are using futures pairs). It is useful to simply add `print(dataframe.columns)` in your `populate_indicators()` after `self.freqai.start()` to see the full list of available features that are returned to the strategy for plotting purposes.
|
||||||
|
|
||||||
## Setting the `startup_candle_count`
|
## Setting the `startup_candle_count`
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ 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 `indicator_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.
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| | **Reinforcement Learning Parameters within the `freqai.rl_config` sub dictionary**
|
| | **Reinforcement Learning Parameters within the `freqai.rl_config` sub dictionary**
|
||||||
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
|
| `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model. <br> **Datatype:** Dictionary.
|
||||||
| `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points. <br> **Datatype:** Integer.
|
| `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points. <br> **Datatype:** Integer.
|
||||||
| `cpu_count` | Number of processors to dedicate to the Reinforcement Learning training process. <br> **Datatype:** int.
|
|
||||||
| `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function. <br> **Datatype:** int.
|
| `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function. <br> **Datatype:** int.
|
||||||
| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website) <br> **Datatype:** string.
|
| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website) <br> **Datatype:** string.
|
||||||
| `policy_type` | One of the available policy types from stable_baselines3 <br> **Datatype:** string.
|
| `policy_type` | One of the available policy types from stable_baselines3 <br> **Datatype:** string.
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ FreqAI stores new model files after each successful training. These files become
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
"freqai": {
|
"freqai": {
|
||||||
"purge_old_models": true,
|
"purge_old_models": 4,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This will automatically purge all models older than the two most recently trained ones to save disk space.
|
This will automatically purge all models older than the four most recently trained ones to save disk space. Inputing "0" will never purge any models.
|
||||||
|
|
||||||
## Backtesting
|
## Backtesting
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to-
|
|||||||
This way, you can return to using any model you wish by simply specifying the `identifier`.
|
This way, you can return to using any model you wish by simply specifying the `identifier`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Backtesting calls `set_freqai_targets()` one time for each backtest window (where the number of windows is the full backtest timerange divided by the `backtest_period_days` parameter). Doing this means that the targets simulate dry/live behavior without look ahead bias. However, the definition of the features in `feature_engineering_*()` is performed once on the entire backtest timerange. This means that you should be sure that features do look-ahead into the future.
|
Backtesting calls `set_freqai_targets()` one time for each backtest window (where the number of windows is the full backtest timerange divided by the `backtest_period_days` parameter). Doing this means that the targets simulate dry/live behavior without look ahead bias. However, the definition of the features in `feature_engineering_*()` is performed once on the entire training timerange. This means that you should be sure that features do not look-ahead into the future.
|
||||||
More details about look-ahead bias can be found in [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies).
|
More details about look-ahead bias can be found in [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ Here we compile some external materials that provide deeper looks into various c
|
|||||||
- [Real-time head-to-head: Adaptive modeling of financial market data using XGBoost and CatBoost](https://emergentmethods.medium.com/real-time-head-to-head-adaptive-modeling-of-financial-market-data-using-xgboost-and-catboost-995a115a7495)
|
- [Real-time head-to-head: Adaptive modeling of financial market data using XGBoost and CatBoost](https://emergentmethods.medium.com/real-time-head-to-head-adaptive-modeling-of-financial-market-data-using-xgboost-and-catboost-995a115a7495)
|
||||||
- [FreqAI - from price to prediction](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665)
|
- [FreqAI - from price to prediction](https://emergentmethods.medium.com/freqai-from-price-to-prediction-6fadac18b665)
|
||||||
|
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
You can find support for FreqAI in a variety of places, including the [Freqtrade discord](https://discord.gg/Jd8JYeWHc4), the dedicated [FreqAI discord](https://discord.gg/7AMWACmbjT), and in [github issues](https://github.com/freqtrade/freqtrade/issues).
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
FreqAI is developed by a group of individuals who all contribute specific skillsets to the project.
|
||||||
|
|||||||
+19
-9
@@ -337,11 +337,15 @@ There are four parameter types each suited for different purposes.
|
|||||||
* `CategoricalParameter` - defines a parameter with a predetermined number of choices.
|
* `CategoricalParameter` - defines a parameter with a predetermined number of choices.
|
||||||
* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters.
|
* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters.
|
||||||
|
|
||||||
!!! Tip "Disabling parameter optimization"
|
### Parameter options
|
||||||
Each parameter takes two boolean parameters:
|
|
||||||
* `load` - when set to `False` it will not load values configured in `buy_params` and `sell_params`.
|
There are two parameter options that can help you to quickly test various ideas:
|
||||||
* `optimize` - when set to `False` parameter will not be included in optimization process.
|
|
||||||
Use these parameters to quickly prototype various ideas.
|
* `optimize` - when set to `False`, the parameter will not be included in optimization process. (Default: True)
|
||||||
|
* `load` - when set to `False`, results of a previous hyperopt run (in `buy_params` and `sell_params` either in your strategy or the JSON output file) will not be used as the starting value for subsequent hyperopts. The default value specified in the parameter will be used instead. (Default: True)
|
||||||
|
|
||||||
|
!!! Tip "Effects of `load=False` on backtesting"
|
||||||
|
Be aware that setting the `load` option to `False` will mean backtesting will also use the default value specified in the parameter and *not* the value found through hyperoptimisation.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
|
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
|
||||||
@@ -435,7 +439,7 @@ While this strategy is most likely too simple to provide consistent profit, it s
|
|||||||
??? Hint "Performance tip"
|
??? Hint "Performance tip"
|
||||||
During normal hyperopting, indicators are calculated once and supplied to each epoch, linearly increasing RAM usage as a factor of increasing cores. As this also has performance implications, there are two alternatives to reduce RAM usage
|
During normal hyperopting, indicators are calculated once and supplied to each epoch, linearly increasing RAM usage as a factor of increasing cores. As this also has performance implications, there are two alternatives to reduce RAM usage
|
||||||
|
|
||||||
* Move `ema_short` and `ema_long` calculations from `populate_indicators()` to `populate_entry_trend()`. Since `populate_entry_trend()` gonna be calculated every epochs, you don't need to use `.range` functionality.
|
* Move `ema_short` and `ema_long` calculations from `populate_indicators()` to `populate_entry_trend()`. Since `populate_entry_trend()` will be calculated every epoch, you don't need to use `.range` functionality.
|
||||||
* hyperopt provides `--analyze-per-epoch` which will move the execution of `populate_indicators()` to the epoch process, calculating a single value per parameter per epoch instead of using the `.range` functionality. In this case, `.range` functionality will only return the actually used value.
|
* hyperopt provides `--analyze-per-epoch` which will move the execution of `populate_indicators()` to the epoch process, calculating a single value per parameter per epoch instead of using the `.range` functionality. In this case, `.range` functionality will only return the actually used value.
|
||||||
|
|
||||||
These alternatives will reduce RAM usage, but increase CPU usage. However, your hyperopting run will be less likely to fail due to Out Of Memory (OOM) issues.
|
These alternatives will reduce RAM usage, but increase CPU usage. However, your hyperopting run will be less likely to fail due to Out Of Memory (OOM) issues.
|
||||||
@@ -922,6 +926,12 @@ Once the optimized strategy has been implemented into your strategy, you should
|
|||||||
|
|
||||||
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||||
|
|
||||||
Should results not match, please double-check to make sure you transferred all conditions correctly.
|
### Why do my backtest results not match my hyperopt results?
|
||||||
Pay special care to the stoploss, max_open_trades and trailing stoploss parameters, as these are often set in configuration files, which override changes to the strategy.
|
Should results not match, check the following factors:
|
||||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss`, `max_open_trades` or `trailing_stop`).
|
|
||||||
|
* You may have added parameters to hyperopt in `populate_indicators()` where they will be calculated only once **for all epochs**. If you are, for example, trying to optimise multiple SMA timeperiod values, the hyperoptable timeperiod parameter should be placed in `populate_entry_trend()` which is calculated every epoch. See [Optimizing an indicator parameter](https://www.freqtrade.io/en/stable/hyperopt/#optimizing-an-indicator-parameter).
|
||||||
|
* If you have disabled the auto-export of hyperopt parameters into the JSON parameters file, double-check to make sure you transferred all hyperopted values into your strategy correctly.
|
||||||
|
* Check the logs to verify what parameters are being set and what values are being used.
|
||||||
|
* Pay special care to the stoploss, max_open_trades and trailing stoploss parameters, as these are often set in configuration files, which override changes to the strategy. Check the logs of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss`, `max_open_trades` or `trailing_stop`).
|
||||||
|
* Verify that you do not have an unexpected parameters JSON file overriding the parameters or the default hyperopt settings in your strategy.
|
||||||
|
* Verify that any protections that are enabled in backtesting are also enabled when hyperopting, and vice versa. When using `--space protection`, protections are auto-enabled for hyperopting.
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl
|
|||||||
!!! Warning "Performance implications when using lookback range"
|
!!! Warning "Performance implications when using lookback range"
|
||||||
If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation.
|
If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation.
|
||||||
|
|
||||||
??? Tip "Unsupported exchanges (Bittrex, Gemini)"
|
??? Tip "Unsupported exchanges"
|
||||||
On some exchanges (like Bittrex and Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume.
|
On some exchanges (like Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume.
|
||||||
To roughly simulate 24h volume, you can use the following configuration.
|
To roughly simulate 24h volume, you can use the following configuration.
|
||||||
Please note that These pairlists will only refresh once per day.
|
Please note that These pairlists will only refresh once per day.
|
||||||
|
|
||||||
@@ -192,7 +192,8 @@ The RemotePairList is defined in the pairlists section of the configuration sett
|
|||||||
"refresh_period": 1800,
|
"refresh_period": 1800,
|
||||||
"keep_pairlist_on_failure": true,
|
"keep_pairlist_on_failure": true,
|
||||||
"read_timeout": 60,
|
"read_timeout": 60,
|
||||||
"bearer_token": "my-bearer-token"
|
"bearer_token": "my-bearer-token",
|
||||||
|
"save_to_file": "user_data/filename.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@@ -207,6 +208,42 @@ In "append" mode, the retrieved pairlist is added to the original pairlist. All
|
|||||||
|
|
||||||
The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist.
|
The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist.
|
||||||
|
|
||||||
|
The `save_to_file` option, when provided with a valid filename, saves the processed pairlist to that file in JSON format. This option is optional, and by default, the pairlist is not saved to a file.
|
||||||
|
|
||||||
|
??? Example "Multi bot with shared pairlist example"
|
||||||
|
|
||||||
|
`save_to_file` can be used to save the pairlist to a file with Bot1:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlists": [
|
||||||
|
{
|
||||||
|
"method": "RemotePairList",
|
||||||
|
"mode": "whitelist",
|
||||||
|
"pairlist_url": "https://example.com/pairlist",
|
||||||
|
"number_assets": 10,
|
||||||
|
"refresh_period": 1800,
|
||||||
|
"keep_pairlist_on_failure": true,
|
||||||
|
"read_timeout": 60,
|
||||||
|
"save_to_file": "user_data/filename.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
This saved pairlist file can be loaded by Bot2, or any additional bot with this configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlists": [
|
||||||
|
{
|
||||||
|
"method": "RemotePairList",
|
||||||
|
"mode": "whitelist",
|
||||||
|
"pairlist_url": "file:///user_data/filename.json",
|
||||||
|
"number_assets": 10,
|
||||||
|
"refresh_period": 10,
|
||||||
|
"keep_pairlist_on_failure": true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
The user is responsible for providing a server or local file that returns a JSON object with the following structure:
|
The user is responsible for providing a server or local file that returns a JSON object with the following structure:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ This section will highlight a few projects from members of the community.
|
|||||||
- [Example freqtrade strategies](https://github.com/freqtrade/freqtrade-strategies/)
|
- [Example freqtrade strategies](https://github.com/freqtrade/freqtrade-strategies/)
|
||||||
- [FrequentHippo - Grafana dashboard with dry/live runs and backtests](http://frequenthippo.ddns.net:3000/) (by hippocritical).
|
- [FrequentHippo - Grafana dashboard with dry/live runs and backtests](http://frequenthippo.ddns.net:3000/) (by hippocritical).
|
||||||
- [Online pairlist generator](https://remotepairlist.com/) (by Blood4rc).
|
- [Online pairlist generator](https://remotepairlist.com/) (by Blood4rc).
|
||||||
- [Freqtrade Backtesting Project](https://bt.robot.co.network/) (by Blood4rc).
|
- [Freqtrade Backtesting Project](https://strat.ninja/) (by Blood4rc).
|
||||||
- [Freqtrade analysis notebook](https://github.com/froggleston/freqtrade_analysis_notebook) (by Froggleston).
|
- [Freqtrade analysis notebook](https://github.com/froggleston/freqtrade_analysis_notebook) (by Froggleston).
|
||||||
- [TUI for freqtrade](https://github.com/froggleston/freqtrade-frogtrade9000) (by Froggleston).
|
- [TUI for freqtrade](https://github.com/froggleston/freqtrade-frogtrade9000) (by Froggleston).
|
||||||
- [Bot Academy](https://botacademy.ddns.net/) (by stash86) - Blog about crypto bot projects.
|
- [Bot Academy](https://botacademy.ddns.net/) (by stash86) - Blog about crypto bot projects.
|
||||||
|
|||||||
+2
-2
@@ -40,9 +40,9 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
|||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bitmart](https://bitmart.com/)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [X] [Huobi](http://huobi.com/)
|
- [X] [HTX](https://www.htx.com/) (Former Huobi)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|||||||
@@ -40,11 +40,41 @@ usage: freqtrade recursive-analysis [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--startup-candle STARTUP_CANDLES [STARTUP_CANDLES ...]]
|
[--startup-candle STARTUP_CANDLES [STARTUP_CANDLES ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-p PAIR, --pairs PAIR
|
-h, --help show this help message and exit
|
||||||
|
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||||
|
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||||
|
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||||
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
|
(default: `feather`).
|
||||||
|
-p PAIR, --pairs PAIR
|
||||||
Limit command to this pair.
|
Limit command to this pair.
|
||||||
--startup-candle STARTUP_CANDLE [STARTUP_CANDLE ...]
|
--startup-candle STARTUP_CANDLE [STARTUP_CANDLE ...]
|
||||||
Provide a space-separated list of startup_candle_count to
|
Provide a space-separated list of startup_candle_count to
|
||||||
be checked. Default : `199 399 499 999 1999`.
|
be checked. Default : `199 399 499 999 1999`.
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
Specify configuration file (default:
|
||||||
|
`userdir/config.json` or `config.json` whichever
|
||||||
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
Strategy arguments:
|
||||||
|
-s NAME, --strategy NAME
|
||||||
|
Specify strategy class name which will be used by the
|
||||||
|
bot.
|
||||||
|
--strategy-path PATH Specify additional strategy lookup path.
|
||||||
|
--timerange TIMERANGE
|
||||||
|
Specify what timerange of data to use.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why are odd-numbered default startup candles used?
|
### Why are odd-numbered default startup candles used?
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.4.4
|
markdown==3.5.2
|
||||||
mkdocs==1.5.3
|
mkdocs==1.5.3
|
||||||
mkdocs-material==9.4.1
|
mkdocs-material==9.5.6
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.3
|
pymdown-extensions==10.7
|
||||||
jinja2==3.1.2
|
jinja2==3.1.3
|
||||||
|
|||||||
+6
-3
@@ -134,13 +134,16 @@ 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.
|
||||||
| `trade/<tradeid>` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
| `trades/<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.
|
| `trades/<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.
|
| `trades/<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.
|
||||||
| `count` | Displays number of trades used and available.
|
| `count` | Displays number of trades used and available.
|
||||||
|
| `entries [pair]` | Shows profit statistics for each enter tags for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||||
|
| `exits [pair]` | Shows profit statistics for each exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||||
|
| `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional.
|
||||||
| `locks` | Displays currently locked pairs.
|
| `locks` | Displays currently locked pairs.
|
||||||
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
|
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
|
||||||
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
|
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ The Order-type will be ignored if only one mode is available.
|
|||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| Binance | limit |
|
| Binance | limit |
|
||||||
| Binance Futures | market, limit |
|
| Binance Futures | market, limit |
|
||||||
| Huobi | limit |
|
| HTX (former Huobi) | limit |
|
||||||
| kraken | market, limit |
|
| kraken | market, limit |
|
||||||
| Gate | limit |
|
| Gate | limit |
|
||||||
| Okx | limit |
|
| Okx | limit |
|
||||||
|
|||||||
@@ -489,7 +489,7 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
|
|||||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
|
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
|
||||||
candle = dataframe.iloc[-1].squeeze()
|
candle = dataframe.iloc[-1].squeeze()
|
||||||
sign = 1 if trade.is_short else -1
|
side = 1 if trade.is_short else -1
|
||||||
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
|
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
|
||||||
current_rate, is_short=trade.is_short,
|
current_rate, is_short=trade.is_short,
|
||||||
leverage=trade.leverage)
|
leverage=trade.leverage)
|
||||||
@@ -760,19 +760,30 @@ The `position_adjustment_enable` strategy property enables the usage of `adjust_
|
|||||||
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
||||||
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
|
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
|
||||||
|
|
||||||
`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys.
|
|
||||||
|
|
||||||
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased).
|
|
||||||
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
|
||||||
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||||
|
|
||||||
This callback is **not** called when there is an open order (either buy or sell) waiting for execution.
|
This callback is **not** called when there is an open order (either buy or sell) waiting for execution.
|
||||||
|
|
||||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||||
|
|
||||||
Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
||||||
|
|
||||||
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible, and the stake-amount is assumed to be before applying leverage.
|
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
||||||
|
|
||||||
|
### Increase position
|
||||||
|
|
||||||
|
The strategy is expected to return a positive **stake_amount** (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades).
|
||||||
|
|
||||||
|
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
||||||
|
`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries.
|
||||||
|
|
||||||
|
Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
||||||
|
|
||||||
|
### Decrease position
|
||||||
|
|
||||||
|
The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit.
|
||||||
|
Returning the full owned stake at that point (based on the current price) (`-(trade.amount / trade.leverage) * current_exit_rate`) results in a full exit.
|
||||||
|
Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal.
|
||||||
|
|
||||||
!!! Note "About stake size"
|
!!! Note "About stake size"
|
||||||
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
|||||||
|
|
||||||
Out of the box, freqtrade installs the following technical libraries:
|
Out of the box, freqtrade installs the following technical libraries:
|
||||||
|
|
||||||
* [ta-lib](http://mrjbq7.github.io/ta-lib/)
|
- [ta-lib](https://ta-lib.github.io/ta-lib-python/)
|
||||||
* [pandas-ta](https://twopirllc.github.io/pandas-ta/)
|
- [pandas-ta](https://twopirllc.github.io/pandas-ta/)
|
||||||
* [technical](https://github.com/freqtrade/technical/)
|
- [technical](https://github.com/freqtrade/technical/)
|
||||||
|
|
||||||
Additional technical libraries can be installed as necessary, or custom indicators may be written / invented by the strategy author.
|
Additional technical libraries can be installed as necessary, or custom indicators may be written / invented by the strategy author.
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ You can use [recursive-analysis](recursive-analysis.md) to check and find the co
|
|||||||
In this example strategy, this should be set to 400 (`startup_candle_count = 400`), since the minimum needed history for ema100 calculation to make sure the value is correct is 400 candles.
|
In this example strategy, this should be set to 400 (`startup_candle_count = 400`), since the minimum needed history for ema100 calculation to make sure the value is correct is 400 candles.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=400)
|
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||||
```
|
```
|
||||||
|
|
||||||
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
||||||
@@ -367,6 +367,11 @@ class AwesomeStrategy(IStrategy):
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? info "Orders that don't fill immediately"
|
||||||
|
`minimal_roi` will take the `trade.open_date` as reference, which is the time the trade was initialized / the first order for this trade was placed.
|
||||||
|
This will also hold true for limit orders that don't fill immediately (usually in combination with "off-spot" prices through `custom_entry_price()`), as well as for cases where the initial order is replaced through `adjust_entry_price()`.
|
||||||
|
The time used will still be from the initial `trade.open_date` (when the initial order was first placed), not from the newly placed order date.
|
||||||
|
|
||||||
### Stoploss
|
### Stoploss
|
||||||
|
|
||||||
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
||||||
@@ -486,17 +491,18 @@ for more information.
|
|||||||
|
|
||||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
current pair.
|
current pair. Also supports limited pair format strings (see below)
|
||||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
specified, defaults to:
|
specified, defaults to:
|
||||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
* {column}_{timeframe} if asset is not specified.
|
* {column}_{timeframe} if asset is not specified.
|
||||||
Format string supports these format variables:
|
Pair format supports these format variables:
|
||||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
|
||||||
* {base} - base currency in lower case, for example 'eth'.
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
* {BASE} - same as {base}, except in upper case.
|
* {BASE} - same as {base}, except in upper case.
|
||||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
* {QUOTE} - same as {quote}, except in upper case.
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
Format string additionally supports this variables.
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
* {column} - name of dataframe column.
|
* {column} - name of dataframe column.
|
||||||
* {timeframe} - timeframe of informative dataframe.
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
:param ffill: ffill dataframe after merging informative pair.
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
@@ -1003,11 +1009,15 @@ This is a common pain-point, which can cause huge differences between backtestin
|
|||||||
|
|
||||||
The following lists some common patterns which should be avoided to prevent frustration:
|
The following lists some common patterns which should be avoided to prevent frustration:
|
||||||
|
|
||||||
- don't use `shift(-1)`. This uses data from the future, which is not available.
|
- don't use `shift(-1)` or other negative values. This uses data from the future in backtesting, which is not available in dry or live modes.
|
||||||
- don't use `.iloc[-1]` or any other absolute position in the dataframe, this will be different between dry-run and backtesting.
|
- don't use `.iloc[-1]` or any other absolute position in the dataframe within `populate_` functions, as this will be different between dry-run and backtesting. Absolute `iloc` indexing is safe to use in callbacks however - see [Strategy Callbacks](strategy-callbacks.md).
|
||||||
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
||||||
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||||
|
|
||||||
|
!!! Tip "Identifying problems"
|
||||||
|
You may also want to check the 2 helper commands [lookahead-analysis](lookahead-analysis.md) and [recursive-analysis](recursive-analysis.md), which can each help you figure out problems with your strategy in different ways.
|
||||||
|
Please treat them as what they are - helpers to identify most common problems. A negative result of each does not guarantee that there's none of the above errors included.
|
||||||
|
|
||||||
### Colliding signals
|
### Colliding signals
|
||||||
|
|
||||||
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
|
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ def populate_any_indicators(
|
|||||||
```
|
```
|
||||||
|
|
||||||
1. Features - Move to `feature_engineering_expand_all`
|
1. Features - Move to `feature_engineering_expand_all`
|
||||||
2. Basic features, not expanded across `include_periods_candles` - move to`feature_engineering_expand_basic()`.
|
2. Basic features, not expanded across `indicator_periods_candles` - move to`feature_engineering_expand_basic()`.
|
||||||
3. Standard features which should not be expanded - move to `feature_engineering_standard()`.
|
3. Standard features which should not be expanded - move to `feature_engineering_standard()`.
|
||||||
4. Targets - Move this part to `set_freqai_targets()`.
|
4. Targets - Move this part to `set_freqai_targets()`.
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/status` | Lists all open trades
|
| `/status` | Lists all open trades
|
||||||
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
|
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
|
||||||
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
||||||
|
| `/order <trade_id>` | Lists orders of one or more specific trade. Separate multiple <trade_id> with a blank space.
|
||||||
| `/trades [limit]` | List all recently closed trades in a table format.
|
| `/trades [limit]` | List all recently closed trades in a table format.
|
||||||
| `/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.
|
||||||
|
|||||||
+30
-26
@@ -242,7 +242,6 @@ bitkk True missing opt: fetchMyTrades
|
|||||||
bitmart True
|
bitmart True
|
||||||
bitmax True missing opt: fetchMyTrades
|
bitmax True missing opt: fetchMyTrades
|
||||||
bitpanda True
|
bitpanda True
|
||||||
bittrex True
|
|
||||||
bitvavo True
|
bitvavo True
|
||||||
bitz True missing opt: fetchMyTrades
|
bitz True missing opt: fetchMyTrades
|
||||||
btcalpha True missing opt: fetchTicker, fetchTickers
|
btcalpha True missing opt: fetchTicker, fetchTickers
|
||||||
@@ -324,7 +323,6 @@ bitpanda True
|
|||||||
bitso False missing: fetchOHLCV
|
bitso False missing: fetchOHLCV
|
||||||
bitstamp True missing opt: fetchTickers
|
bitstamp True missing opt: fetchTickers
|
||||||
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
||||||
bittrex True
|
|
||||||
bitvavo True
|
bitvavo True
|
||||||
bitz True missing opt: fetchMyTrades
|
bitz True missing opt: fetchMyTrades
|
||||||
bl3p False missing: fetchOrder, fetchOHLCV
|
bl3p False missing: fetchOrder, fetchOHLCV
|
||||||
@@ -427,25 +425,33 @@ zb True missing opt: fetchMyTrades
|
|||||||
Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange.
|
Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1]
|
usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[--exchange EXCHANGE] [-1]
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided.
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
-1, --one-column Print output in one column.
|
-1, --one-column Print output in one column.
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
--logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more details.
|
--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
|
-V, --version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to `-`
|
Specify configuration file (default:
|
||||||
to read config from stdin.
|
`userdir/config.json` or `config.json` whichever
|
||||||
-d PATH, --datadir PATH
|
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.
|
Path to directory with historical backtesting data.
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* Example: see the timeframes for the 'binance' exchange, set in the configuration file:
|
* Example: see the timeframes for the 'binance' exchange, set in the configuration file:
|
||||||
@@ -479,20 +485,17 @@ usage: freqtrade list-markets [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
||||||
[--print-list] [--print-json] [-1] [--print-csv]
|
[--print-list] [--print-json] [-1] [--print-csv]
|
||||||
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
||||||
[--trading-mode {spot,margin,futures}]
|
[-a] [--trading-mode {spot,margin,futures}]
|
||||||
|
|
||||||
usage: freqtrade list-pairs [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
usage: freqtrade list-pairs [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
|
||||||
[--print-list] [--print-json] [-1] [--print-csv]
|
[--print-list] [--print-json] [-1] [--print-csv]
|
||||||
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
||||||
[--trading-mode {spot,margin,futures}]
|
[--trading-mode {spot,margin,futures}]
|
||||||
|
options:
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
--print-list Print list of pairs or market symbols. By default data
|
--print-list Print list of pairs or market symbols. By default data
|
||||||
is printed in the tabular format.
|
is printed in the tabular format.
|
||||||
--print-json Print list of pairs or market symbols in JSON format.
|
--print-json Print list of pairs or market symbols in JSON format.
|
||||||
@@ -504,20 +507,22 @@ optional arguments:
|
|||||||
Specify quote currency(-ies). Space-separated list.
|
Specify quote currency(-ies). Space-separated list.
|
||||||
-a, --all Print all pairs or market symbols. By default only
|
-a, --all Print all pairs or market symbols. By default only
|
||||||
active ones are shown.
|
active ones are shown.
|
||||||
--trading-mode {spot,margin,futures}
|
--trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures}
|
||||||
Select Trading mode
|
Select Trading mode
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
--logfile FILE Log to the file specified. Special values are:
|
--logfile FILE, --log-file FILE
|
||||||
|
Log to the file specified. Special values are:
|
||||||
'syslog', 'journald'. See the documentation for more
|
'syslog', 'journald'. See the documentation for more
|
||||||
details.
|
details.
|
||||||
-V, --version show program's version number and exit
|
-V, --version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
Specify configuration file (default: `config.json`).
|
Specify configuration file (default:
|
||||||
Multiple --config options may be used. Can be set to
|
`userdir/config.json` or `config.json` whichever
|
||||||
`-` to read config from stdin.
|
exists). Multiple --config options may be used. Can be
|
||||||
-d PATH, --datadir PATH
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH, --data-dir PATH
|
||||||
Path to directory with historical backtesting data.
|
Path to directory with historical backtesting data.
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
@@ -532,7 +537,7 @@ Pairs/markets are sorted by its symbol string in the printed output.
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
* Print the list of active pairs with quote currency USD on exchange, specified in the default
|
* Print the list of active pairs with quote currency USD on exchange, specified in the default
|
||||||
configuration file (i.e. pairs on the "Bittrex" exchange) in JSON format:
|
configuration file (i.e. pairs on the "Binance" exchange) in JSON format:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ freqtrade list-pairs --quote USD --print-json
|
$ freqtrade list-pairs --quote USD --print-json
|
||||||
@@ -564,7 +569,7 @@ usage: freqtrade test-pairlist [-h] [--userdir PATH] [-v] [-c PATH]
|
|||||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
||||||
[-1] [--print-json] [--exchange EXCHANGE]
|
[-1] [--print-json] [--exchange EXCHANGE]
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
@@ -578,8 +583,7 @@ optional arguments:
|
|||||||
Specify quote currency(-ies). Space-separated list.
|
Specify quote currency(-ies). Space-separated list.
|
||||||
-1, --one-column Print output in one column.
|
-1, --one-column Print output in one column.
|
||||||
--print-json Print list of pairs or market symbols in JSON format.
|
--print-json Print list of pairs or market symbols in JSON format.
|
||||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
--exchange EXCHANGE Exchange name. Only valid if no config is provided.
|
||||||
config is provided.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
@@ -155,6 +156,7 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
@@ -176,6 +178,7 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
@@ -199,6 +202,7 @@ Possible parameters are:
|
|||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `exit_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
@@ -224,6 +228,7 @@ Possible parameters are:
|
|||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `exit_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
@@ -249,6 +254,7 @@ Possible parameters are:
|
|||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `base_currency`
|
* `base_currency`
|
||||||
|
* `quote_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `exit_reason`
|
* `exit_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
@@ -302,6 +308,7 @@ You can configure this as follows:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
||||||
|
To disable either of the two default values (`entry_fill` / `exit_fill`), you can assign them an empty array (`exit_fill: []`).
|
||||||
|
|
||||||
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ git clone https://github.com/freqtrade/freqtrade.git
|
|||||||
|
|
||||||
### 2. Install ta-lib
|
### 2. Install ta-lib
|
||||||
|
|
||||||
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/TA-Lib/ta-lib-python#windows).
|
||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10 and 3.11) and for 64bit Windows.
|
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10 and 3.11) and for 64bit Windows.
|
||||||
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2023.9'
|
__version__ = '2024.1'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ ARGS_BUILD_CONFIG = ["config"]
|
|||||||
|
|
||||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||||
|
|
||||||
|
ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", "exchange"]
|
||||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
||||||
|
|
||||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"]
|
||||||
|
|
||||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||||
@@ -219,27 +219,35 @@ class Arguments:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add trade subcommand
|
# Add trade subcommand
|
||||||
trade_cmd = subparsers.add_parser('trade', help='Trade module.',
|
trade_cmd = subparsers.add_parser(
|
||||||
parents=[_common_parser, _strategy_parser])
|
'trade',
|
||||||
|
help='Trade module.',
|
||||||
|
parents=[_common_parser, _strategy_parser]
|
||||||
|
)
|
||||||
trade_cmd.set_defaults(func=start_trading)
|
trade_cmd.set_defaults(func=start_trading)
|
||||||
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
|
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
|
||||||
|
|
||||||
# add create-userdir subcommand
|
# add create-userdir subcommand
|
||||||
create_userdir_cmd = subparsers.add_parser('create-userdir',
|
create_userdir_cmd = subparsers.add_parser(
|
||||||
|
'create-userdir',
|
||||||
help="Create user-data directory.",
|
help="Create user-data directory.",
|
||||||
)
|
)
|
||||||
create_userdir_cmd.set_defaults(func=start_create_userdir)
|
create_userdir_cmd.set_defaults(func=start_create_userdir)
|
||||||
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
|
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
|
||||||
|
|
||||||
# add new-config subcommand
|
# add new-config subcommand
|
||||||
build_config_cmd = subparsers.add_parser('new-config',
|
build_config_cmd = subparsers.add_parser(
|
||||||
help="Create new config")
|
'new-config',
|
||||||
|
help="Create new config",
|
||||||
|
)
|
||||||
build_config_cmd.set_defaults(func=start_new_config)
|
build_config_cmd.set_defaults(func=start_new_config)
|
||||||
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
|
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
|
||||||
|
|
||||||
# add new-strategy subcommand
|
# add new-strategy subcommand
|
||||||
build_strategy_cmd = subparsers.add_parser('new-strategy',
|
build_strategy_cmd = subparsers.add_parser(
|
||||||
help="Create new strategy")
|
'new-strategy',
|
||||||
|
help="Create new strategy",
|
||||||
|
)
|
||||||
build_strategy_cmd.set_defaults(func=start_new_strategy)
|
build_strategy_cmd.set_defaults(func=start_new_strategy)
|
||||||
self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd)
|
self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd)
|
||||||
|
|
||||||
@@ -268,7 +276,7 @@ class Arguments:
|
|||||||
parents=[_common_parser],
|
parents=[_common_parser],
|
||||||
)
|
)
|
||||||
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
||||||
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
self._build_args(optionlist=ARGS_CONVERT_DATA_TRADES, parser=convert_trade_data_cmd)
|
||||||
|
|
||||||
# Add trades-to-ohlcv subcommand
|
# Add trades-to-ohlcv subcommand
|
||||||
convert_trade_data_cmd = subparsers.add_parser(
|
convert_trade_data_cmd = subparsers.add_parser(
|
||||||
@@ -289,8 +297,11 @@ class Arguments:
|
|||||||
self._build_args(optionlist=ARGS_LIST_DATA, parser=list_data_cmd)
|
self._build_args(optionlist=ARGS_LIST_DATA, parser=list_data_cmd)
|
||||||
|
|
||||||
# Add backtesting subcommand
|
# Add backtesting subcommand
|
||||||
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.',
|
backtesting_cmd = subparsers.add_parser(
|
||||||
parents=[_common_parser, _strategy_parser])
|
'backtesting',
|
||||||
|
help='Backtesting module.',
|
||||||
|
parents=[_common_parser, _strategy_parser]
|
||||||
|
)
|
||||||
backtesting_cmd.set_defaults(func=start_backtesting)
|
backtesting_cmd.set_defaults(func=start_backtesting)
|
||||||
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
||||||
|
|
||||||
@@ -304,20 +315,27 @@ class Arguments:
|
|||||||
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
||||||
|
|
||||||
# Add backtesting analysis subcommand
|
# Add backtesting analysis subcommand
|
||||||
analysis_cmd = subparsers.add_parser('backtesting-analysis',
|
analysis_cmd = subparsers.add_parser(
|
||||||
|
'backtesting-analysis',
|
||||||
help='Backtest Analysis module.',
|
help='Backtest Analysis module.',
|
||||||
parents=[_common_parser])
|
parents=[_common_parser]
|
||||||
|
)
|
||||||
analysis_cmd.set_defaults(func=start_analysis_entries_exits)
|
analysis_cmd.set_defaults(func=start_analysis_entries_exits)
|
||||||
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
|
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
|
||||||
|
|
||||||
# Add edge subcommand
|
# Add edge subcommand
|
||||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
edge_cmd = subparsers.add_parser(
|
||||||
parents=[_common_parser, _strategy_parser])
|
'edge',
|
||||||
|
help='Edge module.',
|
||||||
|
parents=[_common_parser, _strategy_parser]
|
||||||
|
)
|
||||||
edge_cmd.set_defaults(func=start_edge)
|
edge_cmd.set_defaults(func=start_edge)
|
||||||
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
||||||
|
|
||||||
# Add hyperopt subcommand
|
# Add hyperopt subcommand
|
||||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.',
|
hyperopt_cmd = subparsers.add_parser(
|
||||||
|
'hyperopt',
|
||||||
|
help='Hyperopt module.',
|
||||||
parents=[_common_parser, _strategy_parser],
|
parents=[_common_parser, _strategy_parser],
|
||||||
)
|
)
|
||||||
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
||||||
@@ -447,16 +465,20 @@ class Arguments:
|
|||||||
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)
|
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)
|
||||||
|
|
||||||
# Add webserver subcommand
|
# Add webserver subcommand
|
||||||
webserver_cmd = subparsers.add_parser('webserver', help='Webserver module.',
|
webserver_cmd = subparsers.add_parser(
|
||||||
parents=[_common_parser])
|
'webserver',
|
||||||
|
help='Webserver module.',
|
||||||
|
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
|
# Add strategy_updater subcommand
|
||||||
strategy_updater_cmd = subparsers.add_parser('strategy-updater',
|
strategy_updater_cmd = subparsers.add_parser(
|
||||||
help='updates outdated strategy'
|
'strategy-updater',
|
||||||
'files to the current version',
|
help='updates outdated strategy files to the current version',
|
||||||
parents=[_common_parser])
|
parents=[_common_parser]
|
||||||
|
)
|
||||||
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
||||||
self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd)
|
self._build_args(optionlist=ARGS_STRATEGY_UPDATER, parser=strategy_updater_cmd)
|
||||||
|
|
||||||
@@ -464,8 +486,8 @@ class Arguments:
|
|||||||
lookahead_analayis_cmd = subparsers.add_parser(
|
lookahead_analayis_cmd = subparsers.add_parser(
|
||||||
'lookahead-analysis',
|
'lookahead-analysis',
|
||||||
help="Check for potential look ahead bias.",
|
help="Check for potential look ahead bias.",
|
||||||
parents=[_common_parser, _strategy_parser])
|
parents=[_common_parser, _strategy_parser]
|
||||||
|
)
|
||||||
lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis)
|
lookahead_analayis_cmd.set_defaults(func=start_lookahead_analysis)
|
||||||
|
|
||||||
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS,
|
self._build_args(optionlist=ARGS_LOOKAHEAD_ANALYSIS,
|
||||||
@@ -475,8 +497,8 @@ class Arguments:
|
|||||||
recursive_analayis_cmd = subparsers.add_parser(
|
recursive_analayis_cmd = subparsers.add_parser(
|
||||||
'recursive-analysis',
|
'recursive-analysis',
|
||||||
help="Check for potential recursive formula issue.",
|
help="Check for potential recursive formula issue.",
|
||||||
parents=[_common_parser, _strategy_parser])
|
parents=[_common_parser, _strategy_parser]
|
||||||
|
)
|
||||||
recursive_analayis_cmd.set_defaults(func=start_recursive_analysis)
|
recursive_analayis_cmd.set_defaults(func=start_recursive_analysis)
|
||||||
|
|
||||||
self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS,
|
self._build_args(optionlist=ARGS_RECURSIVE_ANALYSIS,
|
||||||
|
|||||||
@@ -108,9 +108,8 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"choices": [
|
"choices": [
|
||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
|
||||||
"gate",
|
"gate",
|
||||||
"huobi",
|
"htx",
|
||||||
"kraken",
|
"kraken",
|
||||||
"kucoin",
|
"kucoin",
|
||||||
"okx",
|
"okx",
|
||||||
|
|||||||
@@ -421,6 +421,12 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
'desired timeframe as specified as --timeframes/-t.',
|
'desired timeframe as specified as --timeframes/-t.',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
),
|
),
|
||||||
|
"format_from_trades": Arg(
|
||||||
|
'--format-from',
|
||||||
|
help='Source format for data conversion.',
|
||||||
|
choices=constants.AVAILABLE_DATAHANDLERS + ['kraken_csv'],
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
"format_from": Arg(
|
"format_from": Arg(
|
||||||
'--format-from',
|
'--format-from',
|
||||||
help='Source format for data conversion.',
|
help='Source format for data conversion.',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from freqtrade.enums import RunMode, TradingMode
|
|||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
from freqtrade.util.migrations import migrate_data
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -78,14 +78,14 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
if ohlcv:
|
if ohlcv:
|
||||||
migrate_binance_futures_data(config)
|
migrate_data(config)
|
||||||
convert_ohlcv_format(config,
|
convert_ohlcv_format(config,
|
||||||
convert_from=args['format_from'],
|
convert_from=args['format_from'],
|
||||||
convert_to=args['format_to'],
|
convert_to=args['format_to'],
|
||||||
erase=args['erase'])
|
erase=args['erase'])
|
||||||
else:
|
else:
|
||||||
convert_trades_format(config,
|
convert_trades_format(config,
|
||||||
convert_from=args['format_from'], convert_to=args['format_to'],
|
convert_from=args['format_from_trades'], convert_to=args['format_to'],
|
||||||
erase=args['erase'])
|
erase=args['erase'])
|
||||||
|
|
||||||
|
|
||||||
@@ -134,10 +134,10 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
|||||||
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), length)
|
||||||
for pair, timeframe, candle_type, start, end in sorted(
|
for pair, timeframe, candle_type, start, end, length in sorted(
|
||||||
paircombs1,
|
paircombs1,
|
||||||
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
|
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
|
||||||
],
|
],
|
||||||
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
headers=("Pair", "Timeframe", "Type", 'From', 'To', 'Candles'),
|
||||||
tablefmt='psql', stralign='right'))
|
tablefmt='psql', stralign='right'))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from freqtrade import constants
|
|||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import round_coin_value
|
from freqtrade.util import fmt_coin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -29,8 +29,8 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
|||||||
# tradable_balance_ratio
|
# tradable_balance_ratio
|
||||||
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||||
and config['stake_amount'] > wallet_size):
|
and config['stake_amount'] > wallet_size):
|
||||||
wallet = round_coin_value(wallet_size, config['stake_currency'])
|
wallet = fmt_coin(wallet_size, config['stake_currency'])
|
||||||
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
stake = fmt_coin(config['stake_amount'], config['stake_currency'])
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
||||||
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
||||||
@@ -140,7 +140,7 @@ def start_lookahead_analysis(args: Dict[str, Any]) -> None:
|
|||||||
:param args: Cli args from Arguments()
|
:param args: Cli args from Arguments()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
from freqtrade.optimize.lookahead_analysis_helpers import LookaheadAnalysisSubFunctions
|
from freqtrade.optimize.analysis.lookahead_helpers import LookaheadAnalysisSubFunctions
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
LookaheadAnalysisSubFunctions.start(config)
|
LookaheadAnalysisSubFunctions.start(config)
|
||||||
@@ -152,7 +152,7 @@ def start_recursive_analysis(args: Dict[str, Any]) -> None:
|
|||||||
:param args: Cli args from Arguments()
|
:param args: Cli args from Arguments()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
from freqtrade.optimize.recursive_analysis_helpers import RecursiveAnalysisSubFunctions
|
from freqtrade.optimize.analysis.recursive_helpers import RecursiveAnalysisSubFunctions
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
RecursiveAnalysisSubFunctions.start(config)
|
RecursiveAnalysisSubFunctions.start(config)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
Test Pairlist configuration
|
Test Pairlist configuration
|
||||||
"""
|
"""
|
||||||
|
from freqtrade.persistence import FtNoDBContext
|
||||||
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)
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
|
|||||||
if not quote_currencies:
|
if not quote_currencies:
|
||||||
quote_currencies = [config.get('stake_currency')]
|
quote_currencies = [config.get('stake_currency')]
|
||||||
results = {}
|
results = {}
|
||||||
|
with FtNoDBContext():
|
||||||
for curr in quote_currencies:
|
for curr in quote_currencies:
|
||||||
config['stake_currency'] = curr
|
config['stake_currency'] = curr
|
||||||
pairlists = PairListManager(exchange, config)
|
pairlists = PairListManager(exchange, config)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False) -> None:
|
def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Validate the configuration consistency.
|
Validate the configuration consistency.
|
||||||
Should be ran after loading both configuration and strategy,
|
Should be ran after loading both configuration and strategy,
|
||||||
@@ -86,7 +86,7 @@ def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False)
|
|||||||
_validate_ask_orderbook(conf)
|
_validate_ask_orderbook(conf)
|
||||||
_validate_freqai_hyperopt(conf)
|
_validate_freqai_hyperopt(conf)
|
||||||
_validate_freqai_backtest(conf)
|
_validate_freqai_backtest(conf)
|
||||||
_validate_freqai_include_timeframes(conf)
|
_validate_freqai_include_timeframes(conf, preliminary=preliminary)
|
||||||
_validate_consumers(conf)
|
_validate_consumers(conf)
|
||||||
validate_migrated_strategy_settings(conf)
|
validate_migrated_strategy_settings(conf)
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
|
|||||||
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.')
|
'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.')
|
||||||
|
|
||||||
|
|
||||||
def _validate_freqai_include_timeframes(conf: Dict[str, Any]) -> None:
|
def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None:
|
||||||
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
|
freqai_enabled = conf.get('freqai', {}).get('enabled', False)
|
||||||
if freqai_enabled:
|
if freqai_enabled:
|
||||||
main_tf = conf.get('timeframe', '5m')
|
main_tf = conf.get('timeframe', '5m')
|
||||||
@@ -355,7 +355,7 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any]) -> None:
|
|||||||
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
|
f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}")
|
||||||
|
|
||||||
# Ensure that the base timeframe is included in the include_timeframes list
|
# Ensure that the base timeframe is included in the include_timeframes list
|
||||||
if main_tf not in freqai_include_timeframes:
|
if not preliminary and main_tf not in freqai_include_timeframes:
|
||||||
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {})
|
feature_parameters = conf.get('freqai', {}).get('feature_parameters', {})
|
||||||
include_timeframes = [main_tf] + freqai_include_timeframes
|
include_timeframes = [main_tf] + freqai_include_timeframes
|
||||||
conf.get('freqai', {}).get('feature_parameters', {}) \
|
conf.get('freqai', {}).get('feature_parameters', {}) \
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logging
|
|||||||
import warnings
|
import warnings
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||||
@@ -68,6 +68,8 @@ class Configuration:
|
|||||||
config: Config = load_from_files(self.args.get("config", []))
|
config: Config = load_from_files(self.args.get("config", []))
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
|
from freqtrade.commands.arguments import NO_CONF_ALLOWED
|
||||||
|
if self.args.get('command') not in NO_CONF_ALLOWED:
|
||||||
env_data = enironment_vars_to_dict()
|
env_data = enironment_vars_to_dict()
|
||||||
config = deep_merge_dicts(env_data, config)
|
config = deep_merge_dicts(env_data, config)
|
||||||
|
|
||||||
@@ -233,54 +235,37 @@ class Configuration:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self._args_to_config(config, argname='timeframe_detail',
|
configurations = [
|
||||||
logstring='Parameter --timeframe-detail detected, '
|
('timeframe_detail',
|
||||||
'using {} for intra-candle backtesting ...')
|
'Parameter --timeframe-detail detected, using {} for intra-candle backtesting ...'),
|
||||||
|
('backtest_show_pair_list', 'Parameter --show-pair-list detected.'),
|
||||||
|
('stake_amount',
|
||||||
|
'Parameter --stake-amount detected, overriding stake_amount to: {} ...'),
|
||||||
|
('dry_run_wallet',
|
||||||
|
'Parameter --dry-run-wallet detected, overriding dry_run_wallet to: {} ...'),
|
||||||
|
('fee', 'Parameter --fee detected, setting fee to: {} ...'),
|
||||||
|
('timerange', 'Parameter --timerange detected: {} ...'),
|
||||||
|
]
|
||||||
|
|
||||||
self._args_to_config(config, argname='backtest_show_pair_list',
|
self._args_to_config_loop(config, configurations)
|
||||||
logstring='Parameter --show-pair-list detected.')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='stake_amount',
|
|
||||||
logstring='Parameter --stake-amount detected, '
|
|
||||||
'overriding stake_amount to: {} ...')
|
|
||||||
self._args_to_config(config, argname='dry_run_wallet',
|
|
||||||
logstring='Parameter --dry-run-wallet detected, '
|
|
||||||
'overriding dry_run_wallet to: {} ...')
|
|
||||||
self._args_to_config(config, argname='fee',
|
|
||||||
logstring='Parameter --fee detected, '
|
|
||||||
'setting fee to: {} ...')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='timerange',
|
|
||||||
logstring='Parameter --timerange detected: {} ...')
|
|
||||||
|
|
||||||
self._process_datadir_options(config)
|
self._process_datadir_options(config)
|
||||||
|
|
||||||
self._args_to_config(config, argname='strategy_list',
|
self._args_to_config(config, argname='strategy_list',
|
||||||
logstring='Using strategy list of {} strategies', logfun=len)
|
logstring='Using strategy list of {} strategies', logfun=len)
|
||||||
|
|
||||||
self._args_to_config(
|
configurations = [
|
||||||
config,
|
('recursive_strategy_search',
|
||||||
argname='recursive_strategy_search',
|
'Recursively searching for a strategy in the strategies folder.'),
|
||||||
logstring='Recursively searching for a strategy in the strategies folder.',
|
('timeframe', 'Overriding timeframe with Command line argument'),
|
||||||
)
|
('export', 'Parameter --export detected: {} ...'),
|
||||||
|
('backtest_breakdown', 'Parameter --breakdown detected ...'),
|
||||||
self._args_to_config(config, argname='timeframe',
|
('backtest_cache', 'Parameter --cache={} detected ...'),
|
||||||
logstring='Overriding timeframe with Command line argument')
|
('disableparamexport', 'Parameter --disableparamexport detected: {} ...'),
|
||||||
|
('freqai_backtest_live_models',
|
||||||
self._args_to_config(config, argname='export',
|
'Parameter --freqai-backtest-live-models detected ...'),
|
||||||
logstring='Parameter --export detected: {} ...')
|
]
|
||||||
|
self._args_to_config_loop(config, configurations)
|
||||||
self._args_to_config(config, argname='backtest_breakdown',
|
|
||||||
logstring='Parameter --breakdown detected ...')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='backtest_cache',
|
|
||||||
logstring='Parameter --cache={} detected ...')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='disableparamexport',
|
|
||||||
logstring='Parameter --disableparamexport detected: {} ...')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='freqai_backtest_live_models',
|
|
||||||
logstring='Parameter --freqai-backtest-live-models detected ...')
|
|
||||||
|
|
||||||
# Edge section:
|
# Edge section:
|
||||||
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
|
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
|
||||||
@@ -291,31 +276,18 @@ class Configuration:
|
|||||||
logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"])
|
logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"])
|
||||||
|
|
||||||
# Hyperopt section
|
# Hyperopt section
|
||||||
self._args_to_config(config, argname='hyperopt',
|
|
||||||
logstring='Using Hyperopt class name: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_path',
|
configurations = [
|
||||||
logstring='Using additional Hyperopt lookup path: {}')
|
('hyperopt', 'Using Hyperopt class name: {}'),
|
||||||
|
('hyperopt_path', 'Using additional Hyperopt lookup path: {}'),
|
||||||
self._args_to_config(config, argname='hyperoptexportfilename',
|
('hyperoptexportfilename', 'Using hyperopt file: {}'),
|
||||||
logstring='Using hyperopt file: {}')
|
('lookahead_analysis_exportfilename', 'Saving lookahead analysis results into {} ...'),
|
||||||
|
('epochs', 'Parameter --epochs detected ... Will run Hyperopt with for {} epochs ...'),
|
||||||
self._args_to_config(config, argname='lookahead_analysis_exportfilename',
|
('spaces', 'Parameter -s/--spaces detected: {}'),
|
||||||
logstring='Saving lookahead analysis results into {} ...')
|
('analyze_per_epoch', 'Parameter --analyze-per-epoch detected.'),
|
||||||
|
('print_all', 'Parameter --print-all detected ...'),
|
||||||
self._args_to_config(config, argname='epochs',
|
]
|
||||||
logstring='Parameter --epochs detected ... '
|
self._args_to_config_loop(config, configurations)
|
||||||
'Will run Hyperopt with for {} epochs ...'
|
|
||||||
)
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='spaces',
|
|
||||||
logstring='Parameter -s/--spaces detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='analyze_per_epoch',
|
|
||||||
logstring='Parameter --analyze-per-epoch detected.')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='print_all',
|
|
||||||
logstring='Parameter --print-all detected ...')
|
|
||||||
|
|
||||||
if 'print_colorized' in self.args and not self.args["print_colorized"]:
|
if 'print_colorized' in self.args and not self.args["print_colorized"]:
|
||||||
logger.info('Parameter --no-color detected ...')
|
logger.info('Parameter --no-color detected ...')
|
||||||
@@ -323,123 +295,55 @@ class Configuration:
|
|||||||
else:
|
else:
|
||||||
config.update({'print_colorized': True})
|
config.update({'print_colorized': True})
|
||||||
|
|
||||||
self._args_to_config(config, argname='print_json',
|
configurations = [
|
||||||
logstring='Parameter --print-json detected ...')
|
('print_json', 'Parameter --print-json detected ...'),
|
||||||
|
('export_csv', 'Parameter --export-csv detected: {}'),
|
||||||
|
('hyperopt_jobs', 'Parameter -j/--job-workers detected: {}'),
|
||||||
|
('hyperopt_random_state', 'Parameter --random-state detected: {}'),
|
||||||
|
('hyperopt_min_trades', 'Parameter --min-trades detected: {}'),
|
||||||
|
('hyperopt_loss', 'Using Hyperopt loss class name: {}'),
|
||||||
|
('hyperopt_show_index', 'Parameter -n/--index detected: {}'),
|
||||||
|
('hyperopt_list_best', 'Parameter --best detected: {}'),
|
||||||
|
('hyperopt_list_profitable', 'Parameter --profitable detected: {}'),
|
||||||
|
('hyperopt_list_min_trades', 'Parameter --min-trades detected: {}'),
|
||||||
|
('hyperopt_list_max_trades', 'Parameter --max-trades detected: {}'),
|
||||||
|
('hyperopt_list_min_avg_time', 'Parameter --min-avg-time detected: {}'),
|
||||||
|
('hyperopt_list_max_avg_time', 'Parameter --max-avg-time detected: {}'),
|
||||||
|
('hyperopt_list_min_avg_profit', 'Parameter --min-avg-profit detected: {}'),
|
||||||
|
('hyperopt_list_max_avg_profit', 'Parameter --max-avg-profit detected: {}'),
|
||||||
|
('hyperopt_list_min_total_profit', 'Parameter --min-total-profit detected: {}'),
|
||||||
|
('hyperopt_list_max_total_profit', 'Parameter --max-total-profit detected: {}'),
|
||||||
|
('hyperopt_list_min_objective', 'Parameter --min-objective detected: {}'),
|
||||||
|
('hyperopt_list_max_objective', 'Parameter --max-objective detected: {}'),
|
||||||
|
('hyperopt_list_no_details', 'Parameter --no-details detected: {}'),
|
||||||
|
('hyperopt_show_no_header', 'Parameter --no-header detected: {}'),
|
||||||
|
('hyperopt_ignore_missing_space', 'Paramter --ignore-missing-space detected: {}'),
|
||||||
|
]
|
||||||
|
|
||||||
self._args_to_config(config, argname='export_csv',
|
self._args_to_config_loop(config, configurations)
|
||||||
logstring='Parameter --export-csv detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_jobs',
|
|
||||||
logstring='Parameter -j/--job-workers detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_random_state',
|
|
||||||
logstring='Parameter --random-state detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_min_trades',
|
|
||||||
logstring='Parameter --min-trades detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_loss',
|
|
||||||
logstring='Using Hyperopt loss class name: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_show_index',
|
|
||||||
logstring='Parameter -n/--index detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_best',
|
|
||||||
logstring='Parameter --best detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_profitable',
|
|
||||||
logstring='Parameter --profitable detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_min_trades',
|
|
||||||
logstring='Parameter --min-trades detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_max_trades',
|
|
||||||
logstring='Parameter --max-trades detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_min_avg_time',
|
|
||||||
logstring='Parameter --min-avg-time detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_max_avg_time',
|
|
||||||
logstring='Parameter --max-avg-time detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_min_avg_profit',
|
|
||||||
logstring='Parameter --min-avg-profit detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_max_avg_profit',
|
|
||||||
logstring='Parameter --max-avg-profit detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_min_total_profit',
|
|
||||||
logstring='Parameter --min-total-profit detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_max_total_profit',
|
|
||||||
logstring='Parameter --max-total-profit detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_min_objective',
|
|
||||||
logstring='Parameter --min-objective detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_max_objective',
|
|
||||||
logstring='Parameter --max-objective detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_no_details',
|
|
||||||
logstring='Parameter --no-details detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_show_no_header',
|
|
||||||
logstring='Parameter --no-header detected: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname="hyperopt_ignore_missing_space",
|
|
||||||
logstring="Paramter --ignore-missing-space detected: {}")
|
|
||||||
|
|
||||||
def _process_plot_options(self, config: Config) -> None:
|
def _process_plot_options(self, config: Config) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='pairs',
|
configurations = [
|
||||||
logstring='Using pairs {}')
|
('pairs', 'Using pairs {}'),
|
||||||
|
('indicators1', 'Using indicators1: {}'),
|
||||||
self._args_to_config(config, argname='indicators1',
|
('indicators2', 'Using indicators2: {}'),
|
||||||
logstring='Using indicators1: {}')
|
('trade_ids', 'Filtering on trade_ids: {}'),
|
||||||
|
('plot_limit', 'Limiting plot to: {}'),
|
||||||
self._args_to_config(config, argname='indicators2',
|
('plot_auto_open', 'Parameter --auto-open detected.'),
|
||||||
logstring='Using indicators2: {}')
|
('trade_source', 'Using trades from: {}'),
|
||||||
|
('prepend_data', 'Prepend detected. Allowing data prepending.'),
|
||||||
self._args_to_config(config, argname='trade_ids',
|
('erase', 'Erase detected. Deleting existing data.'),
|
||||||
logstring='Filtering on trade_ids: {}')
|
('no_trades', 'Parameter --no-trades detected.'),
|
||||||
|
('timeframes', 'timeframes --timeframes: {}'),
|
||||||
self._args_to_config(config, argname='plot_limit',
|
('days', 'Detected --days: {}'),
|
||||||
logstring='Limiting plot to: {}')
|
('include_inactive', 'Detected --include-inactive-pairs: {}'),
|
||||||
|
('download_trades', 'Detected --dl-trades: {}'),
|
||||||
self._args_to_config(config, argname='plot_auto_open',
|
('dataformat_ohlcv', 'Using "{}" to store OHLCV data.'),
|
||||||
logstring='Parameter --auto-open detected.')
|
('dataformat_trades', 'Using "{}" to store trades data.'),
|
||||||
|
('show_timerange', 'Detected --show-timerange'),
|
||||||
self._args_to_config(config, argname='trade_source',
|
]
|
||||||
logstring='Using trades from: {}')
|
self._args_to_config_loop(config, configurations)
|
||||||
|
|
||||||
self._args_to_config(config, argname='prepend_data',
|
|
||||||
logstring='Prepend detected. Allowing data prepending.')
|
|
||||||
self._args_to_config(config, argname='erase',
|
|
||||||
logstring='Erase detected. Deleting existing data.')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='no_trades',
|
|
||||||
logstring='Parameter --no-trades detected.')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='timeframes',
|
|
||||||
logstring='timeframes --timeframes: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='days',
|
|
||||||
logstring='Detected --days: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='include_inactive',
|
|
||||||
logstring='Detected --include-inactive-pairs: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='download_trades',
|
|
||||||
logstring='Detected --dl-trades: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='dataformat_ohlcv',
|
|
||||||
logstring='Using "{}" to store OHLCV data.')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='dataformat_trades',
|
|
||||||
logstring='Using "{}" to store trades data.')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='show_timerange',
|
|
||||||
logstring='Detected --show-timerange')
|
|
||||||
|
|
||||||
def _process_data_options(self, config: Config) -> None:
|
def _process_data_options(self, config: Config) -> None:
|
||||||
self._args_to_config(config, argname='new_pairs_days',
|
self._args_to_config(config, argname='new_pairs_days',
|
||||||
@@ -453,45 +357,27 @@ class Configuration:
|
|||||||
logstring='Detected --candle-types: {}')
|
logstring='Detected --candle-types: {}')
|
||||||
|
|
||||||
def _process_analyze_options(self, config: Config) -> None:
|
def _process_analyze_options(self, config: Config) -> None:
|
||||||
self._args_to_config(config, argname='analysis_groups',
|
configurations = [
|
||||||
logstring='Analysis reason groups: {}')
|
('analysis_groups', 'Analysis reason groups: {}'),
|
||||||
|
('enter_reason_list', 'Analysis enter tag list: {}'),
|
||||||
self._args_to_config(config, argname='enter_reason_list',
|
('exit_reason_list', 'Analysis exit tag list: {}'),
|
||||||
logstring='Analysis enter tag list: {}')
|
('indicator_list', 'Analysis indicator list: {}'),
|
||||||
|
('timerange', 'Filter trades by timerange: {}'),
|
||||||
self._args_to_config(config, argname='exit_reason_list',
|
('analysis_rejected', 'Analyse rejected signals: {}'),
|
||||||
logstring='Analysis exit tag list: {}')
|
('analysis_to_csv', 'Store analysis tables to CSV: {}'),
|
||||||
|
('analysis_csv_path', 'Path to store analysis CSVs: {}'),
|
||||||
self._args_to_config(config, argname='indicator_list',
|
|
||||||
logstring='Analysis indicator list: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='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: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='analysis_csv_path',
|
|
||||||
logstring='Path to store analysis CSVs: {}')
|
|
||||||
|
|
||||||
# Lookahead analysis results
|
# Lookahead analysis results
|
||||||
self._args_to_config(config, argname='targeted_trade_amount',
|
('targeted_trade_amount', 'Targeted Trade amount: {}'),
|
||||||
logstring='Targeted Trade amount: {}')
|
('minimum_trade_amount', 'Minimum Trade amount: {}'),
|
||||||
|
('lookahead_analysis_exportfilename', 'Path to store lookahead-analysis-results: {}'),
|
||||||
|
('startup_candle', 'Startup candle to be used on recursive analysis: {}'),
|
||||||
|
]
|
||||||
|
self._args_to_config_loop(config, configurations)
|
||||||
|
|
||||||
self._args_to_config(config, argname='minimum_trade_amount',
|
def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None:
|
||||||
logstring='Minimum Trade amount: {}')
|
|
||||||
|
|
||||||
self._args_to_config(config, argname='lookahead_analysis_exportfilename',
|
for argname, logstring in configurations:
|
||||||
logstring='Path to store lookahead-analysis-results: {}')
|
self._args_to_config(config, argname=argname, logstring=logstring)
|
||||||
|
|
||||||
self._args_to_config(config, argname='startup_candle',
|
|
||||||
logstring='Startup candle to be used on recursive analysis: {}')
|
|
||||||
|
|
||||||
def _process_runmode(self, config: Config) -> None:
|
def _process_runmode(self, config: Config) -> None:
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from freqtrade.misc import deep_merge_dicts
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_var_typed(val):
|
def _get_var_typed(val):
|
||||||
try:
|
try:
|
||||||
return int(val)
|
return int(val)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -24,7 +24,7 @@ def get_var_typed(val):
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str, Any]:
|
def _flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Environment variables must be prefixed with FREQTRADE.
|
Environment variables must be prefixed with FREQTRADE.
|
||||||
FREQTRADE__{section}__{key}
|
FREQTRADE__{section}__{key}
|
||||||
@@ -40,7 +40,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
|||||||
logger.info(f"Loading variable '{env_var}'")
|
logger.info(f"Loading variable '{env_var}'")
|
||||||
key = env_var.replace(prefix, '')
|
key = env_var.replace(prefix, '')
|
||||||
for k in reversed(key.split('__')):
|
for k in reversed(key.split('__')):
|
||||||
val = {k.lower(): get_var_typed(val)
|
val = {k.lower(): _get_var_typed(val)
|
||||||
if not isinstance(val, dict) and k not in no_convert else val}
|
if not isinstance(val, dict) and k not in no_convert else val}
|
||||||
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||||
return relevant_vars
|
return relevant_vars
|
||||||
@@ -52,4 +52,4 @@ def enironment_vars_to_dict() -> Dict[str, Any]:
|
|||||||
Relevant variables must follow the FREQTRADE__{section}__{key} pattern
|
Relevant variables must follow the FREQTRADE__{section}__{key} pattern
|
||||||
:return: Nested dict based on available and relevant variables.
|
:return: Nested dict based on available and relevant variables.
|
||||||
"""
|
"""
|
||||||
return flat_vars_to_nested_dict(os.environ.copy(), ENV_VAR_PREFIX)
|
return _flat_vars_to_nested_dict(os.environ.copy(), ENV_VAR_PREFIX)
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ SUPPORTED_FIAT = [
|
|||||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||||
"RUB", "UAH", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR",
|
"RUB", "UAH", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR",
|
||||||
"USD", "BTC", "ETH", "XRP", "LTC", "BCH"
|
"USD", "BTC", "ETH", "XRP", "LTC", "BCH", "BNB"
|
||||||
]
|
]
|
||||||
|
|
||||||
MINIMAL_CONFIG = {
|
MINIMAL_CONFIG = {
|
||||||
|
|||||||
@@ -175,36 +175,40 @@ def _get_backtest_files(dirname: Path) -> List[Path]:
|
|||||||
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
||||||
|
|
||||||
|
|
||||||
def get_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||||
"""
|
metadata = load_backtest_metadata(filename)
|
||||||
Get backtest result read from metadata file
|
|
||||||
"""
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'filename': filename.stem,
|
'filename': filename.stem,
|
||||||
'strategy': s,
|
'strategy': s,
|
||||||
'notes': v.get('notes', ''),
|
|
||||||
'run_id': v['run_id'],
|
'run_id': v['run_id'],
|
||||||
|
'notes': v.get('notes', ''),
|
||||||
|
# Backtest "run" time
|
||||||
'backtest_start_time': v['backtest_start_time'],
|
'backtest_start_time': v['backtest_start_time'],
|
||||||
} for s, v in load_backtest_metadata(filename).items()
|
# Backtest timerange
|
||||||
|
'backtest_start_ts': v.get('backtest_start_ts', None),
|
||||||
|
'backtest_end_ts': v.get('backtest_end_ts', None),
|
||||||
|
'timeframe': v.get('timeframe', None),
|
||||||
|
'timeframe_detail': v.get('timeframe_detail', None),
|
||||||
|
} for s, v in metadata.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||||
|
"""
|
||||||
|
Get backtest result read from metadata file
|
||||||
|
"""
|
||||||
|
return _extract_backtest_result(filename)
|
||||||
|
|
||||||
|
|
||||||
def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]:
|
def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]:
|
||||||
"""
|
"""
|
||||||
Get list of backtest results read from metadata files
|
Get list of backtest results read from metadata files
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
{
|
result
|
||||||
'filename': filename.stem,
|
|
||||||
'strategy': s,
|
|
||||||
'run_id': v['run_id'],
|
|
||||||
'notes': v.get('notes', ''),
|
|
||||||
'backtest_start_time': v['backtest_start_time'],
|
|
||||||
}
|
|
||||||
for filename in _get_backtest_files(dirname)
|
for filename in _get_backtest_files(dirname)
|
||||||
for s, v in load_backtest_metadata(filename).items()
|
for result in _extract_backtest_result(filename)
|
||||||
if v
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -326,7 +330,10 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
|||||||
"Please specify a strategy.")
|
"Please specify a strategy.")
|
||||||
|
|
||||||
if strategy not in data['strategy']:
|
if strategy not in data['strategy']:
|
||||||
raise ValueError(f"Strategy {strategy} not available in the backtest result.")
|
raise ValueError(
|
||||||
|
f"Strategy {strategy} not available in the backtest result. "
|
||||||
|
f"Available strategies are '{','.join(data['strategy'].keys())}'"
|
||||||
|
)
|
||||||
|
|
||||||
data = data['strategy'][strategy]['trades']
|
data = data['strategy'][strategy]['trades']
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
@@ -350,10 +357,10 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
|
|||||||
:param timeframe: Timeframe used for backtest
|
:param timeframe: Timeframe used for backtest
|
||||||
:return: dataframe with open-counts per time-period in timeframe
|
:return: dataframe with open-counts per time-period in timeframe
|
||||||
"""
|
"""
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_resample_freq
|
||||||
timeframe_min = timeframe_to_minutes(timeframe)
|
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
||||||
dates = [pd.Series(pd.date_range(row[1]['open_date'], row[1]['close_date'],
|
dates = [pd.Series(pd.date_range(row[1]['open_date'], row[1]['close_date'],
|
||||||
freq=f"{timeframe_min}min"))
|
freq=timeframe_freq))
|
||||||
for row in results[['open_date', 'close_date']].iterrows()]
|
for row in results[['open_date', 'close_date']].iterrows()]
|
||||||
deltas = [len(x) for x in dates]
|
deltas = [len(x) for x in dates]
|
||||||
dates = pd.Series(pd.concat(dates).values, name='date')
|
dates = pd.Series(pd.concat(dates).values, name='date')
|
||||||
@@ -361,7 +368,7 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
|
|||||||
|
|
||||||
df2 = pd.concat([dates, df2], axis=1)
|
df2 = pd.concat([dates, df2], axis=1)
|
||||||
df2 = df2.set_index('date')
|
df2 = df2.set_index('date')
|
||||||
df_final = df2.resample(f"{timeframe_min}min")[['pair']].count()
|
df_final = df2.resample(timeframe_freq)[['pair']].count()
|
||||||
df_final = df_final.rename({'pair': 'open_trades'}, axis=1)
|
df_final = df_final.rename({'pair': 'open_trades'}, axis=1)
|
||||||
return df_final
|
return df_final
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
|||||||
using the previous close as price for "open", "high" "low" and "close", volume is set to 0
|
using the previous close as price for "open", "high" "low" and "close", volume is set to 0
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_resample_freq
|
||||||
|
|
||||||
ohlcv_dict = {
|
ohlcv_dict = {
|
||||||
'open': 'first',
|
'open': 'first',
|
||||||
@@ -93,13 +93,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
|||||||
'close': 'last',
|
'close': 'last',
|
||||||
'volume': 'sum'
|
'volume': 'sum'
|
||||||
}
|
}
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
resample_interval = timeframe_to_resample_freq(timeframe)
|
||||||
resample_interval = f'{timeframe_minutes}min'
|
|
||||||
if timeframe_minutes >= 43200 and timeframe_minutes < 525600:
|
|
||||||
# Monthly candles need special treatment to stick to the 1st of the month
|
|
||||||
resample_interval = f'{timeframe}S'
|
|
||||||
elif timeframe_minutes > 43200:
|
|
||||||
resample_interval = timeframe
|
|
||||||
# Resample to create "NAN" values
|
# Resample to create "NAN" values
|
||||||
df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict)
|
df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict)
|
||||||
|
|
||||||
@@ -116,8 +110,8 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
|||||||
len_after = len(df)
|
len_after = len(df)
|
||||||
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
||||||
if len_before != len_after:
|
if len_before != len_after:
|
||||||
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
|
message = (f"Missing data fillup for {pair}, {timeframe}: "
|
||||||
f" - {pct_missing:.2%}")
|
f"before: {len_before} - after: {len_after} - {pct_missing:.2%}")
|
||||||
if pct_missing > 0.01:
|
if pct_missing > 0.01:
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from freqtrade.configuration import TimeRange
|
|||||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES,
|
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES,
|
||||||
Config, TradeList)
|
Config, TradeList)
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -69,14 +70,13 @@ def trades_to_ohlcv(trades: DataFrame, timeframe: str) -> DataFrame:
|
|||||||
:return: OHLCV Dataframe.
|
:return: OHLCV Dataframe.
|
||||||
:raises: ValueError if no trades are provided
|
:raises: ValueError if no trades are provided
|
||||||
"""
|
"""
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_resample_freq
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
|
||||||
if trades.empty:
|
if trades.empty:
|
||||||
raise ValueError('Trade-list empty.')
|
raise ValueError('Trade-list empty.')
|
||||||
df = trades.set_index('date', drop=True)
|
df = trades.set_index('date', drop=True)
|
||||||
|
resample_interval = timeframe_to_resample_freq(timeframe)
|
||||||
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
df_new = df['price'].resample(resample_interval).ohlc()
|
||||||
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
df_new['volume'] = df['amount'].resample(resample_interval).sum()
|
||||||
df_new['date'] = df_new.index
|
df_new['date'] = df_new.index
|
||||||
# Drop 0 volume rows
|
# Drop 0 volume rows
|
||||||
df_new = df_new.dropna()
|
df_new = df_new.dropna()
|
||||||
@@ -127,6 +127,16 @@ def convert_trades_format(config: Config, convert_from: str, convert_to: str, er
|
|||||||
:param convert_to: Target format
|
:param convert_to: Target format
|
||||||
:param erase: Erase source data (does not apply if source and target format are identical)
|
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||||
"""
|
"""
|
||||||
|
if convert_from == 'kraken_csv':
|
||||||
|
if config['exchange']['name'] != 'kraken':
|
||||||
|
raise OperationalException(
|
||||||
|
'Converting from csv is only supported for kraken.'
|
||||||
|
'Please refer to the documentation for details about this special mode.'
|
||||||
|
)
|
||||||
|
from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv
|
||||||
|
import_kraken_trades_from_csv(config, convert_to)
|
||||||
|
return
|
||||||
|
|
||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
trg = get_datahandler(config['datadir'], convert_to)
|
trg = get_datahandler(config['datadir'], convert_to)
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, Config
|
||||||
|
from freqtrade.data.converter.trade_converter import (trades_convert_types,
|
||||||
|
trades_df_remove_duplicates)
|
||||||
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
KRAKEN_CSV_TRADE_COLUMNS = ['timestamp', 'price', 'amount']
|
||||||
|
|
||||||
|
|
||||||
|
def import_kraken_trades_from_csv(config: Config, convert_to: str):
|
||||||
|
"""
|
||||||
|
Import kraken trades from csv
|
||||||
|
"""
|
||||||
|
if config['exchange']['name'] != 'kraken':
|
||||||
|
raise OperationalException('This function is only for the kraken exchange.')
|
||||||
|
|
||||||
|
datadir: Path = config['datadir']
|
||||||
|
data_handler = get_datahandler(datadir, data_format=convert_to)
|
||||||
|
|
||||||
|
tradesdir: Path = config['datadir'] / 'trades_csv'
|
||||||
|
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||||
|
# iterate through directories in this directory
|
||||||
|
data_symbols = {p.stem for p in tradesdir.rglob('*.csv')}
|
||||||
|
|
||||||
|
# create pair/filename mapping
|
||||||
|
markets = {
|
||||||
|
(m['symbol'], m['altname']) for m in exchange.markets.values()
|
||||||
|
if m.get('altname') in data_symbols
|
||||||
|
}
|
||||||
|
logger.info(f"Found csv files for {', '.join(data_symbols)}.")
|
||||||
|
|
||||||
|
for pair, name in markets:
|
||||||
|
dfs = []
|
||||||
|
# Load and combine all csv files for this pair
|
||||||
|
for f in tradesdir.rglob(f"{name}.csv"):
|
||||||
|
df = pd.read_csv(f, names=KRAKEN_CSV_TRADE_COLUMNS)
|
||||||
|
dfs.append(df)
|
||||||
|
|
||||||
|
# Load existing trades data
|
||||||
|
if not dfs:
|
||||||
|
# edgecase, can only happen if the file was deleted between the above glob and here
|
||||||
|
logger.info(f"No data found for pair {pair}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
trades = pd.concat(dfs, ignore_index=True)
|
||||||
|
|
||||||
|
trades.loc[:, 'timestamp'] = trades['timestamp'] * 1e3
|
||||||
|
trades.loc[:, 'cost'] = trades['price'] * trades['amount']
|
||||||
|
for col in DEFAULT_TRADES_COLUMNS:
|
||||||
|
if col not in trades.columns:
|
||||||
|
trades[col] = ''
|
||||||
|
|
||||||
|
trades = trades[DEFAULT_TRADES_COLUMNS]
|
||||||
|
trades = trades_convert_types(trades)
|
||||||
|
|
||||||
|
trades_df = trades_df_remove_duplicates(trades)
|
||||||
|
logger.info(f"{pair}: {len(trades_df)} trades, from "
|
||||||
|
f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to "
|
||||||
|
f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}")
|
||||||
|
|
||||||
|
data_handler.trades_store(pair, trades_df)
|
||||||
@@ -311,11 +311,13 @@ class DataProvider:
|
|||||||
timerange = TimeRange.parse_timerange(None if self._config.get(
|
timerange = TimeRange.parse_timerange(None if self._config.get(
|
||||||
'timerange') is None else str(self._config.get('timerange')))
|
'timerange') is None else str(self._config.get('timerange')))
|
||||||
|
|
||||||
# It is not necessary to add the training candles, as they
|
startup_candles = self.get_required_startup(str(timeframe))
|
||||||
# were already added at the beginning of the backtest.
|
|
||||||
startup_candles = self.get_required_startup(str(timeframe), False)
|
|
||||||
tf_seconds = timeframe_to_seconds(str(timeframe))
|
tf_seconds = timeframe_to_seconds(str(timeframe))
|
||||||
timerange.subtract_start(tf_seconds * startup_candles)
|
timerange.subtract_start(tf_seconds * startup_candles)
|
||||||
|
|
||||||
|
logger.info(f"Loading data for {pair} {timeframe} "
|
||||||
|
f"from {timerange.start_fmt} to {timerange.stop_fmt}")
|
||||||
|
|
||||||
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
@@ -327,7 +329,7 @@ class DataProvider:
|
|||||||
)
|
)
|
||||||
return self.__cached_pairs_backtesting[saved_pair].copy()
|
return self.__cached_pairs_backtesting[saved_pair].copy()
|
||||||
|
|
||||||
def get_required_startup(self, timeframe: str, add_train_candles: bool = True) -> int:
|
def get_required_startup(self, timeframe: str) -> int:
|
||||||
freqai_config = self._config.get('freqai', {})
|
freqai_config = self._config.get('freqai', {})
|
||||||
if not freqai_config.get('enabled', False):
|
if not freqai_config.get('enabled', False):
|
||||||
return self._config.get('startup_candle_count', 0)
|
return self._config.get('startup_candle_count', 0)
|
||||||
@@ -337,11 +339,10 @@ class DataProvider:
|
|||||||
# make sure the startupcandles is at least the set maximum indicator periods
|
# make sure the startupcandles is at least the set maximum indicator periods
|
||||||
self._config['startup_candle_count'] = max(startup_candles, max(indicator_periods))
|
self._config['startup_candle_count'] = max(startup_candles, max(indicator_periods))
|
||||||
tf_seconds = timeframe_to_seconds(timeframe)
|
tf_seconds = timeframe_to_seconds(timeframe)
|
||||||
train_candles = 0
|
|
||||||
if add_train_candles:
|
|
||||||
train_candles = freqai_config['train_period_days'] * 86400 / tf_seconds
|
train_candles = freqai_config['train_period_days'] * 86400 / tf_seconds
|
||||||
total_candles = int(self._config['startup_candle_count'] + train_candles)
|
total_candles = int(self._config['startup_candle_count'] + train_candles)
|
||||||
logger.info(f'Increasing startup_candle_count for freqai to {total_candles}')
|
logger.info(
|
||||||
|
f'Increasing startup_candle_count for freqai on {timeframe} to {total_candles}')
|
||||||
return total_candles
|
return total_candles
|
||||||
|
|
||||||
def get_pair_dataframe(
|
def get_pair_dataframe(
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ def prepare_results(analysed_trades, stratname,
|
|||||||
timerange=None):
|
timerange=None):
|
||||||
res_df = pd.DataFrame()
|
res_df = pd.DataFrame()
|
||||||
for pair, trades in analysed_trades[stratname].items():
|
for pair, trades in analysed_trades[stratname].items():
|
||||||
|
if (trades.shape[0] > 0):
|
||||||
trades.dropna(subset=['close_date'], inplace=True)
|
trades.dropna(subset=['close_date'], inplace=True)
|
||||||
res_df = pd.concat([res_df, trades], ignore_index=True)
|
res_df = pd.concat([res_df, trades], ignore_index=True)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pandas import DataFrame, concat
|
|||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
||||||
DL_DATA_TIMEFRAMES, Config)
|
DL_DATA_TIMEFRAMES, DOCS_LINK, Config)
|
||||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_ohlcv,
|
from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_ohlcv,
|
||||||
ohlcv_to_dataframe, trades_df_remove_duplicates,
|
ohlcv_to_dataframe, trades_df_remove_duplicates,
|
||||||
trades_list_to_df)
|
trades_list_to_df)
|
||||||
@@ -18,8 +18,8 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||||
from freqtrade.util import dt_ts, format_ms_time
|
from freqtrade.util import dt_ts, format_ms_time
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
|
||||||
from freqtrade.util.datetime_helpers import dt_now
|
from freqtrade.util.datetime_helpers import dt_now
|
||||||
|
from freqtrade.util.migrations import migrate_data
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -311,15 +311,19 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
# Predefined candletype (and timeframe) depending on exchange
|
# Predefined candletype (and timeframe) depending on exchange
|
||||||
# Downloads what is necessary to backtest based on futures data.
|
# Downloads what is necessary to backtest based on futures data.
|
||||||
tf_mark = exchange.get_option('mark_ohlcv_timeframe')
|
tf_mark = exchange.get_option('mark_ohlcv_timeframe')
|
||||||
|
tf_funding_rate = exchange.get_option('funding_fee_timeframe')
|
||||||
|
|
||||||
fr_candle_type = CandleType.from_string(exchange.get_option('mark_ohlcv_price'))
|
fr_candle_type = CandleType.from_string(exchange.get_option('mark_ohlcv_price'))
|
||||||
# All exchanges need FundingRate for futures trading.
|
# All exchanges need FundingRate for futures trading.
|
||||||
# The timeframe is aligned to the mark-price timeframe.
|
# The timeframe is aligned to the mark-price timeframe.
|
||||||
for funding_candle_type in (CandleType.FUNDING_RATE, fr_candle_type):
|
combs = ((CandleType.FUNDING_RATE, tf_funding_rate), (fr_candle_type, tf_mark))
|
||||||
|
for candle_type_f, tf in combs:
|
||||||
|
logger.debug(f'Downloading pair {pair}, {candle_type_f}, interval {tf}.')
|
||||||
_download_pair_history(pair=pair, process=process,
|
_download_pair_history(pair=pair, process=process,
|
||||||
datadir=datadir, exchange=exchange,
|
datadir=datadir, exchange=exchange,
|
||||||
timerange=timerange, data_handler=data_handler,
|
timerange=timerange, data_handler=data_handler,
|
||||||
timeframe=str(tf_mark), new_pairs_days=new_pairs_days,
|
timeframe=str(tf), new_pairs_days=new_pairs_days,
|
||||||
candle_type=funding_candle_type,
|
candle_type=candle_type_f,
|
||||||
erase=erase, prepend=prepend)
|
erase=erase, prepend=prepend)
|
||||||
|
|
||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
@@ -500,6 +504,12 @@ def download_data_main(config: Config) -> None:
|
|||||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||||
|
|
||||||
|
if len(expanded_pairs) == 0:
|
||||||
|
logger.warning(
|
||||||
|
"No pairs available for download. "
|
||||||
|
"Please make sure you're using the correct Pair naming for your selected trade mode. \n"
|
||||||
|
f"More info: {DOCS_LINK}/bot-basics/#pair-naming")
|
||||||
|
|
||||||
for timeframe in config['timeframes']:
|
for timeframe in config['timeframes']:
|
||||||
exchange.validate_timeframes(timeframe)
|
exchange.validate_timeframes(timeframe)
|
||||||
|
|
||||||
@@ -527,7 +537,7 @@ def download_data_main(config: Config) -> None:
|
|||||||
"Please use `--dl-trades` instead for this exchange "
|
"Please use `--dl-trades` instead for this exchange "
|
||||||
"(will unfortunately take a long time)."
|
"(will unfortunately take a long time)."
|
||||||
)
|
)
|
||||||
migrate_binance_futures_data(config)
|
migrate_data(config, exchange)
|
||||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
datadir=config['datadir'], timerange=timerange,
|
datadir=config['datadir'], timerange=timerange,
|
||||||
|
|||||||
@@ -94,21 +94,22 @@ class IDataHandler(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def ohlcv_data_min_max(self, pair: str, timeframe: str,
|
def ohlcv_data_min_max(self, pair: str, timeframe: str,
|
||||||
candle_type: CandleType) -> Tuple[datetime, datetime]:
|
candle_type: CandleType) -> Tuple[datetime, datetime, int]:
|
||||||
"""
|
"""
|
||||||
Returns the min and max timestamp for the given pair and timeframe.
|
Returns the min and max timestamp for the given pair and timeframe.
|
||||||
:param pair: Pair to get min/max for
|
:param pair: Pair to get min/max for
|
||||||
:param timeframe: Timeframe to get min/max for
|
:param timeframe: Timeframe to get min/max for
|
||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
:return: (min, max)
|
:return: (min, max, len)
|
||||||
"""
|
"""
|
||||||
data = self._ohlcv_load(pair, timeframe, None, candle_type)
|
df = self._ohlcv_load(pair, timeframe, None, candle_type)
|
||||||
if data.empty:
|
if df.empty:
|
||||||
return (
|
return (
|
||||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||||
datetime.fromtimestamp(0, tz=timezone.utc)
|
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
return data.iloc[0]['date'].to_pydatetime(), data.iloc[-1]['date'].to_pydatetime()
|
return df.iloc[0]['date'].to_pydatetime(), df.iloc[-1]['date'].to_pydatetime(), len(df)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange],
|
def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange],
|
||||||
@@ -403,6 +404,34 @@ class IDataHandler(ABC):
|
|||||||
return
|
return
|
||||||
file_old.rename(file_new)
|
file_old.rename(file_new)
|
||||||
|
|
||||||
|
def fix_funding_fee_timeframe(self, ff_timeframe: str):
|
||||||
|
"""
|
||||||
|
Temporary method to migrate data from old funding fee timeframe to the correct timeframe
|
||||||
|
Applies to bybit and okx, where funding-fee and mark candles have different timeframes.
|
||||||
|
"""
|
||||||
|
paircombs = self.ohlcv_get_available_data(self._datadir, TradingMode.FUTURES)
|
||||||
|
funding_rate_combs = [
|
||||||
|
f for f in paircombs if f[2] == CandleType.FUNDING_RATE and f[1] != ff_timeframe
|
||||||
|
]
|
||||||
|
|
||||||
|
if funding_rate_combs:
|
||||||
|
logger.warning(
|
||||||
|
f'Migrating {len(funding_rate_combs)} funding fees to correct timeframe.')
|
||||||
|
|
||||||
|
for pair, timeframe, candletype in funding_rate_combs:
|
||||||
|
old_name = self._pair_data_filename(self._datadir, pair, timeframe, candletype)
|
||||||
|
new_name = self._pair_data_filename(self._datadir, pair, ff_timeframe, candletype)
|
||||||
|
|
||||||
|
if not Path(old_name).exists():
|
||||||
|
logger.warning(f'{old_name} does not exist, skipping.')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if Path(new_name).exists():
|
||||||
|
logger.warning(f'{new_name} already exists, Removing.')
|
||||||
|
Path(new_name).unlink()
|
||||||
|
|
||||||
|
Path(old_name).rename(new_name)
|
||||||
|
|
||||||
|
|
||||||
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
|||||||
"""
|
"""
|
||||||
if len(trades) == 0:
|
if len(trades) == 0:
|
||||||
raise ValueError("Trade dataframe empty.")
|
raise ValueError("Trade dataframe empty.")
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_resample_freq
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
||||||
# Resample to timeframe to make sure trades match candles
|
# Resample to timeframe to make sure trades match candles
|
||||||
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date'
|
_trades_sum = trades.resample(timeframe_freq, on='close_date'
|
||||||
)[['profit_abs']].sum()
|
)[['profit_abs']].sum()
|
||||||
df.loc[:, col_name] = _trades_sum['profit_abs'].cumsum()
|
df.loc[:, col_name] = _trades_sum['profit_abs'].cumsum()
|
||||||
# Set first value to 0
|
# Set first value to 0
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_
|
|||||||
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.bitmart import Bitmart
|
||||||
from freqtrade.exchange.bitpanda import Bitpanda
|
from freqtrade.exchange.bitpanda import Bitpanda
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
|
||||||
from freqtrade.exchange.bitvavo import Bitvavo
|
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
|
||||||
@@ -17,10 +17,11 @@ from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_c
|
|||||||
market_is_active, price_to_precision,
|
market_is_active, price_to_precision,
|
||||||
timeframe_to_minutes, timeframe_to_msecs,
|
timeframe_to_minutes, timeframe_to_msecs,
|
||||||
timeframe_to_next_date, timeframe_to_prev_date,
|
timeframe_to_next_date, timeframe_to_prev_date,
|
||||||
timeframe_to_seconds, validate_exchange)
|
timeframe_to_resample_freq, timeframe_to_seconds,
|
||||||
|
validate_exchange)
|
||||||
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.htx import Htx
|
||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
from freqtrade.exchange.kucoin import Kucoin
|
from freqtrade.exchange.kucoin import Kucoin
|
||||||
from freqtrade.exchange.okx import Okx
|
from freqtrade.exchange.okx import Okx
|
||||||
|
|||||||
@@ -123,10 +123,14 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
def funding_fee_cutoff(self, open_date: datetime):
|
def funding_fee_cutoff(self, open_date: datetime):
|
||||||
"""
|
"""
|
||||||
|
Funding fees are only charged at full hours (usually every 4-8h).
|
||||||
|
Therefore a trade opening at 10:00:01 will not be charged a funding fee until the next hour.
|
||||||
|
On binance, this cutoff is 15s.
|
||||||
|
https://github.com/freqtrade/freqtrade/pull/5779#discussion_r740175931
|
||||||
:param open_date: The open date for a trade
|
:param open_date: The open date for a trade
|
||||||
:return: The cutoff open time for when a funding fee is charged
|
:return: True if the date falls on a full hour, False otherwise
|
||||||
"""
|
"""
|
||||||
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)
|
return open_date.minute == 0 and open_date.second < 15
|
||||||
|
|
||||||
def dry_run_liquidation_price(
|
def dry_run_liquidation_price(
|
||||||
self,
|
self,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
""" Bitmart exchange subclass """
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Bitmart(Exchange):
|
||||||
|
"""
|
||||||
|
Bitmart exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
|
with this exchange.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": False, # Bitmart API does not support stoploss orders
|
||||||
|
"ohlcv_candle_limit": 200,
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
""" Bittrex exchange subclass """
|
|
||||||
import logging
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from freqtrade.exchange import Exchange
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Bittrex(Exchange):
|
|
||||||
"""
|
|
||||||
Bittrex exchange class. Contains adjustments needed for Freqtrade to work
|
|
||||||
with this exchange.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_ft_has: Dict = {
|
|
||||||
"ohlcv_candle_limit_per_timeframe": {
|
|
||||||
'1m': 1440,
|
|
||||||
'5m': 288,
|
|
||||||
'1h': 744,
|
|
||||||
'1d': 365,
|
|
||||||
},
|
|
||||||
"l2_limit_range": [1, 25, 500],
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import ccxt
|
|||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||||
@@ -29,6 +29,7 @@ class Bybit(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
@@ -202,8 +203,11 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
# Bybit does not provide "applied" funding fees per position.
|
# Bybit does not provide "applied" funding fees per position.
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
try:
|
||||||
return self._fetch_and_calculate_funding_fees(
|
return self._fetch_and_calculate_funding_fees(
|
||||||
pair, amount, is_short, open_date)
|
pair, amount, is_short, open_date)
|
||||||
|
except ExchangeError:
|
||||||
|
logger.warning(f"Could not update funding fees for {pair}.")
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
|
def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
|
||||||
|
|||||||
@@ -48,13 +48,14 @@ MAP_EXCHANGE_CHILDCLASS = {
|
|||||||
'binanceusdm': 'binance',
|
'binanceusdm': 'binance',
|
||||||
'okex': 'okx',
|
'okex': 'okx',
|
||||||
'gateio': 'gate',
|
'gateio': 'gate',
|
||||||
|
'huboi': 'htx',
|
||||||
}
|
}
|
||||||
|
|
||||||
SUPPORTED_EXCHANGES = [
|
SUPPORTED_EXCHANGES = [
|
||||||
'binance',
|
'binance',
|
||||||
'bittrex',
|
'bitmart',
|
||||||
'gate',
|
'gate',
|
||||||
'huobi',
|
'htx',
|
||||||
'kraken',
|
'kraken',
|
||||||
'okx',
|
'okx',
|
||||||
]
|
]
|
||||||
|
|||||||
+104
-56
@@ -80,6 +80,7 @@ class Exchange:
|
|||||||
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
||||||
"mark_ohlcv_price": "mark",
|
"mark_ohlcv_price": "mark",
|
||||||
"mark_ohlcv_timeframe": "8h",
|
"mark_ohlcv_timeframe": "8h",
|
||||||
|
"funding_fee_timeframe": "8h",
|
||||||
"ccxt_futures_name": "swap",
|
"ccxt_futures_name": "swap",
|
||||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||||
@@ -121,11 +122,12 @@ class Exchange:
|
|||||||
# Cache for 10 minutes ...
|
# Cache for 10 minutes ...
|
||||||
self._cache_lock = Lock()
|
self._cache_lock = Lock()
|
||||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
|
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
|
||||||
# Cache values for 1800 to avoid frequent polling of the exchange for prices
|
# Cache values for 300 to avoid frequent polling of the exchange for prices
|
||||||
# Caching only applies to RPC methods, so prices for open trades are still
|
# Caching only applies to RPC methods, so prices for open trades are still
|
||||||
# refreshed once every iteration.
|
# refreshed once every iteration.
|
||||||
self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
# Shouldn't be too high either, as it'll freeze UI updates in case of open orders.
|
||||||
self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
|
||||||
|
self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
|
||||||
|
|
||||||
# Holds candles
|
# Holds candles
|
||||||
self._klines: Dict[PairWithTimeframe, DataFrame] = {}
|
self._klines: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
@@ -319,10 +321,11 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _log_exchange_response(self, endpoint, response) -> None:
|
def _log_exchange_response(self, endpoint: str, response, *, add_info=None) -> None:
|
||||||
""" Log exchange responses """
|
""" Log exchange responses """
|
||||||
if self.log_responses:
|
if self.log_responses:
|
||||||
logger.info(f"API {endpoint}: {response}")
|
add_info_str = "" if add_info is None else f" {add_info}: "
|
||||||
|
logger.info(f"API {endpoint}: {add_info_str}{response}")
|
||||||
|
|
||||||
def ohlcv_candle_limit(
|
def ohlcv_candle_limit(
|
||||||
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
||||||
@@ -330,6 +333,7 @@ class Exchange:
|
|||||||
Exchange ohlcv candle limit
|
Exchange ohlcv candle limit
|
||||||
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
|
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
|
||||||
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
|
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
|
||||||
|
TODO: this is most likely no longer needed since only bittrex needed this.
|
||||||
:param timeframe: Timeframe to check
|
:param timeframe: Timeframe to check
|
||||||
:param candle_type: Candle-type
|
:param candle_type: Candle-type
|
||||||
:param since_ms: Starting timestamp
|
:param since_ms: Starting timestamp
|
||||||
@@ -486,11 +490,14 @@ class Exchange:
|
|||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception('Unable to initialize markets.')
|
logger.exception('Unable to initialize markets.')
|
||||||
|
|
||||||
def reload_markets(self) -> None:
|
def reload_markets(self, force: bool = False) -> None:
|
||||||
"""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 + self.markets_refresh_interval > dt_ts()):
|
not force
|
||||||
|
and self._last_markets_refresh > 0
|
||||||
|
and (self._last_markets_refresh + self.markets_refresh_interval > dt_ts())
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
logger.debug("Performing scheduled market reload..")
|
logger.debug("Performing scheduled market reload..")
|
||||||
try:
|
try:
|
||||||
@@ -1228,16 +1235,16 @@ class Exchange:
|
|||||||
return order
|
return order
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise InsufficientFundsError(
|
raise InsufficientFundsError(
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to sell amount {amount} at rate {limit_rate}. '
|
f'Tried to {side} amount {amount} at rate {limit_rate} with '
|
||||||
f'Message: {e}') from e
|
f'stop-price {stop_price_norm}. Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except (ccxt.InvalidOrder, ccxt.BadRequest) as e:
|
||||||
# Errors:
|
# Errors:
|
||||||
# `Order would trigger immediately.`
|
# `Order would trigger immediately.`
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f'Could not create {ordertype} sell order on market {pair}. '
|
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to sell amount {amount} at rate {limit_rate}. '
|
f'Tried to {side} amount {amount} at rate {limit_rate} with '
|
||||||
f'Message: {e}') from e
|
f'stop-price {stop_price_norm}. Message: {e}') from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
@@ -1380,7 +1387,7 @@ class Exchange:
|
|||||||
order = self.fetch_stoploss_order(order_id, pair)
|
order = self.fetch_stoploss_order(order_id, pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
|
logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
|
||||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
order = {'id': order_id, 'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
@@ -1496,8 +1503,9 @@ class Exchange:
|
|||||||
@retrier
|
@retrier
|
||||||
def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||||
"""
|
"""
|
||||||
|
:param symbols: List of symbols to fetch
|
||||||
:param cached: Allow cached result
|
:param cached: Allow cached result
|
||||||
:return: fetch_tickers result
|
:return: fetch_bids_asks result
|
||||||
"""
|
"""
|
||||||
if not self.exchange_has('fetchBidsAsks'):
|
if not self.exchange_has('fetchBidsAsks'):
|
||||||
return {}
|
return {}
|
||||||
@@ -1546,6 +1554,12 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self._api.name} does not support fetching tickers in batch. '
|
f'Exchange {self._api.name} does not support fetching tickers in batch. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
|
except ccxt.BadSymbol as e:
|
||||||
|
logger.warning(f"Could not load tickers due to {e.__class__.__name__}. Message: {e} ."
|
||||||
|
"Reloading markets.")
|
||||||
|
self.reload_markets(True)
|
||||||
|
# Re-raise exception to repeat the call.
|
||||||
|
raise TemporaryError from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
@@ -1954,7 +1968,7 @@ class Exchange:
|
|||||||
|
|
||||||
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||||
for res in results:
|
for res in results:
|
||||||
if isinstance(res, Exception):
|
if isinstance(res, BaseException):
|
||||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||||
if raise_:
|
if raise_:
|
||||||
raise
|
raise
|
||||||
@@ -2202,13 +2216,13 @@ class Exchange:
|
|||||||
@retrier_async
|
@retrier_async
|
||||||
async def _async_fetch_trades(self, pair: str,
|
async def _async_fetch_trades(self, pair: str,
|
||||||
since: Optional[int] = None,
|
since: Optional[int] = None,
|
||||||
params: Optional[dict] = None) -> List[List]:
|
params: Optional[dict] = None) -> Tuple[List[List], Any]:
|
||||||
"""
|
"""
|
||||||
Asyncronously gets trade history using fetch_trades.
|
Asyncronously gets trade history using fetch_trades.
|
||||||
Handles exchange errors, does one call to the exchange.
|
Handles exchange errors, does one call to the exchange.
|
||||||
:param pair: Pair to fetch trade data for
|
:param pair: Pair to fetch trade data for
|
||||||
:param since: Since as integer timestamp in milliseconds
|
:param since: Since as integer timestamp in milliseconds
|
||||||
returns: List of dicts containing trades
|
returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# fetch trades asynchronously
|
# fetch trades asynchronously
|
||||||
@@ -2223,7 +2237,8 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
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)
|
||||||
return trades_dict_to_list(trades)
|
pagination_value = self._get_trade_pagination_next_value(trades)
|
||||||
|
return trades_dict_to_list(trades), pagination_value
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self._api.name} does not support fetching historical trade data.'
|
f'Exchange {self._api.name} does not support fetching historical trade data.'
|
||||||
@@ -2236,6 +2251,25 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(f'Could not fetch trade data. Msg: {e}') from e
|
raise OperationalException(f'Could not fetch trade data. Msg: {e}') from e
|
||||||
|
|
||||||
|
def _valid_trade_pagination_id(self, pair: str, from_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Verify trade-pagination id is valid.
|
||||||
|
Workaround for odd Kraken issue where ID is sometimes wrong.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_trade_pagination_next_value(self, trades: List[Dict]):
|
||||||
|
"""
|
||||||
|
Extract pagination id for the next "from_id" value
|
||||||
|
Applies only to fetch_trade_history by id.
|
||||||
|
"""
|
||||||
|
if not trades:
|
||||||
|
return None
|
||||||
|
if self._trades_pagination == 'id':
|
||||||
|
return trades[-1].get('id')
|
||||||
|
else:
|
||||||
|
return trades[-1].get('timestamp')
|
||||||
|
|
||||||
async def _async_get_trade_history_id(self, pair: str,
|
async def _async_get_trade_history_id(self, pair: str,
|
||||||
until: int,
|
until: int,
|
||||||
since: Optional[int] = None,
|
since: Optional[int] = None,
|
||||||
@@ -2251,34 +2285,37 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
trades: List[List] = []
|
trades: List[List] = []
|
||||||
|
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||||
|
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||||
|
has_overlap = self._ft_has.get('trades_pagination_overlap', True)
|
||||||
|
# Skip last trade by default since its the key for the next call
|
||||||
|
x = slice(None, -1) if has_overlap else slice(None)
|
||||||
|
|
||||||
if not from_id:
|
if not from_id or not self._valid_trade_pagination_id(pair, from_id):
|
||||||
# Fetch first elements using timebased method to get an ID to paginate on
|
# Fetch first elements using timebased method to get an ID to paginate on
|
||||||
# Depending on the Exchange, this can introduce a drift at the start of the interval
|
# Depending on the Exchange, this can introduce a drift at the start of the interval
|
||||||
# of up to an hour.
|
# of up to an hour.
|
||||||
# e.g. Binance returns the "last 1000" candles within a 1h time interval
|
# e.g. Binance returns the "last 1000" candles within a 1h time interval
|
||||||
# - so we will miss the first trades.
|
# - so we will miss the first trades.
|
||||||
t = await self._async_fetch_trades(pair, since=since)
|
t, from_id = await self._async_fetch_trades(pair, since=since)
|
||||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
trades.extend(t[x])
|
||||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
|
||||||
from_id = t[-1][1]
|
|
||||||
trades.extend(t[:-1])
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
t = await self._async_fetch_trades(pair,
|
t, from_id_next = await self._async_fetch_trades(
|
||||||
params={self._trades_pagination_arg: from_id})
|
pair, params={self._trades_pagination_arg: from_id})
|
||||||
if t:
|
if t:
|
||||||
# Skip last id since its the key for the next call
|
trades.extend(t[x])
|
||||||
trades.extend(t[:-1])
|
if from_id == from_id_next or t[-1][0] > until:
|
||||||
if from_id == t[-1][1] or t[-1][0] > until:
|
|
||||||
logger.debug(f"Stopping because from_id did not change. "
|
logger.debug(f"Stopping because from_id did not change. "
|
||||||
f"Reached {t[-1][0]} > {until}")
|
f"Reached {t[-1][0]} > {until}")
|
||||||
# Reached the end of the defined-download period - add last trade as well.
|
# Reached the end of the defined-download period - add last trade as well.
|
||||||
|
if has_overlap:
|
||||||
trades.extend(t[-1:])
|
trades.extend(t[-1:])
|
||||||
break
|
break
|
||||||
|
|
||||||
from_id = t[-1][1]
|
from_id = from_id_next
|
||||||
else:
|
else:
|
||||||
|
logger.debug("Stopping as no more trades were returned.")
|
||||||
break
|
break
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||||
@@ -2302,16 +2339,22 @@ class Exchange:
|
|||||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
t = await self._async_fetch_trades(pair, since=since)
|
t, since_next = await self._async_fetch_trades(pair, since=since)
|
||||||
if t:
|
if t:
|
||||||
since = t[-1][0]
|
# No more trades to download available at the exchange,
|
||||||
|
# So we repeatedly get the same trade over and over again.
|
||||||
|
if since == since_next and len(t) == 1:
|
||||||
|
logger.debug("Stopping because no more trades are available.")
|
||||||
|
break
|
||||||
|
since = since_next
|
||||||
trades.extend(t)
|
trades.extend(t)
|
||||||
# Reached the end of the defined-download period
|
# Reached the end of the defined-download period
|
||||||
if until and t[-1][0] > until:
|
if until and since_next > until:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Stopping because until was reached. {t[-1][0]} > {until}")
|
f"Stopping because until was reached. {since_next} > {until}")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
logger.debug("Stopping as no more trades were returned.")
|
||||||
break
|
break
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||||
@@ -2396,6 +2439,8 @@ class Exchange:
|
|||||||
symbol=pair,
|
symbol=pair,
|
||||||
since=since
|
since=since
|
||||||
)
|
)
|
||||||
|
self._log_exchange_response('funding_history', funding_history,
|
||||||
|
add_info=f"pair: {pair}, since: {since}")
|
||||||
return sum(fee['amount'] for fee in funding_history)
|
return sum(fee['amount'] for fee in funding_history)
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
@@ -2653,12 +2698,14 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def funding_fee_cutoff(self, open_date: datetime):
|
def funding_fee_cutoff(self, open_date: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
|
Funding fees are only charged at full hours (usually every 4-8h).
|
||||||
|
Therefore a trade opening at 10:00:01 will not be charged a funding fee until the next hour.
|
||||||
:param open_date: The open date for a trade
|
:param open_date: The open date for a trade
|
||||||
:return: The cutoff open time for when a funding fee is charged
|
:return: True if the date falls on a full hour, False otherwise
|
||||||
"""
|
"""
|
||||||
return open_date.minute > 0 or open_date.second > 0
|
return open_date.minute == 0 and open_date.second == 0
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
|
def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
|
||||||
@@ -2706,23 +2753,23 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if self.funding_fee_cutoff(open_date):
|
if self.funding_fee_cutoff(open_date):
|
||||||
open_date += timedelta(hours=1)
|
# Shift back to 1h candle to avoid missing funding fees
|
||||||
|
# Only really relevant for trades very close to the full hour
|
||||||
|
open_date = timeframe_to_prev_date('1h', open_date)
|
||||||
timeframe = self._ft_has['mark_ohlcv_timeframe']
|
timeframe = self._ft_has['mark_ohlcv_timeframe']
|
||||||
timeframe_ff = self._ft_has.get('funding_fee_timeframe',
|
timeframe_ff = self._ft_has['funding_fee_timeframe']
|
||||||
self._ft_has['mark_ohlcv_timeframe'])
|
mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
|
||||||
|
|
||||||
if not close_date:
|
if not close_date:
|
||||||
close_date = datetime.now(timezone.utc)
|
close_date = datetime.now(timezone.utc)
|
||||||
open_timestamp = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
|
since_ms = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
|
||||||
# close_timestamp = int(close_date.timestamp()) * 1000
|
|
||||||
|
|
||||||
mark_comb: PairWithTimeframe = (
|
|
||||||
pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
|
|
||||||
|
|
||||||
|
mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
|
||||||
funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
||||||
|
|
||||||
candle_histories = self.refresh_latest_ohlcv(
|
candle_histories = self.refresh_latest_ohlcv(
|
||||||
[mark_comb, funding_comb],
|
[mark_comb, funding_comb],
|
||||||
since_ms=open_timestamp,
|
since_ms=since_ms,
|
||||||
cache=False,
|
cache=False,
|
||||||
drop_incomplete=False,
|
drop_incomplete=False,
|
||||||
)
|
)
|
||||||
@@ -2733,8 +2780,7 @@ class Exchange:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise ExchangeError("Could not find funding rates.") from None
|
raise ExchangeError("Could not find funding rates.") from None
|
||||||
|
|
||||||
funding_mark_rates = self.combine_funding_and_mark(
|
funding_mark_rates = self.combine_funding_and_mark(funding_rates, mark_rates)
|
||||||
funding_rates=funding_rates, mark_rates=mark_rates)
|
|
||||||
|
|
||||||
return self.calculate_funding_fees(
|
return self.calculate_funding_fees(
|
||||||
funding_mark_rates,
|
funding_mark_rates,
|
||||||
@@ -2781,7 +2827,7 @@ class Exchange:
|
|||||||
amount: float,
|
amount: float,
|
||||||
is_short: bool,
|
is_short: bool,
|
||||||
open_date: datetime,
|
open_date: datetime,
|
||||||
close_date: Optional[datetime] = None,
|
close_date: datetime,
|
||||||
time_in_ratio: Optional[float] = None
|
time_in_ratio: Optional[float] = None
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -2797,8 +2843,8 @@ class Exchange:
|
|||||||
fees: float = 0
|
fees: float = 0
|
||||||
|
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
|
df1 = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
|
||||||
fees = sum(df['open_fund'] * df['open_mark'] * amount)
|
fees = sum(df1['open_fund'] * df1['open_mark'] * amount)
|
||||||
|
|
||||||
# Negate fees for longs as funding_fees expects it this way based on live endpoints.
|
# Negate fees for longs as funding_fees expects it this way based on live endpoints.
|
||||||
return fees if is_short else -fees
|
return fees if is_short else -fees
|
||||||
@@ -2813,16 +2859,18 @@ class Exchange:
|
|||||||
:param amount: Trade amount
|
:param amount: Trade amount
|
||||||
:param open_date: Open date of the trade
|
:param open_date: Open date of the trade
|
||||||
:return: funding fee since open_date
|
:return: funding fee since open_date
|
||||||
:raises: ExchangeError if something goes wrong.
|
|
||||||
"""
|
"""
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
try:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
funding_fees = self._fetch_and_calculate_funding_fees(
|
funding_fees = self._fetch_and_calculate_funding_fees(
|
||||||
pair, amount, is_short, open_date)
|
pair, amount, is_short, open_date)
|
||||||
else:
|
else:
|
||||||
funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
|
funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
|
||||||
return funding_fees
|
return funding_fees
|
||||||
else:
|
except ExchangeError:
|
||||||
|
logger.warning(f"Could not update funding fees for {pair}.")
|
||||||
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def get_liquidation_price(
|
def get_liquidation_price(
|
||||||
|
|||||||
@@ -118,6 +118,27 @@ def timeframe_to_msecs(timeframe: str) -> int:
|
|||||||
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
||||||
|
|
||||||
|
|
||||||
|
def timeframe_to_resample_freq(timeframe: str) -> str:
|
||||||
|
"""
|
||||||
|
Translates the timeframe interval value written in the human readable
|
||||||
|
form ('1m', '5m', '1h', '1d', '1w', etc.) to the resample frequency
|
||||||
|
used by pandas ('1T', '5T', '1H', '1D', '1W', etc.)
|
||||||
|
"""
|
||||||
|
if timeframe == '1y':
|
||||||
|
return '1YS'
|
||||||
|
timeframe_seconds = timeframe_to_seconds(timeframe)
|
||||||
|
timeframe_minutes = timeframe_seconds // 60
|
||||||
|
resample_interval = f'{timeframe_seconds}s'
|
||||||
|
if 10000 < timeframe_minutes < 43200:
|
||||||
|
resample_interval = '1W-MON'
|
||||||
|
elif timeframe_minutes >= 43200 and timeframe_minutes < 525600:
|
||||||
|
# Monthly candles need special treatment to stick to the 1st of the month
|
||||||
|
resample_interval = f'{timeframe}S'
|
||||||
|
elif timeframe_minutes > 43200:
|
||||||
|
resample_interval = timeframe
|
||||||
|
return resample_interval
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
||||||
"""
|
"""
|
||||||
Use Timeframe and determine the candle start date for this date.
|
Use Timeframe and determine the candle start date for this date.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" Huobi exchange subclass """
|
""" HTX exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
@@ -9,9 +9,9 @@ from freqtrade.exchange import Exchange
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Huobi(Exchange):
|
class Htx(Exchange):
|
||||||
"""
|
"""
|
||||||
Huobi exchange class. Contains adjustments needed for Freqtrade to work
|
HTX exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
with this exchange.
|
with this exchange.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -8,11 +8,9 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
from freqtrade.exceptions import DDosProtection, 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
|
||||||
|
|
||||||
|
|
||||||
@@ -24,12 +22,15 @@ class Kraken(Exchange):
|
|||||||
_params: Dict = {"trading_agreement": "agree"}
|
_params: Dict = {"trading_agreement": "agree"}
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"stop_price_param": "stopPrice",
|
"stop_price_param": "stopLossPrice",
|
||||||
"stop_price_prop": "stopPrice",
|
"stop_price_prop": "stopLossPrice",
|
||||||
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
|
"order_time_in_force": ["GTC", "IOC", "PO"],
|
||||||
"ohlcv_candle_limit": 720,
|
"ohlcv_candle_limit": 720,
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
|
"trades_pagination_overlap": False,
|
||||||
"mark_ohlcv_timeframe": "4h",
|
"mark_ohlcv_timeframe": "4h",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,75 +90,6 @@ class Kraken(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
|
||||||
"""
|
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
|
||||||
Returns True if adjustment is necessary.
|
|
||||||
"""
|
|
||||||
return (order['type'] in ('stop-loss', 'stop-loss-limit') and (
|
|
||||||
(side == "sell" and stop_loss > float(order['price'])) or
|
|
||||||
(side == "buy" and stop_loss < float(order['price']))
|
|
||||||
))
|
|
||||||
|
|
||||||
@retrier(retries=0)
|
|
||||||
def create_stoploss(self, pair: str, amount: float, stop_price: float,
|
|
||||||
order_types: Dict, side: BuySell, leverage: float) -> Dict:
|
|
||||||
"""
|
|
||||||
Creates a stoploss market order.
|
|
||||||
Stoploss market orders is the only stoploss type supported by kraken.
|
|
||||||
TODO: investigate if this can be combined with generic implementation
|
|
||||||
(careful, prices are reversed)
|
|
||||||
"""
|
|
||||||
params = self._params.copy()
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
params.update({'reduceOnly': True})
|
|
||||||
|
|
||||||
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
|
||||||
if order_types.get('stoploss', 'market') == 'limit':
|
|
||||||
ordertype = "stop-loss-limit"
|
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
|
||||||
if side == "sell":
|
|
||||||
limit_rate = stop_price * limit_price_pct
|
|
||||||
else:
|
|
||||||
limit_rate = stop_price * (2 - limit_price_pct)
|
|
||||||
params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
|
||||||
else:
|
|
||||||
ordertype = "stop-loss"
|
|
||||||
|
|
||||||
stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
|
||||||
|
|
||||||
if self._config['dry_run']:
|
|
||||||
dry_order = self.create_dry_run_order(
|
|
||||||
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
|
|
||||||
return dry_order
|
|
||||||
|
|
||||||
try:
|
|
||||||
amount = self.amount_to_precision(pair, amount)
|
|
||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
|
||||||
amount=amount, price=stop_price, params=params)
|
|
||||||
self._log_exchange_response('create_stoploss_order', order)
|
|
||||||
logger.info('stoploss order added for %s. '
|
|
||||||
'stop price: %s.', pair, stop_price)
|
|
||||||
return order
|
|
||||||
except ccxt.InsufficientFunds as e:
|
|
||||||
raise InsufficientFundsError(
|
|
||||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
|
||||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.InvalidOrder as e:
|
|
||||||
raise InvalidOrderException(
|
|
||||||
f'Could not create {ordertype} {side} order on market {pair}. '
|
|
||||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.DDoSProtection as e:
|
|
||||||
raise DDosProtection(e) from e
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
|
||||||
raise TemporaryError(
|
|
||||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
|
||||||
except ccxt.BaseError as e:
|
|
||||||
raise OperationalException(e) from e
|
|
||||||
|
|
||||||
def _set_leverage(
|
def _set_leverage(
|
||||||
self,
|
self,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
@@ -187,6 +119,9 @@ class Kraken(Exchange):
|
|||||||
)
|
)
|
||||||
if leverage > 1.0:
|
if leverage > 1.0:
|
||||||
params['leverage'] = round(leverage)
|
params['leverage'] = round(leverage)
|
||||||
|
if time_in_force == 'PO':
|
||||||
|
params.pop('timeInForce', None)
|
||||||
|
params['postOnly'] = True
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def calculate_funding_fees(
|
def calculate_funding_fees(
|
||||||
@@ -195,7 +130,7 @@ class Kraken(Exchange):
|
|||||||
amount: float,
|
amount: float,
|
||||||
is_short: bool,
|
is_short: bool,
|
||||||
open_date: datetime,
|
open_date: datetime,
|
||||||
close_date: Optional[datetime] = None,
|
close_date: datetime,
|
||||||
time_in_ratio: Optional[float] = None
|
time_in_ratio: Optional[float] = None
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -223,18 +158,30 @@ class Kraken(Exchange):
|
|||||||
|
|
||||||
return fees if is_short else -fees
|
return fees if is_short else -fees
|
||||||
|
|
||||||
def _trades_contracts_to_amount(self, trades: List) -> List:
|
def _get_trade_pagination_next_value(self, trades: List[Dict]):
|
||||||
"""
|
"""
|
||||||
Fix "last" id issue for kraken data downloads
|
Extract pagination id for the next "from_id" value
|
||||||
This whole override can probably be removed once the following
|
Applies only to fetch_trade_history by id.
|
||||||
issue is closed in ccxt: https://github.com/ccxt/ccxt/issues/15827
|
|
||||||
"""
|
"""
|
||||||
super()._trades_contracts_to_amount(trades)
|
if len(trades) > 0:
|
||||||
if (
|
if (
|
||||||
len(trades) > 0
|
isinstance(trades[-1].get('info'), list)
|
||||||
and isinstance(trades[-1].get('info'), list)
|
|
||||||
and len(trades[-1].get('info', [])) > 7
|
and len(trades[-1].get('info', [])) > 7
|
||||||
):
|
):
|
||||||
|
# Trade response's "last" value.
|
||||||
|
return trades[-1].get('info', [])[-1]
|
||||||
|
# Fall back to timestamp if info is somehow empty.
|
||||||
|
return trades[-1].get('timestamp')
|
||||||
|
return None
|
||||||
|
|
||||||
trades[-1]['id'] = trades[-1].get('info', [])[-1]
|
def _valid_trade_pagination_id(self, pair: str, from_id: str) -> bool:
|
||||||
return trades
|
"""
|
||||||
|
Verify trade-pagination id is valid.
|
||||||
|
Workaround for odd Kraken issue where ID is sometimes wrong.
|
||||||
|
"""
|
||||||
|
# Regular id's are in timestamp format 1705443695120072285
|
||||||
|
# If the id is smaller than 19 characters, it's not a valid timestamp.
|
||||||
|
if len(from_id) >= 19:
|
||||||
|
return True
|
||||||
|
logger.debug(f"{pair} - trade-pagination id is not valid. Fallback to timestamp.")
|
||||||
|
return False
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ class Okx(Exchange):
|
|||||||
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
|
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
|
||||||
|
|
||||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||||
if order['type'] == 'stop':
|
if order.get('type', '') == 'stop':
|
||||||
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||||
return order['id']
|
return order['id']
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ class BaseEnvironment(gym.Env):
|
|||||||
function is designed for tracking incremented objects,
|
function is designed for tracking incremented objects,
|
||||||
events, actions inside the training environment.
|
events, actions inside the training environment.
|
||||||
For example, a user can call this to track the
|
For example, a user can call this to track the
|
||||||
frequency of occurence of an `is_valid` call in
|
frequency of occurrence of an `is_valid` call in
|
||||||
their `calculate_reward()`:
|
their `calculate_reward()`:
|
||||||
|
|
||||||
def calculate_reward(self, action: int) -> float:
|
def calculate_reward(self, action: int) -> float:
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from joblib import Parallel
|
|
||||||
from sklearn.base import is_classifier
|
from sklearn.base import is_classifier
|
||||||
from sklearn.multioutput import MultiOutputClassifier, _fit_estimator
|
from sklearn.multioutput import MultiOutputClassifier, _fit_estimator
|
||||||
from sklearn.utils.fixes import delayed
|
|
||||||
from sklearn.utils.multiclass import check_classification_targets
|
from sklearn.utils.multiclass import check_classification_targets
|
||||||
|
from sklearn.utils.parallel import Parallel, delayed
|
||||||
from sklearn.utils.validation import has_fit_parameter
|
from sklearn.utils.validation import has_fit_parameter
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from joblib import Parallel
|
|
||||||
from sklearn.multioutput import MultiOutputRegressor, _fit_estimator
|
from sklearn.multioutput import MultiOutputRegressor, _fit_estimator
|
||||||
from sklearn.utils.fixes import delayed
|
from sklearn.utils.parallel import Parallel, delayed
|
||||||
from sklearn.utils.validation import has_fit_parameter
|
from sklearn.utils.validation import has_fit_parameter
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import psutil
|
import psutil
|
||||||
import rapidjson
|
import rapidjson
|
||||||
from joblib import dump, load
|
|
||||||
from joblib.externals import cloudpickle
|
from joblib.externals import cloudpickle
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@@ -285,6 +284,10 @@ class FreqaiDataDrawer:
|
|||||||
new_pred["date_pred"] = dataframe["date"]
|
new_pred["date_pred"] = dataframe["date"]
|
||||||
hist_preds = self.historic_predictions[pair].copy()
|
hist_preds = self.historic_predictions[pair].copy()
|
||||||
|
|
||||||
|
# ensure both dataframes have the same date format so they can be merged
|
||||||
|
new_pred["date_pred"] = pd.to_datetime(new_pred["date_pred"])
|
||||||
|
hist_preds["date_pred"] = pd.to_datetime(hist_preds["date_pred"])
|
||||||
|
|
||||||
# find the closest common date between new_pred and historic predictions
|
# find the closest common date between new_pred and historic predictions
|
||||||
# and cut off the new_pred dataframe at that date
|
# and cut off the new_pred dataframe at that date
|
||||||
common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner")
|
common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner")
|
||||||
@@ -295,9 +298,10 @@ class FreqaiDataDrawer:
|
|||||||
"predictions. You likely left your FreqAI instance offline "
|
"predictions. You likely left your FreqAI instance offline "
|
||||||
f"for more than {len(dataframe.index)} candles.")
|
f"for more than {len(dataframe.index)} candles.")
|
||||||
|
|
||||||
df_concat = pd.concat([hist_preds, new_pred], ignore_index=True, keys=hist_preds.keys())
|
# reindex new_pred columns to match the historic predictions dataframe
|
||||||
# remove last row because we will append that later in append_model_predictions()
|
new_pred_reindexed = new_pred.reindex(columns=hist_preds.columns)
|
||||||
df_concat = df_concat.iloc[:-1]
|
df_concat = pd.concat([hist_preds, new_pred_reindexed], ignore_index=True)
|
||||||
|
|
||||||
# any missing values will get zeroed out so users can see the exact
|
# any missing values will get zeroed out so users can see the exact
|
||||||
# downtime in FreqUI
|
# downtime in FreqUI
|
||||||
df_concat = df_concat.fillna(0)
|
df_concat = df_concat.fillna(0)
|
||||||
@@ -319,9 +323,9 @@ class FreqaiDataDrawer:
|
|||||||
index = self.historic_predictions[pair].index[-1:]
|
index = self.historic_predictions[pair].index[-1:]
|
||||||
columns = self.historic_predictions[pair].columns
|
columns = self.historic_predictions[pair].columns
|
||||||
|
|
||||||
nan_df = pd.DataFrame(np.nan, index=index, columns=columns)
|
zeros_df = pd.DataFrame(np.zeros((1, len(columns))), index=index, columns=columns)
|
||||||
self.historic_predictions[pair] = pd.concat(
|
self.historic_predictions[pair] = pd.concat(
|
||||||
[self.historic_predictions[pair], nan_df], ignore_index=True, axis=0)
|
[self.historic_predictions[pair], zeros_df], ignore_index=True, axis=0)
|
||||||
df = self.historic_predictions[pair]
|
df = self.historic_predictions[pair]
|
||||||
|
|
||||||
# model outputs and associated statistics
|
# model outputs and associated statistics
|
||||||
@@ -472,7 +476,8 @@ class FreqaiDataDrawer:
|
|||||||
|
|
||||||
# Save the trained model
|
# Save the trained model
|
||||||
if self.model_type == 'joblib':
|
if self.model_type == 'joblib':
|
||||||
dump(model, save_path / f"{dk.model_filename}_model.joblib")
|
with (save_path / f"{dk.model_filename}_model.joblib").open("wb") as fp:
|
||||||
|
cloudpickle.dump(model, fp)
|
||||||
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 self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]:
|
elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]:
|
||||||
@@ -559,7 +564,8 @@ class FreqaiDataDrawer:
|
|||||||
if dk.live and coin in self.model_dictionary:
|
if dk.live and coin in self.model_dictionary:
|
||||||
model = self.model_dictionary[coin]
|
model = self.model_dictionary[coin]
|
||||||
elif self.model_type == 'joblib':
|
elif self.model_type == 'joblib':
|
||||||
model = load(dk.data_path / f"{dk.model_filename}_model.joblib")
|
with (dk.data_path / f"{dk.model_filename}_model.joblib").open("rb") as fp:
|
||||||
|
model = cloudpickle.load(fp)
|
||||||
elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type:
|
elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type:
|
||||||
mod = importlib.import_module(
|
mod = importlib.import_module(
|
||||||
self.model_type, self.freqai_info['rl_config']['model_type'])
|
self.model_type, self.freqai_info['rl_config']['model_type'])
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ class FreqaiDataKitchen:
|
|||||||
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
||||||
f" due to NaNs in populated dataset {len(unfiltered_df)}."
|
f" due to NaNs in populated dataset {len(unfiltered_df)}."
|
||||||
)
|
)
|
||||||
if len(unfiltered_df) == 0 and not self.live:
|
if len(filtered_df) == 0 and not self.live:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"{self.pair}: all training data dropped due to NaNs. "
|
f"{self.pair}: all training data dropped due to NaNs. "
|
||||||
"You likely did not download enough training data prior "
|
"You likely did not download enough training data prior "
|
||||||
@@ -255,7 +255,7 @@ class FreqaiDataKitchen:
|
|||||||
if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live:
|
if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live:
|
||||||
worst_indicator = str(unfiltered_df.count().idxmin())
|
worst_indicator = str(unfiltered_df.count().idxmin())
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f" {(1 - len(filtered_df)/len(unfiltered_df)) * 100:.0f} percent "
|
f" {(1 - len(filtered_df) / len(unfiltered_df)) * 100:.0f} percent "
|
||||||
" of training data dropped due to NaNs, model may perform inconsistent "
|
" of training data dropped due to NaNs, model may perform inconsistent "
|
||||||
f"with expectations. Verify {worst_indicator}"
|
f"with expectations. Verify {worst_indicator}"
|
||||||
)
|
)
|
||||||
@@ -432,8 +432,12 @@ class FreqaiDataKitchen:
|
|||||||
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0:
|
||||||
append_df["DI_values"] = self.DI_values
|
append_df["DI_values"] = self.DI_values
|
||||||
|
|
||||||
|
user_cols = [col for col in dataframe_backtest.columns if col.startswith("%%")]
|
||||||
|
cols = ["date"]
|
||||||
|
cols.extend(user_cols)
|
||||||
|
|
||||||
dataframe_backtest.reset_index(drop=True, inplace=True)
|
dataframe_backtest.reset_index(drop=True, inplace=True)
|
||||||
merged_df = pd.concat([dataframe_backtest["date"], append_df], axis=1)
|
merged_df = pd.concat([dataframe_backtest[cols], append_df], axis=1)
|
||||||
return merged_df
|
return merged_df
|
||||||
|
|
||||||
def append_predictions(self, append_df: DataFrame) -> None:
|
def append_predictions(self, append_df: DataFrame) -> None:
|
||||||
@@ -451,7 +455,8 @@ class FreqaiDataKitchen:
|
|||||||
Back fill values to before the backtesting range so that the dataframe matches size
|
Back fill values to before the backtesting range so that the dataframe matches size
|
||||||
when it goes back to the strategy. These rows are not included in the backtest.
|
when it goes back to the strategy. These rows are not included in the backtest.
|
||||||
"""
|
"""
|
||||||
to_keep = [col for col in dataframe.columns if not col.startswith("&")]
|
to_keep = [col for col in dataframe.columns if
|
||||||
|
not col.startswith("&") and not col.startswith("%%")]
|
||||||
self.return_dataframe = pd.merge(dataframe[to_keep],
|
self.return_dataframe = pd.merge(dataframe[to_keep],
|
||||||
self.full_df, how='left', on='date')
|
self.full_df, how='left', on='date')
|
||||||
self.return_dataframe[self.full_df.columns] = (
|
self.return_dataframe[self.full_df.columns] = (
|
||||||
@@ -709,6 +714,8 @@ class FreqaiDataKitchen:
|
|||||||
pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs)
|
pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs)
|
||||||
informative_copy = informative_df.copy()
|
informative_copy = informative_df.copy()
|
||||||
|
|
||||||
|
logger.debug(f"Populating features for {pair} {tf}")
|
||||||
|
|
||||||
for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]:
|
for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]:
|
||||||
df_features = strategy.feature_engineering_expand_all(
|
df_features = strategy.feature_engineering_expand_all(
|
||||||
informative_copy.copy(), t, metadata=metadata)
|
informative_copy.copy(), t, metadata=metadata)
|
||||||
@@ -788,6 +795,7 @@ class FreqaiDataKitchen:
|
|||||||
|
|
||||||
if not prediction_dataframe.empty:
|
if not prediction_dataframe.empty:
|
||||||
dataframe = prediction_dataframe.copy()
|
dataframe = prediction_dataframe.copy()
|
||||||
|
base_dataframes[self.config["timeframe"]] = dataframe.copy()
|
||||||
else:
|
else:
|
||||||
dataframe = base_dataframes[self.config["timeframe"]].copy()
|
dataframe = base_dataframes[self.config["timeframe"]].copy()
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
|||||||
...
|
...
|
||||||
"freqai": {
|
"freqai": {
|
||||||
...
|
...
|
||||||
|
"conv_width": 30, // PyTorchTransformer is based on windowing
|
||||||
|
"feature_parameters": {
|
||||||
|
...
|
||||||
|
"include_shifted_candles": 0, // which removes the need for shifted candles
|
||||||
|
...
|
||||||
|
},
|
||||||
"model_training_parameters" : {
|
"model_training_parameters" : {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
@@ -120,16 +126,16 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
|||||||
# create empty torch tensor
|
# create empty torch tensor
|
||||||
self.model.model.eval()
|
self.model.model.eval()
|
||||||
yb = torch.empty(0).to(self.device)
|
yb = torch.empty(0).to(self.device)
|
||||||
if x.shape[1] > 1:
|
if x.shape[1] > self.window_size:
|
||||||
ws = self.window_size
|
ws = self.window_size
|
||||||
for i in range(0, x.shape[1] - ws):
|
for i in range(0, x.shape[1] - ws):
|
||||||
xb = x[:, i:i + ws, :].to(self.device)
|
xb = x[:, i:i + ws, :].to(self.device)
|
||||||
y = self.model.model(xb)
|
y = self.model.model(xb)
|
||||||
yb = torch.cat((yb, y), dim=0)
|
yb = torch.cat((yb, y), dim=1)
|
||||||
else:
|
else:
|
||||||
yb = self.model.model(x)
|
yb = self.model.model(x)
|
||||||
|
|
||||||
yb = yb.cpu().squeeze()
|
yb = yb.cpu().squeeze(0)
|
||||||
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
||||||
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Type
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
import torch as th
|
import torch as th
|
||||||
|
from stable_baselines3.common.callbacks import ProgressBarCallback
|
||||||
|
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions
|
||||||
@@ -73,19 +74,27 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
|||||||
'trained agent.')
|
'trained agent.')
|
||||||
model = self.dd.model_dictionary[dk.pair]
|
model = self.dd.model_dictionary[dk.pair]
|
||||||
model.set_env(self.train_env)
|
model.set_env(self.train_env)
|
||||||
|
callbacks: List[Any] = [self.eval_callback, self.tensorboard_callback]
|
||||||
|
progressbar_callback: Optional[ProgressBarCallback] = None
|
||||||
|
if self.rl_config.get('progress_bar', False):
|
||||||
|
progressbar_callback = ProgressBarCallback()
|
||||||
|
callbacks.insert(0, progressbar_callback)
|
||||||
|
|
||||||
|
try:
|
||||||
model.learn(
|
model.learn(
|
||||||
total_timesteps=int(total_timesteps),
|
total_timesteps=int(total_timesteps),
|
||||||
callback=[self.eval_callback, self.tensorboard_callback],
|
callback=callbacks,
|
||||||
progress_bar=self.rl_config.get('progress_bar', False)
|
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
if progressbar_callback:
|
||||||
|
progressbar_callback.on_training_end()
|
||||||
|
|
||||||
if Path(dk.data_path / "best_model.zip").is_file():
|
if Path(dk.data_path / "best_model.zip").is_file():
|
||||||
logger.info('Callback found a best model.')
|
logger.info('Callback found a best model.')
|
||||||
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
best_model = self.MODELCLASS.load(dk.data_path / "best_model")
|
||||||
return best_model
|
return best_model
|
||||||
|
|
||||||
logger.info('Couldnt find best model, using final model instead.')
|
logger.info("Couldn't find best model, using final model instead.")
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
from pandas import DataFrame
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
from sklearn.preprocessing import LabelEncoder
|
||||||
|
|
||||||
|
from freqtrade.freqai.base_models.BaseClassifierModel import BaseClassifierModel
|
||||||
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SKLearnRandomForestClassifier(BaseClassifierModel):
|
||||||
|
"""
|
||||||
|
User created prediction model. The class inherits IFreqaiModel, which
|
||||||
|
means it has full access to all Frequency AI functionality. Typically,
|
||||||
|
users would use this to override the common `fit()`, `train()`, or
|
||||||
|
`predict()` methods to add their custom data handling tools or change
|
||||||
|
various aspects of the training that cannot be configured via the
|
||||||
|
top level config.json file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
|
"""
|
||||||
|
User sets up the training and test data to fit their desired model here
|
||||||
|
:param data_dictionary: the dictionary holding all data for train, test,
|
||||||
|
labels, weights
|
||||||
|
:param dk: The datakitchen object for the current coin/model
|
||||||
|
"""
|
||||||
|
|
||||||
|
X = data_dictionary["train_features"].to_numpy()
|
||||||
|
y = data_dictionary["train_labels"].to_numpy()[:, 0]
|
||||||
|
|
||||||
|
if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0:
|
||||||
|
eval_set = None
|
||||||
|
else:
|
||||||
|
test_features = data_dictionary["test_features"].to_numpy()
|
||||||
|
test_labels = data_dictionary["test_labels"].to_numpy()[:, 0]
|
||||||
|
|
||||||
|
eval_set = (test_features, test_labels)
|
||||||
|
|
||||||
|
if self.freqai_info.get("continual_learning", False):
|
||||||
|
logger.warning("Continual learning is not supported for "
|
||||||
|
"SKLearnRandomForestClassifier, ignoring.")
|
||||||
|
|
||||||
|
train_weights = data_dictionary["train_weights"]
|
||||||
|
|
||||||
|
model = RandomForestClassifier(**self.model_training_parameters)
|
||||||
|
|
||||||
|
model.fit(X=X, y=y, sample_weight=train_weights)
|
||||||
|
if eval_set:
|
||||||
|
logger.info("Score: %s", model.score(eval_set[0], eval_set[1]))
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
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)
|
||||||
|
"""
|
||||||
|
|
||||||
|
(pred_df, dk.do_predict) = super().predict(unfiltered_df, dk, **kwargs)
|
||||||
|
|
||||||
|
le = LabelEncoder()
|
||||||
|
label = dk.label_list[0]
|
||||||
|
labels_before = list(dk.data['labels_std'].keys())
|
||||||
|
labels_after = le.fit_transform(labels_before).tolist()
|
||||||
|
pred_df[label] = le.inverse_transform(pred_df[label])
|
||||||
|
pred_df = pred_df.rename(
|
||||||
|
columns={labels_after[i]: labels_before[i] for i in range(len(labels_before))})
|
||||||
|
|
||||||
|
return (pred_df, dk.do_predict)
|
||||||
@@ -45,7 +45,7 @@ class XGBoostRFRegressor(BaseRegressionModel):
|
|||||||
|
|
||||||
model = XGBRFRegressor(**self.model_training_parameters)
|
model = XGBRFRegressor(**self.model_training_parameters)
|
||||||
|
|
||||||
model.set_params(callbacks=[TBCallback(dk.data_path)], activate=self.activate_tensorboard)
|
model.set_params(callbacks=[TBCallback(dk.data_path)])
|
||||||
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
|
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
|
||||||
sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
|
sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
|
||||||
# set the callbacks to empty so that we can serialize to disk later
|
# set the callbacks to empty so that we can serialize to disk later
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class XGBoostRegressor(BaseRegressionModel):
|
|||||||
|
|
||||||
model = XGBRegressor(**self.model_training_parameters)
|
model = XGBRegressor(**self.model_training_parameters)
|
||||||
|
|
||||||
model.set_params(callbacks=[TBCallback(dk.data_path)], activate=self.activate_tensorboard)
|
model.set_params(callbacks=[TBCallback(dk.data_path)])
|
||||||
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
|
model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set,
|
||||||
sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
|
sample_weight_eval_set=eval_weights, xgb_model=xgb_model)
|
||||||
# set the callbacks to empty so that we can serialize to disk later
|
# set the callbacks to empty so that we can serialize to disk later
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from typing import Any, Dict, Type, Union
|
|||||||
|
|
||||||
from stable_baselines3.common.callbacks import BaseCallback
|
from stable_baselines3.common.callbacks import BaseCallback
|
||||||
from stable_baselines3.common.logger import HParam
|
from stable_baselines3.common.logger import HParam
|
||||||
from stable_baselines3.common.vec_env import VecEnv
|
|
||||||
|
|
||||||
from freqtrade.freqai.RL.BaseEnvironment import BaseActions
|
from freqtrade.freqai.RL.BaseEnvironment import BaseActions
|
||||||
|
|
||||||
@@ -13,13 +12,9 @@ class TensorboardCallback(BaseCallback):
|
|||||||
Custom callback for plotting additional values in tensorboard and
|
Custom callback for plotting additional values in tensorboard and
|
||||||
episodic summary reports.
|
episodic summary reports.
|
||||||
"""
|
"""
|
||||||
# Override training_env type to fix type errors
|
|
||||||
training_env: Union[VecEnv, None] = None
|
|
||||||
|
|
||||||
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
|
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
|
||||||
super().__init__(verbose)
|
super().__init__(verbose)
|
||||||
self.model: Any = None
|
self.model: Any = None
|
||||||
self.logger: Any = None
|
|
||||||
self.actions: Type[Enum] = actions
|
self.actions: Type[Enum] = actions
|
||||||
|
|
||||||
def _on_training_start(self) -> None:
|
def _on_training_start(self) -> None:
|
||||||
@@ -47,8 +42,12 @@ class TensorboardCallback(BaseCallback):
|
|||||||
def _on_step(self) -> bool:
|
def _on_step(self) -> bool:
|
||||||
|
|
||||||
local_info = self.locals["infos"][0]
|
local_info = self.locals["infos"][0]
|
||||||
if self.training_env is None:
|
|
||||||
return True
|
if hasattr(self.training_env, 'envs'):
|
||||||
|
tensorboard_metrics = self.training_env.envs[0].unwrapped.tensorboard_metrics
|
||||||
|
|
||||||
|
else:
|
||||||
|
# For RL-multiproc - usage of [0] might need to be evaluated
|
||||||
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
|
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
|
||||||
|
|
||||||
for metric in local_info:
|
for metric in local_info:
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from freqtrade.data.dataprovider import DataProvider
|
|||||||
from freqtrade.data.history.history_utils import refresh_backtest_ohlcv_data
|
from freqtrade.data.history.history_utils import refresh_backtest_ohlcv_data
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
from freqtrade.exchange.exchange import market_is_active
|
|
||||||
from freqtrade.freqai.data_drawer import FreqaiDataDrawer
|
from freqtrade.freqai.data_drawer import FreqaiDataDrawer
|
||||||
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||||
@@ -33,8 +32,11 @@ def download_all_data_for_training(dp: DataProvider, config: Config) -> None:
|
|||||||
|
|
||||||
if dp._exchange is None:
|
if dp._exchange is None:
|
||||||
raise OperationalException('No exchange object found.')
|
raise OperationalException('No exchange object found.')
|
||||||
markets = [p for p, m in dp._exchange.markets.items() if market_is_active(m)
|
markets = [
|
||||||
or config.get('include_inactive')]
|
p for p in dp._exchange.get_markets(
|
||||||
|
tradable_only=True, active_only=not config.get('include_inactive')
|
||||||
|
).keys()
|
||||||
|
]
|
||||||
|
|
||||||
all_pairs = dynamic_expand_pairlist(config, markets)
|
all_pairs = dynamic_expand_pairlist(config, markets)
|
||||||
|
|
||||||
|
|||||||
+97
-89
@@ -18,8 +18,8 @@ from freqtrade.constants import BuySell, Config, EntryExecuteMode, ExchangeConfi
|
|||||||
from freqtrade.data.converter import order_book_to_dataframe
|
from freqtrade.data.converter import order_book_to_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, SignalDirection,
|
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, SignalDirection, State,
|
||||||
State, TradingMode)
|
TradingMode)
|
||||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, PricingError)
|
InvalidOrderException, PricingError)
|
||||||
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, remove_exchange_credentials,
|
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, remove_exchange_credentials,
|
||||||
@@ -33,12 +33,12 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
|||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
|
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
|
||||||
from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg,
|
from freqtrade.rpc.rpc_types import (ProfitLossStr, RPCCancelMsg, RPCEntryMsg, RPCExitCancelMsg,
|
||||||
RPCSellMsg)
|
RPCExitMsg, RPCProtectionMsg)
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_names
|
from freqtrade.util.migrations import migrate_binance_futures_names
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
@@ -83,6 +83,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
PairLocks.timeframe = self.config['timeframe']
|
PairLocks.timeframe = self.config['timeframe']
|
||||||
|
|
||||||
self.pairlists = PairListManager(self.exchange, self.config)
|
self.pairlists = PairListManager(self.exchange, self.config)
|
||||||
|
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
|
||||||
|
self.last_process: Optional[datetime] = None
|
||||||
|
|
||||||
# RPC runs in separate threads, can start handling external commands just after
|
# RPC runs in separate threads, can start handling external commands just after
|
||||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||||
@@ -119,8 +121,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self._exit_lock = Lock()
|
self._exit_lock = Lock()
|
||||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||||
|
|
||||||
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
|
|
||||||
|
|
||||||
self._schedule = Scheduler()
|
self._schedule = Scheduler()
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
@@ -132,10 +132,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# TODO: This would be more efficient if scheduled in utc time, and performed at each
|
# TODO: This would be more efficient if scheduled in utc time, and performed at each
|
||||||
# TODO: funding interval, specified by funding_fee_times on the exchange classes
|
# TODO: funding interval, specified by funding_fee_times on the exchange classes
|
||||||
for time_slot in range(0, 24):
|
for time_slot in range(0, 24):
|
||||||
for minutes in [0, 15, 30, 45]:
|
for minutes in [1, 31]:
|
||||||
t = str(time(time_slot, minutes, 2))
|
t = str(time(time_slot, minutes, 2))
|
||||||
self._schedule.every().day.at(t).do(update)
|
self._schedule.every().day.at(t).do(update)
|
||||||
self.last_process: Optional[datetime] = None
|
|
||||||
|
|
||||||
self.strategy.ft_bot_start()
|
self.strategy.ft_bot_start()
|
||||||
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
||||||
@@ -199,6 +198,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Only update open orders on startup
|
# Only update open orders on startup
|
||||||
# This will update the database after the initial migration
|
# This will update the database after the initial migration
|
||||||
self.startup_update_open_orders()
|
self.startup_update_open_orders()
|
||||||
|
self.update_funding_fees()
|
||||||
|
|
||||||
def process(self) -> None:
|
def process(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -312,22 +312,19 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
open_trades = Trade.get_open_trade_count()
|
open_trades = Trade.get_open_trade_count()
|
||||||
return max(0, self.config['max_open_trades'] - open_trades)
|
return max(0, self.config['max_open_trades'] - open_trades)
|
||||||
|
|
||||||
def update_funding_fees(self):
|
def update_funding_fees(self) -> None:
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
trades = Trade.get_open_trades()
|
trades: List[Trade] = Trade.get_open_trades()
|
||||||
try:
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
funding_fees = self.exchange.get_funding_fees(
|
trade.set_funding_fees(
|
||||||
|
self.exchange.get_funding_fees(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
is_short=trade.is_short,
|
is_short=trade.is_short,
|
||||||
open_date=trade.date_last_filled_utc
|
open_date=trade.date_last_filled_utc)
|
||||||
)
|
)
|
||||||
trade.funding_fees = funding_fees
|
|
||||||
except ExchangeError:
|
|
||||||
logger.warning("Could not update funding fees for open trades.")
|
|
||||||
|
|
||||||
def startup_backpopulate_precision(self):
|
def startup_backpopulate_precision(self) -> None:
|
||||||
|
|
||||||
trades = Trade.get_trades([Trade.contract_size.is_(None)])
|
trades = Trade.get_trades([Trade.contract_size.is_(None)])
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@@ -374,17 +371,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
fo = order.to_ccxt_object()
|
fo = order.to_ccxt_object()
|
||||||
fo['status'] = 'canceled'
|
fo['status'] = 'canceled'
|
||||||
self.handle_cancel_order(
|
self.handle_cancel_order(
|
||||||
fo, order.order_id, order.trade,
|
fo, order, order.trade, constants.CANCEL_REASON['TIMEOUT']
|
||||||
constants.CANCEL_REASON['TIMEOUT']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
except ExchangeError as e:
|
except ExchangeError as e:
|
||||||
|
|
||||||
logger.warning(f"Error updating Order {order.order_id} due to {e}")
|
logger.warning(f"Error updating Order {order.order_id} due to {e}")
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
|
||||||
self._schedule.run_pending()
|
|
||||||
|
|
||||||
def update_trades_without_assigned_fees(self) -> None:
|
def update_trades_without_assigned_fees(self) -> None:
|
||||||
"""
|
"""
|
||||||
Update closed trades without close fees assigned.
|
Update closed trades without close fees assigned.
|
||||||
@@ -586,7 +579,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
self.log_once(f"Pair {pair} is currently locked.", logger.info)
|
self.log_once(f"Pair {pair} is currently locked.", logger.info)
|
||||||
return False
|
return False
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
|
stake_amount = self.wallets.get_trade_stake_amount(
|
||||||
|
pair, self.config['max_open_trades'], self.edge)
|
||||||
|
|
||||||
bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
|
bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
|
||||||
if ((bid_check_dom.get('enabled', False)) and
|
if ((bid_check_dom.get('enabled', False)) and
|
||||||
@@ -678,20 +672,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount = self.exchange.amount_to_contract_precision(
|
amount = self.exchange.amount_to_contract_precision(
|
||||||
trade.pair,
|
trade.pair,
|
||||||
abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
|
abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
|
||||||
if amount > trade.amount:
|
|
||||||
# This is currently ineffective as remaining would become < min tradable
|
|
||||||
# Fixing this would require checking for 0.0 there -
|
|
||||||
# if we decide that this callback is allowed to "fully exit"
|
|
||||||
logger.info(
|
|
||||||
f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
|
|
||||||
amount = trade.amount
|
|
||||||
|
|
||||||
if amount == 0.0:
|
if amount == 0.0:
|
||||||
logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
|
logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
|
||||||
return
|
return
|
||||||
|
|
||||||
remaining = (trade.amount - amount) * current_exit_rate
|
remaining = (trade.amount - amount) * current_exit_rate
|
||||||
if min_exit_stake and remaining < min_exit_stake:
|
if min_exit_stake and remaining != 0 and remaining < min_exit_stake:
|
||||||
logger.info(f"Remaining amount of {remaining} would be smaller "
|
logger.info(f"Remaining amount of {remaining} would be smaller "
|
||||||
f"than the minimum of {min_exit_stake}.")
|
f"than the minimum of {min_exit_stake}.")
|
||||||
return
|
return
|
||||||
@@ -749,6 +736,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
:param stake_amount: amount of stake-currency for the pair
|
:param stake_amount: amount of stake-currency for the pair
|
||||||
:return: True if a buy order is created, false if it fails.
|
:return: True if a buy order is created, false if it fails.
|
||||||
|
:raise: DependencyException or it's subclasses like ExchangeError.
|
||||||
"""
|
"""
|
||||||
time_in_force = self.strategy.order_time_in_force['entry']
|
time_in_force = self.strategy.order_time_in_force['entry']
|
||||||
|
|
||||||
@@ -835,14 +823,15 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
base_currency = self.exchange.get_pair_base_currency(pair)
|
base_currency = self.exchange.get_pair_base_currency(pair)
|
||||||
open_date = datetime.now(timezone.utc)
|
open_date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
funding_fees = self.exchange.get_funding_fees(
|
||||||
|
pair=pair,
|
||||||
|
amount=amount + trade.amount if trade else amount,
|
||||||
|
is_short=is_short,
|
||||||
|
open_date=trade.date_last_filled_utc if trade else open_date
|
||||||
|
)
|
||||||
|
|
||||||
# This is a new trade
|
# This is a new trade
|
||||||
if trade is None:
|
if trade is None:
|
||||||
funding_fees = 0.0
|
|
||||||
try:
|
|
||||||
funding_fees = self.exchange.get_funding_fees(
|
|
||||||
pair=pair, amount=amount, is_short=is_short, open_date=open_date)
|
|
||||||
except ExchangeError:
|
|
||||||
logger.warning("Could not find funding fee.")
|
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
@@ -878,6 +867,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.fee_open_currency = None
|
trade.fee_open_currency = None
|
||||||
trade.open_rate_requested = enter_limit_requested
|
trade.open_rate_requested = enter_limit_requested
|
||||||
|
trade.set_funding_fees(funding_fees)
|
||||||
|
|
||||||
trade.orders.append(order_obj)
|
trade.orders.append(order_obj)
|
||||||
trade.recalc_trade_from_orders()
|
trade.recalc_trade_from_orders()
|
||||||
@@ -906,7 +896,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
if trade.stoploss_order_id:
|
if trade.stoploss_order_id:
|
||||||
try:
|
try:
|
||||||
logger.info(f"Canceling stoploss on exchange for {trade}")
|
logger.info(f"Cancelling stoploss on exchange for {trade}")
|
||||||
co = self.exchange.cancel_stoploss_order_with_result(
|
co = self.exchange.cancel_stoploss_order_with_result(
|
||||||
trade.stoploss_order_id, trade.pair, trade.amount)
|
trade.stoploss_order_id, trade.pair, trade.amount)
|
||||||
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
|
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
|
||||||
@@ -1012,12 +1002,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if open_rate is None:
|
if open_rate is None:
|
||||||
open_rate = trade.open_rate
|
open_rate = trade.open_rate
|
||||||
|
|
||||||
current_rate = trade.open_rate_requested
|
|
||||||
if self.dataprovider.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
|
||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
|
||||||
|
|
||||||
msg: RPCBuyMsg = {
|
msg: RPCEntryMsg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
|
'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
|
||||||
'buy_tag': trade.enter_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
@@ -1032,6 +1020,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
|
'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
|
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
|
||||||
'open_date': trade.open_date_utc or datetime.now(timezone.utc),
|
'open_date': trade.open_date_utc or datetime.now(timezone.utc),
|
||||||
@@ -1065,6 +1054,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'open_rate': trade.open_rate,
|
'open_rate': trade.open_rate,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
|
'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'amount': trade.amount,
|
'amount': trade.amount,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
@@ -1087,7 +1077,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trades_closed = 0
|
trades_closed = 0
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
|
||||||
if not trade.has_open_orders and not self.wallets.check_exit_amount(trade):
|
if (
|
||||||
|
not trade.has_open_orders
|
||||||
|
and not trade.stoploss_order_id
|
||||||
|
and not self.wallets.check_exit_amount(trade)
|
||||||
|
):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'Not enough {trade.safe_base_currency} in wallet to exit {trade}. '
|
f'Not enough {trade.safe_base_currency} in wallet to exit {trade}. '
|
||||||
'Trying to recover.')
|
'Trying to recover.')
|
||||||
@@ -1331,6 +1325,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
for trade in Trade.get_open_trades():
|
for trade in Trade.get_open_trades():
|
||||||
|
open_order: Order
|
||||||
for open_order in trade.open_orders:
|
for open_order in trade.open_orders:
|
||||||
try:
|
try:
|
||||||
order = self.exchange.fetch_order(open_order.order_id, trade.pair)
|
order = self.exchange.fetch_order(open_order.order_id, trade.pair)
|
||||||
@@ -1345,28 +1340,31 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
not_closed = order['status'] == 'open' or fully_cancelled
|
not_closed = order['status'] == 'open' or fully_cancelled
|
||||||
|
|
||||||
if not_closed:
|
if not_closed:
|
||||||
if fully_cancelled or (
|
if (
|
||||||
|
fully_cancelled or (
|
||||||
open_order and self.strategy.ft_check_timed_out(
|
open_order and self.strategy.ft_check_timed_out(
|
||||||
trade, open_order, datetime.now(timezone.utc)
|
trade, open_order, datetime.now(timezone.utc)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
self.handle_cancel_order(
|
self.handle_cancel_order(
|
||||||
order, open_order.order_id, trade, constants.CANCEL_REASON['TIMEOUT']
|
order, open_order, trade, constants.CANCEL_REASON['TIMEOUT']
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.replace_order(order, open_order, trade)
|
self.replace_order(order, open_order, trade)
|
||||||
|
|
||||||
def handle_cancel_order(self, order: Dict, order_id: str, trade: Trade, reason: str) -> None:
|
def handle_cancel_order(self, order: Dict, order_obj: Order, trade: Trade, reason: str) -> None:
|
||||||
"""
|
"""
|
||||||
Check if current analyzed order timed out and cancel if necessary.
|
Check if current analyzed order timed out and cancel if necessary.
|
||||||
:param order: Order dict grabbed with exchange.fetch_order()
|
:param order: Order dict grabbed with exchange.fetch_order()
|
||||||
|
:param order_obj: Order object from the database.
|
||||||
:param trade: Trade object.
|
:param trade: Trade object.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if order['side'] == trade.entry_side:
|
if order['side'] == trade.entry_side:
|
||||||
self.handle_cancel_enter(trade, order, order_id, reason)
|
self.handle_cancel_enter(trade, order, order_obj, reason)
|
||||||
else:
|
else:
|
||||||
canceled = self.handle_cancel_exit(trade, order, order_id, reason)
|
canceled = self.handle_cancel_exit(trade, order, order_obj, reason)
|
||||||
canceled_count = trade.get_canceled_exit_order_count()
|
canceled_count = trade.get_canceled_exit_order_count()
|
||||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||||
if (canceled and max_timeouts > 0 and canceled_count >= max_timeouts):
|
if (canceled and max_timeouts > 0 and canceled_count >= max_timeouts):
|
||||||
@@ -1426,21 +1424,21 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# New candle
|
# New candle
|
||||||
proposed_rate = self.exchange.get_rate(
|
proposed_rate = self.exchange.get_rate(
|
||||||
trade.pair, side='entry', is_short=trade.is_short, refresh=True)
|
trade.pair, side='entry', is_short=trade.is_short, refresh=True)
|
||||||
adjusted_entry_price = strategy_safe_wrapper(self.strategy.adjust_entry_price,
|
adjusted_entry_price = strategy_safe_wrapper(
|
||||||
default_retval=order_obj.price)(
|
self.strategy.adjust_entry_price, default_retval=order_obj.safe_placement_price)(
|
||||||
trade=trade, order=order_obj, pair=trade.pair,
|
trade=trade, order=order_obj, pair=trade.pair,
|
||||||
current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate,
|
current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate,
|
||||||
current_order_rate=order_obj.safe_price, entry_tag=trade.enter_tag,
|
current_order_rate=order_obj.safe_placement_price, entry_tag=trade.enter_tag,
|
||||||
side=trade.entry_side)
|
side=trade.trade_direction)
|
||||||
|
|
||||||
replacing = True
|
replacing = True
|
||||||
cancel_reason = constants.CANCEL_REASON['REPLACE']
|
cancel_reason = constants.CANCEL_REASON['REPLACE']
|
||||||
if not adjusted_entry_price:
|
if not adjusted_entry_price:
|
||||||
replacing = False
|
replacing = False
|
||||||
cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
|
cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
|
||||||
if order_obj.price != adjusted_entry_price:
|
if order_obj.safe_placement_price != adjusted_entry_price:
|
||||||
# cancel existing order if new price is supplied or None
|
# cancel existing order if new price is supplied or None
|
||||||
res = self.handle_cancel_enter(trade, order, order_obj.order_id, cancel_reason,
|
res = self.handle_cancel_enter(trade, order, order_obj, cancel_reason,
|
||||||
replacing=replacing)
|
replacing=replacing)
|
||||||
if not res:
|
if not res:
|
||||||
self.replace_order_failed(
|
self.replace_order_failed(
|
||||||
@@ -1448,6 +1446,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return
|
return
|
||||||
if adjusted_entry_price:
|
if adjusted_entry_price:
|
||||||
# place new order only if new price is supplied
|
# place new order only if new price is supplied
|
||||||
|
try:
|
||||||
if not self.execute_entry(
|
if not self.execute_entry(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
stake_amount=(
|
stake_amount=(
|
||||||
@@ -1457,6 +1456,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
is_short=trade.is_short,
|
is_short=trade.is_short,
|
||||||
mode='replace',
|
mode='replace',
|
||||||
):
|
):
|
||||||
|
self.replace_order_failed(
|
||||||
|
trade, f"Could not replace order for {trade}.")
|
||||||
|
except DependencyException as exception:
|
||||||
|
logger.warning(
|
||||||
|
f'Unable to replace order for {trade.pair}: {exception}')
|
||||||
self.replace_order_failed(trade, f"Could not replace order for {trade}.")
|
self.replace_order_failed(trade, f"Could not replace order for {trade}.")
|
||||||
|
|
||||||
def cancel_all_open_orders(self) -> None:
|
def cancel_all_open_orders(self) -> None:
|
||||||
@@ -1475,29 +1479,28 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
if order['side'] == trade.entry_side:
|
if order['side'] == trade.entry_side:
|
||||||
self.handle_cancel_enter(
|
self.handle_cancel_enter(
|
||||||
trade, order, open_order.order_id, constants.CANCEL_REASON['ALL_CANCELLED']
|
trade, order, open_order, constants.CANCEL_REASON['ALL_CANCELLED']
|
||||||
)
|
)
|
||||||
|
|
||||||
elif order['side'] == trade.exit_side:
|
elif order['side'] == trade.exit_side:
|
||||||
self.handle_cancel_exit(
|
self.handle_cancel_exit(
|
||||||
trade, order, open_order.order_id, constants.CANCEL_REASON['ALL_CANCELLED']
|
trade, order, open_order, constants.CANCEL_REASON['ALL_CANCELLED']
|
||||||
)
|
)
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
def handle_cancel_enter(
|
def handle_cancel_enter(
|
||||||
self, trade: Trade, order: Dict, order_id: str,
|
self, trade: Trade, order: Dict, order_obj: Order,
|
||||||
reason: str, replacing: Optional[bool] = False
|
reason: str, replacing: Optional[bool] = False
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
entry cancel - cancel order
|
entry cancel - cancel order
|
||||||
|
:param order_obj: Order object from the database.
|
||||||
:param replacing: Replacing order - prevent trade deletion.
|
:param replacing: Replacing order - prevent trade deletion.
|
||||||
:return: True if trade was fully cancelled
|
:return: True if trade was fully cancelled
|
||||||
"""
|
"""
|
||||||
was_trade_fully_canceled = False
|
was_trade_fully_canceled = False
|
||||||
|
order_id = order_obj.order_id
|
||||||
side = trade.entry_side.capitalize()
|
side = trade.entry_side.capitalize()
|
||||||
if not trade.has_open_orders:
|
|
||||||
logger.warning(f"No open order for {trade}.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
filled_val: float = order.get('filled', 0.0) or 0.0
|
filled_val: float = order.get('filled', 0.0) or 0.0
|
||||||
@@ -1510,8 +1513,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"Order {order_id} for {trade.pair} not cancelled, "
|
f"Order {order_id} for {trade.pair} not cancelled, "
|
||||||
f"as the filled amount of {filled_val} would result in an unexitable trade.")
|
f"as the filled amount of {filled_val} would result in an unexitable trade.")
|
||||||
return False
|
return False
|
||||||
corder = self.exchange.cancel_order_with_result(order_id, trade.pair,
|
corder = self.exchange.cancel_order_with_result(order_id, trade.pair, trade.amount)
|
||||||
trade.amount)
|
order_obj.ft_cancel_reason = reason
|
||||||
# if replacing, retry fetching the order 3 times if the status is not what we need
|
# if replacing, retry fetching the order 3 times if the status is not what we need
|
||||||
if replacing:
|
if replacing:
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
@@ -1532,9 +1535,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
# Order was cancelled already, so we can reuse the existing dict
|
# Order was cancelled already, so we can reuse the existing dict
|
||||||
corder = order
|
corder = order
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
if order_obj.ft_cancel_reason is None:
|
||||||
|
order_obj.ft_cancel_reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
|
|
||||||
logger.info(f'{side} order {reason} for {trade}.')
|
logger.info(f'{side} order {order_obj.ft_cancel_reason} for {trade}.')
|
||||||
|
|
||||||
# Using filled to determine the filled amount
|
# Using filled to determine the filled amount
|
||||||
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
|
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
|
||||||
@@ -1547,7 +1551,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if open_order_count < 1 and trade.nr_of_successful_entries == 0 and not replacing:
|
if open_order_count < 1 and trade.nr_of_successful_entries == 0 and not replacing:
|
||||||
logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
|
logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
|
||||||
trade.delete()
|
trade.delete()
|
||||||
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
order_obj.ft_cancel_reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
||||||
else:
|
else:
|
||||||
self.update_trade_state(trade, order_id, corder)
|
self.update_trade_state(trade, order_id, corder)
|
||||||
logger.info(f'{side} Order timeout for {trade}.')
|
logger.info(f'{side} Order timeout for {trade}.')
|
||||||
@@ -1557,21 +1561,21 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.update_trade_state(trade, order_id, corder)
|
self.update_trade_state(trade, order_id, corder)
|
||||||
|
|
||||||
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
||||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
order_obj.ft_cancel_reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||||
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['entry'],
|
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['entry'],
|
||||||
reason=reason)
|
reason=order_obj.ft_cancel_reason)
|
||||||
return was_trade_fully_canceled
|
return was_trade_fully_canceled
|
||||||
|
|
||||||
def handle_cancel_exit(
|
def handle_cancel_exit(
|
||||||
self, trade: Trade, order: Dict, order_id: str,
|
self, trade: Trade, order: Dict, order_obj: Order, reason: str
|
||||||
reason: str
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
exit order cancel - cancel order and update trade
|
exit order cancel - cancel order and update trade
|
||||||
:return: True if exit order was cancelled, false otherwise
|
:return: True if exit order was cancelled, false otherwise
|
||||||
"""
|
"""
|
||||||
|
order_id = order_obj.order_id
|
||||||
cancelled = False
|
cancelled = False
|
||||||
# Cancelled orders may have the status of 'canceled' or 'closed'
|
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||||
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
@@ -1596,7 +1600,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
sub_trade=trade.amount != order['amount']
|
sub_trade=trade.amount != order['amount']
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
order_obj.ft_cancel_reason = reason
|
||||||
try:
|
try:
|
||||||
order = self.exchange.cancel_order_with_result(
|
order = self.exchange.cancel_order_with_result(
|
||||||
order['id'], trade.pair, trade.amount)
|
order['id'], trade.pair, trade.amount)
|
||||||
@@ -1615,19 +1619,22 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.exit_reason = exit_reason_prev
|
trade.exit_reason = exit_reason_prev
|
||||||
cancelled = True
|
cancelled = True
|
||||||
else:
|
else:
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
if order_obj.ft_cancel_reason is None:
|
||||||
|
order_obj.ft_cancel_reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
trade.exit_reason = None
|
trade.exit_reason = None
|
||||||
|
|
||||||
self.update_trade_state(trade, order['id'], order)
|
self.update_trade_state(trade, order['id'], order)
|
||||||
|
|
||||||
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
|
logger.info(
|
||||||
|
f'{trade.exit_side.capitalize()} order {order_obj.ft_cancel_reason} for {trade}.')
|
||||||
trade.close_rate = None
|
trade.close_rate = None
|
||||||
trade.close_rate_requested = None
|
trade.close_rate_requested = None
|
||||||
|
|
||||||
self._notify_exit_cancel(
|
self._notify_exit_cancel(
|
||||||
trade,
|
trade,
|
||||||
order_type=self.strategy.order_types['exit'],
|
order_type=self.strategy.order_types['exit'],
|
||||||
reason=reason, order_id=order['id'], sub_trade=trade.amount != order['amount']
|
reason=order_obj.ft_cancel_reason, order_id=order['id'],
|
||||||
|
sub_trade=trade.amount != order['amount']
|
||||||
)
|
)
|
||||||
return cancelled
|
return cancelled
|
||||||
|
|
||||||
@@ -1679,15 +1686,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param exit_check: CheckTuple with signal and reason
|
:param exit_check: CheckTuple with signal and reason
|
||||||
:return: True if it succeeds False
|
:return: True if it succeeds False
|
||||||
"""
|
"""
|
||||||
try:
|
trade.set_funding_fees(
|
||||||
trade.funding_fees = self.exchange.get_funding_fees(
|
self.exchange.get_funding_fees(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
is_short=trade.is_short,
|
is_short=trade.is_short,
|
||||||
open_date=trade.date_last_filled_utc,
|
open_date=trade.date_last_filled_utc)
|
||||||
)
|
)
|
||||||
except ExchangeError:
|
|
||||||
logger.warning("Could not update funding fee.")
|
|
||||||
|
|
||||||
exit_type = 'exit'
|
exit_type = 'exit'
|
||||||
exit_reason = exit_tag or exit_check.exit_reason
|
exit_reason = exit_tag or exit_check.exit_reason
|
||||||
@@ -1781,9 +1786,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order_rate = trade.safe_close_rate
|
order_rate = trade.safe_close_rate
|
||||||
profit = trade.calculate_profit(rate=order_rate)
|
profit = trade.calculate_profit(rate=order_rate)
|
||||||
amount = trade.amount
|
amount = trade.amount
|
||||||
gain = "profit" if profit.profit_ratio > 0 else "loss"
|
gain: ProfitLossStr = "profit" if profit.profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
msg: RPCSellMsg = {
|
msg: RPCExitMsg = {
|
||||||
'type': (RPCMessageType.EXIT_FILL if fill
|
'type': (RPCMessageType.EXIT_FILL if fill
|
||||||
else RPCMessageType.EXIT),
|
else RPCMessageType.EXIT),
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
@@ -1799,20 +1804,22 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'open_rate': trade.open_rate,
|
'open_rate': trade.open_rate,
|
||||||
'close_rate': order_rate,
|
'close_rate': order_rate,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit.profit_abs if fill else profit.total_profit,
|
'profit_amount': profit.profit_abs,
|
||||||
'profit_ratio': profit.profit_ratio,
|
'profit_ratio': profit.profit_ratio,
|
||||||
'buy_tag': trade.enter_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
'enter_tag': trade.enter_tag,
|
'enter_tag': trade.enter_tag,
|
||||||
'sell_reason': trade.exit_reason, # Deprecated
|
|
||||||
'exit_reason': trade.exit_reason,
|
'exit_reason': trade.exit_reason,
|
||||||
'open_date': trade.open_date_utc,
|
'open_date': trade.open_date_utc,
|
||||||
'close_date': trade.close_date_utc or datetime.now(timezone.utc),
|
'close_date': trade.close_date_utc or datetime.now(timezone.utc),
|
||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
|
'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency'),
|
'fiat_currency': self.config.get('fiat_display_currency'),
|
||||||
'sub_trade': sub_trade,
|
'sub_trade': sub_trade,
|
||||||
'cumulative_profit': trade.realized_profit,
|
'cumulative_profit': trade.realized_profit,
|
||||||
|
'final_profit_ratio': trade.close_profit if not trade.is_open else None,
|
||||||
|
'is_final_exit': trade.is_open is False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Send the message
|
# Send the message
|
||||||
@@ -1835,9 +1842,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
profit = trade.calculate_profit(rate=profit_rate)
|
profit = trade.calculate_profit(rate=profit_rate)
|
||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
|
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
|
||||||
gain = "profit" if profit.profit_ratio > 0 else "loss"
|
gain: ProfitLossStr = "profit" if profit.profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
msg: RPCSellCancelMsg = {
|
msg: RPCExitCancelMsg = {
|
||||||
'type': RPCMessageType.EXIT_CANCEL,
|
'type': RPCMessageType.EXIT_CANCEL,
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
@@ -1854,12 +1861,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'profit_ratio': profit.profit_ratio,
|
'profit_ratio': profit.profit_ratio,
|
||||||
'buy_tag': trade.enter_tag,
|
'buy_tag': trade.enter_tag,
|
||||||
'enter_tag': trade.enter_tag,
|
'enter_tag': trade.enter_tag,
|
||||||
'sell_reason': trade.exit_reason, # Deprecated
|
|
||||||
'exit_reason': trade.exit_reason,
|
'exit_reason': trade.exit_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
'base_currency': self.exchange.get_pair_base_currency(trade.pair),
|
||||||
|
'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'reason': reason,
|
'reason': reason,
|
||||||
'sub_trade': sub_trade,
|
'sub_trade': sub_trade,
|
||||||
@@ -1910,7 +1917,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
if self.exchange.check_order_canceled_empty(order):
|
if self.exchange.check_order_canceled_empty(order):
|
||||||
# Trade has been cancelled on exchange
|
# Trade has been cancelled on exchange
|
||||||
# Handling of this will happen in check_handle_timedout.
|
# Handling of this will happen in handle_cancel_order.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
order_obj_or_none = trade.select_order_by_order_id(order_id)
|
order_obj_or_none = trade.select_order_by_order_id(order_id)
|
||||||
@@ -1967,15 +1974,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
|
self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
|
||||||
"""send "fill" notifications"""
|
"""send "fill" notifications"""
|
||||||
|
|
||||||
sub_trade = not isclose(order.safe_amount_after_fee,
|
|
||||||
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
|
|
||||||
if order.ft_order_side == trade.exit_side:
|
if order.ft_order_side == trade.exit_side:
|
||||||
# Exit notification
|
# Exit notification
|
||||||
if send_msg and not stoploss_order and order.order_id not in trade.open_orders_ids:
|
if send_msg and not stoploss_order and order.order_id not in trade.open_orders_ids:
|
||||||
self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order)
|
self._notify_exit(trade, order.order_type, fill=True,
|
||||||
|
sub_trade=trade.is_open, order=order)
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
self.handle_protections(trade.pair, trade.trade_direction)
|
self.handle_protections(trade.pair, trade.trade_direction)
|
||||||
elif send_msg and order.order_id not in trade.open_orders_ids and not stoploss_order:
|
elif send_msg and order.order_id not in trade.open_orders_ids and not stoploss_order:
|
||||||
|
sub_trade = not isclose(order.safe_amount_after_fee,
|
||||||
|
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
|
||||||
# Enter fill
|
# Enter fill
|
||||||
self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
|
self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ logger = logging.getLogger(__name__)
|
|||||||
def set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
|
def set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
|
||||||
"""
|
"""
|
||||||
Set the logging level for third party libraries
|
Set the logging level for third party libraries
|
||||||
|
:param verbosity: Verbosity level. amount of `-v` passed to the command line
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
for logger_name in ('requests', 'urllib3', 'httpcore'):
|
||||||
logging.getLogger('requests').setLevel(
|
logging.getLogger(logger_name).setLevel(
|
||||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
|
||||||
)
|
|
||||||
logging.getLogger("urllib3").setLevel(
|
|
||||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||||
)
|
)
|
||||||
logging.getLogger('ccxt.base.exchange').setLevel(
|
logging.getLogger('ccxt.base.exchange').setLevel(
|
||||||
|
|||||||
+2
-30
@@ -3,6 +3,7 @@ Various tool function for Freqtrade and scripts
|
|||||||
"""
|
"""
|
||||||
import gzip
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union
|
from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -10,41 +11,12 @@ from urllib.parse import urlparse
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
|
|
||||||
from freqtrade.enums import SignalTagType, SignalType
|
from freqtrade.enums import SignalTagType, SignalType
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def decimals_per_coin(coin: str):
|
|
||||||
"""
|
|
||||||
Helper method getting decimal amount for this coin
|
|
||||||
example usage: f".{decimals_per_coin('USD')}f"
|
|
||||||
:param coin: Which coin are we printing the price / value for
|
|
||||||
"""
|
|
||||||
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
|
|
||||||
|
|
||||||
|
|
||||||
def round_coin_value(
|
|
||||||
value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str:
|
|
||||||
"""
|
|
||||||
Get price value for this coin
|
|
||||||
:param value: Value to be printed
|
|
||||||
:param coin: Which coin are we printing the price / value for
|
|
||||||
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
|
|
||||||
:param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2"
|
|
||||||
:return: Formatted / rounded value (with or without coin name)
|
|
||||||
"""
|
|
||||||
val = f"{value:.{decimals_per_coin(coin)}f}"
|
|
||||||
if not keep_trailing_zeros:
|
|
||||||
val = val.rstrip('0').rstrip('.')
|
|
||||||
if show_coin_name:
|
|
||||||
val = f"{val} {coin}"
|
|
||||||
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None:
|
def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Dump JSON data into a file
|
Dump JSON data into a file
|
||||||
@@ -231,7 +203,7 @@ def json_to_dataframe(data: str) -> pd.DataFrame:
|
|||||||
:param data: A JSON string
|
:param data: A JSON string
|
||||||
:returns: A pandas DataFrame from the JSON string
|
:returns: A pandas DataFrame from the JSON string
|
||||||
"""
|
"""
|
||||||
dataframe = pd.read_json(data, orient='split')
|
dataframe = pd.read_json(StringIO(data), orient='split')
|
||||||
if 'date' in dataframe.columns:
|
if 'date' in dataframe.columns:
|
||||||
dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True)
|
dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True)
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ class LookaheadAnalysis(BaseAnalysis):
|
|||||||
# compare_df now comprises tuples with [1] having either 'self' or 'other'
|
# compare_df now comprises tuples with [1] having either 'self' or 'other'
|
||||||
if 'other' in col_name[1]:
|
if 'other' in col_name[1]:
|
||||||
continue
|
continue
|
||||||
self_value = compare_df_row[col_idx]
|
self_value = compare_df_row.iloc[col_idx]
|
||||||
other_value = compare_df_row[col_idx + 1]
|
other_value = compare_df_row.iloc[col_idx + 1]
|
||||||
|
|
||||||
# output differences
|
# output differences
|
||||||
if self_value != other_value:
|
if self_value != other_value:
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.optimize.lookahead_analysis import LookaheadAnalysis
|
from freqtrade.optimize.analysis.lookahead import LookaheadAnalysis
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class RecursiveAnalysis(BaseAnalysis):
|
|||||||
self.dict_recursive[indicator][part.startup_candle] = f"{diff:.3f}%"
|
self.dict_recursive[indicator][part.startup_candle] = f"{diff:.3f}%"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info("No difference found. Stop the process.")
|
logger.info("No variance on indicator(s) found due to recursive formula.")
|
||||||
break
|
break
|
||||||
|
|
||||||
# For lookahead bias check
|
# For lookahead bias check
|
||||||
@@ -100,7 +100,7 @@ class RecursiveAnalysis(BaseAnalysis):
|
|||||||
# logger.info("part value {:.5f}".format(values_diff_other))
|
# logger.info("part value {:.5f}".format(values_diff_other))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info("No lookahead bias on indicators found. Stop the process.")
|
logger.info("No lookahead bias on indicators found.")
|
||||||
|
|
||||||
def prepare_data(self, varholder: VarHolder, pairs_to_load: List[DataFrame]):
|
def prepare_data(self, varholder: VarHolder, pairs_to_load: List[DataFrame]):
|
||||||
|
|
||||||
@@ -120,6 +120,7 @@ class RecursiveAnalysis(BaseAnalysis):
|
|||||||
prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load
|
prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load
|
||||||
|
|
||||||
backtesting = Backtesting(prepare_data_config, self.exchange)
|
backtesting = Backtesting(prepare_data_config, self.exchange)
|
||||||
|
self.exchange = backtesting.exchange
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
|
|
||||||
varholder.data, varholder.timerange = backtesting.load_bt_data()
|
varholder.data, varholder.timerange = backtesting.load_bt_data()
|
||||||
+6
-6
@@ -5,7 +5,7 @@ from typing import Any, Dict, List
|
|||||||
|
|
||||||
from freqtrade.constants import Config
|
from freqtrade.constants import Config
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.optimize.recursive_analysis import RecursiveAnalysis
|
from freqtrade.optimize.analysis.recursive import RecursiveAnalysis
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
|
||||||
|
|
||||||
@@ -31,11 +31,14 @@ class RecursiveAnalysisSubFunctions:
|
|||||||
temp_data.append(values.get(int(candle), '-'))
|
temp_data.append(values.get(int(candle), '-'))
|
||||||
data.append(temp_data)
|
data.append(temp_data)
|
||||||
|
|
||||||
|
if len(data) > 0:
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
table = tabulate(data, headers=headers, tablefmt="orgtbl")
|
table = tabulate(data, headers=headers, tablefmt="orgtbl")
|
||||||
print(table)
|
print(table)
|
||||||
return table, headers, data
|
return table, headers, data
|
||||||
|
|
||||||
|
return None, None, data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calculate_config_overrides(config: Config):
|
def calculate_config_overrides(config: Config):
|
||||||
if 'timerange' not in config:
|
if 'timerange' not in config:
|
||||||
@@ -81,8 +84,7 @@ class RecursiveAnalysisSubFunctions:
|
|||||||
if not (strategy_list := config.get('strategy_list', [])):
|
if not (strategy_list := config.get('strategy_list', [])):
|
||||||
if config.get('strategy') is None:
|
if config.get('strategy') is None:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"No Strategy specified. Please specify a strategy via --strategy or "
|
"No Strategy specified. Please specify a strategy via --strategy"
|
||||||
"--strategy-list"
|
|
||||||
)
|
)
|
||||||
strategy_list = [config['strategy']]
|
strategy_list = [config['strategy']]
|
||||||
|
|
||||||
@@ -100,7 +102,5 @@ class RecursiveAnalysisSubFunctions:
|
|||||||
RecursiveAnalysisSubFunctions.text_table_recursive_analysis_instances(
|
RecursiveAnalysisSubFunctions.text_table_recursive_analysis_instances(
|
||||||
RecursiveAnalysis_instances)
|
RecursiveAnalysis_instances)
|
||||||
else:
|
else:
|
||||||
logger.error("There were no strategies specified neither through "
|
logger.error("There was no strategy specified through --strategy "
|
||||||
"--strategy nor through "
|
|
||||||
"--strategy-list "
|
|
||||||
"or timeframe was not specified.")
|
"or timeframe was not specified.")
|
||||||
@@ -33,14 +33,15 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera
|
|||||||
show_backtest_results,
|
show_backtest_results,
|
||||||
store_backtest_analysis_results,
|
store_backtest_analysis_results,
|
||||||
store_backtest_stats)
|
store_backtest_stats)
|
||||||
from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
|
from freqtrade.persistence import (LocalTrade, Order, PairLocks, Trade, disable_database_use,
|
||||||
|
enable_database_use)
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.types import BacktestResultType, get_BacktestResultType_default
|
from freqtrade.types import BacktestResultType, get_BacktestResultType_default
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
from freqtrade.util.migrations import migrate_data
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
|
|
||||||
@@ -116,8 +117,9 @@ class Backtesting:
|
|||||||
raise OperationalException("Timeframe needs to be set in either "
|
raise OperationalException("Timeframe needs to be set in either "
|
||||||
"configuration or as cli argument `--timeframe 5m`")
|
"configuration or as cli argument `--timeframe 5m`")
|
||||||
self.timeframe = str(self.config.get('timeframe'))
|
self.timeframe = str(self.config.get('timeframe'))
|
||||||
self.disable_database_use()
|
|
||||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||||
|
self.timeframe_td = timedelta(minutes=self.timeframe_min)
|
||||||
|
self.disable_database_use()
|
||||||
self.init_backtest_detail()
|
self.init_backtest_detail()
|
||||||
self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider)
|
self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider)
|
||||||
self._validate_pairlists_for_backtesting()
|
self._validate_pairlists_for_backtesting()
|
||||||
@@ -145,19 +147,20 @@ class Backtesting:
|
|||||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||||
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
||||||
|
|
||||||
if self.config.get('freqai', {}).get('enabled', False):
|
|
||||||
# For FreqAI, increase the required_startup to includes the training data
|
|
||||||
self.required_startup = self.dataprovider.get_required_startup(self.timeframe)
|
|
||||||
|
|
||||||
# Add maximum startup candle count to configuration for informative pairs support
|
# Add maximum startup candle count to configuration for informative pairs support
|
||||||
self.config['startup_candle_count'] = self.required_startup
|
self.config['startup_candle_count'] = self.required_startup
|
||||||
|
|
||||||
|
if self.config.get('freqai', {}).get('enabled', False):
|
||||||
|
# For FreqAI, increase the required_startup to includes the training data
|
||||||
|
# This value should NOT be written to startup_candle_count
|
||||||
|
self.required_startup = self.dataprovider.get_required_startup(self.timeframe)
|
||||||
|
|
||||||
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
||||||
# strategies which define "can_short=True" will fail to load in Spot mode.
|
# strategies which define "can_short=True" will fail to load in Spot mode.
|
||||||
self._can_short = self.trading_mode != TradingMode.SPOT
|
self._can_short = self.trading_mode != TradingMode.SPOT
|
||||||
self._position_stacking: bool = self.config.get('position_stacking', False)
|
self._position_stacking: bool = self.config.get('position_stacking', False)
|
||||||
self.enable_protections: bool = self.config.get('enable_protections', False)
|
self.enable_protections: bool = self.config.get('enable_protections', False)
|
||||||
migrate_binance_futures_data(config)
|
migrate_data(config, self.exchange)
|
||||||
|
|
||||||
self.init_backtest()
|
self.init_backtest()
|
||||||
|
|
||||||
@@ -176,8 +179,7 @@ class Backtesting:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def cleanup():
|
def cleanup():
|
||||||
LoggingMixin.show_output = True
|
LoggingMixin.show_output = True
|
||||||
PairLocks.use_db = True
|
enable_database_use()
|
||||||
Trade.use_db = True
|
|
||||||
|
|
||||||
def init_backtest_detail(self) -> None:
|
def init_backtest_detail(self) -> None:
|
||||||
# Load detail timeframe if specified
|
# Load detail timeframe if specified
|
||||||
@@ -239,7 +241,7 @@ class Backtesting:
|
|||||||
pairs=self.pairlists.whitelist,
|
pairs=self.pairlists.whitelist,
|
||||||
timeframe=self.timeframe,
|
timeframe=self.timeframe,
|
||||||
timerange=self.timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=self.config['startup_candle_count'],
|
startup_candles=self.required_startup,
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
data_format=self.config['dataformat_ohlcv'],
|
data_format=self.config['dataformat_ohlcv'],
|
||||||
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
|
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
|
||||||
@@ -276,11 +278,15 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
self.detail_data = {}
|
self.detail_data = {}
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
self.funding_fee_timeframe: str = self.exchange.get_option('funding_fee_timeframe')
|
||||||
|
self.funding_fee_timeframe_secs: int = timeframe_to_seconds(self.funding_fee_timeframe)
|
||||||
|
mark_timeframe: str = self.exchange.get_option('mark_ohlcv_timeframe')
|
||||||
|
|
||||||
# Load additional futures data.
|
# Load additional futures data.
|
||||||
funding_rates_dict = history.load_data(
|
funding_rates_dict = history.load_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config['datadir'],
|
||||||
pairs=self.pairlists.whitelist,
|
pairs=self.pairlists.whitelist,
|
||||||
timeframe=self.exchange.get_option('mark_ohlcv_timeframe'),
|
timeframe=self.funding_fee_timeframe,
|
||||||
timerange=self.timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=0,
|
startup_candles=0,
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
@@ -292,7 +298,7 @@ class Backtesting:
|
|||||||
mark_rates_dict = history.load_data(
|
mark_rates_dict = history.load_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config['datadir'],
|
||||||
pairs=self.pairlists.whitelist,
|
pairs=self.pairlists.whitelist,
|
||||||
timeframe=self.exchange.get_option('mark_ohlcv_timeframe'),
|
timeframe=mark_timeframe,
|
||||||
timerange=self.timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=0,
|
startup_candles=0,
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
@@ -320,9 +326,7 @@ class Backtesting:
|
|||||||
self.futures_data = {}
|
self.futures_data = {}
|
||||||
|
|
||||||
def disable_database_use(self):
|
def disable_database_use(self):
|
||||||
PairLocks.use_db = False
|
disable_database_use(self.timeframe)
|
||||||
PairLocks.timeframe = self.timeframe
|
|
||||||
Trade.use_db = False
|
|
||||||
|
|
||||||
def prepare_backtest(self, enable_protections):
|
def prepare_backtest(self, enable_protections):
|
||||||
"""
|
"""
|
||||||
@@ -525,10 +529,10 @@ class Backtesting:
|
|||||||
# This should not be reached...
|
# This should not be reached...
|
||||||
return row[OPEN_IDX]
|
return row[OPEN_IDX]
|
||||||
|
|
||||||
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
|
def _get_adjust_trade_entry_for_candle(
|
||||||
|
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
||||||
) -> LocalTrade:
|
) -> LocalTrade:
|
||||||
current_rate = row[OPEN_IDX]
|
current_rate: float = row[OPEN_IDX]
|
||||||
current_date = row[DATE_IDX].to_pydatetime()
|
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
||||||
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
||||||
@@ -536,7 +540,7 @@ class Backtesting:
|
|||||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||||
default_retval=None, supress_error=True)(
|
default_retval=None, supress_error=True)(
|
||||||
trade=trade, # type: ignore[arg-type]
|
trade=trade, # type: ignore[arg-type]
|
||||||
current_time=current_date, current_rate=current_rate,
|
current_time=current_time, current_rate=current_rate,
|
||||||
current_profit=current_profit, min_stake=min_stake,
|
current_profit=current_profit, min_stake=min_stake,
|
||||||
max_stake=min(max_stake, stake_available),
|
max_stake=min(max_stake, stake_available),
|
||||||
current_entry_rate=current_rate, current_exit_rate=current_rate,
|
current_entry_rate=current_rate, current_exit_rate=current_rate,
|
||||||
@@ -561,18 +565,15 @@ class Backtesting:
|
|||||||
self.precision_mode, trade.contract_size)
|
self.precision_mode, trade.contract_size)
|
||||||
if amount == 0.0:
|
if amount == 0.0:
|
||||||
return trade
|
return trade
|
||||||
if amount > trade.amount:
|
|
||||||
# This is currently ineffective as remaining would become < min tradable
|
|
||||||
amount = trade.amount
|
|
||||||
remaining = (trade.amount - amount) * current_rate
|
remaining = (trade.amount - amount) * current_rate
|
||||||
if remaining < min_stake:
|
if min_stake and remaining != 0 and remaining < min_stake:
|
||||||
# Remaining stake is too low to be sold.
|
# Remaining stake is too low to be sold.
|
||||||
return trade
|
return trade
|
||||||
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
|
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
|
||||||
pos_trade = self._get_exit_for_signal(trade, row, exit_, amount)
|
pos_trade = self._get_exit_for_signal(trade, row, exit_, current_time, amount)
|
||||||
if pos_trade is not None:
|
if pos_trade is not None:
|
||||||
order = pos_trade.orders[-1]
|
order = pos_trade.orders[-1]
|
||||||
if self._try_close_open_order(order, trade, current_date, row):
|
if self._try_close_open_order(order, trade, current_time, row):
|
||||||
trade.recalc_trade_from_orders()
|
trade.recalc_trade_from_orders()
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
return pos_trade
|
return pos_trade
|
||||||
@@ -597,6 +598,8 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
if order and self._get_order_filled(order.ft_price, row):
|
if order and self._get_order_filled(order.ft_price, row):
|
||||||
order.close_bt_order(current_date, trade)
|
order.close_bt_order(current_date, trade)
|
||||||
|
self._run_funding_fees(trade, current_date, force=True)
|
||||||
|
|
||||||
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
||||||
# trade is still open
|
# trade is still open
|
||||||
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
||||||
@@ -615,11 +618,11 @@ class Backtesting:
|
|||||||
|
|
||||||
def _get_exit_for_signal(
|
def _get_exit_for_signal(
|
||||||
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
|
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
|
||||||
|
current_time: datetime,
|
||||||
amount: Optional[float] = None) -> Optional[LocalTrade]:
|
amount: Optional[float] = None) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
|
||||||
if exit_.exit_flag:
|
if exit_.exit_flag:
|
||||||
trade.close_date = exit_candle_time
|
trade.close_date = current_time
|
||||||
exit_reason = exit_.exit_reason
|
exit_reason = exit_.exit_reason
|
||||||
amount_ = amount if amount is not None else trade.amount
|
amount_ = amount if amount is not None else trade.amount
|
||||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||||
@@ -647,10 +650,10 @@ class Backtesting:
|
|||||||
default_retval=close_rate)(
|
default_retval=close_rate)(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
trade=trade, # type: ignore[arg-type]
|
trade=trade, # type: ignore[arg-type]
|
||||||
current_time=exit_candle_time,
|
current_time=current_time,
|
||||||
proposed_rate=close_rate, current_profit=current_profit,
|
proposed_rate=close_rate, current_profit=current_profit,
|
||||||
exit_tag=exit_reason)
|
exit_tag=exit_reason)
|
||||||
if rate != close_rate:
|
if rate is not None and rate != close_rate:
|
||||||
close_rate = price_to_precision(rate, trade.price_precision,
|
close_rate = price_to_precision(rate, trade.price_precision,
|
||||||
self.precision_mode)
|
self.precision_mode)
|
||||||
# We can't place orders lower than current low.
|
# We can't place orders lower than current low.
|
||||||
@@ -673,7 +676,7 @@ class Backtesting:
|
|||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
sell_reason=exit_reason, # deprecated
|
sell_reason=exit_reason, # deprecated
|
||||||
exit_reason=exit_reason,
|
exit_reason=exit_reason,
|
||||||
current_time=exit_candle_time)):
|
current_time=current_time)):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
trade.exit_reason = exit_reason
|
trade.exit_reason = exit_reason
|
||||||
@@ -714,21 +717,15 @@ class Backtesting:
|
|||||||
trade.orders.append(order)
|
trade.orders.append(order)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
def _check_trade_exit(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
def _check_trade_exit(
|
||||||
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
||||||
|
) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
self._run_funding_fees(trade, current_time)
|
||||||
trade.funding_fees = self.exchange.calculate_funding_fees(
|
|
||||||
self.futures_data[trade.pair],
|
|
||||||
amount=trade.amount,
|
|
||||||
is_short=trade.is_short,
|
|
||||||
open_date=trade.date_last_filled_utc,
|
|
||||||
close_date=exit_candle_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if we need to adjust our current positions
|
# Check if we need to adjust our current positions
|
||||||
if self.strategy.position_adjustment_enable:
|
if self.strategy.position_adjustment_enable:
|
||||||
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
trade = self._get_adjust_trade_entry_for_candle(trade, row, current_time)
|
||||||
|
|
||||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||||
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||||
@@ -738,11 +735,32 @@ class Backtesting:
|
|||||||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||||
)
|
)
|
||||||
for exit_ in exits:
|
for exit_ in exits:
|
||||||
t = self._get_exit_for_signal(trade, row, exit_)
|
t = self._get_exit_for_signal(trade, row, exit_, current_time)
|
||||||
if t:
|
if t:
|
||||||
return t
|
return t
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _run_funding_fees(self, trade: LocalTrade, current_time: datetime, force: bool = False):
|
||||||
|
"""
|
||||||
|
Calculate funding fees if necessary and add them to the trade.
|
||||||
|
"""
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
|
||||||
|
if (
|
||||||
|
force
|
||||||
|
or (current_time.timestamp() % self.funding_fee_timeframe_secs) == 0
|
||||||
|
):
|
||||||
|
# Funding fee interval.
|
||||||
|
trade.set_funding_fees(
|
||||||
|
self.exchange.calculate_funding_fees(
|
||||||
|
self.futures_data[trade.pair],
|
||||||
|
amount=trade.amount,
|
||||||
|
is_short=trade.is_short,
|
||||||
|
open_date=trade.date_last_filled_utc,
|
||||||
|
close_date=current_time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_valid_price_and_stake(
|
def get_valid_price_and_stake(
|
||||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
|
||||||
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
direction: LongShort, current_time: datetime, entry_tag: Optional[str],
|
||||||
@@ -760,7 +778,7 @@ class Backtesting:
|
|||||||
) # default value is the open rate
|
) # default value is the open rate
|
||||||
# We can't place orders higher than current high (otherwise it'd be a stop limit entry)
|
# We can't place orders higher than current high (otherwise it'd be a stop limit entry)
|
||||||
# which freqtrade does not support in live.
|
# which freqtrade does not support in live.
|
||||||
if new_rate != propose_rate:
|
if new_rate is not None and new_rate != propose_rate:
|
||||||
propose_rate = price_to_precision(new_rate, price_precision,
|
propose_rate = price_to_precision(new_rate, price_precision,
|
||||||
self.precision_mode)
|
self.precision_mode)
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
@@ -772,7 +790,8 @@ class Backtesting:
|
|||||||
leverage = trade.leverage if trade else 1.0
|
leverage = trade.leverage if trade else 1.0
|
||||||
if not pos_adjust:
|
if not pos_adjust:
|
||||||
try:
|
try:
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
|
stake_amount = self.wallets.get_trade_stake_amount(
|
||||||
|
pair, self.strategy.max_open_trades, update=False)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
return 0, 0, 0, 0
|
return 0, 0, 0, 0
|
||||||
|
|
||||||
@@ -954,7 +973,7 @@ class Backtesting:
|
|||||||
|
|
||||||
def trade_slot_available(self, open_trade_count: int) -> bool:
|
def trade_slot_available(self, open_trade_count: int) -> bool:
|
||||||
# Always allow trades when max_open_trades is enabled.
|
# Always allow trades when max_open_trades is enabled.
|
||||||
max_open_trades: IntOrInf = self.config['max_open_trades']
|
max_open_trades: IntOrInf = self.strategy.max_open_trades
|
||||||
if max_open_trades <= 0 or open_trade_count < max_open_trades:
|
if max_open_trades <= 0 or open_trade_count < max_open_trades:
|
||||||
return True
|
return True
|
||||||
# Rejected trade
|
# Rejected trade
|
||||||
@@ -1145,7 +1164,7 @@ class Backtesting:
|
|||||||
|
|
||||||
# 4. Create exit orders (if any)
|
# 4. Create exit orders (if any)
|
||||||
if not trade.has_open_orders:
|
if not trade.has_open_orders:
|
||||||
self._check_trade_exit(trade, row) # Place exit order if necessary
|
self._check_trade_exit(trade, row, current_time) # Place exit order if necessary
|
||||||
|
|
||||||
# 5. Process exit orders.
|
# 5. Process exit orders.
|
||||||
order = trade.select_order(trade.exit_side, is_open=True)
|
order = trade.select_order(trade.exit_side, is_open=True)
|
||||||
@@ -1187,10 +1206,10 @@ class Backtesting:
|
|||||||
|
|
||||||
# Indexes per pair, so some pairs are allowed to have a missing start.
|
# Indexes per pair, so some pairs are allowed to have a missing start.
|
||||||
indexes: Dict = defaultdict(int)
|
indexes: Dict = defaultdict(int)
|
||||||
current_time = start_date + timedelta(minutes=self.timeframe_min)
|
current_time = start_date + self.timeframe_td
|
||||||
|
|
||||||
self.progress.init_step(BacktestState.BACKTEST, int(
|
self.progress.init_step(BacktestState.BACKTEST, int(
|
||||||
(end_date - start_date) / timedelta(minutes=self.timeframe_min)))
|
(end_date - start_date) / self.timeframe_td))
|
||||||
# Loop timerange and get candle for each pair at that point in time
|
# Loop timerange and get candle for each pair at that point in time
|
||||||
while current_time <= end_date:
|
while current_time <= end_date:
|
||||||
open_trade_count_start = LocalTrade.bt_open_open_trade_count
|
open_trade_count_start = LocalTrade.bt_open_open_trade_count
|
||||||
@@ -1217,7 +1236,7 @@ class Backtesting:
|
|||||||
# Spread out into detail timeframe.
|
# Spread out into detail timeframe.
|
||||||
# Should only happen when we are either in a trade for this pair
|
# Should only happen when we are either in a trade for this pair
|
||||||
# or when we got the signal for a new trade.
|
# or when we got the signal for a new trade.
|
||||||
exit_candle_end = current_detail_time + timedelta(minutes=self.timeframe_min)
|
exit_candle_end = current_detail_time + self.timeframe_td
|
||||||
|
|
||||||
detail_data = self.detail_data[pair]
|
detail_data = self.detail_data[pair]
|
||||||
detail_data = detail_data.loc[
|
detail_data = detail_data.loc[
|
||||||
@@ -1253,7 +1272,7 @@ class Backtesting:
|
|||||||
|
|
||||||
# Move time one configured time_interval ahead.
|
# Move time one configured time_interval ahead.
|
||||||
self.progress.increment()
|
self.progress.increment()
|
||||||
current_time += timedelta(minutes=self.timeframe_min)
|
current_time += self.timeframe_td
|
||||||
|
|
||||||
self.handle_left_open(LocalTrade.bt_trades_open_pp, data=data)
|
self.handle_left_open(LocalTrade.bt_trades_open_pp, data=data)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class BaseAnalysis:
|
|||||||
self.full_varHolder.from_dt = parsed_timerange.startdt
|
self.full_varHolder.from_dt = parsed_timerange.startdt
|
||||||
|
|
||||||
if parsed_timerange.stopdt is None:
|
if parsed_timerange.stopdt is None:
|
||||||
self.full_varHolder.to_dt = datetime.utcnow()
|
self.full_varHolder.to_dt = datetime.now(timezone.utc)
|
||||||
else:
|
else:
|
||||||
self.full_varHolder.to_dt = parsed_timerange.stopdt
|
self.full_varHolder.to_dt = parsed_timerange.stopdt
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user