mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Compare commits
919 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bbc0eefed | ||
|
|
9806699592 | ||
|
|
4025ec9900 | ||
|
|
6a397f579e | ||
|
|
c31f118d0c | ||
|
|
2f005d6be9 | ||
|
|
45f5394d79 | ||
|
|
7e214d8e4c | ||
|
|
ed10048394 | ||
|
|
43f2ef226c | ||
|
|
42b5a0977e | ||
|
|
3b1252207d | ||
|
|
4ac53f1549 | ||
|
|
21b807aa85 | ||
|
|
28e0398c68 | ||
|
|
637ec60644 | ||
|
|
6ba9316e15 | ||
|
|
60e3e626e4 | ||
|
|
9db915853a | ||
|
|
5237723f22 | ||
|
|
eb07f1fee9 | ||
|
|
8d92f8b362 | ||
|
|
49f0a72121 | ||
|
|
5978b7bb93 | ||
|
|
e09408f9b7 | ||
|
|
0268bfdbd4 | ||
|
|
b994f5c273 | ||
|
|
e9de088209 | ||
|
|
d05db077e2 | ||
|
|
d2f2473070 | ||
|
|
47b6b56566 | ||
|
|
27cc73f47e | ||
|
|
cc91ccad3e | ||
|
|
0102413f58 | ||
|
|
665e0570ae | ||
|
|
a75fb3d4be | ||
|
|
2d86510acf | ||
|
|
5c3b14069e | ||
|
|
6c0a1fc42c | ||
|
|
d066ab2620 | ||
|
|
3a5bd4c03e | ||
|
|
93b2621651 | ||
|
|
6aa1ec2a4c | ||
|
|
cc9fc41318 | ||
|
|
fe40636ae1 | ||
|
|
577b1fd965 | ||
|
|
0f97a999fb | ||
|
|
22af7f7881 | ||
|
|
6ffb8b7a70 | ||
|
|
95e725c2b6 | ||
|
|
b18ad7a834 | ||
|
|
cfa76fa600 | ||
|
|
d226fff111 | ||
|
|
0c6164df7e | ||
|
|
d8bc350445 | ||
|
|
242ff26e21 | ||
|
|
ab0adabd39 | ||
|
|
ba4db0da49 | ||
|
|
7aa42f8868 | ||
|
|
3245ebccd4 | ||
|
|
b1a3e213ae | ||
|
|
2fcddfc866 | ||
|
|
313091eb1c | ||
|
|
508a35fc20 | ||
|
|
9cedbc1345 | ||
|
|
e66fa1cec6 | ||
|
|
1cd8ed0c1a | ||
|
|
74a0f44230 | ||
|
|
dc825c249c | ||
|
|
15a4df4c49 | ||
|
|
f0cf8d6a81 | ||
|
|
7fff1f5ce1 | ||
|
|
c625058f41 | ||
|
|
50b4563912 | ||
|
|
69f29e8907 | ||
|
|
ee6ad51a44 | ||
|
|
e8657d2444 | ||
|
|
a42000e1dd | ||
|
|
c3e19507bf | ||
|
|
27238d97d5 | ||
|
|
e9a75e57b8 | ||
|
|
5cbc073dd1 | ||
|
|
b7da02aab4 | ||
|
|
f3e3a8fcbe | ||
|
|
44fe0478ea | ||
|
|
9dc9bc2346 | ||
|
|
9c1cce6fe2 | ||
|
|
cab394a058 | ||
|
|
9d9ace2b22 | ||
|
|
c2462ee87b | ||
|
|
39f41def54 | ||
|
|
76e45883bd | ||
|
|
19ce7180be | ||
|
|
b00467c8ef | ||
|
|
2cf045c53e | ||
|
|
e2a100c925 | ||
|
|
eda1ec652f | ||
|
|
0135784589 | ||
|
|
5e654620b7 | ||
|
|
16b4ae8396 | ||
|
|
a5f3b68bff | ||
|
|
f163240710 | ||
|
|
c5f455d660 | ||
|
|
c8d191a5c9 | ||
|
|
e6ec8f9f30 | ||
|
|
4d566e8bad | ||
|
|
e6ccc1427c | ||
|
|
52b186eabe | ||
|
|
67ff48ce3e | ||
|
|
045ca8739d | ||
|
|
64b404068f | ||
|
|
dda513c923 | ||
|
|
6c5eff4a7c | ||
|
|
6884ad2211 | ||
|
|
9e4effaa14 | ||
|
|
849d694c27 | ||
|
|
1d781ea9e0 | ||
|
|
acf3b751f0 | ||
|
|
9bdfaf3803 | ||
|
|
f8eb1cd58a | ||
|
|
3b4bbe7a18 | ||
|
|
2bd59de002 | ||
|
|
ac413c65dc | ||
|
|
c01953daf2 | ||
|
|
a9ecdc7764 | ||
|
|
869a5b4901 | ||
|
|
2081d7552f | ||
|
|
e298e77319 | ||
|
|
35580b135a | ||
|
|
f987e6e0f9 | ||
|
|
85f1291597 | ||
|
|
5ea739f943 | ||
|
|
94d2790ab5 | ||
|
|
9aa7db103d | ||
|
|
3398f31b87 | ||
|
|
0e39cc1187 | ||
|
|
a218946f52 | ||
|
|
2a79c1eed2 | ||
|
|
7dc3e67bba | ||
|
|
3c869a8032 | ||
|
|
edba5a0014 | ||
|
|
b4a0591429 | ||
|
|
adbc0159ae | ||
|
|
a5510d14e9 | ||
|
|
9f5d4a5252 | ||
|
|
42d2ecba68 | ||
|
|
ceb1f91d9d | ||
|
|
3430850421 | ||
|
|
c5726e88e8 | ||
|
|
867a3273ce | ||
|
|
2a236db18f | ||
|
|
242ac4d8f4 | ||
|
|
3e0edc7ee2 | ||
|
|
0bb1127cb6 | ||
|
|
9d2c6c8de2 | ||
|
|
9513115ce0 | ||
|
|
f2cbc5fb8f | ||
|
|
26d76cdb19 | ||
|
|
65a516e229 | ||
|
|
edda122ed0 | ||
|
|
3044b861bd | ||
|
|
13932f55f5 | ||
|
|
3d028f512e | ||
|
|
bb2d8fefd7 | ||
|
|
623e8f6984 | ||
|
|
865e0d3af9 | ||
|
|
45cfdbbda7 | ||
|
|
2b00a5d90a | ||
|
|
bd2ecf8ce3 | ||
|
|
972b8a1726 | ||
|
|
fe631ffd04 | ||
|
|
bde82e9654 | ||
|
|
df481eb642 | ||
|
|
6ff83abb61 | ||
|
|
4fdf8a75cd | ||
|
|
2e49125e87 | ||
|
|
7e56704767 | ||
|
|
95b89e059a | ||
|
|
ef8386c065 | ||
|
|
7af445adf3 | ||
|
|
ee68f743c7 | ||
|
|
e39d911177 | ||
|
|
48ac37a1b8 | ||
|
|
e8f37666ea | ||
|
|
e2e0015119 | ||
|
|
3343b34725 | ||
|
|
e107290230 | ||
|
|
1b66f01ec0 | ||
|
|
f9c7a2cacb | ||
|
|
5ce63cd54a | ||
|
|
03f3d0dc8b | ||
|
|
74578b8752 | ||
|
|
caec5ac941 | ||
|
|
88f823f899 | ||
|
|
9a6a89c238 | ||
|
|
e8614abc5d | ||
|
|
87ae2430df | ||
|
|
dc9fda76f3 | ||
|
|
3b15cce07a | ||
|
|
8cad90f9e6 | ||
|
|
9c60ab796d | ||
|
|
05789c4b92 | ||
|
|
962d487edb | ||
|
|
04335ddd89 | ||
|
|
8b6010255f | ||
|
|
ccdc0ed26d | ||
|
|
3f6c0ba6d6 | ||
|
|
fe9d7b033e | ||
|
|
12e893ff2d | ||
|
|
cda2a2586b | ||
|
|
bf4e3f55f4 | ||
|
|
51ad05efdb | ||
|
|
89f5cf8291 | ||
|
|
949ab2a17c | ||
|
|
08b090c707 | ||
|
|
c64beb3f76 | ||
|
|
aae9c3194f | ||
|
|
20c9c93b3e | ||
|
|
771519e311 | ||
|
|
f91557f549 | ||
|
|
514860ac3b | ||
|
|
9d7ebc65e7 | ||
|
|
6aab3fe25a | ||
|
|
7c0a49a6f9 | ||
|
|
292df115e8 | ||
|
|
9f53e9f5dd | ||
|
|
ee808abfea | ||
|
|
ab3e3797a5 | ||
|
|
7fc156648a | ||
|
|
f0c0f5618b | ||
|
|
7c36e571d2 | ||
|
|
040ba5662c | ||
|
|
2886fa288a | ||
|
|
736deaae32 | ||
|
|
c9e15c2f86 | ||
|
|
d48f03c32e | ||
|
|
1760a8dfbc | ||
|
|
f278fcfc3f | ||
|
|
816d942ded | ||
|
|
423805c9ca | ||
|
|
a7e45c5a73 | ||
|
|
d060d27745 | ||
|
|
75dc174c76 | ||
|
|
d977695d48 | ||
|
|
b6b7dcd61c | ||
|
|
43ef831bf7 | ||
|
|
cabe291006 | ||
|
|
6b3d25b54b | ||
|
|
68adfc6607 | ||
|
|
ba0d7aa09c | ||
|
|
50b572a657 | ||
|
|
9634e516a9 | ||
|
|
c38f3a2b9a | ||
|
|
44780837f1 | ||
|
|
c6bb68bd30 | ||
|
|
8923c02222 | ||
|
|
b4685151ce | ||
|
|
756f44fcbd | ||
|
|
51fbeed71f | ||
|
|
d66fb86449 | ||
|
|
40df303122 | ||
|
|
a504abf00c | ||
|
|
d9c2b7d460 | ||
|
|
0e62b8bd85 | ||
|
|
6d1c54ed92 | ||
|
|
b5789203f2 | ||
|
|
92011f8294 | ||
|
|
b3db1ec1a7 | ||
|
|
5f60fcb602 | ||
|
|
30a7be457c | ||
|
|
93198ed28b | ||
|
|
6af5135802 | ||
|
|
6b233eb862 | ||
|
|
75e3d22043 | ||
|
|
e5da5f7fe7 | ||
|
|
4fcfb1eaca | ||
|
|
bfc68ec792 | ||
|
|
ae8c5eb75f | ||
|
|
513e84880e | ||
|
|
626b9bbf64 | ||
|
|
da7da2ce52 | ||
|
|
3232251fea | ||
|
|
e603cca7a5 | ||
|
|
8f8acf5b06 | ||
|
|
565a543b7b | ||
|
|
5e12b05424 | ||
|
|
a4c8b5bf5d | ||
|
|
cbf09b5ad9 | ||
|
|
2c66b33fd1 | ||
|
|
067c122bf3 | ||
|
|
defa1c027d | ||
|
|
ea179a8e38 | ||
|
|
8a17615b5a | ||
|
|
95920f3b6b | ||
|
|
365b9c3e9c | ||
|
|
3f6eeda3f0 | ||
|
|
3121206afe | ||
|
|
240936eb19 | ||
|
|
1336781f8f | ||
|
|
661cd65bdd | ||
|
|
fb498795ad | ||
|
|
2ae398913d | ||
|
|
d711b8c0e9 | ||
|
|
395414ccde | ||
|
|
9f29ad77bd | ||
|
|
545e5c5bc6 | ||
|
|
1b374fcf7e | ||
|
|
518d7dfde8 | ||
|
|
f8ddb10607 | ||
|
|
0ef13be577 | ||
|
|
c559f95703 | ||
|
|
f7cb75ff93 | ||
|
|
29076acc69 | ||
|
|
99b2be90fd | ||
|
|
f8c72feea8 | ||
|
|
69c2b12879 | ||
|
|
3820a38e79 | ||
|
|
60bc9f4f5e | ||
|
|
a8842f38ca | ||
|
|
667a623310 | ||
|
|
067208bc9d | ||
|
|
70ebd09de4 | ||
|
|
782f4112cd | ||
|
|
447bcf98e1 | ||
|
|
d19b11a00f | ||
|
|
ad6de07d2b | ||
|
|
0e81d7204c | ||
|
|
91b0394433 | ||
|
|
b2ef8f4e14 | ||
|
|
81925dfadf | ||
|
|
098159ad41 | ||
|
|
fe12d2e3b7 | ||
|
|
df1f57392c | ||
|
|
949ca1abf8 | ||
|
|
e52d5e32aa | ||
|
|
aaeeb9c0c6 | ||
|
|
d2958fc0f5 | ||
|
|
f8235aec74 | ||
|
|
13ffb39245 | ||
|
|
75b2db4424 | ||
|
|
14aaf8976f | ||
|
|
fcb0ff1b60 | ||
|
|
31669fde03 | ||
|
|
17b3f01b28 | ||
|
|
cadf573170 | ||
|
|
a12876da92 | ||
|
|
eebf39a1df | ||
|
|
210f66e48b | ||
|
|
91e72ba081 | ||
|
|
be308ff914 | ||
|
|
4ee35438a7 | ||
|
|
11dab2b9ca | ||
|
|
f02adf2a45 | ||
|
|
9e24992835 | ||
|
|
e9e2a83436 | ||
|
|
af51ff4162 | ||
|
|
e8ee087e9d | ||
|
|
8cc477f353 | ||
|
|
c63856dac4 | ||
|
|
8d1a575a9b | ||
|
|
9e8ca8d4bf | ||
|
|
491d742bf9 | ||
|
|
dc35a8022b | ||
|
|
70b1a05d97 | ||
|
|
785c3e9e61 | ||
|
|
9ad9ce0da1 | ||
|
|
042e47543c | ||
|
|
71d612f6e4 | ||
|
|
a4ede02ced | ||
|
|
ea4db0ffb6 | ||
|
|
d785d76370 | ||
|
|
b6462cd51f | ||
|
|
611850bf91 | ||
|
|
ddfadbb69e | ||
|
|
045ac1019e | ||
|
|
ee7ba96e85 | ||
|
|
8e96ac8765 | ||
|
|
acf1e734ec | ||
|
|
0a478bc0dc | ||
|
|
9005447590 | ||
|
|
d300964691 | ||
|
|
407a3bca62 | ||
|
|
310e438706 | ||
|
|
8a2a8ab8b5 | ||
|
|
5e440a4cdc | ||
|
|
3a1b641db1 | ||
|
|
2cffc3228a | ||
|
|
7fa6d804ce | ||
|
|
a398eea244 | ||
|
|
0e87cc8c84 | ||
|
|
764bab8eb9 | ||
|
|
351740fc80 | ||
|
|
9143ea13ad | ||
|
|
4711d66cab | ||
|
|
09967d4ff8 | ||
|
|
e0335705b2 | ||
|
|
4ce3cc66d5 | ||
|
|
fce3d7586f | ||
|
|
cda912bd8c | ||
|
|
84a0f9ea42 | ||
|
|
08fa5136e1 | ||
|
|
7a79b292e4 | ||
|
|
a53e9e3a98 | ||
|
|
f7d5280f47 | ||
|
|
29c56f4447 | ||
|
|
c9207bcc00 | ||
|
|
132f28ad44 | ||
|
|
b2c215029d | ||
|
|
89257832d7 | ||
|
|
219d0b7fb0 | ||
|
|
4e308a1a3e | ||
|
|
3c15e3ebdd | ||
|
|
8655e521d7 | ||
|
|
05deb9e09b | ||
|
|
91886120a7 | ||
|
|
09286d4918 | ||
|
|
161db08745 | ||
|
|
8aaaab4163 | ||
|
|
53db382695 | ||
|
|
1b6051e4df | ||
|
|
8d206f8308 | ||
|
|
b94f3e80c4 | ||
|
|
2a842778e3 | ||
|
|
e525275d10 | ||
|
|
4fa92ec0fa | ||
|
|
69eff89049 | ||
|
|
12677f2d42 | ||
|
|
a94a89086f | ||
|
|
80a71323cc | ||
|
|
fd77f699df | ||
|
|
93cf2cd19b | ||
|
|
585536835a | ||
|
|
f5e437d8c7 | ||
|
|
14c4854987 | ||
|
|
3af5691b91 | ||
|
|
9f26c4ebdc | ||
|
|
11790fbf01 | ||
|
|
f3e6bcb20c | ||
|
|
e0e50115d2 | ||
|
|
b2a22f1afb | ||
|
|
9d3322df8c | ||
|
|
91d1061c73 | ||
|
|
0ffb184eba | ||
|
|
5b9711c002 | ||
|
|
096a6426db | ||
|
|
84baef922c | ||
|
|
51c3a31bb5 | ||
|
|
06fa07e73e | ||
|
|
4da2bfefb7 | ||
|
|
3b30aab8a7 | ||
|
|
c2e9685e04 | ||
|
|
d6f5f6b7ba | ||
|
|
a4ab42560f | ||
|
|
a76136c010 | ||
|
|
e35a349229 | ||
|
|
3d36747b92 | ||
|
|
c0784b7c33 | ||
|
|
828315f675 | ||
|
|
4c4ba08e85 | ||
|
|
94196c84e9 | ||
|
|
9d476b5ab2 | ||
|
|
0a07dfc5cf | ||
|
|
d69f7ae471 | ||
|
|
974d899b33 | ||
|
|
6948e0ba84 | ||
|
|
a325f1ce2b | ||
|
|
997eb7574a | ||
|
|
8873e0072c | ||
|
|
c29389f5f3 | ||
|
|
4b8eaaf7aa | ||
|
|
8d813fa728 | ||
|
|
28e318b646 | ||
|
|
2961efdc18 | ||
|
|
3c589bb877 | ||
|
|
d8dbea9d5b | ||
|
|
f960ea039e | ||
|
|
de80234165 | ||
|
|
906be7be7c | ||
|
|
482847a994 | ||
|
|
58d308fd05 | ||
|
|
59acd5ec7c | ||
|
|
ca739f71fb | ||
|
|
23a70932d2 | ||
|
|
1a34b9b61c | ||
|
|
8f92912852 | ||
|
|
2600cb7b64 | ||
|
|
200b6ea10f | ||
|
|
8c1efec43a | ||
|
|
dd30d74688 | ||
|
|
6f42d6658f | ||
|
|
c4cdd85e80 | ||
|
|
0bd71db5df | ||
|
|
feced71a6d | ||
|
|
444ee274d7 | ||
|
|
bb0b160001 | ||
|
|
241d510096 | ||
|
|
c042d08bb7 | ||
|
|
1ce63b5b42 | ||
|
|
dd0ba183f8 | ||
|
|
933a553dd4 | ||
|
|
af67bbde31 | ||
|
|
6310b40fc6 | ||
|
|
2463a4af2a | ||
|
|
51ad8f5ab4 | ||
|
|
615ce6aa69 | ||
|
|
43b41324e2 | ||
|
|
91b0db138a | ||
|
|
197ce0b670 | ||
|
|
002003292e | ||
|
|
0b367a14f1 | ||
|
|
e5dcd520ba | ||
|
|
90b75afdb1 | ||
|
|
2d60e4b18b | ||
|
|
c5d8499ad2 | ||
|
|
b77c0d2813 | ||
|
|
a636dda07d | ||
|
|
dc5719e1f4 | ||
|
|
d53f63023a | ||
|
|
0221607318 | ||
|
|
a1b5c7242e | ||
|
|
a225672c87 | ||
|
|
4b4fcc7034 | ||
|
|
85094a59e6 | ||
|
|
e02e64fc07 | ||
|
|
176beefa88 | ||
|
|
1a85e3b4cd | ||
|
|
5209ce5bfa | ||
|
|
2c5a499a8b | ||
|
|
6d89da45b0 | ||
|
|
eb328037b7 | ||
|
|
afba31c3f9 | ||
|
|
c4cbe79b48 | ||
|
|
8ba7657007 | ||
|
|
48d8376878 | ||
|
|
74e583a612 | ||
|
|
29619ccf1c | ||
|
|
ab092fc77f | ||
|
|
28d8fc871a | ||
|
|
ad6a249832 | ||
|
|
50c9679e23 | ||
|
|
8eb39178ea | ||
|
|
dd35ba5e81 | ||
|
|
3cc772c8e9 | ||
|
|
247d7475e1 | ||
|
|
51d59e673b | ||
|
|
ae39f6fba5 | ||
|
|
15cf5ac2d7 | ||
|
|
de99942499 | ||
|
|
ccf3c69874 | ||
|
|
8ad5afd3a1 | ||
|
|
0d4a2c6c3a | ||
|
|
02b2de5c73 | ||
|
|
2bc67b4a96 | ||
|
|
9df1c23c71 | ||
|
|
7a47d81b7b | ||
|
|
831e708897 | ||
|
|
757538f114 | ||
|
|
cc4900f66c | ||
|
|
7d02580a2b | ||
|
|
3d3b0938e5 | ||
|
|
9c5773ca0a | ||
|
|
092776442b | ||
|
|
0267976044 | ||
|
|
5864968ce9 | ||
|
|
33bc8a2404 | ||
|
|
dfce202034 | ||
|
|
ea46bb3b84 | ||
|
|
8418dfbaed | ||
|
|
caf4580346 | ||
|
|
a90ced1f38 | ||
|
|
6c0c77b3a1 | ||
|
|
16d4a4723f | ||
|
|
327e653fae | ||
|
|
81f773054d | ||
|
|
7e91a0f4a8 | ||
|
|
9d471f3c9a | ||
|
|
7e46a9833b | ||
|
|
988a0245c2 | ||
|
|
0376630f7a | ||
|
|
c7d0329754 | ||
|
|
bc2e920ae2 | ||
|
|
3721610a63 | ||
|
|
e060516cc7 | ||
|
|
20abd4b833 | ||
|
|
904381058c | ||
|
|
5e64d629a3 | ||
|
|
d71102c45a | ||
|
|
403f7668d5 | ||
|
|
930c25f7f1 | ||
|
|
187d029d20 | ||
|
|
9914198a6c | ||
|
|
c6444a10a8 | ||
|
|
383b24ab84 | ||
|
|
9cbab35de0 | ||
|
|
eeecdd4e5a | ||
|
|
2af663dccb | ||
|
|
0be7e2ef70 | ||
|
|
4d1ce8178c | ||
|
|
c5ccf44750 | ||
|
|
e4380b533b | ||
|
|
62262d0bb5 | ||
|
|
52d92cba90 | ||
|
|
0df5932593 | ||
|
|
d1838dceec | ||
|
|
c6bd143785 | ||
|
|
d51fd1a5d0 | ||
|
|
c4e30862ee | ||
|
|
3dd6fe2703 | ||
|
|
fe796c46c3 | ||
|
|
f200f52a16 | ||
|
|
d59608f764 | ||
|
|
b3e6e710d8 | ||
|
|
8ab07e0451 | ||
|
|
ad55faafa8 | ||
|
|
bbd58e772e | ||
|
|
e8b2ae0b85 | ||
|
|
13620df717 | ||
|
|
fb103dd162 | ||
|
|
3b65c986ee | ||
|
|
cad7d9135a | ||
|
|
b152d1a7ab | ||
|
|
aa8f44f68c | ||
|
|
1810d86555 | ||
|
|
39e8e507d9 | ||
|
|
3eb571f34c | ||
|
|
e8be357624 | ||
|
|
32605fa10a | ||
|
|
0b9b5f3993 | ||
|
|
86aa18efe6 | ||
|
|
76d22bc743 | ||
|
|
01cd30984b | ||
|
|
fceb411154 | ||
|
|
0413598d7b | ||
|
|
3ccfe88ad8 | ||
|
|
065ebd39ef | ||
|
|
bcccdda7c0 | ||
|
|
4c005e7086 | ||
|
|
2a141af42e | ||
|
|
472690a55f | ||
|
|
8cef567abc | ||
|
|
5d22d541f2 | ||
|
|
c3d14ab9b9 | ||
|
|
0488525888 | ||
|
|
b8713a515e | ||
|
|
b976f24672 | ||
|
|
8f1f416a52 | ||
|
|
0d9d23a888 | ||
|
|
a5fb3e08f7 | ||
|
|
59caff8fb1 | ||
|
|
f825e81d0e | ||
|
|
7bea0007c7 | ||
|
|
8dd8addd3a | ||
|
|
e14dd4974f | ||
|
|
7a97995d81 | ||
|
|
e64509f1b4 | ||
|
|
0ac5440fc2 | ||
|
|
fde3411c8b | ||
|
|
8066aba6fe | ||
|
|
5ba0aa8082 | ||
|
|
3e95b7d8a5 | ||
|
|
0f632201e0 | ||
|
|
ebca1e4357 | ||
|
|
a3620c60ad | ||
|
|
9f70ebecf1 | ||
|
|
0fd91e4450 | ||
|
|
fe088dc8c3 | ||
|
|
5a6e20a6aa | ||
|
|
02bfe2dad3 | ||
|
|
50edd4cfdd | ||
|
|
03e60b9ea4 | ||
|
|
0677472c56 | ||
|
|
c1bc1e3137 | ||
|
|
b691fb7f2d | ||
|
|
73ac98da80 | ||
|
|
14b43b504b | ||
|
|
a3c605f147 | ||
|
|
333413d298 | ||
|
|
9de8d7276e | ||
|
|
432b106d58 | ||
|
|
2c7a248307 | ||
|
|
113947132c | ||
|
|
0a253d66d0 | ||
|
|
ae0e001187 | ||
|
|
eab82fdec7 | ||
|
|
da755d1c83 | ||
|
|
1b2581f0cb | ||
|
|
56c8bdbaa2 | ||
|
|
23435512c4 | ||
|
|
6c3a0eb1d6 | ||
|
|
c85cd13ca1 | ||
|
|
e4b994381b | ||
|
|
de2a2473f5 | ||
|
|
e6b036b413 | ||
|
|
08a3d26328 | ||
|
|
bc299067aa | ||
|
|
908a0277e5 | ||
|
|
c2deb1db25 | ||
|
|
16716ad028 | ||
|
|
fef8fe8525 | ||
|
|
3d5268368f | ||
|
|
20b51da180 | ||
|
|
785a7a22bc | ||
|
|
1ac4a7e116 | ||
|
|
327e505273 | ||
|
|
bf1c197a37 | ||
|
|
3c3a902a69 | ||
|
|
0c14176cd7 | ||
|
|
7ee971c3e3 | ||
|
|
098a23adc6 | ||
|
|
10c69387fd | ||
|
|
4b8b2f7c5b | ||
|
|
e1b8ff798f | ||
|
|
05b1854946 | ||
|
|
f58668fd67 | ||
|
|
e8843c31e6 | ||
|
|
05be16e9e1 | ||
|
|
e9b77298a7 | ||
|
|
a0cecc6c52 | ||
|
|
cf6113068c | ||
|
|
0c2c094db6 | ||
|
|
60cf56e235 | ||
|
|
482f5f7a26 | ||
|
|
04382d4b44 | ||
|
|
44b2261c34 | ||
|
|
76b9d781ee | ||
|
|
bd0faaf702 | ||
|
|
e0cd34c9e1 | ||
|
|
6c41ca4b8c | ||
|
|
7add015a75 | ||
|
|
d6b6e59ab8 | ||
|
|
a213674a98 | ||
|
|
41f24898e5 | ||
|
|
d2ad32eef8 | ||
|
|
1fea6d394a | ||
|
|
dcddfce5bc | ||
|
|
e6528be63d | ||
|
|
08ca260e82 | ||
|
|
88eb93da52 | ||
|
|
b35efd96dc | ||
|
|
89db5c6bab | ||
|
|
790838d897 | ||
|
|
4d0cf9ec8e | ||
|
|
299f673a8e | ||
|
|
fa8904978b | ||
|
|
4a144d1c18 | ||
|
|
415c96204a | ||
|
|
7af24dc486 | ||
|
|
e01c0ab4d6 | ||
|
|
8b4827ad85 | ||
|
|
43d5ec2d4a | ||
|
|
75a0998ed2 | ||
|
|
fbd229810f | ||
|
|
d27e791f32 | ||
|
|
50d2950e6b | ||
|
|
96564d0dad | ||
|
|
3e5abd18ca | ||
|
|
545ff6f9f1 | ||
|
|
49b95fe008 | ||
|
|
b8704e12b7 | ||
|
|
639a4d5cf7 | ||
|
|
0e500de1a0 | ||
|
|
c5b244419d | ||
|
|
8ccfc0f316 | ||
|
|
e126c55a5a | ||
|
|
be26ba8f8f | ||
|
|
1493771087 | ||
|
|
192d7ad735 | ||
|
|
12679da5da | ||
|
|
ec49b22af3 | ||
|
|
d23179e25c | ||
|
|
7d62bb8c53 | ||
|
|
c4e55d78d5 | ||
|
|
07a1c48e8c | ||
|
|
7be25313a5 | ||
|
|
55e8092cbf | ||
|
|
e5170582de | ||
|
|
710443d200 | ||
|
|
2a20423be6 | ||
|
|
8096a1fb04 | ||
|
|
2fedae6060 | ||
|
|
b1b4048f97 | ||
|
|
107f00ff8f | ||
|
|
5144e98a82 | ||
|
|
210d70b0c7 | ||
|
|
3ae94520c3 | ||
|
|
cbe25178d7 | ||
|
|
a3b7e1f774 | ||
|
|
bbab5fef0c | ||
|
|
007703156b | ||
|
|
9cae2900d4 | ||
|
|
876cae2807 | ||
|
|
e955b1ae09 | ||
|
|
dadf8adb3e | ||
|
|
4238ee090d | ||
|
|
65f77306d3 | ||
|
|
efbc7cccb1 | ||
|
|
f0206a90b1 | ||
|
|
a8f3f2bc1a | ||
|
|
25822d1717 | ||
|
|
9887cb997e | ||
|
|
7e2be96516 | ||
|
|
2e1269c474 | ||
|
|
b499e74502 | ||
|
|
7536f6adbd | ||
|
|
4be02bc207 | ||
|
|
bbfbd87a9f | ||
|
|
7e103e34f8 | ||
|
|
94e6fb89b3 | ||
|
|
1bdffcc73b | ||
|
|
e993e010f4 | ||
|
|
bc1b5f477d | ||
|
|
6a43128019 | ||
|
|
c474e2ac86 | ||
|
|
7763b4cf5b | ||
|
|
322227bf67 | ||
|
|
27cb1a4174 | ||
|
|
c4fb0fd6ca | ||
|
|
87ff1e8cb0 | ||
|
|
61b24180f0 | ||
|
|
15d2cbd6df | ||
|
|
f7a2428deb | ||
|
|
6c2415d32f | ||
|
|
84d3868994 | ||
|
|
f89b2a18e0 | ||
|
|
8114d790a5 | ||
|
|
082065cd50 | ||
|
|
a65b5f8e02 | ||
|
|
d8f133aaf3 | ||
|
|
8e272e5774 | ||
|
|
ce2a5b2838 | ||
|
|
bcf2bc6f8c | ||
|
|
17800c8ca5 | ||
|
|
5c6039fd8b | ||
|
|
40fe2d2c16 | ||
|
|
1bcf2737fe | ||
|
|
fcdbe846e5 | ||
|
|
d055dc0c6e | ||
|
|
e19c192570 | ||
|
|
b80cef964e | ||
|
|
b43594e4eb | ||
|
|
0908863e07 | ||
|
|
b3644f7fa0 | ||
|
|
d41b8cc96e | ||
|
|
91fb9d0113 | ||
|
|
85ac217abc | ||
|
|
687381f42c | ||
|
|
c91add203d | ||
|
|
1e4f459a26 | ||
|
|
06ad04e5fa | ||
|
|
80bf5c9756 | ||
|
|
0d601fd111 | ||
|
|
01904d3c1e | ||
|
|
0c7d14fe50 | ||
|
|
cdeb649d0b | ||
|
|
79ae3c2f2e | ||
|
|
59818af69c | ||
|
|
44e0500958 | ||
|
|
db59d39e2c | ||
|
|
587d71efb5 | ||
|
|
c7a4a16eec | ||
|
|
0b517584aa | ||
|
|
5a11ffcad8 | ||
|
|
0a184d380e | ||
|
|
6b387d320e | ||
|
|
348513c151 | ||
|
|
0d5e94b147 | ||
|
|
88545d882c | ||
|
|
42ea0a19d2 | ||
|
|
c87d27048b | ||
|
|
700bab7279 | ||
|
|
c3db4ebbc3 | ||
|
|
8aa327cb8a | ||
|
|
4218d569de | ||
|
|
e50eee59cf | ||
|
|
4506832925 | ||
|
|
a0cdc63a5d | ||
|
|
79b4e2dc85 | ||
|
|
edd3fc8825 | ||
|
|
e8796e009c | ||
|
|
044be3b93e | ||
|
|
0436811cf0 | ||
|
|
152e138c17 | ||
|
|
4f5e212f87 | ||
|
|
21bf01a24c | ||
|
|
16a9e6b72f | ||
|
|
700bc087d3 | ||
|
|
8b99348e98 | ||
|
|
045f34e851 | ||
|
|
e5a8030dd7 | ||
|
|
6643b83afe | ||
|
|
98681b78b4 | ||
|
|
f8dd0b0cb3 | ||
|
|
f04d49886b | ||
|
|
3043a8d9c9 | ||
|
|
4459fdf1b1 | ||
|
|
086d690df7 | ||
|
|
05d93cda16 | ||
|
|
6fc6eaf742 | ||
|
|
596cee2dc1 | ||
|
|
1d5c3f34ae | ||
|
|
ca7080c2bb | ||
|
|
a89112a133 | ||
|
|
353437bbd1 | ||
|
|
8e92fc62a3 | ||
|
|
c106534663 | ||
|
|
b92c6cdf35 | ||
|
|
ca5093901b | ||
|
|
ba7a0dde06 | ||
|
|
27798c1683 | ||
|
|
ee312ac230 | ||
|
|
7e82be53cd | ||
|
|
7017e46ba1 | ||
|
|
7166674d6c | ||
|
|
e1daf02735 | ||
|
|
1b156e0f34 | ||
|
|
c1ee5d69c9 | ||
|
|
7fbdf36c64 | ||
|
|
144e053a4e | ||
|
|
a8efb1e1c8 |
@@ -1,6 +1,6 @@
|
||||
[run]
|
||||
omit =
|
||||
scripts/*
|
||||
freqtrade/tests/*
|
||||
freqtrade/vendor/*
|
||||
freqtrade/__main__.py
|
||||
tests/*
|
||||
|
||||
17
.dependabot/config.yml
Normal file
17
.dependabot/config.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: 1
|
||||
|
||||
update_configs:
|
||||
- package_manager: "python"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
allowed_updates:
|
||||
- match:
|
||||
update_type: "all"
|
||||
target_branch: "develop"
|
||||
|
||||
- package_manager: "docker"
|
||||
directory: "/"
|
||||
update_schedule: "daily"
|
||||
allowed_updates:
|
||||
- match:
|
||||
update_type: "all"
|
||||
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@@ -5,6 +5,7 @@ If it hasn't been reported, please create a new issue.
|
||||
|
||||
## Step 2: Describe your environment
|
||||
|
||||
* Operating system: ____
|
||||
* Python Version: _____ (`python -V`)
|
||||
* CCXT version: _____ (`pip freeze | grep ccxt`)
|
||||
* Branch: Master | Develop
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,12 +1,12 @@
|
||||
# Freqtrade rules
|
||||
freqtrade/tests/testdata/*.json
|
||||
hyperopt_conf.py
|
||||
config*.json
|
||||
*.sqlite
|
||||
.hyperopt
|
||||
logfile.txt
|
||||
hyperopt_trials.pickle
|
||||
user_data/
|
||||
user_data/*
|
||||
!user_data/strategy/sample_strategy.py
|
||||
!user_data/notebooks
|
||||
user_data/notebooks/*
|
||||
!user_data/notebooks/*example.ipynb
|
||||
freqtrade-plot.html
|
||||
freqtrade-profit-plot.html
|
||||
|
||||
@@ -80,8 +80,7 @@ docs/_build/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
*.ipynb
|
||||
*.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
@@ -93,3 +92,6 @@ target/
|
||||
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
|
||||
#exceptions
|
||||
!*.gitkeep
|
||||
|
||||
37
.pyup.yml
37
.pyup.yml
@@ -1,37 +0,0 @@
|
||||
# autogenerated pyup.io config file
|
||||
# see https://pyup.io/docs/configuration/ for all available options
|
||||
|
||||
# configure updates globally
|
||||
# default: all
|
||||
# allowed: all, insecure, False
|
||||
update: all
|
||||
|
||||
# configure dependency pinning globally
|
||||
# default: True
|
||||
# allowed: True, False
|
||||
pin: True
|
||||
|
||||
# update schedule
|
||||
# default: empty
|
||||
# allowed: "every day", "every week", ..
|
||||
schedule: "every week"
|
||||
|
||||
|
||||
search: False
|
||||
# Specify requirement files by hand, default is empty
|
||||
# default: empty
|
||||
# allowed: list
|
||||
requirements:
|
||||
- requirements.txt
|
||||
- requirements-dev.txt
|
||||
- requirements-plot.txt
|
||||
- requirements-common.txt
|
||||
|
||||
|
||||
# configure the branch prefix the bot is using
|
||||
# default: pyup-
|
||||
branch_prefix: pyup/
|
||||
|
||||
# allow to close stale PRs
|
||||
# default: True
|
||||
close_prs: True
|
||||
28
.travis.yml
28
.travis.yml
@@ -10,16 +10,11 @@ services:
|
||||
env:
|
||||
global:
|
||||
- IMAGE_NAME=freqtradeorg/freqtrade
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libelf-dev
|
||||
- libdw-dev
|
||||
- binutils-dev
|
||||
install:
|
||||
- cd build_helpers && ./install_ta-lib.sh; cd ..
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||
- pip install --upgrade pytest-random-order
|
||||
- cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||
- export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||
- export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
- export TA_INCLUDE_PATH=${HOME}/dependencies/lib/include
|
||||
- pip install -r requirements-dev.txt
|
||||
- pip install -e .
|
||||
jobs:
|
||||
@@ -27,20 +22,25 @@ jobs:
|
||||
include:
|
||||
- stage: tests
|
||||
script:
|
||||
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||
- pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||
# Allow failure for coveralls
|
||||
- coveralls || true
|
||||
name: pytest
|
||||
- script:
|
||||
- cp config.json.example config.json
|
||||
- python freqtrade --datadir freqtrade/tests/testdata backtesting
|
||||
- freqtrade --datadir tests/testdata backtesting
|
||||
name: backtest
|
||||
- script:
|
||||
- cp config.json.example config.json
|
||||
- python freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5
|
||||
- freqtrade --datadir tests/testdata hyperopt -e 5
|
||||
name: hyperopt
|
||||
- script: flake8 freqtrade scripts
|
||||
- script: flake8
|
||||
name: flake8
|
||||
- script:
|
||||
# Test Documentation boxes -
|
||||
# !!! <TYPE>: is not allowed!
|
||||
- grep -Er '^!{3}\s\S+:' docs/*; test $? -ne 0
|
||||
name: doc syntax
|
||||
- script: mypy freqtrade scripts
|
||||
name: mypy
|
||||
|
||||
@@ -56,4 +56,4 @@ notifications:
|
||||
cache:
|
||||
pip: True
|
||||
directories:
|
||||
- /usr/local/lib
|
||||
- $HOME/dependencies
|
||||
|
||||
@@ -11,7 +11,7 @@ Few pointers for contributions:
|
||||
- Create your PR against the `develop` branch, not `master`.
|
||||
- New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100).
|
||||
|
||||
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg)
|
||||
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE)
|
||||
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
||||
|
||||
## Getting started
|
||||
@@ -28,19 +28,19 @@ make it pass. It means you have introduced a regression.
|
||||
#### Test the whole project
|
||||
|
||||
```bash
|
||||
pytest freqtrade
|
||||
pytest
|
||||
```
|
||||
|
||||
#### Test only one file
|
||||
|
||||
```bash
|
||||
pytest freqtrade/tests/test_<file_name>.py
|
||||
pytest tests/test_<file_name>.py
|
||||
```
|
||||
|
||||
#### Test only one method from one file
|
||||
|
||||
```bash
|
||||
pytest freqtrade/tests/test_<file_name>.py::test_<method_name>
|
||||
pytest tests/test_<file_name>.py::test_<method_name>
|
||||
```
|
||||
|
||||
### 2. Test if your code is PEP8 compliant
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.7.3-slim-stretch
|
||||
FROM python:3.7.4-slim-stretch
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install curl build-essential libssl-dev \
|
||||
|
||||
@@ -22,13 +22,13 @@ RUN tar -xzf /freqtrade/ta-lib-0.4.0-src.tar.gz \
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
|
||||
# Install berryconda
|
||||
RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh \
|
||||
RUN wget -q https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh \
|
||||
&& bash ./Berryconda3-2.0.0-Linux-armv7l.sh -b \
|
||||
&& rm Berryconda3-2.0.0-Linux-armv7l.sh
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements-common.txt /freqtrade/
|
||||
RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \
|
||||
RUN ~/berryconda3/bin/conda install -y numpy pandas \
|
||||
&& ~/berryconda3/bin/pip install -r requirements-common.txt --no-cache-dir
|
||||
|
||||
# Install and execute
|
||||
|
||||
@@ -2,4 +2,3 @@ include LICENSE
|
||||
include README.md
|
||||
include config.json.example
|
||||
recursive-include freqtrade *.py
|
||||
include freqtrade/tests/testdata/*.json
|
||||
|
||||
@@ -141,7 +141,7 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ
|
||||
For any questions not covered by the documentation or for further
|
||||
information about the bot, we encourage you to join our slack channel.
|
||||
|
||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg).
|
||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE).
|
||||
|
||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||
|
||||
@@ -172,7 +172,7 @@ to understand the requirements before sending your pull-requests.
|
||||
Coding is not a neccessity to contribute - maybe start with improving our documentation?
|
||||
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
||||
|
||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||
|
||||
**Important:** Always create your PR against the `develop` branch, not `master`.
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from freqtrade.main import main, set_loggers
|
||||
|
||||
set_loggers()
|
||||
from freqtrade.main import main
|
||||
|
||||
warnings.warn(
|
||||
"Deprecated - To continue to run the bot like this, please run `pip install -e .` again.",
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
if [ ! -f "/usr/local/lib/libta_lib.a" ]; then
|
||||
if [ -z "$1" ]; then
|
||||
INSTALL_LOC=/usr/local
|
||||
else
|
||||
INSTALL_LOC=${1}
|
||||
fi
|
||||
echo "Installing to ${INSTALL_LOC}"
|
||||
if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||
cd ta-lib \
|
||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||
&& ./configure \
|
||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||
&& make \
|
||||
&& which sudo && sudo make install || make install \
|
||||
&& cd ..
|
||||
|
||||
@@ -23,7 +23,7 @@ if [ $? -ne 0 ]; then
|
||||
fi
|
||||
|
||||
# Run backtest
|
||||
docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro freqtrade:${TAG} --datadir freqtrade/tests/testdata backtesting
|
||||
docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} --datadir /tests/testdata backtesting
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed running backtest"
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"order_types": {
|
||||
"buy": "limit",
|
||||
"sell": "limit",
|
||||
"emergencysell": "market",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": false,
|
||||
"stoploss_on_exchange_interval": 60
|
||||
@@ -123,5 +124,5 @@
|
||||
"process_throttle_secs": 5
|
||||
},
|
||||
"strategy": "DefaultStrategy",
|
||||
"strategy_path": "/some/folder/"
|
||||
"strategy_path": "user_data/strategies/"
|
||||
}
|
||||
|
||||
BIN
docs/assets/plot-dataframe.png
Normal file
BIN
docs/assets/plot-dataframe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
docs/assets/plot-profit.png
Normal file
BIN
docs/assets/plot-profit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -1,136 +1,103 @@
|
||||
# Backtesting
|
||||
|
||||
This page explains how to validate your strategy performance by using
|
||||
Backtesting.
|
||||
This page explains how to validate your strategy performance by using Backtesting.
|
||||
|
||||
Backtesting requires historic data to be available.
|
||||
To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation.
|
||||
|
||||
## Test your strategy with Backtesting
|
||||
|
||||
Now you have good Buy and Sell strategies, you want to test it against
|
||||
Now you have good Buy and Sell strategies and some historic data, you want to test it against
|
||||
real data. This is what we call
|
||||
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
||||
|
||||
Backtesting will use the crypto-currencies (pair) from your config file
|
||||
and load static tickers located in
|
||||
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
|
||||
If the 5 min and 1 min ticker for the crypto-currencies to test is not
|
||||
already in the `testdata` folder, backtesting will download them
|
||||
automatically. Testdata files will not be updated until you specify it.
|
||||
Backtesting will use the crypto-currencies (pairs) from your config file
|
||||
and load ticker data from `user_data/data/<exchange>` by default.
|
||||
If no data is available for the exchange / pair / ticker interval combination, backtesting will
|
||||
ask you to download them first using `freqtrade download-data`.
|
||||
For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation.
|
||||
|
||||
The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
|
||||
|
||||
The backtesting is very easy with freqtrade.
|
||||
The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
|
||||
|
||||
### Run a backtesting against the currencies listed in your config file
|
||||
|
||||
#### With 5 min tickers (Per default)
|
||||
|
||||
```bash
|
||||
python3 freqtrade backtesting
|
||||
freqtrade backtesting
|
||||
```
|
||||
|
||||
#### With 1 min tickers
|
||||
|
||||
```bash
|
||||
python3 freqtrade backtesting --ticker-interval 1m
|
||||
```
|
||||
|
||||
#### Update cached pairs with the latest data
|
||||
|
||||
```bash
|
||||
python3 freqtrade backtesting --refresh-pairs-cached
|
||||
```
|
||||
|
||||
#### With live data (do not alter your testdata files)
|
||||
|
||||
```bash
|
||||
python3 freqtrade backtesting --live
|
||||
freqtrade backtesting --ticker-interval 1m
|
||||
```
|
||||
|
||||
#### Using a different on-disk ticker-data source
|
||||
|
||||
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
|
||||
You can then use this data for backtesting as follows:
|
||||
|
||||
```bash
|
||||
python3 freqtrade backtesting --datadir freqtrade/tests/testdata-20180101
|
||||
freqtrade backtesting --datadir user_data/data/bittrex-20180101
|
||||
```
|
||||
|
||||
#### With a (custom) strategy file
|
||||
|
||||
```bash
|
||||
python3 freqtrade -s TestStrategy backtesting
|
||||
freqtrade -s SampleStrategy backtesting
|
||||
```
|
||||
|
||||
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
|
||||
Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory.
|
||||
|
||||
#### Comparing multiple Strategies
|
||||
|
||||
```bash
|
||||
freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --ticker-interval 5m
|
||||
```
|
||||
|
||||
Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
|
||||
|
||||
#### Exporting trades to file
|
||||
|
||||
```bash
|
||||
python3 freqtrade backtesting --export trades
|
||||
freqtrade backtesting --export trades
|
||||
```
|
||||
|
||||
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts folder.
|
||||
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory.
|
||||
|
||||
#### Exporting trades to file specifying a custom filename
|
||||
|
||||
```bash
|
||||
python3 freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json
|
||||
freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json
|
||||
```
|
||||
|
||||
#### Running backtest with smaller testset
|
||||
#### Running backtest with smaller testset by using timerange
|
||||
|
||||
Use the `--timerange` argument to change how much of the testset
|
||||
you want to use. The last N ticks/timeframes will be used.
|
||||
Use the `--timerange` argument to change how much of the testset you want to use.
|
||||
|
||||
Example:
|
||||
|
||||
For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata.
|
||||
|
||||
```bash
|
||||
python3 freqtrade backtesting --timerange=-200
|
||||
freqtrade backtesting --timerange=20190501-
|
||||
```
|
||||
|
||||
#### Advanced use of timerange
|
||||
|
||||
Doing `--timerange=-200` will get the last 200 timeframes
|
||||
from your inputdata. You can also specify specific dates,
|
||||
or a range span indexed by start and stop.
|
||||
You can also specify particular dates or a range span indexed by start and stop.
|
||||
|
||||
The full timerange specification:
|
||||
|
||||
- Use last 123 tickframes of data: `--timerange=-123`
|
||||
- Use first 123 tickframes of data: `--timerange=123-`
|
||||
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
||||
- Use tickframes till 2018/01/31: `--timerange=-20180131`
|
||||
- Use tickframes since 2018/01/31: `--timerange=20180131-`
|
||||
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
||||
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
||||
`--timerange=1527595200-1527618600`
|
||||
- Use last 123 tickframes of data: `--timerange=-123`
|
||||
- Use first 123 tickframes of data: `--timerange=123-`
|
||||
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
||||
|
||||
#### Downloading new set of ticker data
|
||||
|
||||
To download new set of backtesting ticker data, you can use a download script.
|
||||
|
||||
If you are using Binance for example:
|
||||
|
||||
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
|
||||
- update the `pairs.json` to contain the currency pairs you are interested in.
|
||||
|
||||
```bash
|
||||
mkdir -p user_data/data/binance
|
||||
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
python scripts/download_backtest_data.py --exchange binance
|
||||
```
|
||||
|
||||
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
||||
|
||||
- To use a different folder than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||
- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`.
|
||||
- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`.
|
||||
- To download ticker data for only 10 days, use `--days 10`.
|
||||
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options.
|
||||
|
||||
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
|
||||
!!! warning
|
||||
Be carefull when using non-date functions - these do not allow you to specify precise dates, so if you updated the test-data it will probably use a different dataset.
|
||||
|
||||
## Understand the backtesting result
|
||||
|
||||
@@ -176,11 +143,12 @@ A backtesting result will look like that:
|
||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 |
|
||||
```
|
||||
|
||||
The 1st table will contain all trades the bot made.
|
||||
The 1st table contains all trades the bot made, including "left open trades".
|
||||
|
||||
The 2nd table will contain a recap of sell reasons.
|
||||
The 2nd table contains a recap of sell reasons.
|
||||
|
||||
The 3rd table will contain all trades the bot had to `forcesell` at the end of the backtest period to present a full picture.
|
||||
The 3rd table contains all trades the bot had to `forcesell` at the end of the backtest period to present a full picture.
|
||||
This is necessary to simulate realistic behaviour, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
|
||||
These trades are also included in the first table, but are extracted separately for clarity.
|
||||
|
||||
The last line will give you the overall performance of your strategy,
|
||||
@@ -190,22 +158,16 @@ here:
|
||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
|
||||
```
|
||||
|
||||
We understand the bot has made `429` trades for an average duration of
|
||||
`4:12:00`, with a performance of `76.20%` (profit), that means it has
|
||||
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
|
||||
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
|
||||
|
||||
The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums all the profits/losses.
|
||||
The column `tot profit %` shows instead the total profit % in relation to allocated capital
|
||||
(`max_open_trades * stake_amount`). In the above results we have `max_open_trades=2 stake_amount=0.005` in config
|
||||
so `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`.
|
||||
The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums up all the profits/losses.
|
||||
The column `tot profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`).
|
||||
In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `tot_profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`.
|
||||
|
||||
As you will see your strategy performance will be influenced by your buy
|
||||
strategy, your sell strategy, and also by the `minimal_roi` and
|
||||
`stop_loss` you have set.
|
||||
Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set.
|
||||
|
||||
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
|
||||
expect the bot to make more profit than 1% (because it will sell every
|
||||
time a trade will reach 1%).
|
||||
For example, if your `minimal_roi` is only `"0": 0.01` you cannot expect the bot to make more profit than 1% (because it will sell every time a trade reaches 1%).
|
||||
|
||||
```json
|
||||
"minimal_roi": {
|
||||
@@ -214,32 +176,43 @@ time a trade will reach 1%).
|
||||
```
|
||||
|
||||
On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
|
||||
(55%), there is a lot of chance that the bot will never reach this
|
||||
profit. Hence, keep in mind that your performance is a mix of your
|
||||
strategies, your configuration, and the crypto-currency you have set up.
|
||||
(55%), there is almost no chance that the bot will ever reach this profit.
|
||||
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
|
||||
|
||||
### Assumptions made by backtesting
|
||||
|
||||
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
|
||||
|
||||
- Buys happen at open-price
|
||||
- Low happens before high for stoploss, protecting capital first.
|
||||
- ROI sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%)
|
||||
- Stoploss sells happen exactly at stoploss price, even if low was lower
|
||||
- Trailing stoploss
|
||||
- High happens first - adjusting stoploss
|
||||
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
|
||||
- Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used)
|
||||
|
||||
### Further backtest-result analysis
|
||||
|
||||
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
||||
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
|
||||
|
||||
|
||||
## Backtesting multiple strategies
|
||||
|
||||
To backtest multiple strategies, a list of Strategies can be provided.
|
||||
To compare multiple strategies, a list of Strategies can be provided to backtesting.
|
||||
|
||||
This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple
|
||||
strategies you'd like to compare, this should 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 folder.
|
||||
All listed Strategies need to be in the same directory.
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades
|
||||
```
|
||||
|
||||
This will save the results to `user_data/backtest_data/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
||||
This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
||||
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.
|
||||
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
|
||||
|
||||
```
|
||||
=========================================================== Strategy Summary ===========================================================
|
||||
|
||||
@@ -2,62 +2,70 @@
|
||||
|
||||
This page explains the different parameters of the bot and how to run it.
|
||||
|
||||
!!! Note
|
||||
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
|
||||
|
||||
|
||||
## Bot commands
|
||||
|
||||
```
|
||||
usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH]
|
||||
[-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]]
|
||||
usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
||||
[--db-url PATH] [--sd-notify]
|
||||
{backtesting,edge,hyperopt} ...
|
||||
{backtesting,edge,hyperopt,create-userdir,list-exchanges} ...
|
||||
|
||||
Free, open source crypto trading bot
|
||||
|
||||
positional arguments:
|
||||
{backtesting,edge,hyperopt}
|
||||
{backtesting,edge,hyperopt,create-userdir,list-exchanges}
|
||||
backtesting Backtesting module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
create-userdir Create user-data directory.
|
||||
list-exchanges Print available exchanges.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified
|
||||
--version show program's version number and exit
|
||||
--logfile FILE Log to the file specified.
|
||||
-V, --version show program's version number and exit
|
||||
-c PATH, --config PATH
|
||||
Specify configuration file (default: None). Multiple
|
||||
--config options may be used. Can be set to '-' to
|
||||
read config from stdin.
|
||||
Specify configuration file (default: `config.json`).
|
||||
Multiple --config options may be used. Can be set to
|
||||
`-` to read config from stdin.
|
||||
-d PATH, --datadir PATH
|
||||
Path to backtest data.
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
-s NAME, --strategy NAME
|
||||
Specify strategy class name (default:
|
||||
DefaultStrategy).
|
||||
`DefaultStrategy`).
|
||||
--strategy-path PATH Specify additional strategy lookup path.
|
||||
--dynamic-whitelist [INT]
|
||||
Dynamically generate and update whitelist based on 24h
|
||||
BaseVolume (default: 20). DEPRECATED.
|
||||
--db-url PATH Override trades database URL, this is useful if
|
||||
dry_run is enabled or in custom deployments (default:
|
||||
None).
|
||||
--db-url PATH Override trades database URL, this is useful in custom
|
||||
deployments (default: `sqlite:///tradesv3.sqlite` for
|
||||
Live Run mode, `sqlite://` for Dry Run).
|
||||
--sd-notify Notify systemd service manager.
|
||||
|
||||
```
|
||||
|
||||
### How to use a different configuration file?
|
||||
### How to specify which configuration file be used?
|
||||
|
||||
The bot allows you to select which configuration file you want to use. Per
|
||||
default, the bot will load the file `./config.json`
|
||||
The bot allows you to select which configuration file you want to use by means of
|
||||
the `-c/--config` command line option:
|
||||
|
||||
```bash
|
||||
python3 freqtrade -c path/far/far/away/config.json
|
||||
freqtrade -c path/far/far/away/config.json
|
||||
```
|
||||
|
||||
Per default, the bot loads the `config.json` configuration file from the current
|
||||
working directory.
|
||||
|
||||
### How to use multiple configuration files?
|
||||
|
||||
The bot allows you to use multiple configuration files by specifying multiple
|
||||
`-c/--config` configuration options in the command line. Configuration parameters
|
||||
defined in the last configuration file override parameters with the same name
|
||||
defined in the previous configuration file specified in the command line.
|
||||
`-c/--config` options in the command line. Configuration parameters
|
||||
defined in the latter configuration files override parameters with the same name
|
||||
defined in the previous configuration files specified in the command line earlier.
|
||||
|
||||
For example, you can make a separate configuration file with your key and secrete
|
||||
for the Exchange you use for trading, specify default configuration file with
|
||||
@@ -65,13 +73,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua
|
||||
require them):
|
||||
|
||||
```bash
|
||||
python3 freqtrade -c ./config.json
|
||||
freqtrade -c ./config.json
|
||||
```
|
||||
|
||||
and specify both configuration files when running in the normal Live Trade Mode:
|
||||
|
||||
```bash
|
||||
python3 freqtrade -c ./config.json -c path/to/secrets/keys.config.json
|
||||
freqtrade -c ./config.json -c path/to/secrets/keys.config.json
|
||||
```
|
||||
|
||||
This could help you hide your private Exchange key and Exchange secrete on you local machine
|
||||
@@ -82,6 +90,29 @@ of your configuration in the project issues or in the Internet.
|
||||
See more details on this technique with examples in the documentation page on
|
||||
[configuration](configuration.md).
|
||||
|
||||
### Where to store custom data
|
||||
|
||||
Freqtrade allows the creation of a user-data directory using `freqtrade create-userdir --userdir someDirectory`.
|
||||
This directory will look as follows:
|
||||
|
||||
```
|
||||
user_data/
|
||||
├── backtest_results
|
||||
├── data
|
||||
├── hyperopts
|
||||
├── hyperopts_results
|
||||
├── plot
|
||||
└── strategies
|
||||
```
|
||||
|
||||
You can add the entry "user_data_dir" setting to your configuration, to always point your bot to this directory.
|
||||
Alternatively, pass in `--userdir` to every command.
|
||||
The bot will fail to start if the directory does not exist, but will create necessary subdirectories.
|
||||
|
||||
This directory should contain your custom strategies, custom hyperopts and hyperopt loss functions, backtesting historical data (downloaded using either backtesting command or the download script) and plot outputs.
|
||||
|
||||
It is recommended to use version control to keep track of changes to your strategies.
|
||||
|
||||
### How to use **--strategy**?
|
||||
|
||||
This parameter will allow you to load your custom strategy class.
|
||||
@@ -97,7 +128,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
|
||||
a strategy class called `AwesomeStrategy` to load it:
|
||||
|
||||
```bash
|
||||
python3 freqtrade --strategy AwesomeStrategy
|
||||
freqtrade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
If the bot does not find your strategy file, it will display in an error
|
||||
@@ -109,27 +140,17 @@ Learn more about strategy file in
|
||||
### How to use **--strategy-path**?
|
||||
|
||||
This parameter allows you to add an additional strategy lookup path, which gets
|
||||
checked before the default locations (The passed path must be a folder!):
|
||||
checked before the default locations (The passed path must be a directory!):
|
||||
|
||||
```bash
|
||||
python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder
|
||||
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||
```
|
||||
|
||||
#### How to install a strategy?
|
||||
|
||||
This is very simple. Copy paste your strategy file into the folder
|
||||
This is very simple. Copy paste your strategy file into the directory
|
||||
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
|
||||
|
||||
### How to use **--dynamic-whitelist**?
|
||||
|
||||
!!! danger "DEPRECATED"
|
||||
This command line option is deprecated. Please move your configurations using it
|
||||
to the configurations that utilize the `StaticPairList` or `VolumePairList` methods set
|
||||
in the configuration file
|
||||
as outlined [here](configuration/#dynamic-pairlists)
|
||||
|
||||
Description of this deprecated feature was moved to [here](deprecated.md).
|
||||
Please no longer use it.
|
||||
|
||||
### How to use **--db-url**?
|
||||
|
||||
When you run the bot in Dry-run mode, per default no transactions are
|
||||
@@ -138,7 +159,7 @@ using `--db-url`. This can also be used to specify a custom database
|
||||
in production mode. Example command:
|
||||
|
||||
```bash
|
||||
python3 freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
|
||||
freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
|
||||
```
|
||||
|
||||
## Backtesting commands
|
||||
@@ -163,10 +184,6 @@ optional arguments:
|
||||
Specify max_open_trades to use.
|
||||
--stake_amount STAKE_AMOUNT
|
||||
Specify stake_amount.
|
||||
-r, --refresh-pairs-cached
|
||||
Refresh the pairs files in tests/testdata with the
|
||||
latest data from the exchange. Use it if you want to
|
||||
run your optimization commands with up-to-date data.
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
@@ -174,9 +191,8 @@ optional arguments:
|
||||
Disable applying `max_open_trades` during backtest
|
||||
(same as setting `max_open_trades` to a very high
|
||||
number).
|
||||
-l, --live Use live data.
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
Provide a commaseparated list of strategies to
|
||||
Provide a space-separated list of strategies to
|
||||
backtest Please note that ticker-interval needs to be
|
||||
set either in config or via command line. When using
|
||||
this together with --export trades, the strategy-name
|
||||
@@ -187,24 +203,16 @@ optional arguments:
|
||||
--export-filename PATH
|
||||
Save backtest results to this filename requires
|
||||
--export to be set as well Example --export-
|
||||
filename=user_data/backtest_data/backtest_today.json
|
||||
(default: user_data/backtest_data/backtest-
|
||||
filename=user_data/backtest_results/backtest_today.json
|
||||
(default: user_data/backtest_results/backtest-
|
||||
result.json)
|
||||
```
|
||||
|
||||
### How to use **--refresh-pairs-cached** parameter?
|
||||
### Getting historic data for backtesting
|
||||
|
||||
The first time your run Backtesting, it will take the pairs you have
|
||||
set in your config file and download data from the Exchange.
|
||||
|
||||
If for any reason you want to update your data set, you use
|
||||
`--refresh-pairs-cached` to force Backtesting to update the data it has.
|
||||
|
||||
!!! Note
|
||||
Use it only if you want to update your data set. You will not be able to come back to the previous version.
|
||||
|
||||
To test your strategy with latest data, we recommend continuing using
|
||||
the parameter `-l` or `--live`.
|
||||
The first time your run Backtesting, you will need to download some historic data first.
|
||||
This can be accomplished by using `freqtrade download-data`.
|
||||
Check the corresponding [help page section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) for more details
|
||||
|
||||
## Hyperopt commands
|
||||
|
||||
@@ -213,52 +221,70 @@ to find optimal parameter values for your stategy.
|
||||
|
||||
```
|
||||
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||
[--max_open_trades MAX_OPEN_TRADES]
|
||||
[--max_open_trades INT]
|
||||
[--stake_amount STAKE_AMOUNT] [-r]
|
||||
[--customhyperopt NAME] [--eps] [--dmmp] [-e INT]
|
||||
[--customhyperopt NAME] [--hyperopt-path PATH]
|
||||
[--eps] [-e INT]
|
||||
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
||||
[--print-all] [-j JOBS]
|
||||
[--dmmp] [--print-all] [--no-color] [-j JOBS]
|
||||
[--random-state INT] [--min-trades INT] [--continue]
|
||||
[--hyperopt-loss NAME]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||
Specify ticker interval (1m, 5m, 30m, 1h, 1d).
|
||||
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
|
||||
`1d`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
--max_open_trades MAX_OPEN_TRADES
|
||||
--max_open_trades INT
|
||||
Specify max_open_trades to use.
|
||||
--stake_amount STAKE_AMOUNT
|
||||
Specify stake_amount.
|
||||
-r, --refresh-pairs-cached
|
||||
Refresh the pairs files in tests/testdata with the
|
||||
latest data from the exchange. Use it if you want to
|
||||
run your optimization commands with up-to-date data.
|
||||
--customhyperopt NAME
|
||||
Specify hyperopt class name (default:
|
||||
DefaultHyperOpts).
|
||||
`DefaultHyperOpts`).
|
||||
--hyperopt-path PATH Specify additional lookup path for Hyperopts and
|
||||
Hyperopt Loss functions.
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
-e INT, --epochs INT Specify number of epochs (default: 100).
|
||||
-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]
|
||||
Specify which parameters to hyperopt. Space-separated
|
||||
list. Default: `all`.
|
||||
--dmmp, --disable-max-market-positions
|
||||
Disable applying `max_open_trades` during backtest
|
||||
(same as setting `max_open_trades` to a very high
|
||||
number).
|
||||
-e INT, --epochs INT Specify number of epochs (default: 100).
|
||||
-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]
|
||||
Specify which parameters to hyperopt. Space separate
|
||||
list. Default: all.
|
||||
--print-all Print all results, not only the best ones.
|
||||
--no-color Disable colorization of hyperopt results. May be
|
||||
useful if you are redirecting output to a file.
|
||||
-j JOBS, --job-workers JOBS
|
||||
The number of concurrently running jobs for
|
||||
hyperoptimization (hyperopt worker processes). If -1
|
||||
(default), all CPUs are used, for -2, all CPUs but one
|
||||
are used, etc. If 1 is given, no parallel computing
|
||||
code is used at all.
|
||||
--random-state INT Set random state to some positive integer for
|
||||
reproducible hyperopt results.
|
||||
--min-trades INT Set minimal desired number of trades for evaluations
|
||||
in the hyperopt optimization path (default: 1).
|
||||
--continue Continue hyperopt from previous runs. By default,
|
||||
temporary files will be removed and hyperopt will
|
||||
start from scratch.
|
||||
--hyperopt-loss NAME Specify the class name of the hyperopt loss function
|
||||
class (IHyperOptLoss). Different functions can
|
||||
generate completely different results, since the
|
||||
target for optimization is different. Built-in
|
||||
Hyperopt-loss-functions are: DefaultHyperOptLoss,
|
||||
OnlyProfitHyperOptLoss, SharpeHyperOptLoss.
|
||||
(default: `DefaultHyperOptLoss`).
|
||||
```
|
||||
|
||||
## Edge commands
|
||||
|
||||
To know your trade expectacny and winrate against historical data, you can use Edge.
|
||||
To know your trade expectancy and winrate against historical data, you can use Edge.
|
||||
|
||||
```
|
||||
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||
@@ -276,10 +302,6 @@ optional arguments:
|
||||
Specify max_open_trades to use.
|
||||
--stake_amount STAKE_AMOUNT
|
||||
Specify stake_amount.
|
||||
-r, --refresh-pairs-cached
|
||||
Refresh the pairs files in tests/testdata with the
|
||||
latest data from the exchange. Use it if you want to
|
||||
run your optimization commands with up-to-date data.
|
||||
--stoplosses STOPLOSS_RANGE
|
||||
Defines a range of stoploss against which edge will
|
||||
assess the strategy the format is "min,max,step"
|
||||
@@ -289,11 +311,6 @@ optional arguments:
|
||||
|
||||
To understand edge and how to read the results, please read the [edge documentation](edge.md).
|
||||
|
||||
## A parameter missing in the configuration?
|
||||
|
||||
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
|
||||
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
|
||||
|
||||
## Next step
|
||||
|
||||
The optimal strategy of the bot will change with time depending of the market trends. The next step is to
|
||||
|
||||
@@ -1,21 +1,48 @@
|
||||
# Configure the bot
|
||||
|
||||
This page explains how to configure your `config.json` file.
|
||||
Freqtrade has many configurable features and possibilities.
|
||||
By default, these settings are configured via the configuration file (see below).
|
||||
|
||||
## Setup config.json
|
||||
## The Freqtrade configuration file
|
||||
|
||||
We recommend to copy and use the `config.json.example` as a template
|
||||
The bot uses a set of configuration parameters during its operation that all together conform the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file).
|
||||
|
||||
Per default, the bot loads the configuration from the `config.json` file, located in the current working directory.
|
||||
|
||||
You can specify a different configuration file used by the bot with the `-c/--config` command line option.
|
||||
|
||||
In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
|
||||
|
||||
If you used the [Quick start](installation.md/#quick-start) method for installing
|
||||
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
|
||||
|
||||
If default configuration file is not created we recommend you to copy and use the `config.json.example` as a template
|
||||
for your bot configuration.
|
||||
|
||||
The table below will list all configuration parameters.
|
||||
The Freqtrade configuration file is to be written in the JSON format.
|
||||
|
||||
Mandatory Parameters are marked as **Required**.
|
||||
Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters.
|
||||
|
||||
Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates syntax of the configuration file at startup and will warn you if you made any errors editing it, pointing out problematic lines.
|
||||
|
||||
## Configuration parameters
|
||||
|
||||
The table below will list all configuration parameters available.
|
||||
|
||||
Freqtrade can also load many options via command line (CLI) arguments (check out the commands `--help` output for details).
|
||||
The prevelance for all Options is as follows:
|
||||
|
||||
- CLI arguments override any other option
|
||||
- Configuration files are used in sequence (last file wins), and override Strategy configurations.
|
||||
- Strategy configurations are only used if they are not set via configuration or via command line arguments. These options are market with [Strategy Override](#parameters-in-the-strategy) in the below table.
|
||||
|
||||
Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways.
|
||||
|
||||
| Command | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades)
|
||||
| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `stake_currency` | BTC | **Required.** Crypto-currency used for trading.
|
||||
| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance.
|
||||
| `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals.
|
||||
| `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below.
|
||||
@@ -42,10 +69,11 @@ Mandatory Parameters are marked as **Required**.
|
||||
| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
|
||||
| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||
| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
|
||||
| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode.
|
||||
| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode.
|
||||
| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||
| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||
| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.***
|
||||
| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.***
|
||||
| `exchange.password` | '' | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.***
|
||||
| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)).
|
||||
| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)).
|
||||
| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
||||
| `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
||||
| `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded.
|
||||
@@ -53,11 +81,12 @@ Mandatory Parameters are marked as **Required**.
|
||||
| `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `pairlist.method` | StaticPairList | Use Static whitelist. [More information below](#dynamic-pairlists).
|
||||
| `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
|
||||
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists).
|
||||
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
|
||||
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
|
||||
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
||||
| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.***
|
||||
| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.***
|
||||
| `webhook.enabled` | false | Enable usage of Webhook notifications
|
||||
| `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
|
||||
| `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||
@@ -67,18 +96,17 @@ Mandatory Parameters are marked as **Required**.
|
||||
| `initial_state` | running | Defines the initial application state. More information below.
|
||||
| `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below.
|
||||
| `strategy` | DefaultStrategy | Defines Strategy class to use.
|
||||
| `strategy_path` | null | Adds an additional strategy lookup path (must be a folder).
|
||||
| `strategy_path` | null | Adds an additional strategy lookup path (must be a directory).
|
||||
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
|
||||
| `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
|
||||
| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file.
|
||||
| `user_data_dir` | cwd()/user_data | Directory containing user data. Defaults to `./user_data/`.
|
||||
|
||||
### Parameters in the strategy
|
||||
|
||||
The following parameters can be set in either configuration file or strategy.
|
||||
Values set in the configuration file always overwrite values set in the strategy.
|
||||
|
||||
* `stake_currency`
|
||||
* `stake_amount`
|
||||
* `ticker_interval`
|
||||
* `minimal_roi`
|
||||
* `stoploss`
|
||||
@@ -170,19 +198,20 @@ end up paying more then would probably have been necessary.
|
||||
|
||||
### Understand order_types
|
||||
|
||||
The `order_types` configuration parameter contains a dict mapping order-types to
|
||||
market-types as well as stoploss on or off exchange type and stoploss on exchange
|
||||
update interval in seconds. This allows to buy using limit orders, sell using
|
||||
limit-orders, and create stoploss orders using market. It also allows to set the
|
||||
stoploss "on exchange" which means stoploss order would be placed immediately once
|
||||
the buy order is fulfilled. In case stoploss on exchange and `trailing_stop` are
|
||||
both set, then the bot will use `stoploss_on_exchange_interval` to check it periodically
|
||||
and update it if necessary (e.x. in case of trailing stoploss).
|
||||
This can be set in the configuration file or in the strategy.
|
||||
Values set in the configuration file overwrites values set in the strategy.
|
||||
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
|
||||
|
||||
If this is configured, all 4 values (`buy`, `sell`, `stoploss` and
|
||||
`stoploss_on_exchange`) need to be present, otherwise the bot will warn about it and fail to start.
|
||||
This allows to buy using limit orders, sell using
|
||||
limit-orders, and create stoplosses using using market orders. It also allows to set the
|
||||
stoploss "on exchange" which means stoploss order would be placed immediately once
|
||||
the buy order is fulfilled.
|
||||
If `stoploss_on_exchange` and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check and update the stoploss on exchange periodically.
|
||||
`order_types` can be set in the configuration file or in the strategy.
|
||||
`order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place.
|
||||
|
||||
If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
||||
`stoploss_on_exchange`) need to be present, otherwise the bot will fail to start.
|
||||
|
||||
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
|
||||
The below is the default which is used if this is not configured in either strategy or configuration file.
|
||||
|
||||
Syntax for Strategy:
|
||||
@@ -191,6 +220,7 @@ Syntax for Strategy:
|
||||
order_types = {
|
||||
"buy": "limit",
|
||||
"sell": "limit",
|
||||
"emergencysell": "market",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": False,
|
||||
"stoploss_on_exchange_interval": 60
|
||||
@@ -203,6 +233,7 @@ Configuration:
|
||||
"order_types": {
|
||||
"buy": "limit",
|
||||
"sell": "limit",
|
||||
"emergencysell": "market",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": false,
|
||||
"stoploss_on_exchange_interval": 60
|
||||
@@ -217,11 +248,13 @@ Configuration:
|
||||
!!! Note
|
||||
Stoploss on exchange interval is not mandatory. Do not change its value if you are
|
||||
unsure of what you are doing. For more information about how stoploss works please
|
||||
read [the stoploss documentation](stoploss.md).
|
||||
refer to [the stoploss documentation](stoploss.md).
|
||||
|
||||
!!! Note
|
||||
In case of stoploss on exchange if the stoploss is cancelled manually then
|
||||
the bot would recreate one.
|
||||
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
|
||||
|
||||
!!! Warning stoploss_on_exchange failures
|
||||
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
|
||||
|
||||
### Understand order_time_in_force
|
||||
|
||||
@@ -380,8 +413,6 @@ section of the configuration.
|
||||
* `StaticPairList`
|
||||
* It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
|
||||
* `VolumePairList`
|
||||
* Formerly available as `--dynamic-whitelist [<number_assets>]`. This command line
|
||||
option is deprecated and should no longer be used.
|
||||
* It selects `number_assets` top pairs based on `sort_key`, which can be one of
|
||||
`askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`.
|
||||
* There is a possibility to filter low-value coins that would not allow setting a stop loss
|
||||
|
||||
@@ -1,42 +1,212 @@
|
||||
# Analyzing bot data
|
||||
# Analyzing bot data with Jupyter notebooks
|
||||
|
||||
After performing backtests, or after running the bot for some time, it will be interesting to analyze the results your bot generated.
|
||||
You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/`.
|
||||
|
||||
A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data.
|
||||
## Pro tips
|
||||
|
||||
The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results.
|
||||
* See [jupyter.org](https://jupyter.org/documentation) for usage instructions.
|
||||
* Don't forget to start a Jupyter notebook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*
|
||||
* Copy the example notebook before use so your changes don't get clobbered with the next freqtrade update.
|
||||
|
||||
## Backtesting
|
||||
## Fine print
|
||||
|
||||
To analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
||||
You can then load the trades to perform further analysis.
|
||||
Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually.
|
||||
|
||||
Freqtrade provides the `load_backtest_data()` helper function to easily load the backtest results, which takes the path to the the backtest-results file as parameter.
|
||||
## Recommended workflow
|
||||
|
||||
| Task | Tool |
|
||||
--- | ---
|
||||
Bot operations | CLI
|
||||
Repetitive tasks | Shell scripts
|
||||
Data analysis & visualization | Notebook
|
||||
|
||||
1. Use the CLI to
|
||||
* download historical data
|
||||
* run a backtest
|
||||
* run with real-time data
|
||||
* export results
|
||||
|
||||
1. Collect these actions in shell scripts
|
||||
* save complicated commands with arguments
|
||||
* execute multi-step operations
|
||||
* automate testing strategies and preparing data for analysis
|
||||
|
||||
1. Use a notebook to
|
||||
* visualize data
|
||||
* munge and plot to generate insights
|
||||
|
||||
## Example utility snippets
|
||||
|
||||
### Change directory to root
|
||||
|
||||
Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistent.
|
||||
|
||||
```python
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Change directory
|
||||
# Modify this cell to insure that the output shows the correct path.
|
||||
# Define all paths relative to the project root shown in the cell output
|
||||
project_root = "somedir/freqtrade"
|
||||
i=0
|
||||
try:
|
||||
os.chdirdir(project_root)
|
||||
assert Path('LICENSE').is_file()
|
||||
except:
|
||||
while i<4 and (not Path('LICENSE').is_file()):
|
||||
os.chdir(Path(Path.cwd(), '../'))
|
||||
i+=1
|
||||
project_root = Path.cwd()
|
||||
print(Path.cwd())
|
||||
```
|
||||
|
||||
## Load existing objects into a Jupyter notebook
|
||||
|
||||
These examples assume that you have already generated data using the cli. They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload.
|
||||
|
||||
### Load backtest results into a pandas dataframe
|
||||
|
||||
```python
|
||||
from freqtrade.data.btanalysis import load_backtest_data
|
||||
df = load_backtest_data("user_data/backtest-result.json")
|
||||
|
||||
# Load backtest results
|
||||
df = load_backtest_data("user_data/backtest_results/backtest-result.json")
|
||||
|
||||
# Show value-counts per pair
|
||||
df.groupby("pair")["sell_reason"].value_counts()
|
||||
|
||||
```
|
||||
|
||||
This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload.
|
||||
|
||||
If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a Pull Request so the community can benefit from it.
|
||||
|
||||
## Live data
|
||||
|
||||
To analyze the trades your bot generated, you can load them to a DataFrame as follows:
|
||||
### Load live trading results into a pandas dataframe
|
||||
|
||||
``` python
|
||||
from freqtrade.data.btanalysis import load_trades_from_db
|
||||
|
||||
# Fetch trades from database
|
||||
df = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
||||
|
||||
# Display results
|
||||
df.groupby("pair")["sell_reason"].value_counts()
|
||||
```
|
||||
|
||||
### Load multiple configuration files
|
||||
|
||||
This option can be useful to inspect the results of passing in multiple configs.
|
||||
This will also run through the whole Configuration initialization, so the configuration is completely initialized to be passed to other methods.
|
||||
|
||||
``` python
|
||||
import json
|
||||
from freqtrade.configuration import Configuration
|
||||
|
||||
# Load config from multiple files
|
||||
config = Configuration.from_files(["config1.json", "config2.json"])
|
||||
|
||||
# Show the config in memory
|
||||
print(json.dumps(config['original_config'], indent=2))
|
||||
```
|
||||
|
||||
For Interactive environments, have an additional configuration specifying `user_data_dir` and pass this in last, so you don't have to change directories while running the bot.
|
||||
Best avoid relative paths, since this starts at the storage location of the jupyter notebook, unless the directory is changed.
|
||||
|
||||
``` json
|
||||
{
|
||||
"user_data_dir": "~/.freqtrade/"
|
||||
}
|
||||
```
|
||||
|
||||
### Load exchange data to a pandas dataframe
|
||||
|
||||
This loads candle data to a dataframe
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from freqtrade.data.history import load_pair_history
|
||||
|
||||
# Load data using values passed to function
|
||||
ticker_interval = "5m"
|
||||
data_location = Path('user_data', 'data', 'bitrex')
|
||||
pair = "BTC_USDT"
|
||||
candles = load_pair_history(datadir=data_location,
|
||||
ticker_interval=ticker_interval,
|
||||
pair=pair)
|
||||
|
||||
# Confirm success
|
||||
print(f"Loaded len(candles) rows of data for {pair} from {data_location}")
|
||||
candles.head()
|
||||
```
|
||||
|
||||
## Strategy debugging example
|
||||
|
||||
Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data.
|
||||
|
||||
### Define variables used in analyses
|
||||
|
||||
You can override strategy settings as demonstrated below.
|
||||
|
||||
```python
|
||||
# Customize these according to your needs.
|
||||
|
||||
# Define some constants
|
||||
ticker_interval = "5m"
|
||||
# Name of the strategy class
|
||||
strategy_name = 'SampleStrategy'
|
||||
# Path to user data
|
||||
user_data_dir = 'user_data'
|
||||
# Location of the strategy
|
||||
strategy_location = Path(user_data_dir, 'strategies')
|
||||
# Location of the data
|
||||
data_location = Path(user_data_dir, 'data', 'binance')
|
||||
# Pair to analyze - Only use one pair here
|
||||
pair = "BTC_USDT"
|
||||
```
|
||||
|
||||
### Load exchange data
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from freqtrade.data.history import load_pair_history
|
||||
|
||||
# Load data using values set above
|
||||
candles = load_pair_history(datadir=data_location,
|
||||
ticker_interval=ticker_interval,
|
||||
pair=pair)
|
||||
|
||||
# Confirm success
|
||||
print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}")
|
||||
candles.head()
|
||||
```
|
||||
|
||||
### Load and run strategy
|
||||
|
||||
* Rerun each time the strategy file is changed
|
||||
|
||||
```python
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
# Load strategy using values set above
|
||||
strategy = StrategyResolver({'strategy': strategy_name,
|
||||
'user_data_dir': user_data_dir,
|
||||
'strategy_path': strategy_location}).strategy
|
||||
|
||||
# Generate buy/sell signals using strategy
|
||||
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||
```
|
||||
|
||||
### Display the trade details
|
||||
|
||||
* Note that using `data.tail()` is preferable to `data.head()` as most indicators have some "startup" data at the top of the dataframe.
|
||||
* Some possible problems
|
||||
* Columns with NaN values at the end of the dataframe
|
||||
* Columns used in `crossed*()` functions with completely different units
|
||||
* Comparison with full backtest
|
||||
* having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.
|
||||
* Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available.
|
||||
|
||||
```python
|
||||
# Report results
|
||||
print(f"Generated {df['buy'].sum()} buy signals")
|
||||
data = df.set_index('date', drop=True)
|
||||
data.tail()
|
||||
```
|
||||
|
||||
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.
|
||||
|
||||
62
docs/data-download.md
Normal file
62
docs/data-download.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Data Downloading
|
||||
|
||||
## Getting data for backtesting and hyperopt
|
||||
|
||||
To download data (candles / OHLCV) needed for backtesting and hyperoptimization use the `freqtrade download-data` command.
|
||||
|
||||
If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes for the last 30 days.
|
||||
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
|
||||
Otherwise `--exchange` becomes mandatory.
|
||||
|
||||
!!! Tip Updating existing data
|
||||
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data.
|
||||
Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded.
|
||||
|
||||
### Pairs file
|
||||
|
||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||
|
||||
If you are using Binance for example:
|
||||
|
||||
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
||||
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
||||
|
||||
```bash
|
||||
mkdir -p user_data/data/binance
|
||||
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
|
||||
```
|
||||
|
||||
The format of the `pairs.json` file is a simple json list.
|
||||
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
||||
|
||||
``` json
|
||||
[
|
||||
"ETH/BTC",
|
||||
"ETH/USDT",
|
||||
"BTC/USDT",
|
||||
"XRP/ETH"
|
||||
]
|
||||
```
|
||||
|
||||
### start download
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance
|
||||
```
|
||||
|
||||
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
||||
|
||||
### Other Notes
|
||||
|
||||
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||
- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.)
|
||||
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||
- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||
|
||||
## Next step
|
||||
|
||||
Great, you now have backtest data downloaded, so you can now start [backtesting](backtesting.md) your strategy.
|
||||
@@ -4,28 +4,23 @@ This page contains description of the command line arguments, configuration para
|
||||
and the bot features that were declared as DEPRECATED by the bot development team
|
||||
and are no longer supported. Please avoid their usage in your configuration.
|
||||
|
||||
## Removed features
|
||||
|
||||
### the `--refresh-pairs-cached` command line option
|
||||
|
||||
`--refresh-pairs-cached` in the context of backtesting, hyperopt and edge allows to refresh candle data for backtesting.
|
||||
Since this leads to much confusion, and slows down backtesting (while not being part of backtesting) this has been singled out
|
||||
as a seperate freqtrade subcommand `freqtrade download-data`.
|
||||
|
||||
This command line option was deprecated in 2019.7-dev (develop branch) and removed in 2019.9 (master branch).
|
||||
|
||||
### The **--dynamic-whitelist** command line option
|
||||
|
||||
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
||||
on BaseVolume. This value can be changed when you run the script.
|
||||
|
||||
**By Default**
|
||||
Get the 20 currencies based on BaseVolume.
|
||||
|
||||
```bash
|
||||
python3 freqtrade --dynamic-whitelist
|
||||
```
|
||||
|
||||
**Customize the number of currencies to retrieve**
|
||||
Get the 30 currencies based on BaseVolume.
|
||||
|
||||
```bash
|
||||
python3 freqtrade --dynamic-whitelist 30
|
||||
```
|
||||
|
||||
**Exception**
|
||||
`--dynamic-whitelist` must be greater than 0. If you enter 0 or a
|
||||
negative value (e.g -2), `--dynamic-whitelist` will use the default
|
||||
value (20).
|
||||
This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch)
|
||||
and in freqtrade 2019.7 (master branch).
|
||||
|
||||
### the `--live` command line option
|
||||
|
||||
`--live` in the context of backtesting allowed to download the latest tick data for backtesting.
|
||||
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
||||
Removed in 2019-7-dev (develop branch) and in freqtrade 2019-8 (master branch)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running.
|
||||
|
||||
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) where you can ask questions.
|
||||
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) where you can ask questions.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -12,11 +12,34 @@ Special fields for the documentation (like Note boxes, ...) can be found [here](
|
||||
|
||||
## Developer setup
|
||||
|
||||
To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
||||
Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`.
|
||||
To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
||||
Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`.
|
||||
|
||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
||||
|
||||
### Tests
|
||||
|
||||
New code should be covered by basic unittests. Depending on the complexity of the feature, Reviewers may request more in-depth unittests.
|
||||
If necessary, the Freqtrade team can assist and give guidance with writing good tests (however please don't expect anyone to write the tests for you).
|
||||
|
||||
#### Checking log content in tests
|
||||
|
||||
Freqtrade uses 2 main methods to check log content in tests, `log_has()` and `log_has_re()` (to check using regex, in case of dynamic log-messages).
|
||||
These are available from `conftest.py` and can be imported in any test module.
|
||||
|
||||
A sample check looks as follows:
|
||||
|
||||
``` python
|
||||
from tests.conftest import log_has, log_has_re
|
||||
|
||||
def test_method_to_test(caplog):
|
||||
method_to_test()
|
||||
|
||||
assert log_has("This event happened", caplog)
|
||||
# Check regex with trailing number ...
|
||||
assert log_has_re(r"This dynamic event happened and produced \d+", caplog)
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
### Dynamic Pairlist
|
||||
@@ -130,7 +153,7 @@ If the day shows the same day, then the last candle can be assumed as incomplete
|
||||
|
||||
This part of the documentation is aimed at maintainers, and shows how to create a release.
|
||||
|
||||
### create release branch
|
||||
### Create release branch
|
||||
|
||||
``` bash
|
||||
# make sure you're in develop branch
|
||||
@@ -140,11 +163,14 @@ git checkout develop
|
||||
git checkout -b new_release
|
||||
```
|
||||
|
||||
* Edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`)
|
||||
* 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.
|
||||
* Commit this part
|
||||
* push that branch to the remote and create a PR against the master branch
|
||||
|
||||
### create changelog from git commits
|
||||
### Create changelog from git commits
|
||||
|
||||
!!! Note
|
||||
Make sure that both master and develop are up-todate!.
|
||||
|
||||
``` bash
|
||||
# Needs to be done before merging / pulling that branch.
|
||||
@@ -153,6 +179,8 @@ git log --oneline --no-decorate --no-merges master..develop
|
||||
|
||||
### Create github release / tag
|
||||
|
||||
Once the PR against master is merged (best right after merging):
|
||||
|
||||
* Use the button "Draft a new release" in the Github UI (subsection releases)
|
||||
* Use the version-number specified as tag.
|
||||
* Use "master" as reference (this step comes after the above PR is merged).
|
||||
@@ -160,5 +188,5 @@ git log --oneline --no-decorate --no-merges master..develop
|
||||
|
||||
### After-release
|
||||
|
||||
* Update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`).
|
||||
* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`).
|
||||
* Create a PR against develop to update that branch.
|
||||
|
||||
@@ -26,6 +26,10 @@ To update the image, simply run the above commands again and restart your runnin
|
||||
|
||||
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
|
||||
|
||||
!!! Note Docker image update frequency
|
||||
The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate.
|
||||
In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`.
|
||||
|
||||
### Prepare the configuration files
|
||||
|
||||
Even though you will use docker, you'll still need some files from the github repository.
|
||||
@@ -140,7 +144,7 @@ To run a restartable instance in the background (feel free to place your configu
|
||||
|
||||
#### Move your config file and database
|
||||
|
||||
The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands.
|
||||
The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands.
|
||||
|
||||
```bash
|
||||
mkdir ~/.freqtrade
|
||||
|
||||
13
docs/edge.md
13
docs/edge.md
@@ -3,7 +3,7 @@
|
||||
This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss.
|
||||
|
||||
!!! Warning
|
||||
Edge positioning is not compatible with dynamic whitelist. If enabled, it overrides the dynamic whitelist option.
|
||||
Edge positioning is not compatible with dynamic (volume-based) whitelist.
|
||||
|
||||
!!! Note
|
||||
Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation.
|
||||
@@ -209,7 +209,7 @@ Edge will remove sudden pumps in a given market while going through historical d
|
||||
You can run Edge independently in order to see in details the result. Here is an example:
|
||||
|
||||
```bash
|
||||
python3 freqtrade edge
|
||||
freqtrade edge
|
||||
```
|
||||
|
||||
An example of its output:
|
||||
@@ -234,20 +234,19 @@ An example of its output:
|
||||
|
||||
### Update cached pairs with the latest data
|
||||
|
||||
```bash
|
||||
python3 freqtrade edge --refresh-pairs-cached
|
||||
```
|
||||
Edge requires historic data the same way as backtesting does.
|
||||
Please refer to the [download section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) of the documentation for details.
|
||||
|
||||
### Precising stoploss range
|
||||
|
||||
```bash
|
||||
python3 freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
|
||||
freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
|
||||
```
|
||||
|
||||
### Advanced use of timerange
|
||||
|
||||
```bash
|
||||
python3 freqtrade edge --timerange=20181110-20181113
|
||||
freqtrade edge --timerange=20181110-20181113
|
||||
```
|
||||
|
||||
Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop.
|
||||
|
||||
48
docs/faq.md
48
docs/faq.md
@@ -1,14 +1,25 @@
|
||||
# Freqtrade FAQ
|
||||
|
||||
### Freqtrade commons
|
||||
## Freqtrade common issues
|
||||
|
||||
#### I have waited 5 minutes, why hasn't the bot made any trades yet?!
|
||||
### The bot does not start
|
||||
|
||||
Running the bot with `freqtrade --config config.json` does show the output `freqtrade: command not found`.
|
||||
|
||||
This could have the following reasons:
|
||||
|
||||
* The virtual environment is not active
|
||||
* run `source .env/bin/activate` to activate the virtual environment
|
||||
* The installation did not work correctly.
|
||||
* Please check the [Installation documentation](installation.md).
|
||||
|
||||
### I have waited 5 minutes, why hasn't the bot made any trades yet?!
|
||||
|
||||
Depending on the buy strategy, the amount of whitelisted coins, the
|
||||
situation of the market etc, it can take up to hours to find good entry
|
||||
position for a trade. Be patient!
|
||||
|
||||
#### I have made 12 trades already, why is my total profit negative?!
|
||||
### I have made 12 trades already, why is my total profit negative?!
|
||||
|
||||
I understand your disappointment but unfortunately 12 trades is just
|
||||
not enough to say anything. If you run backtesting, you can see that our
|
||||
@@ -19,24 +30,34 @@ of course constantly aim to improve the bot but it will _always_ be a
|
||||
gamble, which should leave you with modest wins on monthly basis but
|
||||
you can't say much from few trades.
|
||||
|
||||
#### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again?
|
||||
### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again?
|
||||
|
||||
Not quite. Trades are persisted to a database but the configuration is
|
||||
currently only read when the bot is killed and restarted. `/stop` more
|
||||
like pauses. You can stop your bot, adjust settings and start it again.
|
||||
|
||||
#### I want to improve the bot with a new strategy
|
||||
### I want to improve the bot with a new strategy
|
||||
|
||||
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
||||
the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
|
||||
|
||||
#### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
|
||||
### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
|
||||
|
||||
You can use the `/forcesell all` command from Telegram.
|
||||
|
||||
### Hyperopt module
|
||||
### I get the message "RESTRICTED_MARKET"
|
||||
|
||||
#### How many epoch do I need to get a good Hyperopt result?
|
||||
Currently known to happen for US Bittrex users.
|
||||
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.
|
||||
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.
|
||||
|
||||
## Hyperopt module
|
||||
|
||||
### How many epoch do I need to get a good Hyperopt result?
|
||||
|
||||
Per default Hyperopts without `-e` or `--epochs` parameter will only
|
||||
run 100 epochs, means 100 evals of your triggers, guards, ... Too few
|
||||
@@ -47,16 +68,16 @@ compute.
|
||||
We recommend you to run it at least 10.000 epochs:
|
||||
|
||||
```bash
|
||||
python3 freqtrade hyperopt -e 10000
|
||||
freqtrade hyperopt -e 10000
|
||||
```
|
||||
|
||||
or if you want intermediate result to see
|
||||
|
||||
```bash
|
||||
for i in {1..100}; do python3 freqtrade hyperopt -e 100; done
|
||||
for i in {1..100}; do freqtrade hyperopt -e 100; done
|
||||
```
|
||||
|
||||
#### Why it is so long to run hyperopt?
|
||||
### Why it is so long to run hyperopt?
|
||||
|
||||
Finding a great Hyperopt results takes time.
|
||||
|
||||
@@ -74,13 +95,14 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals.
|
||||
Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
|
||||
of the search space.
|
||||
|
||||
### Edge module
|
||||
## Edge module
|
||||
|
||||
#### Edge implements interesting approach for controlling position size, is there any theory behind it?
|
||||
### Edge implements interesting approach for controlling position size, is there any theory behind it?
|
||||
|
||||
The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members.
|
||||
|
||||
You can find further info on expectancy, winrate, risk management and position size in the following sources:
|
||||
|
||||
- https://www.tradeciety.com/ultimate-math-guide-for-traders/
|
||||
- http://www.vantharp.com/tharp-concepts/expectancy.asp
|
||||
- https://samuraitradingacademy.com/trading-expectancy/
|
||||
|
||||
261
docs/hyperopt.md
261
docs/hyperopt.md
@@ -6,6 +6,9 @@ algorithms included in the `scikit-optimize` package to accomplish this. The
|
||||
search will burn all your CPU cores, make your laptop sound like a fighter jet
|
||||
and still take a long time.
|
||||
|
||||
Hyperopt requires historic data to be available, just as backtesting does.
|
||||
To learn how to get data for the pairs and exchange you're interrested in, head over to the [Data Downloading](data-download.md) section of the documentation.
|
||||
|
||||
!!! Bug
|
||||
Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
||||
|
||||
@@ -18,23 +21,28 @@ Configuring hyperopt is similar to writing your own strategy, and many tasks wil
|
||||
|
||||
### Checklist on all tasks / possibilities in hyperopt
|
||||
|
||||
Depending on the space you want to optimize, only some of the below are required.
|
||||
Depending on the space you want to optimize, only some of the below are required:
|
||||
|
||||
* fill `populate_indicators` - probably a copy from your strategy
|
||||
* fill `buy_strategy_generator` - for buy signal optimization
|
||||
* fill `indicator_space` - for buy signal optimzation
|
||||
* fill `sell_strategy_generator` - for sell signal optimization
|
||||
* fill `sell_indicator_space` - for sell signal optimzation
|
||||
* fill `roi_space` - for ROI optimization
|
||||
* fill `generate_roi_table` - for ROI optimization (if you need more than 3 entries)
|
||||
* fill `stoploss_space` - stoploss optimization
|
||||
* Optional but recommended
|
||||
|
||||
Optional, but recommended:
|
||||
|
||||
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
|
||||
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
|
||||
|
||||
Rarely you may also need to override:
|
||||
|
||||
* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default)
|
||||
* `generate_roi_table` - for custom ROI optimization (if you need more than 4 entries in the ROI table)
|
||||
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
||||
|
||||
### 1. Install a Custom Hyperopt File
|
||||
|
||||
Put your hyperopt file into the folder`user_data/hyperopts`.
|
||||
Put your hyperopt file into the directory `user_data/hyperopts`.
|
||||
|
||||
Let assume you want a hyperopt file `awesome_hyperopt.py`:
|
||||
Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py`
|
||||
@@ -144,21 +152,94 @@ it will end with telling you which paramter combination produced the best profit
|
||||
|
||||
The search for best parameters starts with a few random combinations and then uses a
|
||||
regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination
|
||||
that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`.
|
||||
that minimizes the value of the [loss function](#loss-functions).
|
||||
|
||||
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
||||
When you want to test an indicator that isn't used by the bot currently, remember to
|
||||
add it to the `populate_indicators()` method in `hyperopt.py`.
|
||||
|
||||
## Loss-functions
|
||||
|
||||
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
||||
|
||||
By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses.
|
||||
|
||||
A different loss function can be specified by using the `--hyperopt-loss <Class-name>` argument.
|
||||
This class should be in its own file within the `user_data/hyperopts/` directory.
|
||||
|
||||
Currently, the following loss functions are builtin:
|
||||
|
||||
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function)
|
||||
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
|
||||
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns)
|
||||
|
||||
### Creating and using a custom loss function
|
||||
|
||||
To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class.
|
||||
For the sample below, you then need to add the command line parameter `--hyperopt-loss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used.
|
||||
|
||||
A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py)
|
||||
|
||||
``` python
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
TARGET_TRADES = 600
|
||||
EXPECTED_MAX_PROFIT = 3.0
|
||||
MAX_ACCEPTED_TRADE_DURATION = 300
|
||||
|
||||
class SuperDuperHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the default loss function for hyperopt
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
This is the legacy algorithm (used until now in freqtrade).
|
||||
Weights are distributed as follows:
|
||||
* 0.4 to trade duration
|
||||
* 0.25: Avoiding trade loss
|
||||
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
||||
"""
|
||||
total_profit = results.profit_percent.sum()
|
||||
trade_duration = results.trade_duration.mean()
|
||||
|
||||
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
|
||||
result = trade_loss + profit_loss + duration_loss
|
||||
return result
|
||||
```
|
||||
|
||||
Currently, the arguments are:
|
||||
|
||||
* `results`: DataFrame containing the result
|
||||
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
|
||||
`pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason`
|
||||
* `trade_count`: Amount of trades (identical to `len(results)`)
|
||||
* `min_date`: Start date of the hyperopting TimeFrame
|
||||
* `min_date`: End date of the hyperopting TimeFrame
|
||||
|
||||
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
|
||||
|
||||
!!! Note
|
||||
This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
|
||||
|
||||
!!! Note
|
||||
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later.
|
||||
|
||||
## Execute Hyperopt
|
||||
|
||||
Once you have updated your hyperopt configuration you can run it.
|
||||
Because hyperopt tries a lot of combinations to find the best parameters it will take time you will have the result (more than 30 mins).
|
||||
Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results.
|
||||
|
||||
We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
|
||||
|
||||
```bash
|
||||
python3 freqtrade -c config.json hyperopt --customhyperopt <hyperoptname> -e 5000 --spaces all
|
||||
freqtrade -c config.json hyperopt --customhyperopt <hyperoptname> -e 5000 --spaces all
|
||||
```
|
||||
|
||||
Use `<hyperoptname>` as the name of the custom hyperopt used.
|
||||
@@ -168,8 +249,11 @@ running at least several thousand evaluations.
|
||||
|
||||
The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below.
|
||||
|
||||
!!! Note
|
||||
By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`.
|
||||
|
||||
!!! Warning
|
||||
When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file.
|
||||
When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed.
|
||||
|
||||
### Execute Hyperopt with Different Ticker-Data Source
|
||||
|
||||
@@ -179,12 +263,11 @@ use data from directory `user_data/data`.
|
||||
|
||||
### Running Hyperopt with Smaller Testset
|
||||
|
||||
Use the `--timerange` argument to change how much of the testset
|
||||
you want to use. The last N ticks/timeframes will be used.
|
||||
Example:
|
||||
Use the `--timerange` argument to change how much of the testset you want to use.
|
||||
For example, to use one month of data, pass the following parameter to the hyperopt call:
|
||||
|
||||
```bash
|
||||
python3 freqtrade hyperopt --timerange -200
|
||||
freqtrade hyperopt --timerange 20180401-20180501
|
||||
```
|
||||
|
||||
### Running Hyperopt with Smaller Search Space
|
||||
@@ -197,12 +280,33 @@ new buy strategy you have.
|
||||
|
||||
Legal values are:
|
||||
|
||||
- `all`: optimize everything
|
||||
- `buy`: just search for a new buy strategy
|
||||
- `sell`: just search for a new sell strategy
|
||||
- `roi`: just optimize the minimal profit table for your strategy
|
||||
- `stoploss`: search for the best stoploss value
|
||||
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||
* `all`: optimize everything
|
||||
* `buy`: just search for a new buy strategy
|
||||
* `sell`: just search for a new sell strategy
|
||||
* `roi`: just optimize the minimal profit table for your strategy
|
||||
* `stoploss`: search for the best stoploss value
|
||||
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||
|
||||
### Position stacking and disabling max market positions
|
||||
|
||||
In some situations, you may need to run Hyperopt (and Backtesting) with the
|
||||
`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments.
|
||||
|
||||
By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one
|
||||
open trade is allowed for every traded pair. The total number of trades open for all pairs
|
||||
is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to
|
||||
some potential trades to be hidden (or masked) by previosly open trades.
|
||||
|
||||
The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times,
|
||||
while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades`
|
||||
during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high
|
||||
number).
|
||||
|
||||
!!! Note
|
||||
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
|
||||
|
||||
You can also enable position stacking in the configuration file by explicitly setting
|
||||
`"position_stacking"=true`.
|
||||
|
||||
## Understand the Hyperopt Result
|
||||
|
||||
@@ -211,8 +315,10 @@ Given the following result from hyperopt:
|
||||
|
||||
```
|
||||
Best result:
|
||||
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
||||
with values:
|
||||
|
||||
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
|
||||
|
||||
Buy hyperspace params:
|
||||
{ 'adx-value': 44,
|
||||
'rsi-value': 29,
|
||||
'adx-enabled': False,
|
||||
@@ -231,7 +337,7 @@ method, what those values match to.
|
||||
|
||||
So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block:
|
||||
|
||||
```
|
||||
``` python
|
||||
(dataframe['rsi'] < 29.0)
|
||||
```
|
||||
|
||||
@@ -249,58 +355,103 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
return dataframe
|
||||
```
|
||||
|
||||
By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line.
|
||||
|
||||
You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option.
|
||||
|
||||
### Understand Hyperopt ROI results
|
||||
|
||||
If you are optimizing ROI, you're result will look as follows and include a ROI table.
|
||||
If you are optimizing ROI (i.e. if optimization search-space contains 'all' or 'roi'), your result will look as follows and include a ROI table:
|
||||
|
||||
```
|
||||
Best result:
|
||||
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
||||
with values:
|
||||
|
||||
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
|
||||
|
||||
Buy hyperspace params:
|
||||
{ 'adx-value': 44,
|
||||
'rsi-value': 29,
|
||||
'adx-enabled': false,
|
||||
'adx-enabled': False,
|
||||
'rsi-enabled': True,
|
||||
'trigger': 'bb_lower',
|
||||
'roi_t1': 40,
|
||||
'roi_t2': 57,
|
||||
'roi_t3': 21,
|
||||
'roi_p1': 0.03634636907306948,
|
||||
'roi_p2': 0.055237357937802885,
|
||||
'roi_p3': 0.015163796015548354,
|
||||
'stoploss': -0.37996664668703606
|
||||
}
|
||||
'trigger': 'bb_lower'}
|
||||
ROI table:
|
||||
{ 0: 0.10674752302642071,
|
||||
21: 0.09158372701087236,
|
||||
78: 0.03634636907306948,
|
||||
{ 0: 0.10674,
|
||||
21: 0.09158,
|
||||
78: 0.03634,
|
||||
118: 0}
|
||||
```
|
||||
|
||||
This would translate to the following ROI table:
|
||||
In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy:
|
||||
|
||||
``` python
|
||||
```
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||
minimal_roi = {
|
||||
"118": 0,
|
||||
"78": 0.0363463,
|
||||
"21": 0.0915,
|
||||
"0": 0.106
|
||||
0: 0.10674,
|
||||
21: 0.09158,
|
||||
78: 0.03634,
|
||||
118: 0
|
||||
}
|
||||
```
|
||||
As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file.
|
||||
|
||||
### Validate backtest result
|
||||
#### Default ROI Search Space
|
||||
|
||||
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values can vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point):
|
||||
|
||||
| # step | 1m | | 5m | | 1h | | 1d | |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
|
||||
| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
|
||||
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
|
||||
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
|
||||
|
||||
These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used.
|
||||
|
||||
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
|
||||
|
||||
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py).
|
||||
|
||||
### Understand Hyperopt Stoploss results
|
||||
|
||||
If you are optimizing stoploss values (i.e. if optimization search-space contains 'all' or 'stoploss'), your result will look as follows and include stoploss:
|
||||
|
||||
```
|
||||
Best result:
|
||||
|
||||
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
|
||||
|
||||
Buy hyperspace params:
|
||||
{ 'adx-value': 44,
|
||||
'rsi-value': 29,
|
||||
'adx-enabled': False,
|
||||
'rsi-enabled': True,
|
||||
'trigger': 'bb_lower'}
|
||||
Stoploss: -0.27996
|
||||
```
|
||||
|
||||
In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy:
|
||||
|
||||
```
|
||||
# Optimal stoploss designed for the strategy
|
||||
# This attribute will be overridden if the config file contains "stoploss"
|
||||
stoploss = -0.27996
|
||||
```
|
||||
As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file.
|
||||
|
||||
#### Default Stoploss Search Space
|
||||
|
||||
If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.35...-0.02, which is sufficient in most cases.
|
||||
|
||||
If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default.
|
||||
|
||||
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py).
|
||||
|
||||
### Validate backtesting results
|
||||
|
||||
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
|
||||
To archive the same results (number of trades, ...) than during hyperopt, please use the command line flags `--disable-max-market-positions` and `--enable-position-stacking` for backtesting.
|
||||
|
||||
This configuration is the default in hyperopt for performance reasons.
|
||||
|
||||
You can overwrite position stacking in the configuration by explicitly setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L191).
|
||||
|
||||
Enabling the market-position for hyperopt is currently not possible.
|
||||
|
||||
!!! Note
|
||||
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
|
||||
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||
|
||||
## Next Step
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||
Help / Slack
|
||||
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel.
|
||||
|
||||
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) to join Slack channel.
|
||||
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join Slack channel.
|
||||
|
||||
## Ready to try?
|
||||
|
||||
|
||||
@@ -4,12 +4,22 @@ This page explains how to prepare your environment for running the bot.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
### Requirements
|
||||
|
||||
Click each one for install guide:
|
||||
|
||||
* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below)
|
||||
|
||||
### API keys
|
||||
|
||||
Before running your bot in production you will need to setup few
|
||||
external API. In production mode, the bot will require valid Exchange API
|
||||
credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended).
|
||||
|
||||
- [Setup your exchange account](#setup-your-exchange-account)
|
||||
|
||||
### Setup your exchange account
|
||||
|
||||
You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script.
|
||||
@@ -18,6 +28,9 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E
|
||||
|
||||
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot.
|
||||
|
||||
!!! Note
|
||||
Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:freqtrade/freqtrade.git
|
||||
cd freqtrade
|
||||
@@ -30,7 +43,7 @@ git checkout develop
|
||||
|
||||
## Easy Installation - Linux Script
|
||||
|
||||
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot.
|
||||
If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot.
|
||||
|
||||
```bash
|
||||
$ ./setup.sh
|
||||
@@ -45,7 +58,7 @@ usage:
|
||||
|
||||
This script will install everything you need to run the bot:
|
||||
|
||||
* Mandatory software as: `Python3`, `ta-lib`, `wget`
|
||||
* Mandatory software as: `ta-lib`
|
||||
* Setup your virtualenv
|
||||
* Configure your `config.json` file
|
||||
|
||||
@@ -70,32 +83,24 @@ Config parameter is a `config.json` configurator. This script will ask you quest
|
||||
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
||||
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
|
||||
|
||||
### Requirements
|
||||
|
||||
Click each one for install guide:
|
||||
|
||||
* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||
!!! Note
|
||||
Python3.6 or higher and the corresponding pip are assumed to be available.
|
||||
|
||||
### Linux - Ubuntu 16.04
|
||||
|
||||
#### Install Python 3.6, Git, and wget
|
||||
#### Install necessary dependencies
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository ppa:jonathonf/python-3.6
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
|
||||
sudo apt-get install build-essential git
|
||||
```
|
||||
|
||||
#### Raspberry Pi / Raspbian
|
||||
|
||||
Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/).
|
||||
|
||||
The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation.
|
||||
It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time.
|
||||
The following assumes that miniconda3 is installed and available in your environment. Since the last miniconda3 installation file uses python 3.4, we will update to python 3.6 on this installation.
|
||||
It's recommended to use (mini)conda for this as installation/compilation of `numpy` and `pandas` takes a long time.
|
||||
|
||||
Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot).
|
||||
|
||||
@@ -104,20 +109,16 @@ conda config --add channels rpi
|
||||
conda install python=3.6
|
||||
conda create -n freqtrade python=3.6
|
||||
conda activate freqtrade
|
||||
conda install scipy pandas numpy
|
||||
conda install pandas numpy
|
||||
|
||||
sudo apt install libffi-dev
|
||||
python3 -m pip install -r requirements-common.txt
|
||||
python3 -m pip install -e .
|
||||
```
|
||||
|
||||
### MacOS
|
||||
|
||||
#### Install Python 3.6, git and wget
|
||||
|
||||
```bash
|
||||
brew install python3 git wget
|
||||
```
|
||||
!!! Note
|
||||
This does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`.
|
||||
We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine.
|
||||
|
||||
### Common
|
||||
|
||||
@@ -159,7 +160,7 @@ git clone https://github.com/freqtrade/freqtrade.git
|
||||
|
||||
```
|
||||
|
||||
Optionally checkout the stable/master branch:
|
||||
Optionally checkout the master branch to get the latest stable release:
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
@@ -177,9 +178,8 @@ cp config.json.example config.json
|
||||
#### 5. Install python dependencies
|
||||
|
||||
``` bash
|
||||
pip3 install --upgrade pip
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -e .
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install -e .
|
||||
```
|
||||
|
||||
#### 6. Run the Bot
|
||||
@@ -187,7 +187,7 @@ pip3 install -e .
|
||||
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
|
||||
|
||||
```bash
|
||||
python3.6 freqtrade -c config.json
|
||||
freqtrade -c config.json
|
||||
```
|
||||
|
||||
*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
|
||||
@@ -222,6 +222,17 @@ as the watchdog.
|
||||
|
||||
------
|
||||
|
||||
## Using Conda
|
||||
|
||||
Freqtrade can also be installed using Anaconda (or Miniconda).
|
||||
|
||||
``` bash
|
||||
conda env create -f environment.yml
|
||||
```
|
||||
|
||||
!!! Note
|
||||
This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first.
|
||||
|
||||
## Windows
|
||||
|
||||
We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure).
|
||||
@@ -237,8 +248,6 @@ If that is not available on your system, feel free to try the instructions below
|
||||
git clone https://github.com/freqtrade/freqtrade.git
|
||||
```
|
||||
|
||||
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
||||
|
||||
#### Install ta-lib
|
||||
|
||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||
|
||||
172
docs/plotting.md
172
docs/plotting.md
@@ -2,9 +2,9 @@
|
||||
|
||||
This page explains how to plot prices, indicators and profits.
|
||||
|
||||
## Installation
|
||||
## Installation / Setup
|
||||
|
||||
Plotting scripts use Plotly library. Install/upgrade it with:
|
||||
Plotting modules use the Plotly library. You can install / upgrade this by running the following command:
|
||||
|
||||
``` bash
|
||||
pip install -U -r requirements-plot.txt
|
||||
@@ -12,98 +12,172 @@ pip install -U -r requirements-plot.txt
|
||||
|
||||
## Plot price and indicators
|
||||
|
||||
Usage for the price plotter:
|
||||
The `freqtrade plot-dataframe` subcommand shows an interactive graph with three subplots:
|
||||
|
||||
* Main plot with candlestics and indicators following price (sma/ema)
|
||||
* Volume bars
|
||||
* Additional indicators as specified by `--indicators2`
|
||||
|
||||

|
||||
|
||||
Possible arguments:
|
||||
|
||||
```
|
||||
usage: freqtrade plot-dataframe [-h] [-p PAIRS [PAIRS ...]]
|
||||
[--indicators1 INDICATORS1 [INDICATORS1 ...]]
|
||||
[--indicators2 INDICATORS2 [INDICATORS2 ...]]
|
||||
[--plot-limit INT] [--db-url PATH]
|
||||
[--trade-source {DB,file}] [--export EXPORT]
|
||||
[--export-filename PATH]
|
||||
[--timerange TIMERANGE]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Show profits for only these pairs. Pairs are space-
|
||||
separated.
|
||||
--indicators1 INDICATORS1 [INDICATORS1 ...]
|
||||
Set indicators from your strategy you want in the
|
||||
first row of the graph. Space-separated list. Example:
|
||||
`ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`.
|
||||
--indicators2 INDICATORS2 [INDICATORS2 ...]
|
||||
Set indicators from your strategy you want in the
|
||||
third row of the graph. Space-separated list. Example:
|
||||
`fastd fastk`. Default: `['macd', 'macdsignal']`.
|
||||
--plot-limit INT Specify tick limit for plotting. Notice: too high
|
||||
values cause huge files. Default: 750.
|
||||
--db-url PATH Override trades database URL, this is useful in custom
|
||||
deployments (default: `sqlite:///tradesv3.sqlite` for
|
||||
Live Run mode, `sqlite://` for Dry Run).
|
||||
--trade-source {DB,file}
|
||||
Specify the source for trades (Can be DB or file
|
||||
(backtest file)) Default: file
|
||||
--export EXPORT Export backtest results, argument are: trades.
|
||||
Example: `--export=trades`
|
||||
--export-filename PATH
|
||||
Save backtest results to the file with this filename
|
||||
(default: `user_data/backtest_results/backtest-
|
||||
result.json`). Requires `--export` to be set as well.
|
||||
Example: `--export-filename=user_data/backtest_results
|
||||
/backtest_today.json`
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
|
||||
``` bash
|
||||
python3 script/plot_dataframe.py [-h] [-p pairs] [--live]
|
||||
```
|
||||
|
||||
Example
|
||||
Example:
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_dataframe.py -p BTC/ETH
|
||||
freqtrade plot-dataframe -p BTC/ETH
|
||||
```
|
||||
|
||||
The `-p` pairs argument can be used to specify pairs you would like to plot.
|
||||
The `-p/--pairs` argument can be used to specify pairs you would like to plot.
|
||||
|
||||
!!! Note
|
||||
The `freqtrade plot-dataframe` subcommand generates one plot-file per pair.
|
||||
|
||||
Specify custom indicators.
|
||||
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
|
||||
|
||||
!!! tip
|
||||
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_dataframe.py -p BTC/ETH --indicators1 sma,ema --indicators2 macd
|
||||
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma ema --indicators2 macd
|
||||
```
|
||||
|
||||
### Advanced use
|
||||
### Further usage examples
|
||||
|
||||
To plot multiple pairs, separate them with a comma:
|
||||
To plot multiple pairs, separate them with a space:
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH
|
||||
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH XRP/ETH
|
||||
```
|
||||
|
||||
To plot the current live price use the `--live` flag:
|
||||
To plot a timerange (to zoom in)
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_dataframe.py -p BTC/ETH --live
|
||||
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --timerange=20180801-20180805
|
||||
```
|
||||
|
||||
To plot a timerange (to zoom in):
|
||||
To plot trades stored in a database use `--db-url` in combination with `--trade-source DB`:
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200
|
||||
```
|
||||
|
||||
Timerange doesn't work with live data.
|
||||
|
||||
To plot trades stored in a database use `--db-url` argument:
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB
|
||||
freqtrade --strategy AwesomeStrategy plot-dataframe --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB
|
||||
```
|
||||
|
||||
To plot trades from a backtesting result, use `--export-filename <filename>`
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH
|
||||
```
|
||||
|
||||
To plot a custom strategy the strategy should have first be backtested.
|
||||
The results may then be plotted with the -s argument:
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data/<exchange_name>/
|
||||
freqtrade --strategy AwesomeStrategy plot-dataframe --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
|
||||
```
|
||||
|
||||
## Plot profit
|
||||
|
||||
The profit plotter shows a picture with three plots:
|
||||

|
||||
|
||||
The `freqtrade plot-profit` subcommand shows an interactive graph with three plots:
|
||||
|
||||
1) Average closing price for all pairs
|
||||
2) The summarized profit made by backtesting.
|
||||
Note that this is not the real-world profit, but
|
||||
more of an estimate.
|
||||
3) Each pair individually profit
|
||||
Note that this is not the real-world profit, but more of an estimate.
|
||||
3) Profit for each individual pair
|
||||
|
||||
The first graph is good to get a grip of how the overall market
|
||||
progresses.
|
||||
The first graph is good to get a grip of how the overall market progresses.
|
||||
|
||||
The second graph will show how your algorithm works or doesn't.
|
||||
Perhaps you want an algorithm that steadily makes small profits,
|
||||
or one that acts less seldom, but makes big swings.
|
||||
The second graph will show if your algorithm works or doesn't.
|
||||
Perhaps you want an algorithm that steadily makes small profits, or one that acts less often, but makes big swings.
|
||||
|
||||
The third graph can be useful to spot outliers, events in pairs
|
||||
that makes profit spikes.
|
||||
The third graph can be useful to spot outliers, events in pairs that cause profit spikes.
|
||||
|
||||
Usage for the profit plotter:
|
||||
Possible options for the `freqtrade plot-profit` subcommand:
|
||||
|
||||
```
|
||||
usage: freqtrade plot-profit [-h] [-p PAIRS [PAIRS ...]]
|
||||
[--timerange TIMERANGE] [--export EXPORT]
|
||||
[--export-filename PATH] [--db-url PATH]
|
||||
[--trade-source {DB,file}]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Show profits for only these pairs. Pairs are space-
|
||||
separated.
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
--export EXPORT Export backtest results, argument are: trades.
|
||||
Example: `--export=trades`
|
||||
--export-filename PATH
|
||||
Save backtest results to the file with this filename
|
||||
(default: `user_data/backtest_results/backtest-
|
||||
result.json`). Requires `--export` to be set as well.
|
||||
Example: `--export-filename=user_data/backtest_results
|
||||
/backtest_today.json`
|
||||
--db-url PATH Override trades database URL, this is useful in custom
|
||||
deployments (default: `sqlite:///tradesv3.sqlite` for
|
||||
Live Run mode, `sqlite://` for Dry Run).
|
||||
--trade-source {DB,file}
|
||||
Specify the source for trades (Can be DB or file
|
||||
(backtest file)) Default: file
|
||||
|
||||
``` bash
|
||||
python3 script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num]
|
||||
```
|
||||
|
||||
The `-p` pair argument, can be used to plot a single pair
|
||||
The `-p/--pairs` argument, can be used to limit the pairs that are considered for this calculation.
|
||||
|
||||
Example
|
||||
Examples:
|
||||
|
||||
Use custom backtest-export file
|
||||
|
||||
``` bash
|
||||
python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC
|
||||
freqtrade plot-profit -p LTC/BTC --export-filename user_data/backtest_results/backtest-result-Strategy005.json
|
||||
```
|
||||
|
||||
Use custom database
|
||||
|
||||
``` bash
|
||||
freqtrade plot-profit -p LTC/BTC --db-url sqlite:///tradesv3.sqlite --trade-source DB
|
||||
```
|
||||
|
||||
``` bash
|
||||
freqtrade plot-profit --datadir user_data/data/binance_save/ -p LTC/BTC
|
||||
```
|
||||
|
||||
@@ -1 +1 @@
|
||||
mkdocs-material==3.1.0
|
||||
mkdocs-material==4.4.2
|
||||
@@ -16,10 +16,10 @@ Sample configuration:
|
||||
},
|
||||
```
|
||||
|
||||
!!! Danger: Security warning
|
||||
!!! Danger Security warning
|
||||
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
||||
|
||||
!!! Danger: Password selection
|
||||
!!! Danger Password selection
|
||||
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
||||
|
||||
You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly.
|
||||
|
||||
@@ -5,8 +5,7 @@ indicators.
|
||||
|
||||
## Install a custom strategy file
|
||||
|
||||
This is very simple. Copy paste your strategy file into the folder
|
||||
`user_data/strategies`.
|
||||
This is very simple. Copy paste your strategy file into the directory `user_data/strategies`.
|
||||
|
||||
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
|
||||
|
||||
@@ -14,7 +13,7 @@ Let assume you have a class called `AwesomeStrategy` in the file `awesome-strate
|
||||
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
||||
|
||||
```bash
|
||||
python3 freqtrade --strategy AwesomeStrategy
|
||||
freqtrade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
## Change your strategy
|
||||
@@ -22,10 +21,10 @@ python3 freqtrade --strategy AwesomeStrategy
|
||||
The bot includes a default strategy file. However, we recommend you to
|
||||
use your own file to not have to lose your parameters every time the default
|
||||
strategy file will be updated on Github. Put your custom strategy file
|
||||
into the folder `user_data/strategies`.
|
||||
into the directory `user_data/strategies`.
|
||||
|
||||
Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes.
|
||||
`cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py`
|
||||
`cp user_data/strategies/sample_strategy.py user_data/strategies/awesome-strategy.py`
|
||||
|
||||
### Anatomy of a strategy
|
||||
|
||||
@@ -37,14 +36,19 @@ A strategy file contains all the information needed to build a good strategy:
|
||||
- Minimal ROI recommended
|
||||
- Stoploss strongly recommended
|
||||
|
||||
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
|
||||
You can test it with the parameter: `--strategy TestStrategy`
|
||||
The bot also include a sample strategy called `SampleStrategy` you can update: `user_data/strategies/sample_strategy.py`.
|
||||
You can test it with the parameter: `--strategy SampleStrategy`
|
||||
|
||||
Additionally, there is an attribute called `INTERFACE_VERSION`, which defines the version of the strategy interface the bot should use.
|
||||
The current version is 2 - which is also the default when it's not set explicitly in the strategy.
|
||||
|
||||
Future versions will require this to be set.
|
||||
|
||||
```bash
|
||||
python3 freqtrade --strategy AwesomeStrategy
|
||||
freqtrade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||
**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py)
|
||||
file as reference.**
|
||||
|
||||
!!! Note Strategies and Backtesting
|
||||
@@ -110,9 +114,8 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||
return dataframe
|
||||
```
|
||||
|
||||
|
||||
!!! Note "Want more indicator examples?"
|
||||
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).<br/>
|
||||
Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py).
|
||||
Then uncomment indicators you need.
|
||||
|
||||
### Buy signal rules
|
||||
@@ -123,7 +126,7 @@ It's important to always return the dataframe without removing/modifying the col
|
||||
|
||||
This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action".
|
||||
|
||||
Sample from `user_data/strategies/test_strategy.py`:
|
||||
Sample from `user_data/strategies/sample_strategy.py`:
|
||||
|
||||
```python
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
@@ -153,7 +156,7 @@ It's important to always return the dataframe without removing/modifying the col
|
||||
|
||||
This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action".
|
||||
|
||||
Sample from `user_data/strategies/test_strategy.py`:
|
||||
Sample from `user_data/strategies/sample_strategy.py`:
|
||||
|
||||
```python
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
@@ -221,7 +224,7 @@ This would signify a stoploss of -10%.
|
||||
|
||||
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
||||
|
||||
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems).
|
||||
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order_types dictionary, so your stoploss is on the exchange and cannot be missed due to network problems, high load or other reasons.
|
||||
|
||||
For more information on order_types please look [here](configuration.md#understand-order_types).
|
||||
|
||||
@@ -275,27 +278,24 @@ Please always check the mode of operation to select the correct method to get da
|
||||
|
||||
#### Possible options for DataProvider
|
||||
|
||||
- `available_pairs` - Property with tuples listing cached pairs with their intervals. (pair, interval)
|
||||
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for all pairs in the whitelist, returns DataFrame or empty DataFrame
|
||||
- `historic_ohlcv(pair, ticker_interval)` - Data stored on disk
|
||||
- `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval).
|
||||
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame.
|
||||
- `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk.
|
||||
- `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
|
||||
- `runmode` - Property containing the current runmode.
|
||||
|
||||
#### ohlcv / historic_ohlcv
|
||||
#### Example: fetch live ohlcv / historic data for the first informative pair
|
||||
|
||||
``` python
|
||||
if self.dp:
|
||||
if self.dp.runmode in ('live', 'dry_run'):
|
||||
if (f'{self.stake_currency}/BTC', self.ticker_interval) in self.dp.available_pairs:
|
||||
data_eth = self.dp.ohlcv(pair='{self.stake_currency}/BTC',
|
||||
ticker_interval=self.ticker_interval)
|
||||
else:
|
||||
# Get historic ohlcv data (cached on disk).
|
||||
history_eth = self.dp.historic_ohlcv(pair='{self.stake_currency}/BTC',
|
||||
ticker_interval='1h')
|
||||
inf_pair, inf_timeframe = self.informative_pairs()[0]
|
||||
informative = self.dp.get_pair_dataframe(pair=inf_pair,
|
||||
ticker_interval=inf_timeframe)
|
||||
```
|
||||
|
||||
!!! Warning Warning about backtesting
|
||||
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` provides the full time-range in one go,
|
||||
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
|
||||
for the backtesting runmode) provides the full time-range in one go,
|
||||
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
|
||||
|
||||
!!! Warning Warning in hyperopt
|
||||
@@ -310,7 +310,9 @@ if self.dp:
|
||||
dataframe['best_bid'] = ob['bids'][0][0]
|
||||
dataframe['best_ask'] = ob['asks'][0][0]
|
||||
```
|
||||
!Warning The order book is not part of the historic data which means backtesting and hyperopt will not work if this
|
||||
|
||||
!!! Warning
|
||||
The order book is not part of the historic data which means backtesting and hyperopt will not work if this
|
||||
method is used.
|
||||
|
||||
#### Available Pairs
|
||||
@@ -321,7 +323,6 @@ if self.dp:
|
||||
print(f"available {pair}, {ticker}")
|
||||
```
|
||||
|
||||
|
||||
#### Get data for non-tradeable pairs
|
||||
|
||||
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
|
||||
@@ -398,10 +399,10 @@ The default buy strategy is located in the file
|
||||
|
||||
### Specify custom strategy location
|
||||
|
||||
If you want to use a strategy from a different folder you can pass `--strategy-path`
|
||||
If you want to use a strategy from a different directory you can pass `--strategy-path`
|
||||
|
||||
```bash
|
||||
python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder
|
||||
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||
```
|
||||
|
||||
### Further strategy ideas
|
||||
@@ -410,7 +411,7 @@ To get additional Ideas for strategies, head over to our [strategy repository](h
|
||||
Feel free to use any of them as inspiration for your own strategies.
|
||||
We're happy to accept Pull Requests containing new Strategies to that repo.
|
||||
|
||||
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) which is a great place to get and/or share ideas.
|
||||
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) which is a great place to get and/or share ideas.
|
||||
|
||||
## Next step
|
||||
|
||||
|
||||
60
environment.yml
Normal file
60
environment.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
name: freqtrade
|
||||
channels:
|
||||
- defaults
|
||||
- conda-forge
|
||||
dependencies:
|
||||
# Required for app
|
||||
- python>=3.6
|
||||
- pip
|
||||
- wheel
|
||||
- numpy
|
||||
- pandas
|
||||
- SQLAlchemy
|
||||
- arrow
|
||||
- requests
|
||||
- urllib3
|
||||
- wrapt
|
||||
- jsonschema
|
||||
- tabulate
|
||||
- python-rapidjson
|
||||
- flask
|
||||
- python-dotenv
|
||||
- cachetools
|
||||
- python-telegram-bot
|
||||
# Optional for plotting
|
||||
- plotly
|
||||
# Optional for hyperopt
|
||||
- scipy
|
||||
- scikit-optimize
|
||||
- scikit-learn
|
||||
- filelock
|
||||
- joblib
|
||||
# Optional for development
|
||||
- flake8
|
||||
- pytest
|
||||
- pytest-mock
|
||||
- pytest-asyncio
|
||||
- pytest-cov
|
||||
- coveralls
|
||||
- mypy
|
||||
# Useful for jupyter
|
||||
- jupyter
|
||||
- ipykernel
|
||||
- isort
|
||||
- yapf
|
||||
- pip:
|
||||
# Required for app
|
||||
- cython
|
||||
- coinmarketcap
|
||||
- ccxt
|
||||
- TA-Lib
|
||||
- py_find_1st
|
||||
- sdnotify
|
||||
# Optional for develpment
|
||||
- flake8-tidy-imports
|
||||
- flake8-type-annotations
|
||||
- pytest-random-order
|
||||
- -e .
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
""" FreqTrade bot """
|
||||
__version__ = '2019.6'
|
||||
__version__ = '2019.9'
|
||||
|
||||
if __version__ == 'develop':
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
__version__ = 'develop-' + subprocess.check_output(
|
||||
['git', 'log', '--format="%h"', '-n 1'],
|
||||
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||
except Exception:
|
||||
# git not available, ignore
|
||||
pass
|
||||
|
||||
|
||||
class DependencyException(Exception):
|
||||
@@ -11,7 +22,7 @@ class DependencyException(Exception):
|
||||
|
||||
class OperationalException(Exception):
|
||||
"""
|
||||
Requires manual intervention.
|
||||
Requires manual intervention and will usually stop the bot.
|
||||
This happens when an exchange returns an unexpected error during runtime
|
||||
or given configuration is invalid.
|
||||
"""
|
||||
|
||||
@@ -1,526 +0,0 @@
|
||||
"""
|
||||
This module contains the argument manager class
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
from typing import List, NamedTuple, Optional
|
||||
import arrow
|
||||
from freqtrade import __version__, constants
|
||||
|
||||
|
||||
class TimeRange(NamedTuple):
|
||||
"""
|
||||
NamedTuple Defining timerange inputs.
|
||||
[start/stop]type defines if [start/stop]ts shall be used.
|
||||
if *type is none, don't use corresponding startvalue.
|
||||
"""
|
||||
starttype: Optional[str] = None
|
||||
stoptype: Optional[str] = None
|
||||
startts: int = 0
|
||||
stopts: int = 0
|
||||
|
||||
|
||||
class Arguments(object):
|
||||
"""
|
||||
Arguments Class. Manage the arguments received by the cli
|
||||
"""
|
||||
|
||||
def __init__(self, args: Optional[List[str]], description: str) -> None:
|
||||
self.args = args
|
||||
self.parsed_arg: Optional[argparse.Namespace] = None
|
||||
self.parser = argparse.ArgumentParser(description=description)
|
||||
|
||||
def _load_args(self) -> None:
|
||||
self.common_options()
|
||||
self.main_options()
|
||||
self._build_subcommands()
|
||||
|
||||
def get_parsed_arg(self) -> argparse.Namespace:
|
||||
"""
|
||||
Return the list of arguments
|
||||
:return: List[str] List of arguments
|
||||
"""
|
||||
if self.parsed_arg is None:
|
||||
self._load_args()
|
||||
self.parsed_arg = self.parse_args()
|
||||
|
||||
return self.parsed_arg
|
||||
|
||||
def parse_args(self, no_default_config: bool = False) -> argparse.Namespace:
|
||||
"""
|
||||
Parses given arguments and returns an argparse Namespace instance.
|
||||
"""
|
||||
parsed_arg = self.parser.parse_args(self.args)
|
||||
|
||||
# Workaround issue in argparse with action='append' and default value
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
if not no_default_config and parsed_arg.config is None:
|
||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
|
||||
def common_options(self) -> None:
|
||||
"""
|
||||
Parses arguments that are common for the main Freqtrade, all subcommands and scripts.
|
||||
"""
|
||||
parser = self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
help='Verbose mode (-vv for more, -vvv to get all messages).',
|
||||
action='count',
|
||||
dest='loglevel',
|
||||
default=0,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--logfile',
|
||||
help='Log to the file specified.',
|
||||
dest='logfile',
|
||||
metavar='FILE',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version=f'%(prog)s {__version__}'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). '
|
||||
f'Multiple --config options may be used. '
|
||||
f'Can be set to `-` to read config from stdin.',
|
||||
dest='config',
|
||||
action='append',
|
||||
metavar='PATH',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-d', '--datadir',
|
||||
help='Path to backtest data.',
|
||||
dest='datadir',
|
||||
metavar='PATH',
|
||||
)
|
||||
|
||||
def main_options(self) -> None:
|
||||
"""
|
||||
Parses arguments for the main Freqtrade.
|
||||
"""
|
||||
parser = self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'-s', '--strategy',
|
||||
help='Specify strategy class name (default: `%(default)s`).',
|
||||
dest='strategy',
|
||||
default='DefaultStrategy',
|
||||
metavar='NAME',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--strategy-path',
|
||||
help='Specify additional strategy lookup path.',
|
||||
dest='strategy_path',
|
||||
metavar='PATH',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dynamic-whitelist',
|
||||
help='Dynamically generate and update whitelist '
|
||||
'based on 24h BaseVolume (default: %(const)s). '
|
||||
'DEPRECATED.',
|
||||
dest='dynamic_whitelist',
|
||||
const=constants.DYNAMIC_WHITELIST,
|
||||
type=int,
|
||||
metavar='INT',
|
||||
nargs='?',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--db-url',
|
||||
help=f'Override trades database URL, this is useful in custom deployments '
|
||||
f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, '
|
||||
f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).',
|
||||
dest='db_url',
|
||||
metavar='PATH',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sd-notify',
|
||||
help='Notify systemd service manager.',
|
||||
action='store_true',
|
||||
dest='sd_notify',
|
||||
)
|
||||
|
||||
def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||
"""
|
||||
Parses arguments common for Backtesting, Edge and Hyperopt modules.
|
||||
:param parser:
|
||||
"""
|
||||
parser = subparser or self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'-i', '--ticker-interval',
|
||||
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||
dest='ticker_interval',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timerange',
|
||||
help='Specify what timerange of data to use.',
|
||||
dest='timerange',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--max_open_trades',
|
||||
help='Specify max_open_trades to use.',
|
||||
type=int,
|
||||
dest='max_open_trades',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--stake_amount',
|
||||
help='Specify stake_amount.',
|
||||
type=float,
|
||||
dest='stake_amount',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r', '--refresh-pairs-cached',
|
||||
help='Refresh the pairs files in tests/testdata with the latest data from the '
|
||||
'exchange. Use it if you want to run your optimization commands with '
|
||||
'up-to-date data.',
|
||||
action='store_true',
|
||||
dest='refresh_pairs',
|
||||
)
|
||||
|
||||
def backtesting_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||
"""
|
||||
Parses given arguments for Backtesting module.
|
||||
"""
|
||||
parser = subparser or self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'--eps', '--enable-position-stacking',
|
||||
help='Allow buying the same pair multiple times (position stacking).',
|
||||
action='store_true',
|
||||
dest='position_stacking',
|
||||
default=False
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dmmp', '--disable-max-market-positions',
|
||||
help='Disable applying `max_open_trades` during backtest '
|
||||
'(same as setting `max_open_trades` to a very high number).',
|
||||
action='store_false',
|
||||
dest='use_max_market_positions',
|
||||
default=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l', '--live',
|
||||
help='Use live data.',
|
||||
action='store_true',
|
||||
dest='live',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--strategy-list',
|
||||
help='Provide a comma-separated list of strategies to backtest. '
|
||||
'Please note that ticker-interval needs to be set either in config '
|
||||
'or via command line. When using this together with `--export trades`, '
|
||||
'the strategy-name is injected into the filename '
|
||||
'(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`',
|
||||
nargs='+',
|
||||
dest='strategy_list',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--export',
|
||||
help='Export backtest results, argument are: trades. '
|
||||
'Example: `--export=trades`',
|
||||
dest='export',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--export-filename',
|
||||
help='Save backtest results to the file with this filename (default: `%(default)s`). '
|
||||
'Requires `--export` to be set as well. '
|
||||
'Example: `--export-filename=user_data/backtest_data/backtest_today.json`',
|
||||
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
|
||||
dest='exportfilename',
|
||||
metavar='PATH',
|
||||
)
|
||||
|
||||
def edge_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||
"""
|
||||
Parses given arguments for Edge module.
|
||||
"""
|
||||
parser = subparser or self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'--stoplosses',
|
||||
help='Defines a range of stoploss values against which edge will assess the strategy. '
|
||||
'The format is "min,max,step" (without any space). '
|
||||
'Example: `--stoplosses=-0.01,-0.1,-0.001`',
|
||||
dest='stoploss_range',
|
||||
)
|
||||
|
||||
def hyperopt_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||
"""
|
||||
Parses given arguments for Hyperopt module.
|
||||
"""
|
||||
parser = subparser or self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'--customhyperopt',
|
||||
help='Specify hyperopt class name (default: `%(default)s`).',
|
||||
dest='hyperopt',
|
||||
default=constants.DEFAULT_HYPEROPT,
|
||||
metavar='NAME',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--eps', '--enable-position-stacking',
|
||||
help='Allow buying the same pair multiple times (position stacking).',
|
||||
action='store_true',
|
||||
dest='position_stacking',
|
||||
default=False
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dmmp', '--disable-max-market-positions',
|
||||
help='Disable applying `max_open_trades` during backtest '
|
||||
'(same as setting `max_open_trades` to a very high number).',
|
||||
action='store_false',
|
||||
dest='use_max_market_positions',
|
||||
default=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'-e', '--epochs',
|
||||
help='Specify number of epochs (default: %(default)d).',
|
||||
dest='epochs',
|
||||
default=constants.HYPEROPT_EPOCH,
|
||||
type=int,
|
||||
metavar='INT',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--spaces',
|
||||
help='Specify which parameters to hyperopt. Space-separated list. '
|
||||
'Default: `%(default)s`.',
|
||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
|
||||
default='all',
|
||||
nargs='+',
|
||||
dest='spaces',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--print-all',
|
||||
help='Print all results, not only the best ones.',
|
||||
action='store_true',
|
||||
dest='print_all',
|
||||
default=False
|
||||
)
|
||||
parser.add_argument(
|
||||
'-j', '--job-workers',
|
||||
help='The number of concurrently running jobs for hyperoptimization '
|
||||
'(hyperopt worker processes). '
|
||||
'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. '
|
||||
'If 1 is given, no parallel computing code is used at all.',
|
||||
dest='hyperopt_jobs',
|
||||
default=-1,
|
||||
type=int,
|
||||
metavar='JOBS',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--random-state',
|
||||
help='Set random state to some positive integer for reproducible hyperopt results.',
|
||||
dest='hyperopt_random_state',
|
||||
type=Arguments.check_int_positive,
|
||||
metavar='INT',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--min-trades',
|
||||
help="Set minimal desired number of trades for evaluations in the hyperopt "
|
||||
"optimization path (default: 1).",
|
||||
dest='hyperopt_min_trades',
|
||||
default=1,
|
||||
type=Arguments.check_int_positive,
|
||||
metavar='INT',
|
||||
)
|
||||
|
||||
def list_exchanges_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||
"""
|
||||
Parses given arguments for the list-exchanges command.
|
||||
"""
|
||||
parser = subparser or self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'-1', '--one-column',
|
||||
help='Print exchanges in one column.',
|
||||
action='store_true',
|
||||
dest='print_one_column',
|
||||
)
|
||||
|
||||
def _build_subcommands(self) -> None:
|
||||
"""
|
||||
Builds and attaches all subcommands.
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
|
||||
from freqtrade.utils import start_list_exchanges
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||
|
||||
# Add backtesting subcommand
|
||||
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.')
|
||||
backtesting_cmd.set_defaults(func=start_backtesting)
|
||||
self.common_optimize_options(backtesting_cmd)
|
||||
self.backtesting_options(backtesting_cmd)
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.')
|
||||
edge_cmd.set_defaults(func=start_edge)
|
||||
self.common_optimize_options(edge_cmd)
|
||||
self.edge_options(edge_cmd)
|
||||
|
||||
# Add hyperopt subcommand
|
||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.')
|
||||
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
||||
self.common_optimize_options(hyperopt_cmd)
|
||||
self.hyperopt_options(hyperopt_cmd)
|
||||
|
||||
# Add list-exchanges subcommand
|
||||
list_exchanges_cmd = subparsers.add_parser(
|
||||
'list-exchanges',
|
||||
help='Print available exchanges.'
|
||||
)
|
||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||
self.list_exchanges_options(list_exchanges_cmd)
|
||||
|
||||
@staticmethod
|
||||
def parse_timerange(text: Optional[str]) -> TimeRange:
|
||||
"""
|
||||
Parse the value of the argument --timerange to determine what is the range desired
|
||||
:param text: value from --timerange
|
||||
:return: Start and End range period
|
||||
"""
|
||||
if text is None:
|
||||
return TimeRange(None, None, 0, 0)
|
||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||
(r'^(\d{8})-$', ('date', None)),
|
||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||
(r'^-(\d{10})$', (None, 'date')),
|
||||
(r'^(\d{10})-$', ('date', None)),
|
||||
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
|
||||
(r'^(-\d+)$', (None, 'line')),
|
||||
(r'^(\d+)-$', ('line', None)),
|
||||
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
||||
for rex, stype in syntax:
|
||||
# Apply the regular expression to text
|
||||
match = re.match(rex, text)
|
||||
if match: # Regex has matched
|
||||
rvals = match.groups()
|
||||
index = 0
|
||||
start: int = 0
|
||||
stop: int = 0
|
||||
if stype[0]:
|
||||
starts = rvals[index]
|
||||
if stype[0] == 'date' and len(starts) == 8:
|
||||
start = arrow.get(starts, 'YYYYMMDD').timestamp
|
||||
else:
|
||||
start = int(starts)
|
||||
index += 1
|
||||
if stype[1]:
|
||||
stops = rvals[index]
|
||||
if stype[1] == 'date' and len(stops) == 8:
|
||||
stop = arrow.get(stops, 'YYYYMMDD').timestamp
|
||||
else:
|
||||
stop = int(stops)
|
||||
return TimeRange(stype[0], stype[1], start, stop)
|
||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||
|
||||
@staticmethod
|
||||
def check_int_positive(value: str) -> int:
|
||||
try:
|
||||
uint = int(value)
|
||||
if uint <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"{value} is invalid for this parameter, should be a positive integer value"
|
||||
)
|
||||
return uint
|
||||
|
||||
def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None:
|
||||
"""
|
||||
Parses arguments common for scripts.
|
||||
"""
|
||||
parser = subparser or self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'-p', '--pairs',
|
||||
help='Show profits for only these pairs. Pairs are comma-separated.',
|
||||
dest='pairs',
|
||||
)
|
||||
|
||||
def download_data_options(self) -> None:
|
||||
"""
|
||||
Parses given arguments for testdata download script
|
||||
"""
|
||||
parser = self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'--pairs-file',
|
||||
help='File containing a list of pairs to download.',
|
||||
dest='pairs_file',
|
||||
metavar='FILE',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--days',
|
||||
help='Download data for given number of days.',
|
||||
dest='days',
|
||||
type=Arguments.check_int_positive,
|
||||
metavar='INT',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--exchange',
|
||||
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
|
||||
f'Only valid if no config is provided.',
|
||||
dest='exchange',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t', '--timeframes',
|
||||
help=f'Specify which tickers to download. Space-separated list. '
|
||||
f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.',
|
||||
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||
nargs='+',
|
||||
dest='timeframes',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--erase',
|
||||
help='Clean all existing data for the selected exchange/pairs/timeframes.',
|
||||
dest='erase',
|
||||
action='store_true'
|
||||
)
|
||||
|
||||
def plot_dataframe_options(self) -> None:
|
||||
"""
|
||||
Parses given arguments for plot dataframe script
|
||||
"""
|
||||
parser = self.parser
|
||||
|
||||
parser.add_argument(
|
||||
'--indicators1',
|
||||
help='Set indicators from your strategy you want in the first row of the graph. '
|
||||
'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.',
|
||||
default='sma,ema3,ema5',
|
||||
dest='indicators1',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--indicators2',
|
||||
help='Set indicators from your strategy you want in the third row of the graph. '
|
||||
'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.',
|
||||
default='macd,macdsignal',
|
||||
dest='indicators2',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--plot-limit',
|
||||
help='Specify tick limit for plotting. Notice: too high values cause huge files. '
|
||||
'Default: %(default)s.',
|
||||
dest='plot_limit',
|
||||
default=750,
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--trade-source',
|
||||
help='Specify the source for trades (Can be DB or file (backtest file)) '
|
||||
'Default: %(default)s',
|
||||
dest='trade_source',
|
||||
default="file",
|
||||
choices=["DB", "file"]
|
||||
)
|
||||
@@ -1,458 +0,0 @@
|
||||
"""
|
||||
This module contains the configuration class
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from jsonschema import Draft4Validator, validators
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.exchange import (is_exchange_bad, is_exchange_available,
|
||||
is_exchange_officially_supported, available_exchanges)
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def set_loggers(log_level: int = 0) -> None:
|
||||
"""
|
||||
Set the logger level for Third party libs
|
||||
:return: None
|
||||
"""
|
||||
|
||||
logging.getLogger('requests').setLevel(logging.INFO if log_level <= 1 else logging.DEBUG)
|
||||
logging.getLogger("urllib3").setLevel(logging.INFO if log_level <= 1 else logging.DEBUG)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(
|
||||
logging.INFO if log_level <= 2 else logging.DEBUG)
|
||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||
|
||||
|
||||
def _extend_validator(validator_class):
|
||||
"""
|
||||
Extended validator for the Freqtrade configuration JSON Schema.
|
||||
Currently it only handles defaults for subschemas.
|
||||
"""
|
||||
validate_properties = validator_class.VALIDATORS['properties']
|
||||
|
||||
def set_defaults(validator, properties, instance, schema):
|
||||
for prop, subschema in properties.items():
|
||||
if 'default' in subschema:
|
||||
instance.setdefault(prop, subschema['default'])
|
||||
|
||||
for error in validate_properties(
|
||||
validator, properties, instance, schema,
|
||||
):
|
||||
yield error
|
||||
|
||||
return validators.extend(
|
||||
validator_class, {'properties': set_defaults}
|
||||
)
|
||||
|
||||
|
||||
FreqtradeValidator = _extend_validator(Draft4Validator)
|
||||
|
||||
|
||||
class Configuration(object):
|
||||
"""
|
||||
Class to read and init the bot configuration
|
||||
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||
"""
|
||||
|
||||
def __init__(self, args: Namespace, runmode: RunMode = None) -> None:
|
||||
self.args = args
|
||||
self.config: Optional[Dict[str, Any]] = None
|
||||
self.runmode = runmode
|
||||
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract information for sys.argv and load the bot configuration
|
||||
:return: Configuration dictionary
|
||||
"""
|
||||
config: Dict[str, Any] = {}
|
||||
# Now expecting a list of config filenames here, not a string
|
||||
for path in self.args.config:
|
||||
logger.info('Using config: %s ...', path)
|
||||
|
||||
# Merge config options, overwriting old values
|
||||
config = deep_merge_dicts(self._load_config_file(path), config)
|
||||
|
||||
if 'internals' not in config:
|
||||
config['internals'] = {}
|
||||
|
||||
logger.info('Validating configuration ...')
|
||||
self._validate_config_schema(config)
|
||||
self._validate_config_consistency(config)
|
||||
|
||||
# Set strategy if not specified in config and or if it's non default
|
||||
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
|
||||
config.update({'strategy': self.args.strategy})
|
||||
|
||||
if self.args.strategy_path:
|
||||
config.update({'strategy_path': self.args.strategy_path})
|
||||
|
||||
# Load Common configuration
|
||||
config = self._load_common_config(config)
|
||||
|
||||
# Load Optimize configurations
|
||||
config = self._load_optimize_config(config)
|
||||
|
||||
# Add plotting options if available
|
||||
config = self._load_plot_config(config)
|
||||
|
||||
# Set runmode
|
||||
if not self.runmode:
|
||||
# Handle real mode, infer dry/live from config
|
||||
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
|
||||
|
||||
config.update({'runmode': self.runmode})
|
||||
|
||||
return config
|
||||
|
||||
def _load_config_file(self, path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Loads a config file from the given path
|
||||
:param path: path as str
|
||||
:return: configuration as dictionary
|
||||
"""
|
||||
try:
|
||||
# Read config from stdin if requested in the options
|
||||
with open(path) if path != '-' else sys.stdin as file:
|
||||
conf = json.load(file)
|
||||
except FileNotFoundError:
|
||||
raise OperationalException(
|
||||
f'Config file "{path}" not found!'
|
||||
' Please create a config file or check whether it exists.')
|
||||
|
||||
return conf
|
||||
|
||||
def _load_logging_config(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Extract information for sys.argv and load logging configuration:
|
||||
the --loglevel, --logfile options
|
||||
"""
|
||||
# Log level
|
||||
if 'loglevel' in self.args and self.args.loglevel:
|
||||
config.update({'verbosity': self.args.loglevel})
|
||||
else:
|
||||
config.update({'verbosity': 0})
|
||||
|
||||
# Log to stdout, not stderr
|
||||
log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)]
|
||||
if 'logfile' in self.args and self.args.logfile:
|
||||
config.update({'logfile': self.args.logfile})
|
||||
|
||||
# Allow setting this as either configuration or argument
|
||||
if 'logfile' in config:
|
||||
log_handlers.append(RotatingFileHandler(config['logfile'],
|
||||
maxBytes=1024 * 1024, # 1Mb
|
||||
backupCount=10))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=log_handlers
|
||||
)
|
||||
set_loggers(config['verbosity'])
|
||||
logger.info('Verbosity set to %s', config['verbosity'])
|
||||
|
||||
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract information for sys.argv and load common configuration
|
||||
:return: configuration as dictionary
|
||||
"""
|
||||
self._load_logging_config(config)
|
||||
|
||||
# Support for sd_notify
|
||||
if self.args.sd_notify:
|
||||
config['internals'].update({'sd_notify': True})
|
||||
|
||||
# Add dynamic_whitelist if found
|
||||
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
||||
# Update to volumePairList (the previous default)
|
||||
config['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': self.args.dynamic_whitelist}
|
||||
}
|
||||
logger.warning(
|
||||
'Parameter --dynamic-whitelist has been deprecated, '
|
||||
'and will be completely replaced by the whitelist dict in the future. '
|
||||
'For now: using dynamically generated whitelist based on VolumePairList. '
|
||||
'(not applicable with Backtesting and Hyperopt)'
|
||||
)
|
||||
|
||||
if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
||||
config.update({'db_url': self.args.db_url})
|
||||
logger.info('Parameter --db-url detected ...')
|
||||
|
||||
if config.get('dry_run', False):
|
||||
logger.info('Dry run is enabled')
|
||||
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
|
||||
# Default to in-memory db for dry_run if not specified
|
||||
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||
else:
|
||||
if not config.get('db_url', None):
|
||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||
logger.info('Dry run is disabled')
|
||||
|
||||
if config.get('forcebuy_enable', False):
|
||||
logger.warning('`forcebuy` RPC message enabled.')
|
||||
|
||||
# Setting max_open_trades to infinite if -1
|
||||
if config.get('max_open_trades') == -1:
|
||||
config['max_open_trades'] = float('inf')
|
||||
|
||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
self.check_exchange(config)
|
||||
|
||||
return config
|
||||
|
||||
def _create_datadir(self, config: Dict[str, Any], datadir: Optional[str] = None) -> str:
|
||||
if not datadir:
|
||||
# set datadir
|
||||
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||
datadir = os.path.join('user_data', 'data', exchange_name)
|
||||
|
||||
if not os.path.isdir(datadir):
|
||||
os.makedirs(datadir)
|
||||
logger.info(f'Created data directory: {datadir}')
|
||||
return datadir
|
||||
|
||||
def _args_to_config(self, config: Dict[str, Any], argname: str,
|
||||
logstring: str, logfun: Optional[Callable] = None) -> None:
|
||||
"""
|
||||
:param config: Configuration dictionary
|
||||
:param argname: Argumentname in self.args - will be copied to config dict.
|
||||
:param logstring: Logging String
|
||||
:param logfun: logfun is applied to the configuration entry before passing
|
||||
that entry to the log string using .format().
|
||||
sample: logfun=len (prints the length of the found
|
||||
configuration instead of the content)
|
||||
"""
|
||||
if argname in self.args and getattr(self.args, argname):
|
||||
|
||||
config.update({argname: getattr(self.args, argname)})
|
||||
if logfun:
|
||||
logger.info(logstring.format(logfun(config[argname])))
|
||||
else:
|
||||
logger.info(logstring.format(config[argname]))
|
||||
|
||||
def _load_datadir_config(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Extract information for sys.argv and load datadir configuration:
|
||||
the --datadir option
|
||||
"""
|
||||
if 'datadir' in self.args and self.args.datadir:
|
||||
config.update({'datadir': self._create_datadir(config, self.args.datadir)})
|
||||
else:
|
||||
config.update({'datadir': self._create_datadir(config, None)})
|
||||
logger.info('Using data folder: %s ...', config.get('datadir'))
|
||||
|
||||
def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract information for sys.argv and load Optimize configuration
|
||||
:return: configuration as dictionary
|
||||
"""
|
||||
|
||||
# This will override the strategy configuration
|
||||
self._args_to_config(config, argname='ticker_interval',
|
||||
logstring='Parameter -i/--ticker-interval detected ... '
|
||||
'Using ticker_interval: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='live',
|
||||
logstring='Parameter -l/--live detected ...')
|
||||
|
||||
self._args_to_config(config, argname='position_stacking',
|
||||
logstring='Parameter --enable-position-stacking detected ...')
|
||||
|
||||
if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions:
|
||||
config.update({'use_max_market_positions': False})
|
||||
logger.info('Parameter --disable-max-market-positions detected ...')
|
||||
logger.info('max_open_trades set to unlimited ...')
|
||||
elif 'max_open_trades' in self.args and self.args.max_open_trades:
|
||||
config.update({'max_open_trades': self.args.max_open_trades})
|
||||
logger.info('Parameter --max_open_trades detected, '
|
||||
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
|
||||
else:
|
||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||
|
||||
self._args_to_config(config, argname='stake_amount',
|
||||
logstring='Parameter --stake_amount detected, '
|
||||
'overriding stake_amount to: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='timerange',
|
||||
logstring='Parameter --timerange detected: {} ...')
|
||||
|
||||
self._load_datadir_config(config)
|
||||
|
||||
self._args_to_config(config, argname='refresh_pairs',
|
||||
logstring='Parameter -r/--refresh-pairs-cached detected ...')
|
||||
|
||||
self._args_to_config(config, argname='strategy_list',
|
||||
logstring='Using strategy list of {} Strategies', logfun=len)
|
||||
|
||||
self._args_to_config(config, argname='ticker_interval',
|
||||
logstring='Overriding ticker interval with Command line argument')
|
||||
|
||||
self._args_to_config(config, argname='export',
|
||||
logstring='Parameter --export detected: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='exportfilename',
|
||||
logstring='Storing backtest results to {} ...')
|
||||
|
||||
# Edge section:
|
||||
if 'stoploss_range' in self.args and self.args.stoploss_range:
|
||||
txt_range = eval(self.args.stoploss_range)
|
||||
config['edge'].update({'stoploss_range_min': txt_range[0]})
|
||||
config['edge'].update({'stoploss_range_max': txt_range[1]})
|
||||
config['edge'].update({'stoploss_range_step': txt_range[2]})
|
||||
logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range)
|
||||
|
||||
# Hyperopt section
|
||||
self._args_to_config(config, argname='hyperopt',
|
||||
logstring='Using Hyperopt file {}')
|
||||
|
||||
self._args_to_config(config, argname='epochs',
|
||||
logstring='Parameter --epochs detected ... '
|
||||
'Will run Hyperopt with for {} epochs ...'
|
||||
)
|
||||
|
||||
self._args_to_config(config, argname='spaces',
|
||||
logstring='Parameter -s/--spaces detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='print_all',
|
||||
logstring='Parameter --print-all 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: {}')
|
||||
|
||||
return config
|
||||
|
||||
def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract information for sys.argv Plotting configuration
|
||||
:return: configuration as dictionary
|
||||
"""
|
||||
|
||||
self._args_to_config(config, argname='pairs',
|
||||
logstring='Using pairs {}')
|
||||
|
||||
self._args_to_config(config, argname='indicators1',
|
||||
logstring='Using indicators1: {}')
|
||||
|
||||
self._args_to_config(config, argname='indicators2',
|
||||
logstring='Using indicators2: {}')
|
||||
|
||||
self._args_to_config(config, argname='plot_limit',
|
||||
logstring='Limiting plot to: {}')
|
||||
self._args_to_config(config, argname='trade_source',
|
||||
logstring='Using trades from: {}')
|
||||
return config
|
||||
|
||||
def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate the configuration follow the Config Schema
|
||||
:param conf: Config in JSON format
|
||||
:return: Returns the config if valid, otherwise throw an exception
|
||||
"""
|
||||
try:
|
||||
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf)
|
||||
return conf
|
||||
except ValidationError as exception:
|
||||
logger.critical(
|
||||
'Invalid configuration. See config.json.example. Reason: %s',
|
||||
exception
|
||||
)
|
||||
raise ValidationError(
|
||||
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
|
||||
)
|
||||
|
||||
def _validate_config_consistency(self, conf: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Validate the configuration consistency
|
||||
:param conf: Config in JSON format
|
||||
:return: Returns None if everything is ok, otherwise throw an OperationalException
|
||||
"""
|
||||
|
||||
# validating trailing stoploss
|
||||
self._validate_trailing_stoploss(conf)
|
||||
|
||||
def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None:
|
||||
# Skip if trailing stoploss is not activated
|
||||
if not conf.get('trailing_stop', False):
|
||||
return
|
||||
|
||||
tsl_positive = float(conf.get('trailing_stop_positive', 0))
|
||||
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0))
|
||||
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False)
|
||||
|
||||
if tsl_only_offset:
|
||||
if tsl_positive == 0.0:
|
||||
raise OperationalException(
|
||||
f'The config trailing_only_offset_is_reached needs '
|
||||
'trailing_stop_positive_offset to be more than 0 in your config.')
|
||||
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
|
||||
raise OperationalException(
|
||||
f'The config trailing_stop_positive_offset needs '
|
||||
'to be greater than trailing_stop_positive_offset in your config.')
|
||||
|
||||
def get_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Return the config. Use this method to get the bot config
|
||||
:return: Dict: Bot config
|
||||
"""
|
||||
if self.config is None:
|
||||
self.config = self.load_config()
|
||||
|
||||
return self.config
|
||||
|
||||
def check_exchange(self, config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||
"""
|
||||
Check if the exchange name in the config file is supported by Freqtrade
|
||||
:param check_for_bad: if True, check the exchange against the list of known 'bad'
|
||||
exchanges
|
||||
:return: False if exchange is 'bad', i.e. is known to work with the bot with
|
||||
critical issues or does not work at all, crashes, etc. True otherwise.
|
||||
raises an exception if the exchange if not supported by ccxt
|
||||
and thus is not known for the Freqtrade at all.
|
||||
"""
|
||||
logger.info("Checking exchange...")
|
||||
|
||||
exchange = config.get('exchange', {}).get('name').lower()
|
||||
if not is_exchange_available(exchange):
|
||||
raise OperationalException(
|
||||
f'Exchange "{exchange}" is not supported by ccxt '
|
||||
f'and therefore not available for the bot.\n'
|
||||
f'The following exchanges are supported by ccxt: '
|
||||
f'{", ".join(available_exchanges())}'
|
||||
)
|
||||
|
||||
if check_for_bad and is_exchange_bad(exchange):
|
||||
logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. '
|
||||
f'Use it only for development and testing purposes.')
|
||||
return False
|
||||
|
||||
if is_exchange_officially_supported(exchange):
|
||||
logger.info(f'Exchange "{exchange}" is officially supported '
|
||||
f'by the Freqtrade development team.')
|
||||
else:
|
||||
logger.warning(f'Exchange "{exchange}" is supported by ccxt '
|
||||
f'and therefore available for the bot but not officially supported '
|
||||
f'by the Freqtrade development team. '
|
||||
f'It may work flawlessly (please report back) or have serious issues. '
|
||||
f'Use it at your own discretion.')
|
||||
|
||||
return True
|
||||
4
freqtrade/configuration/__init__.py
Normal file
4
freqtrade/configuration/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from freqtrade.configuration.arguments import Arguments # noqa: F401
|
||||
from freqtrade.configuration.timerange import TimeRange # noqa: F401
|
||||
from freqtrade.configuration.configuration import Configuration # noqa: F401
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401
|
||||
157
freqtrade/configuration/arguments.py
Normal file
157
freqtrade/configuration/arguments.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
This module contains the argument manager class
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
|
||||
|
||||
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
|
||||
|
||||
ARGS_STRATEGY = ["strategy", "strategy_path"]
|
||||
|
||||
ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"]
|
||||
|
||||
ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
|
||||
"max_open_trades", "stake_amount"]
|
||||
|
||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||
"strategy_list", "export", "exportfilename"]
|
||||
|
||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||
"position_stacking", "epochs", "spaces",
|
||||
"use_max_market_positions", "print_all",
|
||||
"print_colorized", "print_json", "hyperopt_jobs",
|
||||
"hyperopt_random_state", "hyperopt_min_trades",
|
||||
"hyperopt_continue", "hyperopt_loss"]
|
||||
|
||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column"]
|
||||
|
||||
ARGS_CREATE_USERDIR = ["user_data_dir"]
|
||||
|
||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"]
|
||||
|
||||
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
|
||||
"trade_source", "export", "exportfilename", "timerange", "ticker_interval"]
|
||||
|
||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||
"trade_source", "ticker_interval"]
|
||||
|
||||
NO_CONF_REQURIED = ["create-userdir", "download-data", "plot-dataframe", "plot-profit"]
|
||||
|
||||
|
||||
class Arguments:
|
||||
"""
|
||||
Arguments Class. Manage the arguments received by the cli
|
||||
"""
|
||||
def __init__(self, args: Optional[List[str]]) -> None:
|
||||
self.args = args
|
||||
self._parsed_arg: Optional[argparse.Namespace] = None
|
||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||
|
||||
def _load_args(self) -> None:
|
||||
self._build_args(optionlist=ARGS_MAIN)
|
||||
self._build_subcommands()
|
||||
|
||||
def get_parsed_arg(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Return the list of arguments
|
||||
:return: List[str] List of arguments
|
||||
"""
|
||||
if self._parsed_arg is None:
|
||||
self._load_args()
|
||||
self._parsed_arg = self._parse_args()
|
||||
|
||||
return vars(self._parsed_arg)
|
||||
|
||||
def _parse_args(self) -> argparse.Namespace:
|
||||
"""
|
||||
Parses given arguments and returns an argparse Namespace instance.
|
||||
"""
|
||||
parsed_arg = self.parser.parse_args(self.args)
|
||||
|
||||
# When no config is provided, but a config exists, use that configuration!
|
||||
|
||||
# Workaround issue in argparse with action='append' and default value
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
if (parsed_arg.config is None and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or
|
||||
not ('subparser' in parsed_arg and parsed_arg.subparser in NO_CONF_REQURIED))):
|
||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
|
||||
def _build_args(self, optionlist, parser=None):
|
||||
parser = parser or self.parser
|
||||
|
||||
for val in optionlist:
|
||||
opt = AVAILABLE_CLI_OPTIONS[val]
|
||||
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
|
||||
|
||||
def _build_subcommands(self) -> None:
|
||||
"""
|
||||
Builds and attaches all subcommands.
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
|
||||
from freqtrade.utils import start_create_userdir, start_download_data, start_list_exchanges
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||
|
||||
# Add backtesting subcommand
|
||||
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.')
|
||||
backtesting_cmd.set_defaults(func=start_backtesting)
|
||||
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.')
|
||||
edge_cmd.set_defaults(func=start_edge)
|
||||
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
||||
|
||||
# Add hyperopt subcommand
|
||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.')
|
||||
hyperopt_cmd.set_defaults(func=start_hyperopt)
|
||||
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
|
||||
|
||||
# add create-userdir subcommand
|
||||
create_userdir_cmd = subparsers.add_parser('create-userdir',
|
||||
help="Create user-data directory.")
|
||||
create_userdir_cmd.set_defaults(func=start_create_userdir)
|
||||
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
|
||||
|
||||
# Add list-exchanges subcommand
|
||||
list_exchanges_cmd = subparsers.add_parser(
|
||||
'list-exchanges',
|
||||
help='Print available exchanges.'
|
||||
)
|
||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
||||
|
||||
# Add download-data subcommand
|
||||
download_data_cmd = subparsers.add_parser(
|
||||
'download-data',
|
||||
help='Download backtesting data.'
|
||||
)
|
||||
download_data_cmd.set_defaults(func=start_download_data)
|
||||
self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd)
|
||||
|
||||
# Add Plotting subcommand
|
||||
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
|
||||
plot_dataframe_cmd = subparsers.add_parser(
|
||||
'plot-dataframe',
|
||||
help='Plot candles with indicators.'
|
||||
)
|
||||
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
|
||||
self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd)
|
||||
|
||||
# Plot profit
|
||||
plot_profit_cmd = subparsers.add_parser(
|
||||
'plot-profit',
|
||||
help='Generate plot showing profits.'
|
||||
)
|
||||
plot_profit_cmd.set_defaults(func=start_plot_profit)
|
||||
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)
|
||||
60
freqtrade/configuration/check_exchange.py
Normal file
60
freqtrade/configuration/check_exchange.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason,
|
||||
is_exchange_available, is_exchange_bad,
|
||||
is_exchange_officially_supported)
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||
"""
|
||||
Check if the exchange name in the config file is supported by Freqtrade
|
||||
:param check_for_bad: if True, check the exchange against the list of known 'bad'
|
||||
exchanges
|
||||
:return: False if exchange is 'bad', i.e. is known to work with the bot with
|
||||
critical issues or does not work at all, crashes, etc. True otherwise.
|
||||
raises an exception if the exchange if not supported by ccxt
|
||||
and thus is not known for the Freqtrade at all.
|
||||
"""
|
||||
|
||||
if config['runmode'] in [RunMode.PLOT] and not config.get('exchange', {}).get('name'):
|
||||
# Skip checking exchange in plot mode, since it requires no exchange
|
||||
return True
|
||||
logger.info("Checking exchange...")
|
||||
|
||||
exchange = config.get('exchange', {}).get('name').lower()
|
||||
if not exchange:
|
||||
raise OperationalException(
|
||||
f'This command requires a configured exchange. You should either use '
|
||||
f'`--exchange <exchange_name>` or specify a configuration file via `--config`.\n'
|
||||
f'The following exchanges are supported by ccxt: '
|
||||
f'{", ".join(available_exchanges())}'
|
||||
)
|
||||
|
||||
if not is_exchange_available(exchange):
|
||||
raise OperationalException(
|
||||
f'Exchange "{exchange}" is not supported by ccxt '
|
||||
f'and therefore not available for the bot.\n'
|
||||
f'The following exchanges are supported by ccxt: '
|
||||
f'{", ".join(available_exchanges())}'
|
||||
)
|
||||
|
||||
if check_for_bad and is_exchange_bad(exchange):
|
||||
raise OperationalException(f'Exchange "{exchange}" is known to not work with the bot yet. '
|
||||
f'Reason: {get_exchange_bad_reason(exchange)}')
|
||||
|
||||
if is_exchange_officially_supported(exchange):
|
||||
logger.info(f'Exchange "{exchange}" is officially supported '
|
||||
f'by the Freqtrade development team.')
|
||||
else:
|
||||
logger.warning(f'Exchange "{exchange}" is supported by ccxt '
|
||||
f'and therefore available for the bot but not officially supported '
|
||||
f'by the Freqtrade development team. '
|
||||
f'It may work flawlessly (please report back) or have serious issues. '
|
||||
f'Use it at your own discretion.')
|
||||
|
||||
return True
|
||||
314
freqtrade/configuration/cli_options.py
Normal file
314
freqtrade/configuration/cli_options.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
Definition of cli arguments used in arguments.py
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
|
||||
|
||||
def check_int_positive(value: str) -> int:
|
||||
try:
|
||||
uint = int(value)
|
||||
if uint <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"{value} is invalid for this parameter, should be a positive integer value"
|
||||
)
|
||||
return uint
|
||||
|
||||
|
||||
class Arg:
|
||||
# Optional CLI arguments
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cli = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
# List of available command line options
|
||||
AVAILABLE_CLI_OPTIONS = {
|
||||
# Common options
|
||||
"verbosity": Arg(
|
||||
'-v', '--verbose',
|
||||
help='Verbose mode (-vv for more, -vvv to get all messages).',
|
||||
action='count',
|
||||
default=0,
|
||||
),
|
||||
"logfile": Arg(
|
||||
'--logfile',
|
||||
help='Log to the file specified.',
|
||||
metavar='FILE',
|
||||
),
|
||||
"version": Arg(
|
||||
'-V', '--version',
|
||||
action='version',
|
||||
version=f'%(prog)s {__version__}',
|
||||
),
|
||||
"config": Arg(
|
||||
'-c', '--config',
|
||||
help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). '
|
||||
f'Multiple --config options may be used. '
|
||||
f'Can be set to `-` to read config from stdin.',
|
||||
action='append',
|
||||
metavar='PATH',
|
||||
),
|
||||
"datadir": Arg(
|
||||
'-d', '--datadir',
|
||||
help='Path to directory with historical backtesting data.',
|
||||
metavar='PATH',
|
||||
),
|
||||
"user_data_dir": Arg(
|
||||
'--userdir', '--user-data-dir',
|
||||
help='Path to userdata directory.',
|
||||
metavar='PATH',
|
||||
),
|
||||
# Main options
|
||||
"strategy": Arg(
|
||||
'-s', '--strategy',
|
||||
help='Specify strategy class name (default: `%(default)s`).',
|
||||
metavar='NAME',
|
||||
default='DefaultStrategy',
|
||||
),
|
||||
"strategy_path": Arg(
|
||||
'--strategy-path',
|
||||
help='Specify additional strategy lookup path.',
|
||||
metavar='PATH',
|
||||
),
|
||||
"db_url": Arg(
|
||||
'--db-url',
|
||||
help=f'Override trades database URL, this is useful in custom deployments '
|
||||
f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, '
|
||||
f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).',
|
||||
metavar='PATH',
|
||||
),
|
||||
"sd_notify": Arg(
|
||||
'--sd-notify',
|
||||
help='Notify systemd service manager.',
|
||||
action='store_true',
|
||||
),
|
||||
# Optimize common
|
||||
"ticker_interval": Arg(
|
||||
'-i', '--ticker-interval',
|
||||
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||
),
|
||||
"timerange": Arg(
|
||||
'--timerange',
|
||||
help='Specify what timerange of data to use.',
|
||||
),
|
||||
"max_open_trades": Arg(
|
||||
'--max_open_trades',
|
||||
help='Specify max_open_trades to use.',
|
||||
type=int,
|
||||
metavar='INT',
|
||||
),
|
||||
"stake_amount": Arg(
|
||||
'--stake_amount',
|
||||
help='Specify stake_amount.',
|
||||
type=float,
|
||||
),
|
||||
# Backtesting
|
||||
"position_stacking": Arg(
|
||||
'--eps', '--enable-position-stacking',
|
||||
help='Allow buying the same pair multiple times (position stacking).',
|
||||
action='store_true',
|
||||
default=False,
|
||||
),
|
||||
"use_max_market_positions": Arg(
|
||||
'--dmmp', '--disable-max-market-positions',
|
||||
help='Disable applying `max_open_trades` during backtest '
|
||||
'(same as setting `max_open_trades` to a very high number).',
|
||||
action='store_false',
|
||||
default=True,
|
||||
),
|
||||
"strategy_list": Arg(
|
||||
'--strategy-list',
|
||||
help='Provide a space-separated list of strategies to backtest. '
|
||||
'Please note that ticker-interval needs to be set either in config '
|
||||
'or via command line. When using this together with `--export trades`, '
|
||||
'the strategy-name is injected into the filename '
|
||||
'(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`',
|
||||
nargs='+',
|
||||
),
|
||||
"export": Arg(
|
||||
'--export',
|
||||
help='Export backtest results, argument are: trades. '
|
||||
'Example: `--export=trades`',
|
||||
),
|
||||
"exportfilename": Arg(
|
||||
'--export-filename',
|
||||
help='Save backtest results to the file with this filename (default: `%(default)s`). '
|
||||
'Requires `--export` to be set as well. '
|
||||
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
|
||||
metavar='PATH',
|
||||
default=os.path.join('user_data', 'backtest_results',
|
||||
'backtest-result.json'),
|
||||
),
|
||||
# Edge
|
||||
"stoploss_range": Arg(
|
||||
'--stoplosses',
|
||||
help='Defines a range of stoploss values against which edge will assess the strategy. '
|
||||
'The format is "min,max,step" (without any space). '
|
||||
'Example: `--stoplosses=-0.01,-0.1,-0.001`',
|
||||
),
|
||||
# Hyperopt
|
||||
"hyperopt": Arg(
|
||||
'--customhyperopt',
|
||||
help='Specify hyperopt class name (default: `%(default)s`).',
|
||||
metavar='NAME',
|
||||
default=constants.DEFAULT_HYPEROPT,
|
||||
),
|
||||
"hyperopt_path": Arg(
|
||||
'--hyperopt-path',
|
||||
help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.',
|
||||
metavar='PATH',
|
||||
),
|
||||
"epochs": Arg(
|
||||
'-e', '--epochs',
|
||||
help='Specify number of epochs (default: %(default)d).',
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
default=constants.HYPEROPT_EPOCH,
|
||||
),
|
||||
"spaces": Arg(
|
||||
'-s', '--spaces',
|
||||
help='Specify which parameters to hyperopt. Space-separated list. '
|
||||
'Default: `%(default)s`.',
|
||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
|
||||
nargs='+',
|
||||
default='all',
|
||||
),
|
||||
"print_all": Arg(
|
||||
'--print-all',
|
||||
help='Print all results, not only the best ones.',
|
||||
action='store_true',
|
||||
default=False,
|
||||
),
|
||||
"print_colorized": Arg(
|
||||
'--no-color',
|
||||
help='Disable colorization of hyperopt results. May be useful if you are '
|
||||
'redirecting output to a file.',
|
||||
action='store_false',
|
||||
default=True,
|
||||
),
|
||||
"print_json": Arg(
|
||||
'--print-json',
|
||||
help='Print best result detailization in JSON format.',
|
||||
action='store_true',
|
||||
default=False,
|
||||
),
|
||||
"hyperopt_jobs": Arg(
|
||||
'-j', '--job-workers',
|
||||
help='The number of concurrently running jobs for hyperoptimization '
|
||||
'(hyperopt worker processes). '
|
||||
'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. '
|
||||
'If 1 is given, no parallel computing code is used at all.',
|
||||
type=int,
|
||||
metavar='JOBS',
|
||||
default=-1,
|
||||
),
|
||||
"hyperopt_random_state": Arg(
|
||||
'--random-state',
|
||||
help='Set random state to some positive integer for reproducible hyperopt results.',
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
),
|
||||
"hyperopt_min_trades": Arg(
|
||||
'--min-trades',
|
||||
help="Set minimal desired number of trades for evaluations in the hyperopt "
|
||||
"optimization path (default: 1).",
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
default=1,
|
||||
),
|
||||
"hyperopt_continue": Arg(
|
||||
"--continue",
|
||||
help="Continue hyperopt from previous runs. "
|
||||
"By default, temporary files will be removed and hyperopt will start from scratch.",
|
||||
default=False,
|
||||
action='store_true',
|
||||
),
|
||||
"hyperopt_loss": Arg(
|
||||
'--hyperopt-loss',
|
||||
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
||||
'Different functions can generate completely different results, '
|
||||
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
|
||||
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.'
|
||||
'(default: `%(default)s`).',
|
||||
metavar='NAME',
|
||||
default=constants.DEFAULT_HYPEROPT_LOSS,
|
||||
),
|
||||
# List exchanges
|
||||
"print_one_column": Arg(
|
||||
'-1', '--one-column',
|
||||
help='Print exchanges in one column.',
|
||||
action='store_true',
|
||||
),
|
||||
# Script options
|
||||
"pairs": Arg(
|
||||
'-p', '--pairs',
|
||||
help='Show profits for only these pairs. Pairs are space-separated.',
|
||||
nargs='+',
|
||||
),
|
||||
# Download data
|
||||
"pairs_file": Arg(
|
||||
'--pairs-file',
|
||||
help='File containing a list of pairs to download.',
|
||||
metavar='FILE',
|
||||
),
|
||||
"days": Arg(
|
||||
'--days',
|
||||
help='Download data for given number of days.',
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
),
|
||||
"exchange": Arg(
|
||||
'--exchange',
|
||||
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
|
||||
f'Only valid if no config is provided.',
|
||||
),
|
||||
"timeframes": Arg(
|
||||
'-t', '--timeframes',
|
||||
help=f'Specify which tickers to download. Space-separated list. '
|
||||
f'Default: `1m 5m`.',
|
||||
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||
default=['1m', '5m'],
|
||||
nargs='+',
|
||||
),
|
||||
"erase": Arg(
|
||||
'--erase',
|
||||
help='Clean all existing data for the selected exchange/pairs/timeframes.',
|
||||
action='store_true',
|
||||
),
|
||||
# Plot dataframe
|
||||
"indicators1": Arg(
|
||||
'--indicators1',
|
||||
help='Set indicators from your strategy you want in the first row of the graph. '
|
||||
'Space-separated list. Example: `ema3 ema5`. Default: `%(default)s`.',
|
||||
default=['sma', 'ema3', 'ema5'],
|
||||
nargs='+',
|
||||
),
|
||||
"indicators2": Arg(
|
||||
'--indicators2',
|
||||
help='Set indicators from your strategy you want in the third row of the graph. '
|
||||
'Space-separated list. Example: `fastd fastk`. Default: `%(default)s`.',
|
||||
default=['macd', 'macdsignal'],
|
||||
nargs='+',
|
||||
),
|
||||
"plot_limit": Arg(
|
||||
'--plot-limit',
|
||||
help='Specify tick limit for plotting. Notice: too high values cause huge files. '
|
||||
'Default: %(default)s.',
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
default=750,
|
||||
),
|
||||
"trade_source": Arg(
|
||||
'--trade-source',
|
||||
help='Specify the source for trades (Can be DB or file (backtest file)) '
|
||||
'Default: %(default)s',
|
||||
choices=["DB", "file"],
|
||||
default="file",
|
||||
),
|
||||
}
|
||||
113
freqtrade/configuration/config_validation.py
Normal file
113
freqtrade/configuration/config_validation.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from jsonschema import Draft4Validator, validators
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
|
||||
from freqtrade import constants, OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _extend_validator(validator_class):
|
||||
"""
|
||||
Extended validator for the Freqtrade configuration JSON Schema.
|
||||
Currently it only handles defaults for subschemas.
|
||||
"""
|
||||
validate_properties = validator_class.VALIDATORS['properties']
|
||||
|
||||
def set_defaults(validator, properties, instance, schema):
|
||||
for prop, subschema in properties.items():
|
||||
if 'default' in subschema:
|
||||
instance.setdefault(prop, subschema['default'])
|
||||
|
||||
for error in validate_properties(
|
||||
validator, properties, instance, schema,
|
||||
):
|
||||
yield error
|
||||
|
||||
return validators.extend(
|
||||
validator_class, {'properties': set_defaults}
|
||||
)
|
||||
|
||||
|
||||
FreqtradeValidator = _extend_validator(Draft4Validator)
|
||||
|
||||
|
||||
def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate the configuration follow the Config Schema
|
||||
:param conf: Config in JSON format
|
||||
:return: Returns the config if valid, otherwise throw an exception
|
||||
"""
|
||||
try:
|
||||
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf)
|
||||
return conf
|
||||
except ValidationError as e:
|
||||
logger.critical(
|
||||
f"Invalid configuration. See config.json.example. Reason: {e}"
|
||||
)
|
||||
raise ValidationError(
|
||||
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
|
||||
)
|
||||
|
||||
|
||||
def validate_config_consistency(conf: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Validate the configuration consistency.
|
||||
Should be ran after loading both configuration and strategy,
|
||||
since strategies can set certain configuration settings too.
|
||||
:param conf: Config in JSON format
|
||||
:return: Returns None if everything is ok, otherwise throw an OperationalException
|
||||
"""
|
||||
# validating trailing stoploss
|
||||
_validate_trailing_stoploss(conf)
|
||||
_validate_edge(conf)
|
||||
|
||||
|
||||
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
||||
|
||||
if conf.get('stoploss') == 0.0:
|
||||
raise OperationalException(
|
||||
'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
|
||||
)
|
||||
# Skip if trailing stoploss is not activated
|
||||
if not conf.get('trailing_stop', False):
|
||||
return
|
||||
|
||||
tsl_positive = float(conf.get('trailing_stop_positive', 0))
|
||||
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0))
|
||||
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False)
|
||||
|
||||
if tsl_only_offset:
|
||||
if tsl_positive == 0.0:
|
||||
raise OperationalException(
|
||||
'The config trailing_only_offset_is_reached needs '
|
||||
'trailing_stop_positive_offset to be more than 0 in your config.')
|
||||
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
|
||||
raise OperationalException(
|
||||
'The config trailing_stop_positive_offset needs '
|
||||
'to be greater than trailing_stop_positive in your config.')
|
||||
|
||||
# Fetch again without default
|
||||
if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0:
|
||||
raise OperationalException(
|
||||
'The config trailing_stop_positive needs to be different from 0 '
|
||||
'to avoid problems with sell orders.'
|
||||
)
|
||||
|
||||
|
||||
def _validate_edge(conf: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
|
||||
"""
|
||||
|
||||
if not conf.get('edge', {}).get('enabled'):
|
||||
return
|
||||
|
||||
if conf.get('pairlist', {}).get('method') == 'VolumePairList':
|
||||
raise OperationalException(
|
||||
"Edge and VolumePairList are incompatible, "
|
||||
"Edge will override whatever pairs VolumePairlist selects."
|
||||
)
|
||||
370
freqtrade/configuration/configuration.py
Normal file
370
freqtrade/configuration/configuration.py
Normal file
@@ -0,0 +1,370 @@
|
||||
"""
|
||||
This module contains the configuration class
|
||||
"""
|
||||
import logging
|
||||
import warnings
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.configuration.check_exchange import check_exchange
|
||||
from freqtrade.configuration.config_validation import (
|
||||
validate_config_consistency, validate_config_schema)
|
||||
from freqtrade.configuration.directory_operations import (create_datadir,
|
||||
create_userdata_dir)
|
||||
from freqtrade.configuration.load_config import load_config_file
|
||||
from freqtrade.loggers import setup_logging
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Class to read and init the bot configuration
|
||||
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||
"""
|
||||
|
||||
def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None:
|
||||
self.args = args
|
||||
self.config: Optional[Dict[str, Any]] = None
|
||||
self.runmode = runmode
|
||||
|
||||
def get_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Return the config. Use this method to get the bot config
|
||||
:return: Dict: Bot config
|
||||
"""
|
||||
if self.config is None:
|
||||
self.config = self.load_config()
|
||||
|
||||
return self.config
|
||||
|
||||
@staticmethod
|
||||
def from_files(files: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Iterate through the config files passed in, loading all of them
|
||||
and merging their contents.
|
||||
Files are loaded in sequence, parameters in later configuration files
|
||||
override the same parameter from an earlier file (last definition wins).
|
||||
Runs through the whole Configuration initialization, so all expected config entries
|
||||
are available to interactive environments.
|
||||
:param files: List of file paths
|
||||
:return: configuration dictionary
|
||||
"""
|
||||
c = Configuration({"config": files}, RunMode.OTHER)
|
||||
return c.get_config()
|
||||
|
||||
def load_from_files(self, files: List[str]) -> Dict[str, Any]:
|
||||
|
||||
# Keep this method as staticmethod, so it can be used from interactive environments
|
||||
config: Dict[str, Any] = {}
|
||||
|
||||
if not files:
|
||||
return deepcopy(constants.MINIMAL_CONFIG)
|
||||
|
||||
# We expect here a list of config filenames
|
||||
for path in files:
|
||||
logger.info(f'Using config: {path} ...')
|
||||
|
||||
# Merge config options, overwriting old values
|
||||
config = deep_merge_dicts(load_config_file(path), config)
|
||||
|
||||
# Normalize config
|
||||
if 'internals' not in config:
|
||||
config['internals'] = {}
|
||||
|
||||
# validate configuration before returning
|
||||
logger.info('Validating configuration ...')
|
||||
validate_config_schema(config)
|
||||
|
||||
return config
|
||||
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract information for sys.argv and load the bot configuration
|
||||
:return: Configuration dictionary
|
||||
"""
|
||||
# Load all configs
|
||||
config: Dict[str, Any] = self.load_from_files(self.args["config"])
|
||||
|
||||
# Keep a copy of the original configuration file
|
||||
config['original_config'] = deepcopy(config)
|
||||
|
||||
self._process_common_options(config)
|
||||
|
||||
self._process_optimize_options(config)
|
||||
|
||||
self._process_plot_options(config)
|
||||
|
||||
self._process_runmode(config)
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
||||
|
||||
self._resolve_pairs_list(config)
|
||||
|
||||
validate_config_consistency(config)
|
||||
|
||||
return config
|
||||
|
||||
def _process_logging_options(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Extract information for sys.argv and load logging configuration:
|
||||
the -v/--verbose, --logfile options
|
||||
"""
|
||||
# Log level
|
||||
config.update({'verbosity': self.args.get("verbosity", 0)})
|
||||
|
||||
if 'logfile' in self.args and self.args["logfile"]:
|
||||
config.update({'logfile': self.args["logfile"]})
|
||||
|
||||
setup_logging(config)
|
||||
|
||||
def _process_common_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
self._process_logging_options(config)
|
||||
|
||||
# Set strategy if not specified in config and or if it's non default
|
||||
if self.args.get("strategy") != constants.DEFAULT_STRATEGY or not config.get('strategy'):
|
||||
config.update({'strategy': self.args.get("strategy")})
|
||||
|
||||
self._args_to_config(config, argname='strategy_path',
|
||||
logstring='Using additional Strategy lookup path: {}')
|
||||
|
||||
if ('db_url' in self.args and self.args["db_url"] and
|
||||
self.args["db_url"] != constants.DEFAULT_DB_PROD_URL):
|
||||
config.update({'db_url': self.args["db_url"]})
|
||||
logger.info('Parameter --db-url detected ...')
|
||||
|
||||
if config.get('dry_run', False):
|
||||
logger.info('Dry run is enabled')
|
||||
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
|
||||
# Default to in-memory db for dry_run if not specified
|
||||
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||
else:
|
||||
if not config.get('db_url', None):
|
||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||
logger.info('Dry run is disabled')
|
||||
|
||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||
|
||||
if config.get('forcebuy_enable', False):
|
||||
logger.warning('`forcebuy` RPC message enabled.')
|
||||
|
||||
# Setting max_open_trades to infinite if -1
|
||||
if config.get('max_open_trades') == -1:
|
||||
config['max_open_trades'] = float('inf')
|
||||
|
||||
# Support for sd_notify
|
||||
if 'sd_notify' in self.args and self.args["sd_notify"]:
|
||||
config['internals'].update({'sd_notify': True})
|
||||
|
||||
def _process_datadir_options(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Extract information for sys.argv and load directory configurations
|
||||
--user-data, --datadir
|
||||
"""
|
||||
# Check exchange parameter here - otherwise `datadir` might be wrong.
|
||||
if "exchange" in self.args and self.args["exchange"]:
|
||||
config['exchange']['name'] = self.args["exchange"]
|
||||
logger.info(f"Using exchange {config['exchange']['name']}")
|
||||
|
||||
if 'user_data_dir' in self.args and self.args["user_data_dir"]:
|
||||
config.update({'user_data_dir': self.args["user_data_dir"]})
|
||||
elif 'user_data_dir' not in config:
|
||||
# Default to cwd/user_data (legacy option ...)
|
||||
config.update({'user_data_dir': str(Path.cwd() / "user_data")})
|
||||
|
||||
# reset to user_data_dir so this contains the absolute path.
|
||||
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
|
||||
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
|
||||
|
||||
config.update({'datadir': create_datadir(config, self.args.get("datadir", None))})
|
||||
logger.info('Using data directory: %s ...', config.get('datadir'))
|
||||
|
||||
def _process_optimize_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
# This will override the strategy configuration
|
||||
self._args_to_config(config, argname='ticker_interval',
|
||||
logstring='Parameter -i/--ticker-interval detected ... '
|
||||
'Using ticker_interval: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='position_stacking',
|
||||
logstring='Parameter --enable-position-stacking detected ...')
|
||||
|
||||
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
|
||||
config.update({'use_max_market_positions': False})
|
||||
logger.info('Parameter --disable-max-market-positions detected ...')
|
||||
logger.info('max_open_trades set to unlimited ...')
|
||||
elif 'max_open_trades' in self.args and self.args["max_open_trades"]:
|
||||
config.update({'max_open_trades': self.args["max_open_trades"]})
|
||||
logger.info('Parameter --max_open_trades detected, '
|
||||
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
|
||||
else:
|
||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||
|
||||
self._args_to_config(config, argname='stake_amount',
|
||||
logstring='Parameter --stake_amount detected, '
|
||||
'overriding stake_amount to: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='timerange',
|
||||
logstring='Parameter --timerange detected: {} ...')
|
||||
|
||||
self._process_datadir_options(config)
|
||||
|
||||
self._args_to_config(config, argname='strategy_list',
|
||||
logstring='Using strategy list of {} strategies', logfun=len)
|
||||
|
||||
self._args_to_config(config, argname='ticker_interval',
|
||||
logstring='Overriding ticker interval with Command line argument')
|
||||
|
||||
self._args_to_config(config, argname='export',
|
||||
logstring='Parameter --export detected: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='exportfilename',
|
||||
logstring='Storing backtest results to {} ...')
|
||||
|
||||
# Edge section:
|
||||
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
|
||||
txt_range = eval(self.args["stoploss_range"])
|
||||
config['edge'].update({'stoploss_range_min': txt_range[0]})
|
||||
config['edge'].update({'stoploss_range_max': txt_range[1]})
|
||||
config['edge'].update({'stoploss_range_step': txt_range[2]})
|
||||
logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"])
|
||||
|
||||
# Hyperopt section
|
||||
self._args_to_config(config, argname='hyperopt',
|
||||
logstring='Using Hyperopt class name: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_path',
|
||||
logstring='Using additional Hyperopt lookup path: {}')
|
||||
|
||||
self._args_to_config(config, argname='epochs',
|
||||
logstring='Parameter --epochs detected ... '
|
||||
'Will run Hyperopt with for {} epochs ...'
|
||||
)
|
||||
|
||||
self._args_to_config(config, argname='spaces',
|
||||
logstring='Parameter -s/--spaces 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"]:
|
||||
logger.info('Parameter --no-color detected ...')
|
||||
config.update({'print_colorized': False})
|
||||
else:
|
||||
config.update({'print_colorized': True})
|
||||
|
||||
self._args_to_config(config, argname='print_json',
|
||||
logstring='Parameter --print-json 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_continue',
|
||||
logstring='Hyperopt continue: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_loss',
|
||||
logstring='Using Hyperopt loss class name: {}')
|
||||
|
||||
def _process_plot_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
self._args_to_config(config, argname='pairs',
|
||||
logstring='Using pairs {}')
|
||||
|
||||
self._args_to_config(config, argname='indicators1',
|
||||
logstring='Using indicators1: {}')
|
||||
|
||||
self._args_to_config(config, argname='indicators2',
|
||||
logstring='Using indicators2: {}')
|
||||
|
||||
self._args_to_config(config, argname='plot_limit',
|
||||
logstring='Limiting plot to: {}')
|
||||
self._args_to_config(config, argname='trade_source',
|
||||
logstring='Using trades from: {}')
|
||||
|
||||
self._args_to_config(config, argname='erase',
|
||||
logstring='Erase detected. Deleting existing data.')
|
||||
|
||||
self._args_to_config(config, argname='timeframes',
|
||||
logstring='timeframes --timeframes: {}')
|
||||
|
||||
self._args_to_config(config, argname='days',
|
||||
logstring='Detected --days: {}')
|
||||
|
||||
def _process_runmode(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
if not self.runmode:
|
||||
# Handle real mode, infer dry/live from config
|
||||
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
|
||||
logger.info(f"Runmode set to {self.runmode}.")
|
||||
|
||||
config.update({'runmode': self.runmode})
|
||||
|
||||
def _args_to_config(self, config: Dict[str, Any], argname: str,
|
||||
logstring: str, logfun: Optional[Callable] = None,
|
||||
deprecated_msg: Optional[str] = None) -> None:
|
||||
"""
|
||||
:param config: Configuration dictionary
|
||||
:param argname: Argumentname in self.args - will be copied to config dict.
|
||||
:param logstring: Logging String
|
||||
:param logfun: logfun is applied to the configuration entry before passing
|
||||
that entry to the log string using .format().
|
||||
sample: logfun=len (prints the length of the found
|
||||
configuration instead of the content)
|
||||
"""
|
||||
if argname in self.args and self.args[argname]:
|
||||
|
||||
config.update({argname: self.args[argname]})
|
||||
if logfun:
|
||||
logger.info(logstring.format(logfun(config[argname])))
|
||||
else:
|
||||
logger.info(logstring.format(config[argname]))
|
||||
if deprecated_msg:
|
||||
warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning)
|
||||
|
||||
def _resolve_pairs_list(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Helper for download script.
|
||||
Takes first found:
|
||||
* -p (pairs argument)
|
||||
* --pairs-file
|
||||
* whitelist from config
|
||||
"""
|
||||
|
||||
if "pairs" in config:
|
||||
return
|
||||
|
||||
if "pairs_file" in self.args and self.args["pairs_file"]:
|
||||
pairs_file = Path(self.args["pairs_file"])
|
||||
logger.info(f'Reading pairs file "{pairs_file}".')
|
||||
# Download pairs from the pairs file if no config is specified
|
||||
# or if pairs file is specified explicitely
|
||||
if not pairs_file.exists():
|
||||
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
|
||||
with pairs_file.open('r') as f:
|
||||
config['pairs'] = json_load(f)
|
||||
config['pairs'].sort()
|
||||
return
|
||||
|
||||
if "config" in self.args and self.args["config"]:
|
||||
logger.info("Using pairlist from configuration.")
|
||||
config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
|
||||
else:
|
||||
# Fall back to /dl_path/pairs.json
|
||||
pairs_file = Path(config['datadir']) / "pairs.json"
|
||||
if pairs_file.exists():
|
||||
with pairs_file.open('r') as f:
|
||||
config['pairs'] = json_load(f)
|
||||
if 'pairs' in config:
|
||||
config['pairs'].sort()
|
||||
50
freqtrade/configuration/directory_operations.py
Normal file
50
freqtrade/configuration/directory_operations.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
from pathlib import Path
|
||||
|
||||
from freqtrade import OperationalException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str:
|
||||
|
||||
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
|
||||
if not datadir:
|
||||
# set datadir
|
||||
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||
folder = folder.joinpath(exchange_name)
|
||||
|
||||
if not folder.is_dir():
|
||||
folder.mkdir(parents=True)
|
||||
logger.info(f'Created data directory: {datadir}')
|
||||
return str(folder)
|
||||
|
||||
|
||||
def create_userdata_dir(directory: str, create_dir=False) -> Path:
|
||||
"""
|
||||
Create userdata directory structure.
|
||||
if create_dir is True, then the parent-directory will be created if it does not exist.
|
||||
Sub-directories will always be created if the parent directory exists.
|
||||
Raises OperationalException if given a non-existing directory.
|
||||
:param directory: Directory to check
|
||||
:param create_dir: Create directory if it does not exist.
|
||||
:return: Path object containing the directory
|
||||
"""
|
||||
sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "plot", "strategies", ]
|
||||
folder = Path(directory)
|
||||
if not folder.is_dir():
|
||||
if create_dir:
|
||||
folder.mkdir(parents=True)
|
||||
logger.info(f'Created user-data directory: {folder}')
|
||||
else:
|
||||
raise OperationalException(
|
||||
f"Directory `{folder}` does not exist. "
|
||||
"Please use `freqtrade create-userdir` to create a user directory")
|
||||
|
||||
# Create required subdirectories
|
||||
for f in sub_dirs:
|
||||
subfolder = folder / f
|
||||
if not subfolder.is_dir():
|
||||
subfolder.mkdir(parents=False)
|
||||
return folder
|
||||
33
freqtrade/configuration/load_config.py
Normal file
33
freqtrade/configuration/load_config.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
This module contain functions to load the configuration file
|
||||
"""
|
||||
import rapidjson
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS
|
||||
|
||||
|
||||
def load_config_file(path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Loads a config file from the given path
|
||||
:param path: path as str
|
||||
:return: configuration as dictionary
|
||||
"""
|
||||
try:
|
||||
# Read config from stdin if requested in the options
|
||||
with open(path) if path != '-' else sys.stdin as file:
|
||||
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||
except FileNotFoundError:
|
||||
raise OperationalException(
|
||||
f'Config file "{path}" not found!'
|
||||
' Please create a config file or check whether it exists.')
|
||||
|
||||
return config
|
||||
70
freqtrade/configuration/timerange.py
Normal file
70
freqtrade/configuration/timerange.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
This module contains the argument manager class
|
||||
"""
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import arrow
|
||||
|
||||
|
||||
class TimeRange:
|
||||
"""
|
||||
object defining timerange inputs.
|
||||
[start/stop]type defines if [start/stop]ts shall be used.
|
||||
if *type is None, don't use corresponding startvalue.
|
||||
"""
|
||||
|
||||
def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None,
|
||||
startts: int = 0, stopts: int = 0):
|
||||
|
||||
self.starttype: Optional[str] = starttype
|
||||
self.stoptype: Optional[str] = stoptype
|
||||
self.startts: int = startts
|
||||
self.stopts: int = stopts
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Override the default Equals behavior"""
|
||||
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
||||
and self.startts == other.startts and self.stopts == other.stopts)
|
||||
|
||||
@staticmethod
|
||||
def parse_timerange(text: Optional[str]):
|
||||
"""
|
||||
Parse the value of the argument --timerange to determine what is the range desired
|
||||
:param text: value from --timerange
|
||||
:return: Start and End range period
|
||||
"""
|
||||
if text is None:
|
||||
return TimeRange(None, None, 0, 0)
|
||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||
(r'^(\d{8})-$', ('date', None)),
|
||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||
(r'^-(\d{10})$', (None, 'date')),
|
||||
(r'^(\d{10})-$', ('date', None)),
|
||||
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
|
||||
(r'^(-\d+)$', (None, 'line')),
|
||||
(r'^(\d+)-$', ('line', None)),
|
||||
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
||||
for rex, stype in syntax:
|
||||
# Apply the regular expression to text
|
||||
match = re.match(rex, text)
|
||||
if match: # Regex has matched
|
||||
rvals = match.groups()
|
||||
index = 0
|
||||
start: int = 0
|
||||
stop: int = 0
|
||||
if stype[0]:
|
||||
starts = rvals[index]
|
||||
if stype[0] == 'date' and len(starts) == 8:
|
||||
start = arrow.get(starts, 'YYYYMMDD').timestamp
|
||||
else:
|
||||
start = int(starts)
|
||||
index += 1
|
||||
if stype[1]:
|
||||
stops = rvals[index]
|
||||
if stype[1] == 'date' and len(stops) == 8:
|
||||
stop = arrow.get(stops, 'YYYYMMDD').timestamp
|
||||
else:
|
||||
stop = int(stops)
|
||||
return TimeRange(stype[0], stype[1], start, stop)
|
||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||
@@ -5,13 +5,13 @@ bot constants
|
||||
"""
|
||||
DEFAULT_CONFIG = 'config.json'
|
||||
DEFAULT_EXCHANGE = 'bittrex'
|
||||
DYNAMIC_WHITELIST = 20 # pairs
|
||||
PROCESS_THROTTLE_SECS = 5 # sec
|
||||
DEFAULT_TICKER_INTERVAL = 5 # min
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
DEFAULT_STRATEGY = 'DefaultStrategy'
|
||||
DEFAULT_HYPEROPT = 'DefaultHyperOpts'
|
||||
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
|
||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||
@@ -22,7 +22,7 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
||||
DRY_RUN_WALLET = 999.9
|
||||
DEFAULT_DOWNLOAD_TICKER_INTERVALS = '1m 5m'
|
||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||
|
||||
TICKER_INTERVALS = [
|
||||
'1m', '3m', '5m', '15m', '30m',
|
||||
@@ -38,6 +38,20 @@ SUPPORTED_FIAT = [
|
||||
"BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||
]
|
||||
|
||||
MINIMAL_CONFIG = {
|
||||
'stake_currency': '',
|
||||
'dry_run': True,
|
||||
'exchange': {
|
||||
'name': '',
|
||||
'key': '',
|
||||
'secret': '',
|
||||
'pair_whitelist': [],
|
||||
'ccxt_async_config': {
|
||||
'enableRateLimit': True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Required json-schema for user specified config
|
||||
CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
@@ -108,6 +122,7 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss_on_exchange': {'type': 'boolean'},
|
||||
'stoploss_on_exchange_interval': {'type': 'number'}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Module to handle data operations for freqtrade
|
||||
"""
|
||||
|
||||
# limit what's imported when using `from freqtrad.data import *``
|
||||
# limit what's imported when using `from freqtrade.data import *`
|
||||
__all__ = [
|
||||
'converter'
|
||||
]
|
||||
|
||||
@@ -3,6 +3,7 @@ Helpers when analyzing backtest data
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -29,7 +30,7 @@ def load_backtest_data(filename) -> pd.DataFrame:
|
||||
filename = Path(filename)
|
||||
|
||||
if not filename.is_file():
|
||||
raise ValueError("File {filename} does not exist.")
|
||||
raise ValueError(f"File {filename} does not exist.")
|
||||
|
||||
with filename.open() as file:
|
||||
data = json_load(file)
|
||||
@@ -66,7 +67,6 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int
|
||||
dates = pd.Series(pd.concat(dates).values, name='date')
|
||||
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
|
||||
|
||||
df2 = df2.astype(dtype={"open_time": "datetime64", "close_time": "datetime64"})
|
||||
df2 = pd.concat([dates, df2], axis=1)
|
||||
df2 = df2.set_index('date')
|
||||
df_final = df2.resample(freq)[['pair']].count()
|
||||
@@ -81,19 +81,30 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
||||
"""
|
||||
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
|
||||
persistence.init(db_url, clean_open_orders=False)
|
||||
columns = ["pair", "profit", "open_time", "close_time",
|
||||
"open_rate", "close_rate", "duration", "sell_reason",
|
||||
"max_rate", "min_rate"]
|
||||
|
||||
trades = pd.DataFrame([(t.pair, t.calc_profit(),
|
||||
columns = ["pair", "open_time", "close_time", "profit", "profitperc",
|
||||
"open_rate", "close_rate", "amount", "duration", "sell_reason",
|
||||
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
|
||||
"stake_amount", "max_rate", "min_rate", "id", "exchange",
|
||||
"stop_loss", "initial_stop_loss", "strategy", "ticker_interval"]
|
||||
|
||||
trades = pd.DataFrame([(t.pair,
|
||||
t.open_date.replace(tzinfo=pytz.UTC),
|
||||
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None,
|
||||
t.open_rate, t.close_rate,
|
||||
t.close_date.timestamp() - t.open_date.timestamp()
|
||||
if t.close_date else None,
|
||||
t.calc_profit(), t.calc_profit_percent(),
|
||||
t.open_rate, t.close_rate, t.amount,
|
||||
(t.close_date.timestamp() - t.open_date.timestamp()
|
||||
if t.close_date else None),
|
||||
t.sell_reason,
|
||||
t.fee_open, t.fee_close,
|
||||
t.open_rate_requested,
|
||||
t.close_rate_requested,
|
||||
t.stake_amount,
|
||||
t.max_rate,
|
||||
t.min_rate,
|
||||
t.id, t.exchange,
|
||||
t.stop_loss, t.initial_stop_loss,
|
||||
t.strategy, t.ticker_interval
|
||||
)
|
||||
for t in Trade.query.all()],
|
||||
columns=columns)
|
||||
@@ -101,6 +112,18 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
||||
return trades
|
||||
|
||||
|
||||
def load_trades(source: str, db_url: str, exportfilename: str) -> pd.DataFrame:
|
||||
"""
|
||||
Based on configuration option "trade_source":
|
||||
* loads data from DB (using `db_url`)
|
||||
* loads data from backtestfile (using `exportfilename`)
|
||||
"""
|
||||
if source == "DB":
|
||||
return load_trades_from_db(db_url)
|
||||
elif source == "file":
|
||||
return load_backtest_data(Path(exportfilename))
|
||||
|
||||
|
||||
def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Compare trades and backtested pair DataFrames to get trades performed on backtested period
|
||||
@@ -109,3 +132,35 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p
|
||||
trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) &
|
||||
(trades['close_time'] <= dataframe.iloc[-1]['date'])]
|
||||
return trades
|
||||
|
||||
|
||||
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "close"):
|
||||
"""
|
||||
Combine multiple dataframes "column"
|
||||
:param tickers: Dict of Dataframes, dict key should be pair.
|
||||
:param column: Column in the original dataframes to use
|
||||
:return: DataFrame with the column renamed to the dict key, and a column
|
||||
named mean, containing the mean of all pairs.
|
||||
"""
|
||||
df_comb = pd.concat([tickers[pair].set_index('date').rename(
|
||||
{column: pair}, axis=1)[pair] for pair in tickers], axis=1)
|
||||
|
||||
df_comb['mean'] = df_comb.mean(axis=1)
|
||||
|
||||
return df_comb
|
||||
|
||||
|
||||
def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame:
|
||||
"""
|
||||
Adds a column `col_name` with the cumulative profit for the given trades array.
|
||||
:param df: DataFrame with date index
|
||||
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
|
||||
:return: Returns df with one additional column, col_name, containing the cumulative profit.
|
||||
"""
|
||||
# Use groupby/sum().cumsum() to avoid errors when multiple trades sold at the same candle.
|
||||
df[col_name] = trades.groupby('close_time')['profitperc'].sum().cumsum()
|
||||
# Set first value to 0
|
||||
df.loc[df.iloc[0].name, col_name] = 0
|
||||
# FFill to get continuous
|
||||
df[col_name] = df[col_name].ffill()
|
||||
return df
|
||||
|
||||
@@ -17,7 +17,7 @@ from freqtrade.state import RunMode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataProvider(object):
|
||||
class DataProvider:
|
||||
|
||||
def __init__(self, config: dict, exchange: Exchange) -> None:
|
||||
self._config = config
|
||||
@@ -44,36 +44,47 @@ class DataProvider(object):
|
||||
|
||||
def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame:
|
||||
"""
|
||||
get ohlcv data for the given pair as DataFrame
|
||||
Please check `available_pairs` to verify which pairs are currently cached.
|
||||
Get ohlcv data for the given pair as DataFrame
|
||||
Please use the `available_pairs` method to verify which pairs are currently cached.
|
||||
:param pair: pair to get the data for
|
||||
:param ticker_interval: ticker_interval to get pair for
|
||||
:param copy: copy dataframe before returning.
|
||||
Use false only for RO operations (where the dataframe is not modified)
|
||||
:param ticker_interval: ticker interval to get data for
|
||||
:param copy: copy dataframe before returning if True.
|
||||
Use False only for read-only operations (where the dataframe is not modified)
|
||||
"""
|
||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||
if ticker_interval:
|
||||
pairtick = (pair, ticker_interval)
|
||||
else:
|
||||
pairtick = (pair, self._config['ticker_interval'])
|
||||
|
||||
return self._exchange.klines(pairtick, copy=copy)
|
||||
return self._exchange.klines((pair, ticker_interval or self._config['ticker_interval']),
|
||||
copy=copy)
|
||||
else:
|
||||
return DataFrame()
|
||||
|
||||
def historic_ohlcv(self, pair: str, ticker_interval: str) -> DataFrame:
|
||||
def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame:
|
||||
"""
|
||||
get stored historic ohlcv data
|
||||
Get stored historic ohlcv data
|
||||
:param pair: pair to get the data for
|
||||
:param ticker_interval: ticker_interval to get pair for
|
||||
:param ticker_interval: ticker interval to get data for
|
||||
"""
|
||||
return load_pair_history(pair=pair,
|
||||
ticker_interval=ticker_interval,
|
||||
refresh_pairs=False,
|
||||
datadir=Path(self._config['datadir']) if self._config.get(
|
||||
'datadir') else None
|
||||
ticker_interval=ticker_interval or self._config['ticker_interval'],
|
||||
datadir=Path(self._config['datadir'])
|
||||
)
|
||||
|
||||
def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame:
|
||||
"""
|
||||
Return pair ohlcv data, either live or cached historical -- depending
|
||||
on the runmode.
|
||||
:param pair: pair to get the data for
|
||||
:param ticker_interval: ticker interval to get data for
|
||||
"""
|
||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||
# Get live ohlcv data.
|
||||
data = self.ohlcv(pair=pair, ticker_interval=ticker_interval)
|
||||
else:
|
||||
# Get historic ohlcv data (cached on disk).
|
||||
data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval)
|
||||
if len(data) == 0:
|
||||
logger.warning(f"No data found for ({pair}, {ticker_interval}).")
|
||||
return data
|
||||
|
||||
def ticker(self, pair: str):
|
||||
"""
|
||||
Return last ticker data
|
||||
@@ -81,11 +92,14 @@ class DataProvider(object):
|
||||
# TODO: Implement me
|
||||
pass
|
||||
|
||||
def orderbook(self, pair: str, max: int):
|
||||
def orderbook(self, pair: str, maximum: int):
|
||||
"""
|
||||
return latest orderbook data
|
||||
:param pair: pair to get the data for
|
||||
:param maximum: Maximum number of orderbook entries to query
|
||||
:return: dict including bids/asks with a total of `maximum` entries.
|
||||
"""
|
||||
return self._exchange.get_order_book(pair, max)
|
||||
return self._exchange.get_order_book(pair, maximum)
|
||||
|
||||
@property
|
||||
def runmode(self) -> RunMode:
|
||||
|
||||
@@ -16,7 +16,7 @@ import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import OperationalException, misc
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.exchange import Exchange, timeframe_to_minutes
|
||||
|
||||
@@ -43,7 +43,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||
start_index += 1
|
||||
|
||||
if timerange.stoptype == 'line':
|
||||
start_index = len(tickerlist) + timerange.stopts
|
||||
start_index = max(len(tickerlist) + timerange.stopts, 0)
|
||||
if timerange.stoptype == 'index':
|
||||
stop_index = timerange.stopts
|
||||
elif timerange.stoptype == 'date':
|
||||
@@ -57,27 +57,34 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||
return tickerlist[start_index:stop_index]
|
||||
|
||||
|
||||
def load_tickerdata_file(
|
||||
datadir: Optional[Path], pair: str,
|
||||
ticker_interval: str,
|
||||
def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str,
|
||||
timerange: Optional[TimeRange] = None) -> Optional[list]:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
:return: tickerlist or None if unsuccesful
|
||||
:return: tickerlist or None if unsuccessful
|
||||
"""
|
||||
filename = pair_data_filename(datadir, pair, ticker_interval)
|
||||
pairdata = misc.file_load_json(filename)
|
||||
if not pairdata:
|
||||
return None
|
||||
return []
|
||||
|
||||
if timerange:
|
||||
pairdata = trim_tickerlist(pairdata, timerange)
|
||||
return pairdata
|
||||
|
||||
|
||||
def store_tickerdata_file(datadir: Path, pair: str,
|
||||
ticker_interval: str, data: list, is_zip: bool = False):
|
||||
"""
|
||||
Stores tickerdata to file
|
||||
"""
|
||||
filename = pair_data_filename(datadir, pair, ticker_interval)
|
||||
misc.file_dump_json(filename, data, is_zip=is_zip)
|
||||
|
||||
|
||||
def load_pair_history(pair: str,
|
||||
ticker_interval: str,
|
||||
datadir: Optional[Path],
|
||||
datadir: Path,
|
||||
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
||||
refresh_pairs: bool = False,
|
||||
exchange: Optional[Exchange] = None,
|
||||
@@ -122,37 +129,28 @@ def load_pair_history(pair: str,
|
||||
else:
|
||||
logger.warning(
|
||||
f'No history data for pair: "{pair}", interval: {ticker_interval}. '
|
||||
'Use --refresh-pairs-cached option or download_backtest_data.py '
|
||||
'script to download the data'
|
||||
'Use `freqtrade download-data` to download the data'
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def load_data(datadir: Optional[Path],
|
||||
def load_data(datadir: Path,
|
||||
ticker_interval: str,
|
||||
pairs: List[str],
|
||||
refresh_pairs: bool = False,
|
||||
exchange: Optional[Exchange] = None,
|
||||
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
||||
fill_up_missing: bool = True,
|
||||
live: bool = False
|
||||
) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Loads ticker history data for a list of pairs the given parameters
|
||||
Loads ticker history data for a list of pairs
|
||||
:return: dict(<pair>:<tickerlist>)
|
||||
TODO: refresh_pairs is still used by edge to keep the data uptodate.
|
||||
This should be replaced in the future. Instead, writing the current candles to disk
|
||||
from dataprovider should be implemented, as this would avoid loading ohlcv data twice.
|
||||
exchange and refresh_pairs are then not needed here nor in load_pair_history.
|
||||
"""
|
||||
result: Dict[str, DataFrame] = {}
|
||||
if live:
|
||||
if exchange:
|
||||
logger.info('Live: Downloading data for all defined pairs ...')
|
||||
exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs])
|
||||
result = {key[0]: value for key, value in exchange._klines.items() if value is not None}
|
||||
else:
|
||||
raise OperationalException(
|
||||
"Exchange needs to be initialized when using live data."
|
||||
)
|
||||
else:
|
||||
logger.info('Using local backtesting data ...')
|
||||
|
||||
for pair in pairs:
|
||||
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
||||
@@ -165,23 +163,20 @@ def load_data(datadir: Optional[Path],
|
||||
return result
|
||||
|
||||
|
||||
def make_testdata_path(datadir: Optional[Path]) -> Path:
|
||||
"""Return the path where testdata files are stored"""
|
||||
return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve()
|
||||
|
||||
|
||||
def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) -> Path:
|
||||
path = make_testdata_path(datadir)
|
||||
def pair_data_filename(datadir: Path, pair: str, ticker_interval: str) -> Path:
|
||||
pair_s = pair.replace("/", "_")
|
||||
filename = path.joinpath(f'{pair_s}-{ticker_interval}.json')
|
||||
filename = datadir.joinpath(f'{pair_s}-{ticker_interval}.json')
|
||||
return filename
|
||||
|
||||
|
||||
def load_cached_data_for_updating(filename: Path, ticker_interval: str,
|
||||
def load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str,
|
||||
timerange: Optional[TimeRange]) -> Tuple[List[Any],
|
||||
Optional[int]]:
|
||||
"""
|
||||
Load cached data and choose what part of the data should be updated
|
||||
Load cached data to download more data.
|
||||
If timerange is passed in, checks wether data from an before the stored data will be downloaded.
|
||||
If that's the case than what's available should be completely overwritten.
|
||||
Only used by download_pair_history().
|
||||
"""
|
||||
|
||||
since_ms = None
|
||||
@@ -195,9 +190,8 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str,
|
||||
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
|
||||
|
||||
# read the cached file
|
||||
if filename.is_file():
|
||||
with open(filename, "rt") as file:
|
||||
data = misc.json_load(file)
|
||||
# Intentionally don't pass timerange in - since we need to load the full dataset.
|
||||
data = load_tickerdata_file(datadir, pair, ticker_interval)
|
||||
# remove the last item, could be incomplete candle
|
||||
if data:
|
||||
data.pop()
|
||||
@@ -215,7 +209,7 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str,
|
||||
return (data, since_ms)
|
||||
|
||||
|
||||
def download_pair_history(datadir: Optional[Path],
|
||||
def download_pair_history(datadir: Path,
|
||||
exchange: Optional[Exchange],
|
||||
pair: str,
|
||||
ticker_interval: str = '5m',
|
||||
@@ -239,29 +233,28 @@ def download_pair_history(datadir: Optional[Path],
|
||||
)
|
||||
|
||||
try:
|
||||
filename = pair_data_filename(datadir, pair, ticker_interval)
|
||||
|
||||
logger.info(
|
||||
f'Download history data for pair: "{pair}", interval: {ticker_interval} '
|
||||
f'and store in {datadir}.'
|
||||
)
|
||||
|
||||
data, since_ms = load_cached_data_for_updating(filename, ticker_interval, timerange)
|
||||
data, since_ms = load_cached_data_for_updating(datadir, pair, ticker_interval, timerange)
|
||||
|
||||
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
|
||||
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
||||
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
new_data = exchange.get_history(pair=pair, ticker_interval=ticker_interval,
|
||||
new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval,
|
||||
since_ms=since_ms if since_ms
|
||||
else
|
||||
int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000)
|
||||
int(arrow.utcnow().shift(
|
||||
days=-30).float_timestamp) * 1000)
|
||||
data.extend(new_data)
|
||||
|
||||
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
|
||||
logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
|
||||
|
||||
misc.file_dump_json(filename, data)
|
||||
store_tickerdata_file(datadir, pair, ticker_interval, data=data)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -272,6 +265,35 @@ def download_pair_history(datadir: Optional[Path],
|
||||
return False
|
||||
|
||||
|
||||
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
|
||||
dl_path: Path, timerange: TimeRange,
|
||||
erase=False) -> List[str]:
|
||||
"""
|
||||
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
||||
Used by freqtrade download-data
|
||||
:return: Pairs not available
|
||||
"""
|
||||
pairs_not_available = []
|
||||
for pair in pairs:
|
||||
if pair not in exchange.markets:
|
||||
pairs_not_available.append(pair)
|
||||
logger.info(f"Skipping pair {pair}...")
|
||||
continue
|
||||
for ticker_interval in timeframes:
|
||||
|
||||
dl_file = pair_data_filename(dl_path, pair, ticker_interval)
|
||||
if erase and dl_file.exists():
|
||||
logger.info(
|
||||
f'Deleting existing data for pair {pair}, interval {ticker_interval}.')
|
||||
dl_file.unlink()
|
||||
|
||||
logger.info(f'Downloading pair {pair}, interval {ticker_interval}.')
|
||||
download_pair_history(datadir=dl_path, exchange=exchange,
|
||||
pair=pair, ticker_interval=str(ticker_interval),
|
||||
timerange=timerange)
|
||||
return pairs_not_available
|
||||
|
||||
|
||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||
"""
|
||||
Get the maximum timeframe for the given backtest data
|
||||
|
||||
@@ -10,8 +10,7 @@ import utils_find_1st as utf1st
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import constants, OperationalException
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.strategy.interface import SellType
|
||||
|
||||
@@ -29,7 +28,7 @@ class PairInfo(NamedTuple):
|
||||
avg_trade_duration: float
|
||||
|
||||
|
||||
class Edge():
|
||||
class Edge:
|
||||
"""
|
||||
Calculates Win Rate, Risk Reward Ratio, Expectancy
|
||||
against historical data for a give set of markets and a strategy
|
||||
@@ -76,7 +75,7 @@ class Edge():
|
||||
self._stoploss_range_step
|
||||
)
|
||||
|
||||
self._timerange: TimeRange = Arguments.parse_timerange("%s-" % arrow.now().shift(
|
||||
self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift(
|
||||
days=-1 * self._since_number_of_days).format('YYYYMMDD'))
|
||||
|
||||
self.fee = self.exchange.get_fee()
|
||||
@@ -94,7 +93,7 @@ class Edge():
|
||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||
|
||||
data = history.load_data(
|
||||
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||
datadir=Path(self.config['datadir']),
|
||||
pairs=pairs,
|
||||
ticker_interval=self.strategy.ticker_interval,
|
||||
refresh_pairs=self._refresh_pairs,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from freqtrade.exchange.exchange import Exchange # noqa: F401
|
||||
from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401
|
||||
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
|
||||
is_exchange_bad,
|
||||
is_exchange_available,
|
||||
is_exchange_officially_supported,
|
||||
available_exchanges)
|
||||
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
|
||||
timeframe_to_minutes,
|
||||
timeframe_to_msecs)
|
||||
timeframe_to_msecs,
|
||||
timeframe_to_next_date,
|
||||
timeframe_to_prev_date)
|
||||
from freqtrade.exchange.kraken import Kraken # noqa: F401
|
||||
from freqtrade.exchange.binance import Binance # noqa: F401
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -25,3 +29,55 @@ class Binance(Exchange):
|
||||
limit = min(list(filter(lambda x: limit <= x, limit_range)))
|
||||
|
||||
return super().get_order_book(pair, limit)
|
||||
|
||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
this stoploss-limit is binance-specific.
|
||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
||||
|
||||
"""
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
stop_price = self.symbol_price_prec(pair, stop_price)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price <= rate:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
|
||||
rate = self.symbol_price_prec(pair, rate)
|
||||
|
||||
order = self._api.create_order(pair, ordertype, 'sell',
|
||||
amount, rate, params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
# Errors:
|
||||
# `binance Order would trigger immediately.`
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from math import ceil, floor
|
||||
from random import randint
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
@@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
import arrow
|
||||
import ccxt
|
||||
import ccxt.async_support as ccxt_async
|
||||
from ccxt.base.decimal_to_precision import ROUND_UP, ROUND_DOWN
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import (DependencyException, InvalidOrderException,
|
||||
@@ -25,6 +26,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
API_RETRY_COUNT = 4
|
||||
BAD_EXCHANGES = {
|
||||
"bitmex": "Various reasons",
|
||||
"bitstamp": "Does not provide history. "
|
||||
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
|
||||
}
|
||||
|
||||
|
||||
def retrier_async(f):
|
||||
@@ -63,7 +69,7 @@ def retrier(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
class Exchange(object):
|
||||
class Exchange:
|
||||
|
||||
_config: Dict = {}
|
||||
_params: Dict = {}
|
||||
@@ -85,6 +91,9 @@ class Exchange(object):
|
||||
it does basic validation whether the specified exchange and pairs are valid.
|
||||
:return: None
|
||||
"""
|
||||
self._api: ccxt.Exchange = None
|
||||
self._api_async: ccxt_async.Exchange = None
|
||||
|
||||
self._config.update(config)
|
||||
|
||||
self._cached_ticker: Dict[str, Any] = {}
|
||||
@@ -117,9 +126,9 @@ class Exchange(object):
|
||||
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
|
||||
|
||||
# Initialize ccxt objects
|
||||
self._api: ccxt.Exchange = self._init_ccxt(
|
||||
self._api = self._init_ccxt(
|
||||
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
|
||||
self._api_async: ccxt_async.Exchange = self._init_ccxt(
|
||||
self._api_async = self._init_ccxt(
|
||||
exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config'))
|
||||
|
||||
logger.info('Using Exchange "%s"', self.name)
|
||||
@@ -171,8 +180,10 @@ class Exchange(object):
|
||||
try:
|
||||
|
||||
api = getattr(ccxt_module, name.lower())(ex_config)
|
||||
except (KeyError, AttributeError):
|
||||
raise OperationalException(f'Exchange {name} is not supported')
|
||||
except (KeyError, AttributeError) as e:
|
||||
raise OperationalException(f'Exchange {name} is not supported') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
|
||||
|
||||
self.set_sandbox(api, exchange_config, name)
|
||||
|
||||
@@ -255,7 +266,7 @@ class Exchange(object):
|
||||
|
||||
if not self.markets:
|
||||
logger.warning('Unable to validate pairs (assuming they are correct).')
|
||||
# return
|
||||
return
|
||||
|
||||
for pair in pairs:
|
||||
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
|
||||
@@ -264,11 +275,35 @@ class Exchange(object):
|
||||
raise OperationalException(
|
||||
f'Pair {pair} is not available on {self.name}. '
|
||||
f'Please remove {pair} from your whitelist.')
|
||||
elif self.markets[pair].get('info', {}).get('IsRestricted', False):
|
||||
# Warn users about restricted pairs in whitelist.
|
||||
# We cannot determine reliably if Users are affected.
|
||||
logger.warning(f"Pair {pair} is restricted for some users on this exchange."
|
||||
f"Please check if you are impacted by this restriction "
|
||||
f"on the exchange and eventually remove {pair} from your whitelist.")
|
||||
|
||||
def get_valid_pair_combination(self, curr_1, curr_2) -> str:
|
||||
"""
|
||||
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
|
||||
"""
|
||||
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
||||
if pair in self.markets and self.markets[pair].get('active'):
|
||||
return pair
|
||||
raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||
|
||||
def validate_timeframes(self, timeframe: List[str]) -> None:
|
||||
"""
|
||||
Checks if ticker interval from config is a supported timeframe on the exchange
|
||||
"""
|
||||
if not hasattr(self._api, "timeframes") or self._api.timeframes is None:
|
||||
# If timeframes attribute is missing (or is None), the exchange probably
|
||||
# has no fetchOHLCV method.
|
||||
# Therefore we also show that.
|
||||
raise OperationalException(
|
||||
f"The ccxt library does not provide the list of timeframes "
|
||||
f"for the exchange \"{self.name}\" and this exchange "
|
||||
f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
|
||||
|
||||
timeframes = self._api.timeframes
|
||||
if timeframe not in timeframes:
|
||||
raise OperationalException(
|
||||
@@ -286,7 +321,7 @@ class Exchange(object):
|
||||
if (order_types.get("stoploss_on_exchange")
|
||||
and not self._ft_has.get("stoploss_on_exchange", False)):
|
||||
raise OperationalException(
|
||||
'On exchange stoploss is not supported for %s.' % self.name
|
||||
f'On exchange stoploss is not supported for {self.name}.'
|
||||
)
|
||||
|
||||
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
|
||||
@@ -332,7 +367,7 @@ class Exchange(object):
|
||||
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{randint(0, 10**6)}'
|
||||
dry_order = { # TODO: additional entry should be added for stoploss limit
|
||||
dry_order = {
|
||||
"id": order_id,
|
||||
'pair': pair,
|
||||
'price': rate,
|
||||
@@ -342,11 +377,12 @@ class Exchange(object):
|
||||
'side': side,
|
||||
'remaining': amount,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'status': "open",
|
||||
'status': "closed" if ordertype == "market" else "open",
|
||||
'fee': None,
|
||||
"info": {}
|
||||
}
|
||||
self._store_dry_order(dry_order)
|
||||
# Copy order and close it - so the returned order is open unless it's a market order
|
||||
return dry_order
|
||||
|
||||
def _store_dry_order(self, dry_order: Dict) -> None:
|
||||
@@ -357,6 +393,8 @@ class Exchange(object):
|
||||
"filled": closed_order["amount"],
|
||||
"remaining": 0
|
||||
})
|
||||
if closed_order["type"] in ["stop_loss_limit"]:
|
||||
closed_order["info"].update({"stopPrice": closed_order["price"]})
|
||||
self._dry_run_open_orders[closed_order["id"]] = closed_order
|
||||
|
||||
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
@@ -364,7 +402,9 @@ class Exchange(object):
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
||||
needs_price = (ordertype != 'market'
|
||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||
rate = self.symbol_price_prec(pair, rate) if needs_price else None
|
||||
|
||||
return self._api.create_order(pair, ordertype, side,
|
||||
amount, rate, params)
|
||||
@@ -372,18 +412,18 @@ class Exchange(object):
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
|
||||
f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
f'Could not create {ordertype} {side} order on market {pair}.'
|
||||
f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Message: {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}')
|
||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def buy(self, pair: str, ordertype: str, amount: float,
|
||||
rate: float, time_in_force) -> Dict:
|
||||
@@ -414,30 +454,14 @@ class Exchange(object):
|
||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
NOTICE: it is not supported by all exchanges. only binance is tested for now.
|
||||
TODO: implementation maybe needs to be moved to the binance subclass
|
||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
||||
exchange's subclass.
|
||||
The exception below should never raise, since we disallow
|
||||
starting the bot in validate_ordertypes()
|
||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
||||
"""
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
stop_price = self.symbol_price_prec(pair, stop_price)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price <= rate:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
return dry_order
|
||||
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
|
||||
order = self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s' % (pair, stop_price, rate))
|
||||
return order
|
||||
raise OperationalException(f"stoploss_limit is not implemented for {self.name}.")
|
||||
|
||||
@retrier
|
||||
def get_balance(self, currency: str) -> float:
|
||||
@@ -468,9 +492,9 @@ class Exchange(object):
|
||||
return balances
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_tickers(self) -> Dict:
|
||||
@@ -479,18 +503,18 @@ class Exchange(object):
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching tickers in batch.'
|
||||
f'Message: {e}')
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
|
||||
if refresh or pair not in self._cached_ticker.keys():
|
||||
try:
|
||||
if pair not in self._api.markets:
|
||||
if pair not in self._api.markets or not self._api.markets[pair].get('active'):
|
||||
raise DependencyException(f"Pair {pair} not available")
|
||||
data = self._api.fetch_ticker(pair)
|
||||
try:
|
||||
@@ -503,24 +527,29 @@ class Exchange(object):
|
||||
return data
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
else:
|
||||
logger.info("returning cached ticker-data for %s", pair)
|
||||
return self._cached_ticker[pair]
|
||||
|
||||
def get_history(self, pair: str, ticker_interval: str,
|
||||
def get_historic_ohlcv(self, pair: str, ticker_interval: str,
|
||||
since_ms: int) -> List:
|
||||
"""
|
||||
Gets candle history using asyncio and returns the list of candles.
|
||||
Handles all async doing.
|
||||
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
|
||||
:param pair: Pair to download
|
||||
:param ticker_interval: Interval to get
|
||||
:param since_ms: Timestamp in milliseconds to get history from
|
||||
:returns List of tickers
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
self._async_get_history(pair=pair, ticker_interval=ticker_interval,
|
||||
self._async_get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval,
|
||||
since_ms=since_ms))
|
||||
|
||||
async def _async_get_history(self, pair: str,
|
||||
async def _async_get_historic_ohlcv(self, pair: str,
|
||||
ticker_interval: str,
|
||||
since_ms: int) -> List:
|
||||
|
||||
@@ -548,7 +577,10 @@ class Exchange(object):
|
||||
|
||||
def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]:
|
||||
"""
|
||||
Refresh in-memory ohlcv asyncronously and set `_klines` with the result
|
||||
Refresh in-memory ohlcv asynchronously and set `_klines` with the result
|
||||
Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
|
||||
:param pair_list: List of 2 element tuples containing pair, interval to refresh
|
||||
:return: Returns a List of ticker-dataframes.
|
||||
"""
|
||||
logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list))
|
||||
|
||||
@@ -596,7 +628,7 @@ class Exchange(object):
|
||||
async def _async_get_candle_history(self, pair: str, ticker_interval: str,
|
||||
since_ms: Optional[int] = None) -> Tuple[str, str, List]:
|
||||
"""
|
||||
Asyncronously gets candle histories using fetch_ohlcv
|
||||
Asynchronously gets candle histories using fetch_ohlcv
|
||||
returns tuple: (pair, ticker_interval, ohlcv_list)
|
||||
"""
|
||||
try:
|
||||
@@ -626,12 +658,12 @@ class Exchange(object):
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
||||
f'Message: {e}')
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||
raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e
|
||||
|
||||
@retrier
|
||||
def cancel_order(self, order_id: str, pair: str) -> None:
|
||||
@@ -642,28 +674,33 @@ class Exchange(object):
|
||||
return self._api.cancel_order(order_id, pair)
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not cancel order. Message: {e}')
|
||||
f'Could not cancel order. Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
return order
|
||||
except KeyError as e:
|
||||
# Gracefully handle errors with dry-run orders.
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||
try:
|
||||
return self._api.fetch_order(order_id, pair)
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid order (id: {order_id}). Message: {e}')
|
||||
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
@@ -679,12 +716,12 @@ class Exchange(object):
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching order book.'
|
||||
f'Message: {e}')
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order book due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
||||
@@ -694,16 +731,17 @@ class Exchange(object):
|
||||
return []
|
||||
try:
|
||||
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||
my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5)
|
||||
# since needs to be int in milliseconds
|
||||
my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000))
|
||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||
|
||||
return matched_trades
|
||||
|
||||
except ccxt.NetworkError as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get trades due to networking error. Message: {e}')
|
||||
f'Could not get trades due to networking error. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1,
|
||||
@@ -717,21 +755,25 @@ class Exchange(object):
|
||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
raise OperationalException(e) from e
|
||||
|
||||
|
||||
def is_exchange_bad(exchange: str) -> bool:
|
||||
return exchange in ['bitmex']
|
||||
def is_exchange_bad(exchange_name: str) -> bool:
|
||||
return exchange_name in BAD_EXCHANGES
|
||||
|
||||
|
||||
def is_exchange_available(exchange: str, ccxt_module=None) -> bool:
|
||||
return exchange in available_exchanges(ccxt_module)
|
||||
def get_exchange_bad_reason(exchange_name: str) -> str:
|
||||
return BAD_EXCHANGES.get(exchange_name, "")
|
||||
|
||||
|
||||
def is_exchange_officially_supported(exchange: str) -> bool:
|
||||
return exchange in ['bittrex', 'binance']
|
||||
def is_exchange_available(exchange_name: str, ccxt_module=None) -> bool:
|
||||
return exchange_name in available_exchanges(ccxt_module)
|
||||
|
||||
|
||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
return exchange_name in ['bittrex', 'binance']
|
||||
|
||||
|
||||
def available_exchanges(ccxt_module=None) -> List[str]:
|
||||
@@ -749,13 +791,42 @@ def timeframe_to_seconds(ticker_interval: str) -> int:
|
||||
|
||||
def timeframe_to_minutes(ticker_interval: str) -> int:
|
||||
"""
|
||||
Same as above, but returns minutes.
|
||||
Same as timeframe_to_seconds, but returns minutes.
|
||||
"""
|
||||
return ccxt.Exchange.parse_timeframe(ticker_interval) // 60
|
||||
|
||||
|
||||
def timeframe_to_msecs(ticker_interval: str) -> int:
|
||||
"""
|
||||
Same as above, but returns milliseconds.
|
||||
Same as timeframe_to_seconds, but returns milliseconds.
|
||||
"""
|
||||
return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000
|
||||
|
||||
|
||||
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
||||
"""
|
||||
Use Timeframe and determine last possible candle.
|
||||
:param timeframe: timeframe in string format (e.g. "5m")
|
||||
:param date: date to use. Defaults to utcnow()
|
||||
:returns: date of previous candle (with utc timezone)
|
||||
"""
|
||||
if not date:
|
||||
date = datetime.now(timezone.utc)
|
||||
|
||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
|
||||
ROUND_DOWN) // 1000
|
||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||
|
||||
|
||||
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
||||
"""
|
||||
Use Timeframe and determine next candle.
|
||||
:param timeframe: timeframe in string format (e.g. "5m")
|
||||
:param date: date to use. Defaults to utcnow()
|
||||
:returns: date of next candle (with utc timezone)
|
||||
"""
|
||||
if not date:
|
||||
date = datetime.now(timezone.utc)
|
||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
|
||||
ROUND_UP) // 1000
|
||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade import OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange import retrier
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -10,3 +14,33 @@ logger = logging.getLogger(__name__)
|
||||
class Kraken(Exchange):
|
||||
|
||||
_params: Dict = {"trading_agreement": "agree"}
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> dict:
|
||||
if self._config['dry_run']:
|
||||
return {}
|
||||
|
||||
try:
|
||||
balances = self._api.fetch_balance()
|
||||
# Remove additional info from ccxt results
|
||||
balances.pop("info", None)
|
||||
balances.pop("free", None)
|
||||
balances.pop("total", None)
|
||||
balances.pop("used", None)
|
||||
|
||||
orders = self._api.fetch_open_orders()
|
||||
order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1],
|
||||
x["remaining"],
|
||||
# Don't remove the below comment, this can be important for debuggung
|
||||
# x["side"], x["amount"],
|
||||
) for x in orders]
|
||||
for bal in balances:
|
||||
balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal)
|
||||
balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used']
|
||||
|
||||
return balances
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@@ -6,6 +6,7 @@ import copy
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from math import isclose
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
@@ -16,7 +17,8 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx
|
||||
from freqtrade.data.converter import order_book_to_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver
|
||||
@@ -28,7 +30,7 @@ from freqtrade.wallets import Wallets
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FreqtradeBot(object):
|
||||
class FreqtradeBot:
|
||||
"""
|
||||
Freqtrade is the main class of the bot.
|
||||
This is from here the bot start its logic.
|
||||
@@ -51,6 +53,9 @@ class FreqtradeBot(object):
|
||||
|
||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||
|
||||
# Check config consistency here since strategies can set certain options
|
||||
validate_config_consistency(config)
|
||||
|
||||
self.rpc: RPCManager = RPCManager(self)
|
||||
|
||||
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
|
||||
@@ -99,13 +104,12 @@ class FreqtradeBot(object):
|
||||
# Adjust stoploss if it was changed
|
||||
Trade.stoploss_reinitialization(self.strategy.stoploss)
|
||||
|
||||
def process(self) -> bool:
|
||||
def process(self) -> None:
|
||||
"""
|
||||
Queries the persistence layer for open trades and handles them,
|
||||
otherwise a new trade is created.
|
||||
:return: True if one or more trades has been created or closed, False otherwise
|
||||
"""
|
||||
state_changed = False
|
||||
|
||||
# Check whether markets have to be reloaded
|
||||
self.exchange._reload_markets()
|
||||
@@ -132,19 +136,17 @@ class FreqtradeBot(object):
|
||||
|
||||
# First process current opened trades
|
||||
for trade in trades:
|
||||
state_changed |= self.process_maybe_execute_sell(trade)
|
||||
self.process_maybe_execute_sell(trade)
|
||||
|
||||
# Then looking for buy opportunities
|
||||
if len(trades) < self.config['max_open_trades']:
|
||||
state_changed = self.process_maybe_execute_buy()
|
||||
self.process_maybe_execute_buy()
|
||||
|
||||
if 'unfilledtimeout' in self.config:
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
Trade.session.flush()
|
||||
|
||||
return state_changed
|
||||
|
||||
def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]):
|
||||
"""
|
||||
Extend whitelist with pairs from open trades
|
||||
@@ -209,7 +211,7 @@ class FreqtradeBot(object):
|
||||
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
|
||||
open_trades = len(Trade.get_open_trades())
|
||||
if open_trades >= self.config['max_open_trades']:
|
||||
logger.warning('Can\'t open a new trade: max number of trades is reached')
|
||||
logger.warning("Can't open a new trade: max number of trades is reached")
|
||||
return None
|
||||
return available_amount / (self.config['max_open_trades'] - open_trades)
|
||||
|
||||
@@ -253,11 +255,12 @@ class FreqtradeBot(object):
|
||||
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
||||
return min(min_stake_amounts) / amount_reserve_percent
|
||||
|
||||
def create_trade(self) -> bool:
|
||||
def create_trades(self) -> bool:
|
||||
"""
|
||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||
if one pair triggers the buy_signal a new trade record gets created
|
||||
:return: True if a trade object has been created and persisted, False otherwise
|
||||
Checks the implemented trading strategy for buy-signals, using the active pair whitelist.
|
||||
If a pair triggers the buy_signal a new trade record gets created.
|
||||
Checks pairs as long as the open trade count is below `max_open_trades`.
|
||||
:return: True if at least one trade has been created.
|
||||
"""
|
||||
interval = self.strategy.ticker_interval
|
||||
whitelist = copy.deepcopy(self.active_pair_whitelist)
|
||||
@@ -276,15 +279,19 @@ class FreqtradeBot(object):
|
||||
logger.info("No currency pair in whitelist, but checking to sell open trades.")
|
||||
return False
|
||||
|
||||
buycount = 0
|
||||
# running get_signal on historical data fetched
|
||||
for _pair in whitelist:
|
||||
if self.strategy.is_pair_locked(_pair):
|
||||
logger.info(f"Pair {_pair} is currently locked.")
|
||||
continue
|
||||
(buy, sell) = self.strategy.get_signal(
|
||||
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
|
||||
|
||||
if buy and not sell:
|
||||
if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']:
|
||||
stake_amount = self._get_trade_stake_amount(_pair)
|
||||
if not stake_amount:
|
||||
return False
|
||||
continue
|
||||
|
||||
logger.info(f"Buy signal found: about create a new trade with stake_amount: "
|
||||
f"{stake_amount} ...")
|
||||
@@ -294,12 +301,13 @@ class FreqtradeBot(object):
|
||||
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
|
||||
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
|
||||
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
|
||||
return self.execute_buy(_pair, stake_amount)
|
||||
buycount += self.execute_buy(_pair, stake_amount)
|
||||
else:
|
||||
return False
|
||||
return self.execute_buy(_pair, stake_amount)
|
||||
continue
|
||||
|
||||
return False
|
||||
buycount += self.execute_buy(_pair, stake_amount)
|
||||
|
||||
return buycount > 0
|
||||
|
||||
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
|
||||
"""
|
||||
@@ -338,8 +346,8 @@ class FreqtradeBot(object):
|
||||
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
|
||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||
logger.warning(
|
||||
f'Can\'t open a new trade for {pair_s}: stake amount '
|
||||
f'is too small ({stake_amount} < {min_stake_amount})'
|
||||
f"Can't open a new trade for {pair_s}: stake amount "
|
||||
f"is too small ({stake_amount} < {min_stake_amount})"
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -423,21 +431,17 @@ class FreqtradeBot(object):
|
||||
|
||||
return True
|
||||
|
||||
def process_maybe_execute_buy(self) -> bool:
|
||||
def process_maybe_execute_buy(self) -> None:
|
||||
"""
|
||||
Tries to execute a buy trade in a safe way
|
||||
:return: True if executed
|
||||
"""
|
||||
try:
|
||||
# Create entity and execute trade
|
||||
if self.create_trade():
|
||||
return True
|
||||
|
||||
logger.info('Found no buy signals for whitelisted currencies. Trying again..')
|
||||
return False
|
||||
if not self.create_trades():
|
||||
logger.info('Found no buy signals for whitelisted currencies. Trying again...')
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to create trade: %s', exception)
|
||||
return False
|
||||
|
||||
def process_maybe_execute_sell(self, trade: Trade) -> bool:
|
||||
"""
|
||||
@@ -478,8 +482,11 @@ class FreqtradeBot(object):
|
||||
return order_amount
|
||||
|
||||
# use fee from order-dict if possible
|
||||
if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}):
|
||||
if trade.pair.startswith(order['fee']['currency']):
|
||||
if ('fee' in order and order['fee'] is not None and
|
||||
(order['fee'].keys() >= {'currency', 'cost'})):
|
||||
if (order['fee']['currency'] is not None and
|
||||
order['fee']['cost'] is not None and
|
||||
trade.pair.startswith(order['fee']['currency'])):
|
||||
new_amount = order_amount - order['fee']['cost']
|
||||
logger.info("Applying fee on amount for %s (from %s to %s) from Order",
|
||||
trade, order['amount'], new_amount)
|
||||
@@ -496,12 +503,15 @@ class FreqtradeBot(object):
|
||||
fee_abs = 0
|
||||
for exectrade in trades:
|
||||
amount += exectrade['amount']
|
||||
if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}):
|
||||
if ("fee" in exectrade and exectrade['fee'] is not None and
|
||||
(exectrade['fee'].keys() >= {'currency', 'cost'})):
|
||||
# only applies if fee is in quote currency!
|
||||
if trade.pair.startswith(exectrade['fee']['currency']):
|
||||
if (exectrade['fee']['currency'] is not None and
|
||||
exectrade['fee']['cost'] is not None and
|
||||
trade.pair.startswith(exectrade['fee']['currency'])):
|
||||
fee_abs += exectrade['fee']['cost']
|
||||
|
||||
if amount != order_amount:
|
||||
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
||||
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
|
||||
raise OperationalException("Half bought? Amounts don't match")
|
||||
real_amount = amount - fee_abs
|
||||
@@ -518,11 +528,15 @@ class FreqtradeBot(object):
|
||||
if trade.open_order_id:
|
||||
# Update trade with order values
|
||||
logger.info('Found open order for %s', trade)
|
||||
try:
|
||||
order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception)
|
||||
return
|
||||
# Try update amount (binance-fix)
|
||||
try:
|
||||
new_amount = self.get_real_amount(trade, order)
|
||||
if order['amount'] != new_amount:
|
||||
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
||||
order['amount'] = new_amount
|
||||
# Fee was applied, so set to 0
|
||||
trade.fee_open = 0
|
||||
@@ -586,18 +600,45 @@ class FreqtradeBot(object):
|
||||
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||
sell_rate = order_book_rate
|
||||
|
||||
if self.check_sell(trade, sell_rate, buy, sell):
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
else:
|
||||
logger.debug('checking sell')
|
||||
sell_rate = self.get_sell_rate(trade.pair, True)
|
||||
if self.check_sell(trade, sell_rate, buy, sell):
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
logger.debug('Found no sell signal for %s.', trade)
|
||||
return False
|
||||
|
||||
def create_stoploss_order(self, trade: Trade, stop_price: float, rate: float) -> bool:
|
||||
"""
|
||||
Abstracts creating stoploss orders from the logic.
|
||||
Handles errors and updates the trade database object.
|
||||
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
|
||||
:return: True if the order succeeded, and False in case of problems.
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below price
|
||||
LIMIT_PRICE_PCT = 0.99
|
||||
|
||||
try:
|
||||
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
|
||||
stop_price=stop_price,
|
||||
rate=rate * LIMIT_PRICE_PCT)
|
||||
trade.stoploss_order_id = str(stoploss_order['id'])
|
||||
return True
|
||||
except InvalidOrderException as e:
|
||||
trade.stoploss_order_id = None
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Selling the trade forcefully')
|
||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
|
||||
|
||||
except DependencyException:
|
||||
trade.stoploss_order_id = None
|
||||
logger.exception('Unable to place a stoploss order on exchange.')
|
||||
return False
|
||||
|
||||
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
|
||||
"""
|
||||
Check if trade is fulfilled in which case the stoploss
|
||||
@@ -616,53 +657,34 @@ class FreqtradeBot(object):
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||
|
||||
# If trade open order id does not exist: buy order is fulfilled
|
||||
buy_order_fulfilled = not trade.open_order_id
|
||||
|
||||
# Limit price threshold: As limit price should always be below price
|
||||
limit_price_pct = 0.99
|
||||
|
||||
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||
if (buy_order_fulfilled and not stoploss_order):
|
||||
if self.edge:
|
||||
stoploss = self.edge.stoploss(pair=trade.pair)
|
||||
else:
|
||||
stoploss = self.strategy.stoploss
|
||||
if (not trade.open_order_id and not stoploss_order):
|
||||
|
||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||
|
||||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
|
||||
# limit price should be less than stop price.
|
||||
limit_price = stop_price * limit_price_pct
|
||||
|
||||
try:
|
||||
stoploss_order_id = self.exchange.stoploss_limit(
|
||||
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
|
||||
)['id']
|
||||
trade.stoploss_order_id = str(stoploss_order_id)
|
||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
|
||||
trade.stoploss_last_update = datetime.now()
|
||||
return False
|
||||
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to place a stoploss order on exchange: %s', exception)
|
||||
|
||||
# If stoploss order is canceled for some reason we add it
|
||||
if stoploss_order and stoploss_order['status'] == 'canceled':
|
||||
try:
|
||||
stoploss_order_id = self.exchange.stoploss_limit(
|
||||
pair=trade.pair, amount=trade.amount,
|
||||
stop_price=trade.stop_loss, rate=trade.stop_loss * limit_price_pct
|
||||
)['id']
|
||||
trade.stoploss_order_id = str(stoploss_order_id)
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||
rate=trade.stop_loss):
|
||||
return False
|
||||
except DependencyException as exception:
|
||||
logger.warning('Stoploss order was cancelled, '
|
||||
'but unable to recreate one: %s', exception)
|
||||
else:
|
||||
trade.stoploss_order_id = None
|
||||
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
||||
|
||||
# We check if stoploss order is fulfilled
|
||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||
trade.update(stoploss_order)
|
||||
self.notify_sell(trade)
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair,
|
||||
timeframe_to_next_date(self.config['ticker_interval']))
|
||||
self._notify_sell(trade, "stoploss")
|
||||
return True
|
||||
|
||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||
@@ -696,24 +718,23 @@ class FreqtradeBot(object):
|
||||
logger.exception(f"Could not cancel stoploss order {order['id']} "
|
||||
f"for pair {trade.pair}")
|
||||
|
||||
try:
|
||||
# creating the new one
|
||||
stoploss_order_id = self.exchange.stoploss_limit(
|
||||
pair=trade.pair, amount=trade.amount,
|
||||
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
|
||||
)['id']
|
||||
trade.stoploss_order_id = str(stoploss_order_id)
|
||||
except DependencyException:
|
||||
logger.exception(f"Could create trailing stoploss order "
|
||||
# Create new stoploss order
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||
rate=trade.stop_loss):
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"Could not create trailing stoploss order "
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||
if self.edge:
|
||||
stoploss = self.edge.stoploss(trade.pair)
|
||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
||||
buy: bool, sell: bool) -> bool:
|
||||
"""
|
||||
Check and execute sell
|
||||
"""
|
||||
should_sell = self.strategy.should_sell(
|
||||
trade, sell_rate, datetime.utcnow(), buy, sell, force_stoploss=stoploss)
|
||||
else:
|
||||
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell)
|
||||
trade, sell_rate, datetime.utcnow(), buy, sell,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
|
||||
if should_sell.sell_flag:
|
||||
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
||||
@@ -741,7 +762,7 @@ class FreqtradeBot(object):
|
||||
if not trade.open_order_id:
|
||||
continue
|
||||
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
except (RequestException, DependencyException):
|
||||
except (RequestException, DependencyException, InvalidOrderException):
|
||||
logger.info(
|
||||
'Cannot query order for %s due to %s',
|
||||
trade,
|
||||
@@ -856,20 +877,32 @@ class FreqtradeBot(object):
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||
|
||||
ordertype = self.strategy.order_types[sell_type]
|
||||
if sell_reason == SellType.EMERGENCY_SELL:
|
||||
# Emergencysells (default to market!)
|
||||
ordertype = self.strategy.order_types.get("emergencysell", "market")
|
||||
|
||||
# Execute sell and update trade record
|
||||
order_id = self.exchange.sell(pair=str(trade.pair),
|
||||
ordertype=self.strategy.order_types[sell_type],
|
||||
order = self.exchange.sell(pair=str(trade.pair),
|
||||
ordertype=ordertype,
|
||||
amount=trade.amount, rate=limit,
|
||||
time_in_force=self.strategy.order_time_in_force['sell']
|
||||
)['id']
|
||||
)
|
||||
|
||||
trade.open_order_id = order_id
|
||||
trade.open_order_id = order['id']
|
||||
trade.close_rate_requested = limit
|
||||
trade.sell_reason = sell_reason.value
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') == 'closed':
|
||||
trade.update(order)
|
||||
Trade.session.flush()
|
||||
self.notify_sell(trade)
|
||||
|
||||
def notify_sell(self, trade: Trade):
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
|
||||
|
||||
self._notify_sell(trade, ordertype)
|
||||
|
||||
def _notify_sell(self, trade: Trade, order_type: str):
|
||||
"""
|
||||
Sends rpc notification when a sell occured.
|
||||
"""
|
||||
@@ -886,7 +919,7 @@ class FreqtradeBot(object):
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'limit': trade.close_rate_requested,
|
||||
'order_type': self.strategy.order_types['sell'],
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
'current_rate': current_rate,
|
||||
|
||||
50
freqtrade/loggers.py
Normal file
50
freqtrade/loggers.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _set_loggers(verbosity: int = 0) -> None:
|
||||
"""
|
||||
Set the logging level for third party libraries
|
||||
:return: None
|
||||
"""
|
||||
|
||||
logging.getLogger('requests').setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger("urllib3").setLevel(
|
||||
logging.INFO if verbosity <= 1 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(
|
||||
logging.INFO if verbosity <= 2 else logging.DEBUG
|
||||
)
|
||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||
|
||||
|
||||
def setup_logging(config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Process -v/--verbose, --logfile options
|
||||
"""
|
||||
# Log level
|
||||
verbosity = config['verbosity']
|
||||
|
||||
# Log to stdout, not stderr
|
||||
log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)]
|
||||
|
||||
if config.get('logfile'):
|
||||
log_handlers.append(RotatingFileHandler(config['logfile'],
|
||||
maxBytes=1024 * 1024, # 1Mb
|
||||
backupCount=10))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO if verbosity < 1 else logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=log_handlers
|
||||
)
|
||||
_set_loggers(verbosity)
|
||||
logger.info('Verbosity set to %s', verbosity)
|
||||
@@ -11,12 +11,10 @@ if sys.version_info < (3, 6):
|
||||
|
||||
# flake8: noqa E402
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from typing import Any, List
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import set_loggers
|
||||
from freqtrade.configuration import Arguments
|
||||
from freqtrade.worker import Worker
|
||||
|
||||
|
||||
@@ -32,18 +30,13 @@ def main(sysargv: List[str] = None) -> None:
|
||||
return_code: Any = 1
|
||||
worker = None
|
||||
try:
|
||||
set_loggers()
|
||||
|
||||
arguments = Arguments(
|
||||
sysargv,
|
||||
'Free, open source crypto trading bot'
|
||||
)
|
||||
args: Namespace = arguments.get_parsed_arg()
|
||||
arguments = Arguments(sysargv)
|
||||
args = arguments.get_parsed_arg()
|
||||
|
||||
# A subcommand has been issued.
|
||||
# Means if Backtesting or Hyperopt have been called we exit the bot
|
||||
if hasattr(args, 'func'):
|
||||
args.func(args)
|
||||
if 'func' in args:
|
||||
args['func'](args)
|
||||
# TODO: fetch return_code as returned by the command function here
|
||||
return_code = 0
|
||||
else:
|
||||
|
||||
@@ -5,13 +5,12 @@ import gzip
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from pathlib import Path
|
||||
from typing.io import IO
|
||||
|
||||
import numpy as np
|
||||
from pandas import DataFrame
|
||||
import rapidjson
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -41,25 +40,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
||||
return dates.dt.to_pydatetime()
|
||||
|
||||
|
||||
def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
|
||||
"""
|
||||
Return dates from Dataframe
|
||||
:param dfs: Dict with format pair: pair_data
|
||||
:return: List of dates
|
||||
"""
|
||||
alldates = {}
|
||||
for pair, pair_data in dfs.items():
|
||||
dates = datesarray_to_datetimearray(pair_data['date'])
|
||||
for date in dates:
|
||||
alldates[date] = 1
|
||||
lst = []
|
||||
for date, _ in alldates.items():
|
||||
lst.append(date)
|
||||
arr = np.array(lst)
|
||||
return np.sort(arr, axis=0)
|
||||
|
||||
|
||||
def file_dump_json(filename, data, is_zip=False) -> None:
|
||||
def file_dump_json(filename: Path, data, is_zip=False) -> None:
|
||||
"""
|
||||
Dump JSON data into a file
|
||||
:param filename: file to create
|
||||
@@ -69,8 +50,8 @@ def file_dump_json(filename, data, is_zip=False) -> None:
|
||||
logger.info(f'dumping json to "{filename}"')
|
||||
|
||||
if is_zip:
|
||||
if not filename.endswith('.gz'):
|
||||
filename = filename + '.gz'
|
||||
if filename.suffix != '.gz':
|
||||
filename = filename.with_suffix('.gz')
|
||||
with gzip.open(filename, 'w') as fp:
|
||||
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||
else:
|
||||
@@ -80,7 +61,7 @@ def file_dump_json(filename, data, is_zip=False) -> None:
|
||||
logger.debug(f'done json to "{filename}"')
|
||||
|
||||
|
||||
def json_load(datafile):
|
||||
def json_load(datafile: IO):
|
||||
"""
|
||||
load data with rapidjson
|
||||
Use this to have a consistent experience,
|
||||
@@ -133,3 +114,10 @@ def deep_merge_dicts(source, destination):
|
||||
destination[key] = value
|
||||
|
||||
return destination
|
||||
|
||||
|
||||
def round_dict(d, n):
|
||||
"""
|
||||
Rounds float values in the dict to n digits after the decimal point.
|
||||
"""
|
||||
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from typing import Any, Dict
|
||||
|
||||
from filelock import FileLock, Timeout
|
||||
|
||||
from freqtrade import DependencyException, constants
|
||||
from freqtrade import DependencyException, constants, OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.utils import setup_utils_configuration
|
||||
|
||||
@@ -12,7 +9,7 @@ from freqtrade.utils import setup_utils_configuration
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]:
|
||||
def setup_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
|
||||
"""
|
||||
Prepare the configuration for the Hyperopt module
|
||||
:param args: Cli args from Arguments()
|
||||
@@ -25,20 +22,10 @@ def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]:
|
||||
raise DependencyException('stake amount could not be "%s" for backtesting' %
|
||||
constants.UNLIMITED_STAKE_AMOUNT)
|
||||
|
||||
if method == RunMode.HYPEROPT:
|
||||
# Special cases for Hyperopt
|
||||
if config.get('strategy') and config.get('strategy') != 'DefaultStrategy':
|
||||
logger.error("Please don't use --strategy for hyperopt.")
|
||||
logger.error(
|
||||
"Read the documentation at "
|
||||
"https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md "
|
||||
"to understand how to configure hyperopt.")
|
||||
raise DependencyException("--strategy configured but not supported for hyperopt")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def start_backtesting(args: Namespace) -> None:
|
||||
def start_backtesting(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Start Backtesting script
|
||||
:param args: Cli args from Arguments()
|
||||
@@ -57,21 +44,25 @@ def start_backtesting(args: Namespace) -> None:
|
||||
backtesting.start()
|
||||
|
||||
|
||||
def start_hyperopt(args: Namespace) -> None:
|
||||
def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Start hyperopt script
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
# Import here to avoid loading hyperopt module when it's not used
|
||||
from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE
|
||||
|
||||
try:
|
||||
from filelock import FileLock, Timeout
|
||||
from freqtrade.optimize.hyperopt import Hyperopt
|
||||
except ImportError as e:
|
||||
raise OperationalException(
|
||||
f"{e}. Please ensure that the hyperopt dependencies are installed.") from e
|
||||
# Initialize configuration
|
||||
config = setup_configuration(args, RunMode.HYPEROPT)
|
||||
|
||||
logger.info('Starting freqtrade in Hyperopt mode')
|
||||
|
||||
lock = FileLock(HYPEROPT_LOCKFILE)
|
||||
lock = FileLock(Hyperopt.get_lock_filename(config))
|
||||
|
||||
try:
|
||||
with lock.acquire(timeout=1):
|
||||
@@ -95,7 +86,7 @@ def start_hyperopt(args: Namespace) -> None:
|
||||
# Same in Edge and Backtesting start() functions.
|
||||
|
||||
|
||||
def start_edge(args: Namespace) -> None:
|
||||
def start_edge(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Start Edge script
|
||||
:param args: Cli args from Arguments()
|
||||
|
||||
@@ -10,9 +10,9 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, NamedTuple, Optional
|
||||
|
||||
from pandas import DataFrame
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
@@ -21,6 +21,7 @@ from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.strategy.interface import IStrategy, SellType
|
||||
from tabulate import tabulate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,7 +44,7 @@ class BacktestResult(NamedTuple):
|
||||
sell_reason: SellType
|
||||
|
||||
|
||||
class Backtesting(object):
|
||||
class Backtesting:
|
||||
"""
|
||||
Backtesting class, this class contains all the logic to run a backtest
|
||||
|
||||
@@ -80,6 +81,12 @@ class Backtesting(object):
|
||||
# No strategy list specified, only one strategy
|
||||
self.strategylist.append(StrategyResolver(self.config).strategy)
|
||||
|
||||
if "ticker_interval" not in self.config:
|
||||
raise OperationalException("Ticker-interval needs to be set in either configuration "
|
||||
"or as cli argument `--ticker-interval 5m`")
|
||||
self.ticker_interval = str(self.config.get('ticker_interval'))
|
||||
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
|
||||
|
||||
# Load one (first) strategy
|
||||
self._set_strategy(self.strategylist[0])
|
||||
|
||||
@@ -88,11 +95,6 @@ class Backtesting(object):
|
||||
Load strategy into backtesting
|
||||
"""
|
||||
self.strategy = strategy
|
||||
|
||||
self.ticker_interval = self.config.get('ticker_interval')
|
||||
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
|
||||
self.advise_buy = strategy.advise_buy
|
||||
self.advise_sell = strategy.advise_sell
|
||||
# Set stoploss_on_exchange to false for backtesting,
|
||||
# since a "perfect" stoploss-sell is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
@@ -186,7 +188,7 @@ class Backtesting(object):
|
||||
return tabulate(tabular_data, headers=headers, # type: ignore
|
||||
floatfmt=floatfmt, tablefmt="pipe")
|
||||
|
||||
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
|
||||
def _store_backtest_result(self, recordfilename: Path, results: DataFrame,
|
||||
strategyname: Optional[str] = None) -> None:
|
||||
|
||||
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
|
||||
@@ -197,10 +199,10 @@ class Backtesting(object):
|
||||
if records:
|
||||
if strategyname:
|
||||
# Inject strategyname to filename
|
||||
recname = Path(recordfilename)
|
||||
recordfilename = str(Path.joinpath(
|
||||
recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix))
|
||||
logger.info('Dumping backtest results to %s', recordfilename)
|
||||
recordfilename = Path.joinpath(
|
||||
recordfilename.parent,
|
||||
f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix)
|
||||
logger.info(f'Dumping backtest results to {recordfilename}')
|
||||
file_dump_json(recordfilename, records)
|
||||
|
||||
def _get_ticker_list(self, processed) -> Dict[str, DataFrame]:
|
||||
@@ -215,8 +217,8 @@ class Backtesting(object):
|
||||
for pair, pair_data in processed.items():
|
||||
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||
|
||||
ticker_data = self.advise_sell(
|
||||
self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||
ticker_data = self.strategy.advise_sell(
|
||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||
|
||||
# to avoid using data from future, we buy/sell with signal from previous candle
|
||||
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
||||
@@ -235,14 +237,16 @@ class Backtesting(object):
|
||||
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
|
||||
|
||||
trade = Trade(
|
||||
pair=pair,
|
||||
open_rate=buy_row.open,
|
||||
open_date=buy_row.date,
|
||||
stake_amount=stake_amount,
|
||||
amount=stake_amount / buy_row.open,
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
)
|
||||
|
||||
logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
|
||||
# calculate win/lose forwards from buy point
|
||||
for sell_row in partial_ticker:
|
||||
if max_open_trades > 0:
|
||||
@@ -252,22 +256,20 @@ class Backtesting(object):
|
||||
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy,
|
||||
sell_row.sell, low=sell_row.low, high=sell_row.high)
|
||||
if sell.sell_flag:
|
||||
|
||||
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60)
|
||||
# Special handling if high or low hit STOP_LOSS or ROI
|
||||
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||
# Set close_rate to stoploss
|
||||
closerate = trade.stop_loss
|
||||
elif sell.sell_type == (SellType.ROI):
|
||||
# get next entry in min_roi > to trade duration
|
||||
# Interface.py skips on trade_duration <= duration
|
||||
roi_entry = max(list(filter(lambda x: trade_dur >= x,
|
||||
self.strategy.minimal_roi.keys())))
|
||||
roi = self.strategy.minimal_roi[roi_entry]
|
||||
|
||||
roi = self.strategy.min_roi_reached_entry(trade_dur)
|
||||
if roi is not None:
|
||||
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
|
||||
closerate = - (trade.open_rate * roi + trade.open_rate *
|
||||
(1 + trade.fee_open)) / (trade.fee_close - 1)
|
||||
else:
|
||||
# This should not be reached...
|
||||
closerate = sell_row.open
|
||||
else:
|
||||
closerate = sell_row.open
|
||||
|
||||
@@ -287,7 +289,7 @@ class Backtesting(object):
|
||||
if partial_ticker:
|
||||
# no sell condition found - trade stil open at end of backtest period
|
||||
sell_row = partial_ticker[-1]
|
||||
btr = BacktestResult(pair=pair,
|
||||
bt_res = BacktestResult(pair=pair,
|
||||
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||
open_time=buy_row.date,
|
||||
@@ -301,9 +303,11 @@ class Backtesting(object):
|
||||
close_rate=sell_row.open,
|
||||
sell_reason=SellType.FORCE_SELL
|
||||
)
|
||||
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
|
||||
btr.profit_percent, btr.profit_abs)
|
||||
return btr
|
||||
logger.debug(f"{pair} - Force selling still open trade, "
|
||||
f"profit percent: {bt_res.profit_percent}, "
|
||||
f"profit abs: {bt_res.profit_abs}")
|
||||
|
||||
return bt_res
|
||||
return None
|
||||
|
||||
def backtest(self, args: Dict) -> DataFrame:
|
||||
@@ -321,6 +325,9 @@ class Backtesting(object):
|
||||
position_stacking: do we allow position stacking? (default: False)
|
||||
:return: DataFrame
|
||||
"""
|
||||
# Arguments are long and noisy, so this is commented out.
|
||||
# Uncomment if you need to debug the backtest() method.
|
||||
# logger.debug(f"Start backtest, args: {args}")
|
||||
processed = args['processed']
|
||||
stake_amount = args['stake_amount']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
@@ -372,11 +379,15 @@ class Backtesting(object):
|
||||
continue
|
||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||
|
||||
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:],
|
||||
# since indexes has been incremented before, we need to go one step back to
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:],
|
||||
trade_count_lock, stake_amount,
|
||||
max_open_trades)
|
||||
|
||||
if trade_entry:
|
||||
logger.debug(f"{pair} - Locking pair till "
|
||||
f"close_time={trade_entry.close_time}")
|
||||
lock_pair_until[pair] = trade_entry.close_time
|
||||
trades.append(trade_entry)
|
||||
else:
|
||||
@@ -397,16 +408,13 @@ class Backtesting(object):
|
||||
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
||||
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
|
||||
|
||||
timerange = Arguments.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')))
|
||||
data = history.load_data(
|
||||
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||
datadir=Path(self.config['datadir']),
|
||||
pairs=pairs,
|
||||
ticker_interval=self.ticker_interval,
|
||||
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||
exchange=self.exchange,
|
||||
timerange=timerange,
|
||||
live=self.config.get('live', False)
|
||||
)
|
||||
|
||||
if not data:
|
||||
@@ -451,7 +459,7 @@ class Backtesting(object):
|
||||
for strategy, results in all_results.items():
|
||||
|
||||
if self.config.get('export', False):
|
||||
self._store_backtest_result(self.config['exportfilename'], results,
|
||||
self._store_backtest_result(Path(self.config['exportfilename']), results,
|
||||
strategy if len(self.strategylist) > 1 else None)
|
||||
|
||||
print(f"Result for strategy {strategy}")
|
||||
|
||||
@@ -1,52 +1,61 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
|
||||
from functools import reduce
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from typing import Dict, Any, Callable, List
|
||||
from functools import reduce
|
||||
|
||||
from skopt.space import Categorical, Dimension, Integer, Real
|
||||
from skopt.space import Categorical, Dimension, Integer
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
|
||||
class_name = 'DefaultHyperOpts'
|
||||
|
||||
|
||||
class DefaultHyperOpts(IHyperOpt):
|
||||
"""
|
||||
Default hyperopt provided by freqtrade bot.
|
||||
You can override it with your own hyperopt
|
||||
Default hyperopt provided by the Freqtrade bot.
|
||||
You can override it with your own Hyperopt
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Add several indicators needed for buy and sell strategies defined below.
|
||||
"""
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
# MFI
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
# Stochastic Fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
# Minus-DI
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
# SAR
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
return dataframe
|
||||
|
||||
@staticmethod
|
||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Define the buy strategy parameters to be used by hyperopt
|
||||
Define the buy strategy parameters to be used by Hyperopt.
|
||||
"""
|
||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Buy strategy Hyperopt will build and use
|
||||
Buy strategy Hyperopt will build and use.
|
||||
"""
|
||||
conditions = []
|
||||
|
||||
# GUARDS AND TRENDS
|
||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||
@@ -82,7 +91,7 @@ class DefaultHyperOpts(IHyperOpt):
|
||||
@staticmethod
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching strategy parameters
|
||||
Define your Hyperopt space for searching buy strategy parameters.
|
||||
"""
|
||||
return [
|
||||
Integer(10, 25, name='mfi-value'),
|
||||
@@ -99,14 +108,14 @@ class DefaultHyperOpts(IHyperOpt):
|
||||
@staticmethod
|
||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Define the sell strategy parameters to be used by hyperopt
|
||||
Define the sell strategy parameters to be used by Hyperopt.
|
||||
"""
|
||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Sell strategy Hyperopt will build and use
|
||||
Sell strategy Hyperopt will build and use.
|
||||
"""
|
||||
# print(params)
|
||||
conditions = []
|
||||
|
||||
# GUARDS AND TRENDS
|
||||
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
||||
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
||||
@@ -142,7 +151,7 @@ class DefaultHyperOpts(IHyperOpt):
|
||||
@staticmethod
|
||||
def sell_indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching sell strategy parameters
|
||||
Define your Hyperopt space for searching sell strategy parameters.
|
||||
"""
|
||||
return [
|
||||
Integer(75, 100, name='sell-mfi-value'),
|
||||
@@ -158,47 +167,11 @@ class DefaultHyperOpts(IHyperOpt):
|
||||
'sell-sar_reversal'], name='sell-trigger')
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||
"""
|
||||
Generate the ROI table that will be used by Hyperopt
|
||||
"""
|
||||
roi_table = {}
|
||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
||||
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
||||
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
||||
|
||||
return roi_table
|
||||
|
||||
@staticmethod
|
||||
def stoploss_space() -> List[Dimension]:
|
||||
"""
|
||||
Stoploss Value to search
|
||||
"""
|
||||
return [
|
||||
Real(-0.5, -0.02, name='stoploss'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def roi_space() -> List[Dimension]:
|
||||
"""
|
||||
Values to search for each ROI steps
|
||||
"""
|
||||
return [
|
||||
Integer(10, 120, name='roi_t1'),
|
||||
Integer(10, 60, name='roi_t2'),
|
||||
Integer(10, 40, name='roi_t3'),
|
||||
Real(0.01, 0.04, name='roi_p1'),
|
||||
Real(0.01, 0.07, name='roi_p2'),
|
||||
Real(0.01, 0.20, name='roi_p3'),
|
||||
]
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators. Should be a copy of from strategy
|
||||
must align to populate_indicators in this file
|
||||
Only used when --spaces does not include buy
|
||||
Based on TA indicators. Should be a copy of same method from strategy.
|
||||
Must align to populate_indicators in this file.
|
||||
Only used when --spaces does not include buy space.
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
@@ -213,9 +186,9 @@ class DefaultHyperOpts(IHyperOpt):
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators. Should be a copy of from strategy
|
||||
must align to populate_indicators in this file
|
||||
Only used when --spaces does not include sell
|
||||
Based on TA indicators. Should be a copy of same method from strategy.
|
||||
Must align to populate_indicators in this file.
|
||||
Only used when --spaces does not include sell space.
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
@@ -225,4 +198,5 @@ class DefaultHyperOpts(IHyperOpt):
|
||||
(dataframe['fastd'] > 54)
|
||||
),
|
||||
'sell'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
52
freqtrade/optimize/default_hyperopt_loss.py
Normal file
52
freqtrade/optimize/default_hyperopt_loss.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
DefaultHyperOptLoss
|
||||
This module defines the default HyperoptLoss class which is being used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
from math import exp
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
# Set TARGET_TRADES to suit your number concurrent trades so its realistic
|
||||
# to the number of days
|
||||
TARGET_TRADES = 600
|
||||
|
||||
# This is assumed to be expected avg profit * expected trade count.
|
||||
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
|
||||
# expected max profit = 3.85
|
||||
# Check that the reported Σ% values do not exceed this!
|
||||
# Note, this is ratio. 3.85 stated above means 385Σ%.
|
||||
EXPECTED_MAX_PROFIT = 3.0
|
||||
|
||||
# Max average trade duration in minutes.
|
||||
# If eval ends with higher value, we consider it a failed eval.
|
||||
MAX_ACCEPTED_TRADE_DURATION = 300
|
||||
|
||||
|
||||
class DefaultHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the default loss function for hyperopt
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
This is the Default algorithm
|
||||
Weights are distributed as follows:
|
||||
* 0.4 to trade duration
|
||||
* 0.25: Avoiding trade loss
|
||||
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
|
||||
"""
|
||||
total_profit = results.profit_percent.sum()
|
||||
trade_duration = results.trade_duration.mean()
|
||||
|
||||
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
|
||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
|
||||
result = trade_loss + profit_loss + duration_loss
|
||||
return result
|
||||
@@ -9,14 +9,14 @@ from tabulate import tabulate
|
||||
from freqtrade import constants
|
||||
from freqtrade.edge import Edge
|
||||
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EdgeCli(object):
|
||||
class EdgeCli:
|
||||
"""
|
||||
EdgeCli class, this class contains all the logic to run edge backtesting
|
||||
|
||||
@@ -39,9 +39,10 @@ class EdgeCli(object):
|
||||
self.strategy = StrategyResolver(self.config).strategy
|
||||
|
||||
self.edge = Edge(config, self.exchange, self.strategy)
|
||||
self.edge._refresh_pairs = self.config.get('refresh_pairs', False)
|
||||
# Set refresh_pairs to false for edge-cli (it must be true for edge)
|
||||
self.edge._refresh_pairs = False
|
||||
|
||||
self.timerange = Arguments.parse_timerange(None if self.config.get(
|
||||
self.timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
|
||||
self.edge._timerange = self.timerange
|
||||
|
||||
@@ -5,36 +5,46 @@ This module contains the hyperopt logic
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from math import exp
|
||||
|
||||
from collections import OrderedDict
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import rapidjson
|
||||
|
||||
from colorama import init as colorama_init
|
||||
from colorama import Fore, Style
|
||||
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count
|
||||
from pandas import DataFrame
|
||||
from skopt import Optimizer
|
||||
from skopt.space import Dimension
|
||||
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.history import load_data, get_timeframe
|
||||
from freqtrade.misc import round_dict
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INITIAL_POINTS = 30
|
||||
|
||||
# Keep no more than 2*SKOPT_MODELS_MAX_NUM models
|
||||
# in the skopt models list
|
||||
SKOPT_MODELS_MAX_NUM = 10
|
||||
|
||||
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
||||
TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl')
|
||||
TRIALSDATA_PICKLE = os.path.join('user_data', 'hyperopt_results.pickle')
|
||||
HYPEROPT_LOCKFILE = os.path.join('user_data', 'hyperopt.lock')
|
||||
|
||||
|
||||
class Hyperopt(Backtesting):
|
||||
class Hyperopt:
|
||||
"""
|
||||
Hyperopt class, this class contains all the logic to run a hyperopt simulation
|
||||
|
||||
@@ -43,32 +53,75 @@ class Hyperopt(Backtesting):
|
||||
hyperopt.start()
|
||||
"""
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
|
||||
|
||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic
|
||||
# to the number of days
|
||||
self.target_trades = 600
|
||||
self.total_tries = config.get('epochs', 0)
|
||||
self.backtesting = Backtesting(self.config)
|
||||
|
||||
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
|
||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||
|
||||
self.trials_file = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||
self.tickerdata_pickle = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
|
||||
self.total_epochs = config.get('epochs', 0)
|
||||
|
||||
self.current_best_loss = 100
|
||||
|
||||
# max average trade duration in minutes
|
||||
# if eval ends with higher value, we consider it a failed eval
|
||||
self.max_accepted_trade_duration = 300
|
||||
|
||||
# This is assumed to be expected avg profit * expected trade count.
|
||||
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
|
||||
# self.expected_max_profit = 3.85
|
||||
# Check that the reported Σ% values do not exceed this!
|
||||
# Note, this is ratio. 3.85 stated above means 385Σ%.
|
||||
self.expected_max_profit = 3.0
|
||||
if not self.config.get('hyperopt_continue'):
|
||||
self.clean_hyperopt()
|
||||
else:
|
||||
logger.info("Continuing on previous hyperopt results.")
|
||||
|
||||
# Previous evaluations
|
||||
self.trials_file = TRIALSDATA_PICKLE
|
||||
self.trials: List = []
|
||||
|
||||
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
||||
self.backtesting.strategy.advise_indicators = \
|
||||
self.custom_hyperopt.populate_indicators # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||
self.backtesting.strategy.advise_sell = \
|
||||
self.custom_hyperopt.populate_sell_trend # type: ignore
|
||||
|
||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||
if self.config.get('use_max_market_positions', True):
|
||||
self.max_open_trades = self.config['max_open_trades']
|
||||
else:
|
||||
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
||||
self.max_open_trades = 0
|
||||
self.position_stacking = self.config.get('position_stacking', False)
|
||||
|
||||
if self.has_space('sell'):
|
||||
# Make sure experimental is enabled
|
||||
if 'experimental' not in self.config:
|
||||
self.config['experimental'] = {}
|
||||
self.config['experimental']['use_sell_signal'] = True
|
||||
|
||||
@staticmethod
|
||||
def get_lock_filename(config) -> str:
|
||||
|
||||
return str(config['user_data_dir'] / 'hyperopt.lock')
|
||||
|
||||
def clean_hyperopt(self):
|
||||
"""
|
||||
Remove hyperopt pickle files to restart hyperopt.
|
||||
"""
|
||||
for f in [self.tickerdata_pickle, self.trials_file]:
|
||||
p = Path(f)
|
||||
if p.is_file():
|
||||
logger.info(f"Removing `{p}`.")
|
||||
p.unlink()
|
||||
|
||||
def get_args(self, params):
|
||||
dimensions = self.hyperopt_space()
|
||||
|
||||
dimensions = self.dimensions
|
||||
|
||||
# Ensure the number of dimensions match
|
||||
# the number of parameters in the list x.
|
||||
if len(params) != len(dimensions):
|
||||
@@ -85,16 +138,16 @@ class Hyperopt(Backtesting):
|
||||
Save hyperopt trials to file
|
||||
"""
|
||||
if self.trials:
|
||||
logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file)
|
||||
logger.info("Saving %d evaluations to '%s'", len(self.trials), self.trials_file)
|
||||
dump(self.trials, self.trials_file)
|
||||
|
||||
def read_trials(self) -> List:
|
||||
"""
|
||||
Read hyperopt trials file
|
||||
"""
|
||||
logger.info('Reading Trials from \'%s\'', self.trials_file)
|
||||
logger.info("Reading Trials from '%s'", self.trials_file)
|
||||
trials = load(self.trials_file)
|
||||
os.remove(self.trials_file)
|
||||
self.trials_file.unlink()
|
||||
return trials
|
||||
|
||||
def log_trials_result(self) -> None:
|
||||
@@ -103,108 +156,150 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
results = sorted(self.trials, key=itemgetter('loss'))
|
||||
best_result = results[0]
|
||||
logger.info(
|
||||
'Best result:\n%s\nwith values:\n',
|
||||
best_result['result']
|
||||
params = best_result['params']
|
||||
log_str = self.format_results_logstring(best_result)
|
||||
print(f"\nBest result:\n\n{log_str}\n")
|
||||
|
||||
if self.config.get('print_json'):
|
||||
result_dict: Dict = {}
|
||||
if self.has_space('buy') or self.has_space('sell'):
|
||||
result_dict['params'] = {}
|
||||
if self.has_space('buy'):
|
||||
result_dict['params'].update({p.name: params.get(p.name)
|
||||
for p in self.hyperopt_space('buy')})
|
||||
if self.has_space('sell'):
|
||||
result_dict['params'].update({p.name: params.get(p.name)
|
||||
for p in self.hyperopt_space('sell')})
|
||||
if self.has_space('roi'):
|
||||
# Convert keys in min_roi dict to strings because
|
||||
# rapidjson cannot dump dicts with integer keys...
|
||||
# OrderedDict is used to keep the numeric order of the items
|
||||
# in the dict.
|
||||
result_dict['minimal_roi'] = OrderedDict(
|
||||
(str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items()
|
||||
)
|
||||
pprint(best_result['params'], indent=4)
|
||||
if 'roi_t1' in best_result['params']:
|
||||
logger.info('ROI table:')
|
||||
pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4)
|
||||
if self.has_space('stoploss'):
|
||||
result_dict['stoploss'] = params.get('stoploss')
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||
else:
|
||||
if self.has_space('buy'):
|
||||
print('Buy hyperspace params:')
|
||||
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')},
|
||||
indent=4)
|
||||
if self.has_space('sell'):
|
||||
print('Sell hyperspace params:')
|
||||
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')},
|
||||
indent=4)
|
||||
if self.has_space('roi'):
|
||||
print("ROI table:")
|
||||
# Round printed values to 5 digits after the decimal point
|
||||
pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4)
|
||||
if self.has_space('stoploss'):
|
||||
# Also round to 5 digits after the decimal point
|
||||
print(f"Stoploss: {round(params.get('stoploss'), 5)}")
|
||||
|
||||
def log_results(self, results) -> None:
|
||||
"""
|
||||
Log results if it is better than any previous evaluation
|
||||
"""
|
||||
print_all = self.config.get('print_all', False)
|
||||
if print_all or results['loss'] < self.current_best_loss:
|
||||
# Output human-friendly index here (starting from 1)
|
||||
current = results['current_tries'] + 1
|
||||
total = results['total_tries']
|
||||
res = results['result']
|
||||
loss = results['loss']
|
||||
is_best_loss = results['loss'] < self.current_best_loss
|
||||
if print_all or is_best_loss:
|
||||
if is_best_loss:
|
||||
self.current_best_loss = results['loss']
|
||||
log_msg = f'{current:5d}/{total}: {res} Objective: {loss:.5f}'
|
||||
log_msg = f'*{log_msg}' if results['initial_point'] else f' {log_msg}'
|
||||
log_str = self.format_results_logstring(results)
|
||||
# Colorize output
|
||||
if self.config.get('print_colorized', False):
|
||||
if results['total_profit'] > 0:
|
||||
log_str = Fore.GREEN + log_str
|
||||
if print_all and is_best_loss:
|
||||
log_str = Style.BRIGHT + log_str
|
||||
if print_all:
|
||||
print(log_msg)
|
||||
print(log_str)
|
||||
else:
|
||||
print('\n' + log_msg)
|
||||
print('\n' + log_str)
|
||||
else:
|
||||
print('.', end='')
|
||||
sys.stdout.flush()
|
||||
|
||||
def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for more optimal results
|
||||
"""
|
||||
trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8)
|
||||
profit_loss = max(0, 1 - total_profit / self.expected_max_profit)
|
||||
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1)
|
||||
result = trade_loss + profit_loss + duration_loss
|
||||
return result
|
||||
def format_results_logstring(self, results) -> str:
|
||||
# Output human-friendly index here (starting from 1)
|
||||
current = results['current_epoch'] + 1
|
||||
total = self.total_epochs
|
||||
res = results['results_explanation']
|
||||
loss = results['loss']
|
||||
log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}'
|
||||
log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}'
|
||||
return log_str
|
||||
|
||||
def has_space(self, space: str) -> bool:
|
||||
"""
|
||||
Tell if a space value is contained in the configuration
|
||||
"""
|
||||
if space in self.config['spaces'] or 'all' in self.config['spaces']:
|
||||
return True
|
||||
return False
|
||||
return any(s in self.config['spaces'] for s in [space, 'all'])
|
||||
|
||||
def hyperopt_space(self) -> List[Dimension]:
|
||||
def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]:
|
||||
"""
|
||||
Return the space to use during Hyperopt
|
||||
Return the dimensions in the hyperoptimization space.
|
||||
:param space: Defines hyperspace to return dimensions for.
|
||||
If None, then the self.has_space() will be used to return dimensions
|
||||
for all hyperspaces used.
|
||||
"""
|
||||
spaces: List[Dimension] = []
|
||||
if self.has_space('buy'):
|
||||
if space == 'buy' or (space is None and self.has_space('buy')):
|
||||
logger.debug("Hyperopt has 'buy' space")
|
||||
spaces += self.custom_hyperopt.indicator_space()
|
||||
if self.has_space('sell'):
|
||||
if space == 'sell' or (space is None and self.has_space('sell')):
|
||||
logger.debug("Hyperopt has 'sell' space")
|
||||
spaces += self.custom_hyperopt.sell_indicator_space()
|
||||
# Make sure experimental is enabled
|
||||
if 'experimental' not in self.config:
|
||||
self.config['experimental'] = {}
|
||||
self.config['experimental']['use_sell_signal'] = True
|
||||
if self.has_space('roi'):
|
||||
if space == 'roi' or (space is None and self.has_space('roi')):
|
||||
logger.debug("Hyperopt has 'roi' space")
|
||||
spaces += self.custom_hyperopt.roi_space()
|
||||
if self.has_space('stoploss'):
|
||||
if space == 'stoploss' or (space is None and self.has_space('stoploss')):
|
||||
logger.debug("Hyperopt has 'stoploss' space")
|
||||
spaces += self.custom_hyperopt.stoploss_space()
|
||||
return spaces
|
||||
|
||||
def generate_optimizer(self, _params: Dict) -> Dict:
|
||||
def generate_optimizer(self, _params: Dict, iteration=None) -> Dict:
|
||||
"""
|
||||
Used Optimize function. Called once per epoch to optimize whatever is configured.
|
||||
Keep this function as optimized as possible!
|
||||
"""
|
||||
params = self.get_args(_params)
|
||||
|
||||
if self.has_space('roi'):
|
||||
self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params)
|
||||
self.backtesting.strategy.minimal_roi = \
|
||||
self.custom_hyperopt.generate_roi_table(params)
|
||||
|
||||
if self.has_space('buy'):
|
||||
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
|
||||
elif hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||
self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.buy_strategy_generator(params)
|
||||
|
||||
if self.has_space('sell'):
|
||||
self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params)
|
||||
elif hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||
self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore
|
||||
self.backtesting.strategy.advise_sell = \
|
||||
self.custom_hyperopt.sell_strategy_generator(params)
|
||||
|
||||
if self.has_space('stoploss'):
|
||||
self.strategy.stoploss = params['stoploss']
|
||||
self.backtesting.strategy.stoploss = params['stoploss']
|
||||
|
||||
processed = load(self.tickerdata_pickle)
|
||||
|
||||
processed = load(TICKERDATA_PICKLE)
|
||||
min_date, max_date = get_timeframe(processed)
|
||||
results = self.backtest(
|
||||
|
||||
results = self.backtesting.backtest(
|
||||
{
|
||||
'stake_amount': self.config['stake_amount'],
|
||||
'processed': processed,
|
||||
'position_stacking': self.config.get('position_stacking', True),
|
||||
'max_open_trades': self.max_open_trades,
|
||||
'position_stacking': self.position_stacking,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
}
|
||||
)
|
||||
result_explanation = self.format_results(results)
|
||||
results_explanation = self.format_results(results)
|
||||
|
||||
total_profit = results.profit_percent.sum()
|
||||
trade_count = len(results.index)
|
||||
trade_duration = results.trade_duration.mean()
|
||||
total_profit = results.profit_abs.sum()
|
||||
|
||||
# If this evaluation contains too short amount of trades to be
|
||||
# interesting -- consider it as 'bad' (assigned max. loss value)
|
||||
@@ -214,20 +309,23 @@ class Hyperopt(Backtesting):
|
||||
return {
|
||||
'loss': MAX_LOSS,
|
||||
'params': params,
|
||||
'result': result_explanation,
|
||||
'results_explanation': results_explanation,
|
||||
'total_profit': total_profit,
|
||||
}
|
||||
|
||||
loss = self.calculate_loss(total_profit, trade_count, trade_duration)
|
||||
loss = self.calculate_loss(results=results, trade_count=trade_count,
|
||||
min_date=min_date.datetime, max_date=max_date.datetime)
|
||||
|
||||
return {
|
||||
'loss': loss,
|
||||
'params': params,
|
||||
'result': result_explanation,
|
||||
'results_explanation': results_explanation,
|
||||
'total_profit': total_profit,
|
||||
}
|
||||
|
||||
def format_results(self, results: DataFrame) -> str:
|
||||
"""
|
||||
Return the format result in a string
|
||||
Return the formatted results explanation in a string
|
||||
"""
|
||||
trades = len(results.index)
|
||||
avg_profit = results.profit_percent.mean() * 100.0
|
||||
@@ -240,9 +338,9 @@ class Hyperopt(Backtesting):
|
||||
f'Total profit {total_profit: 11.8f} {stake_cur} '
|
||||
f'({profit: 7.2f}Σ%). Avg duration {duration:5.1f} mins.')
|
||||
|
||||
def get_optimizer(self, cpu_count) -> Optimizer:
|
||||
def get_optimizer(self, dimensions, cpu_count) -> Optimizer:
|
||||
return Optimizer(
|
||||
self.hyperopt_space(),
|
||||
dimensions,
|
||||
base_estimator="ET",
|
||||
acq_optimizer="auto",
|
||||
n_initial_points=INITIAL_POINTS,
|
||||
@@ -250,13 +348,30 @@ class Hyperopt(Backtesting):
|
||||
random_state=self.config.get('hyperopt_random_state', None)
|
||||
)
|
||||
|
||||
def run_optimizer_parallel(self, parallel, asked) -> List:
|
||||
def fix_optimizer_models_list(self):
|
||||
"""
|
||||
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
|
||||
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
|
||||
|
||||
This may cease working when skopt updates if implementation of this intrinsic
|
||||
part changes.
|
||||
"""
|
||||
n = len(self.opt.models) - SKOPT_MODELS_MAX_NUM
|
||||
# Keep no more than 2*SKOPT_MODELS_MAX_NUM models in the skopt models list,
|
||||
# remove the old ones. These are actually of no use, the current model
|
||||
# from the estimator is the only one used in the skopt optimizer.
|
||||
# Freqtrade code also does not inspect details of the models.
|
||||
if n >= SKOPT_MODELS_MAX_NUM:
|
||||
logger.debug(f"Fixing skopt models list, removing {n} old items...")
|
||||
del self.opt.models[0:n]
|
||||
|
||||
def run_optimizer_parallel(self, parallel, asked, i) -> List:
|
||||
return parallel(delayed(
|
||||
wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked)
|
||||
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
|
||||
|
||||
def load_previous_results(self):
|
||||
""" read trials file if we have one """
|
||||
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
||||
if self.trials_file.is_file() and self.trials_file.stat().st_size > 0:
|
||||
self.trials = self.read_trials()
|
||||
logger.info(
|
||||
'Loaded %d previous evaluations from disk.',
|
||||
@@ -264,14 +379,12 @@ class Hyperopt(Backtesting):
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
timerange = Arguments.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')))
|
||||
data = load_data(
|
||||
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||
datadir=Path(self.config['datadir']),
|
||||
pairs=self.config['exchange']['pair_whitelist'],
|
||||
ticker_interval=self.ticker_interval,
|
||||
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||
exchange=self.exchange,
|
||||
ticker_interval=self.backtesting.ticker_interval,
|
||||
timerange=timerange
|
||||
)
|
||||
|
||||
@@ -288,48 +401,44 @@ class Hyperopt(Backtesting):
|
||||
(max_date - min_date).days
|
||||
)
|
||||
|
||||
if self.has_space('buy') or self.has_space('sell'):
|
||||
self.strategy.advise_indicators = \
|
||||
self.custom_hyperopt.populate_indicators # type: ignore
|
||||
preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
|
||||
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
||||
|
||||
dump(preprocessed, TICKERDATA_PICKLE)
|
||||
dump(preprocessed, self.tickerdata_pickle)
|
||||
|
||||
# We don't need exchange instance anymore while running hyperopt
|
||||
self.exchange = None # type: ignore
|
||||
self.backtesting.exchange = None # type: ignore
|
||||
|
||||
self.load_previous_results()
|
||||
|
||||
cpus = cpu_count()
|
||||
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
|
||||
logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
|
||||
config_jobs = self.config.get('hyperopt_jobs', -1)
|
||||
logger.info(f'Number of parallel jobs set as: {config_jobs}')
|
||||
|
||||
opt = self.get_optimizer(config_jobs)
|
||||
self.dimensions = self.hyperopt_space()
|
||||
self.opt = self.get_optimizer(self.dimensions, config_jobs)
|
||||
|
||||
if self.config.get('print_colorized', False):
|
||||
colorama_init(autoreset=True)
|
||||
|
||||
try:
|
||||
with Parallel(n_jobs=config_jobs) as parallel:
|
||||
jobs = parallel._effective_n_jobs()
|
||||
logger.info(f'Effective number of parallel workers used: {jobs}')
|
||||
EVALS = max(self.total_tries // jobs, 1)
|
||||
EVALS = max(self.total_epochs // jobs, 1)
|
||||
for i in range(EVALS):
|
||||
asked = opt.ask(n_points=jobs)
|
||||
f_val = self.run_optimizer_parallel(parallel, asked)
|
||||
opt.tell(asked, [i['loss'] for i in f_val])
|
||||
|
||||
self.trials += f_val
|
||||
asked = self.opt.ask(n_points=jobs)
|
||||
f_val = self.run_optimizer_parallel(parallel, asked, i)
|
||||
self.opt.tell(asked, [v['loss'] for v in f_val])
|
||||
self.fix_optimizer_models_list()
|
||||
for j in range(jobs):
|
||||
current = i * jobs + j
|
||||
self.log_results({
|
||||
'loss': f_val[j]['loss'],
|
||||
'current_tries': current,
|
||||
'initial_point': current < INITIAL_POINTS,
|
||||
'total_tries': self.total_tries,
|
||||
'result': f_val[j]['result'],
|
||||
})
|
||||
logger.debug(f"Optimizer params: {f_val[j]['params']}")
|
||||
for j in range(jobs):
|
||||
logger.debug(f"Optimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}")
|
||||
val = f_val[j]
|
||||
val['current_epoch'] = current
|
||||
val['is_initial_point'] = current < INITIAL_POINTS
|
||||
self.log_results(val)
|
||||
self.trials.append(val)
|
||||
logger.debug(f"Optimizer epoch evaluated: {val}")
|
||||
except KeyboardInterrupt:
|
||||
print('User interrupted..')
|
||||
|
||||
|
||||
@@ -2,80 +2,196 @@
|
||||
IHyperOpt interface
|
||||
This module defines the interface to apply for hyperopts
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Callable, List
|
||||
|
||||
from pandas import DataFrame
|
||||
from skopt.space import Dimension
|
||||
from skopt.space import Dimension, Integer, Real
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import round_dict
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_exception_message(method: str, space: str) -> str:
|
||||
return (f"The '{space}' space is included into the hyperoptimization "
|
||||
f"but {method}() method is not found in your "
|
||||
f"custom Hyperopt class. You should either implement this "
|
||||
f"method or remove the '{space}' space from hyperoptimization.")
|
||||
|
||||
|
||||
class IHyperOpt(ABC):
|
||||
"""
|
||||
Interface for freqtrade hyperopts
|
||||
Defines the mandatory structure must follow any custom strategies
|
||||
Defines the mandatory structure must follow any custom hyperopts
|
||||
|
||||
Attributes you can use:
|
||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||
stoploss -> float: optimal stoploss designed for the strategy
|
||||
Class attributes you can use:
|
||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
||||
"""
|
||||
ticker_interval: str
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
self.config = config
|
||||
|
||||
# Assign ticker_interval to be used in hyperopt
|
||||
IHyperOpt.ticker_interval = str(config['ticker_interval'])
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
Populate indicators that will be used in the Buy and Sell strategy.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe().
|
||||
:return: A Dataframe with all mandatory indicators for the strategies.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Create a buy strategy generator
|
||||
Create a buy strategy generator.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy'))
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||
"""
|
||||
Create a sell strategy generator
|
||||
Create a sell strategy generator.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Create an indicator space
|
||||
Create an indicator space.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('indicator_space', 'buy'))
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def sell_indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Create a sell indicator space
|
||||
Create a sell indicator space.
|
||||
"""
|
||||
raise OperationalException(_format_exception_message('sell_indicator_space', 'sell'))
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||
"""
|
||||
Create an roi table
|
||||
Create a ROI table.
|
||||
|
||||
Generates the ROI table that will be used by Hyperopt.
|
||||
You may override it in your custom Hyperopt class.
|
||||
"""
|
||||
roi_table = {}
|
||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
||||
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
||||
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
||||
|
||||
return roi_table
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def stoploss_space() -> List[Dimension]:
|
||||
"""
|
||||
Create a stoploss space
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def roi_space() -> List[Dimension]:
|
||||
"""
|
||||
Create a roi space
|
||||
Create a ROI space.
|
||||
|
||||
Defines values to search for each ROI steps.
|
||||
|
||||
This method implements adaptive roi hyperspace with varied
|
||||
ranges for parameters which automatically adapts to the
|
||||
ticker interval used.
|
||||
|
||||
It's used by Freqtrade by default, if no custom roi_space method is defined.
|
||||
"""
|
||||
|
||||
# Default scaling coefficients for the roi hyperspace. Can be changed
|
||||
# to adjust resulting ranges of the ROI tables.
|
||||
# Increase if you need wider ranges in the roi hyperspace, decrease if shorter
|
||||
# ranges are needed.
|
||||
roi_t_alpha = 1.0
|
||||
roi_p_alpha = 1.0
|
||||
|
||||
ticker_interval_mins = timeframe_to_minutes(IHyperOpt.ticker_interval)
|
||||
|
||||
# We define here limits for the ROI space parameters automagically adapted to the
|
||||
# ticker_interval used by the bot:
|
||||
#
|
||||
# * 'roi_t' (limits for the time intervals in the ROI tables) components
|
||||
# are scaled linearly.
|
||||
# * 'roi_p' (limits for the ROI value steps) components are scaled logarithmically.
|
||||
#
|
||||
# The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space()
|
||||
# method for the 5m ticker interval.
|
||||
roi_t_scale = ticker_interval_mins / 5
|
||||
roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5)
|
||||
roi_limits = {
|
||||
'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t2_min': int(10 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t2_max': int(60 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t3_min': int(10 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t3_max': int(40 * roi_t_scale * roi_t_alpha),
|
||||
'roi_p1_min': 0.01 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p1_max': 0.04 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p2_min': 0.01 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p2_max': 0.07 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p3_min': 0.01 * roi_p_scale * roi_p_alpha,
|
||||
'roi_p3_max': 0.20 * roi_p_scale * roi_p_alpha,
|
||||
}
|
||||
logger.debug(f"Using roi space limits: {roi_limits}")
|
||||
p = {
|
||||
'roi_t1': roi_limits['roi_t1_min'],
|
||||
'roi_t2': roi_limits['roi_t2_min'],
|
||||
'roi_t3': roi_limits['roi_t3_min'],
|
||||
'roi_p1': roi_limits['roi_p1_min'],
|
||||
'roi_p2': roi_limits['roi_p2_min'],
|
||||
'roi_p3': roi_limits['roi_p3_min'],
|
||||
}
|
||||
logger.info(f"Min roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}")
|
||||
p = {
|
||||
'roi_t1': roi_limits['roi_t1_max'],
|
||||
'roi_t2': roi_limits['roi_t2_max'],
|
||||
'roi_t3': roi_limits['roi_t3_max'],
|
||||
'roi_p1': roi_limits['roi_p1_max'],
|
||||
'roi_p2': roi_limits['roi_p2_max'],
|
||||
'roi_p3': roi_limits['roi_p3_max'],
|
||||
}
|
||||
logger.info(f"Max roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}")
|
||||
|
||||
return [
|
||||
Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'),
|
||||
Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'),
|
||||
Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'),
|
||||
Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'),
|
||||
Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'),
|
||||
Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def stoploss_space() -> List[Dimension]:
|
||||
"""
|
||||
Create a stoploss space.
|
||||
|
||||
Defines range of stoploss values to search.
|
||||
You may override it in your custom Hyperopt class.
|
||||
"""
|
||||
return [
|
||||
Real(-0.35, -0.02, name='stoploss'),
|
||||
]
|
||||
|
||||
# This is needed for proper unpickling the class attribute ticker_interval
|
||||
# which is set to the actual value by the resolver.
|
||||
# Why do I still need such shamanic mantras in modern python?
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
state['ticker_interval'] = self.ticker_interval
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
IHyperOpt.ticker_interval = state['ticker_interval']
|
||||
|
||||
25
freqtrade/optimize/hyperopt_loss_interface.py
Normal file
25
freqtrade/optimize/hyperopt_loss_interface.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
IHyperOptLoss interface
|
||||
This module defines the interface for the loss-function for hyperopts
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
|
||||
class IHyperOptLoss(ABC):
|
||||
"""
|
||||
Interface for freqtrade hyperopts Loss functions.
|
||||
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
|
||||
"""
|
||||
ticker_interval: str
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime, *args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
"""
|
||||
38
freqtrade/optimize/hyperopt_loss_onlyprofit.py
Normal file
38
freqtrade/optimize/hyperopt_loss_onlyprofit.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
OnlyProfitHyperOptLoss
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
# This is assumed to be expected avg profit * expected trade count.
|
||||
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
|
||||
# expected max profit = 3.85
|
||||
#
|
||||
# Note, this is ratio. 3.85 stated above means 385Σ%, 3.0 means 300Σ%.
|
||||
#
|
||||
# In this implementation it's only used in calculation of the resulting value
|
||||
# of the objective function as a normalization coefficient and does not
|
||||
# represent any limit for profits as in the Freqtrade legacy default loss function.
|
||||
EXPECTED_MAX_PROFIT = 3.0
|
||||
|
||||
|
||||
class OnlyProfitHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation takes only profit into account.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results.
|
||||
"""
|
||||
total_profit = results.profit_percent.sum()
|
||||
return 1 - total_profit / EXPECTED_MAX_PROFIT
|
||||
45
freqtrade/optimize/hyperopt_loss_sharpe.py
Normal file
45
freqtrade/optimize/hyperopt_loss_sharpe.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
SharpeHyperOptLoss
|
||||
|
||||
This module defines the alternative HyperOptLoss class which can be used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
import numpy as np
|
||||
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation uses the Sharpe Ratio calculation.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for more optimal results.
|
||||
|
||||
Uses Sharpe Ratio calculation.
|
||||
"""
|
||||
total_profit = results.profit_percent
|
||||
days_period = (max_date - min_date).days
|
||||
|
||||
# adding slippage of 0.1% per trade
|
||||
total_profit = total_profit - 0.0005
|
||||
expected_yearly_return = total_profit.sum() / days_period
|
||||
|
||||
if (np.std(total_profit) != 0.):
|
||||
sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365)
|
||||
else:
|
||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||
sharp_ratio = -20.
|
||||
|
||||
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
|
||||
return -sharp_ratio
|
||||
@@ -55,7 +55,6 @@ class VolumePairList(IPairList):
|
||||
# Generate dynamic whitelist
|
||||
self._whitelist = self._gen_pair_whitelist(
|
||||
self._config['stake_currency'], self._sort_key)[:self._number_pairs]
|
||||
logger.info(f"Searching pairs: {self._whitelist}")
|
||||
|
||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||
def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]:
|
||||
@@ -92,4 +91,6 @@ class VolumePairList(IPairList):
|
||||
valid_tickers.remove(t)
|
||||
|
||||
pairs = [s['symbol'] for s in valid_tickers]
|
||||
logger.info(f"Searching pairs: {self._whitelist}")
|
||||
|
||||
return pairs
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
This module contains the class to persist trades into SQLite
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
@@ -19,8 +18,10 @@ from sqlalchemy.pool import StaticPool
|
||||
|
||||
from freqtrade import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_DECL_BASE: Any = declarative_base()
|
||||
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||
|
||||
@@ -48,8 +49,8 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
try:
|
||||
engine = create_engine(db_url, **kwargs)
|
||||
except NoSuchModuleError:
|
||||
raise OperationalException(f'Given value for db_url: \'{db_url}\' '
|
||||
f'is no valid database URL! (See {_SQL_DOCS_URL})')
|
||||
raise OperationalException(f"Given value for db_url: '{db_url}' "
|
||||
f"is no valid database URL! (See {_SQL_DOCS_URL})")
|
||||
|
||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||
Trade.session = session()
|
||||
@@ -209,7 +210,7 @@ class Trade(_DECL_BASE):
|
||||
ticker_interval = Column(Integer, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||
open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed'
|
||||
|
||||
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
||||
@@ -250,7 +251,6 @@ class Trade(_DECL_BASE):
|
||||
:param initial: Called to initiate stop_loss.
|
||||
Skips everything if self.stop_loss is already set.
|
||||
"""
|
||||
|
||||
if initial and not (self.stop_loss is None or self.stop_loss == 0):
|
||||
# Don't modify if called with initial and nothing to do
|
||||
return
|
||||
@@ -259,7 +259,7 @@ class Trade(_DECL_BASE):
|
||||
|
||||
# no stop loss assigned yet
|
||||
if not self.stop_loss:
|
||||
logger.debug("assigning new stop loss")
|
||||
logger.debug(f"{self.pair} - Assigning new stoploss...")
|
||||
self.stop_loss = new_loss
|
||||
self.stop_loss_pct = -1 * abs(stoploss)
|
||||
self.initial_stop_loss = new_loss
|
||||
@@ -269,21 +269,20 @@ class Trade(_DECL_BASE):
|
||||
# evaluate if the stop loss needs to be updated
|
||||
else:
|
||||
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
||||
self.stop_loss = new_loss
|
||||
self.stop_loss_pct = -1 * abs(stoploss)
|
||||
self.stoploss_last_update = datetime.utcnow()
|
||||
logger.debug("adjusted stop loss")
|
||||
else:
|
||||
logger.debug("keeping current stop loss")
|
||||
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
||||
|
||||
logger.debug(
|
||||
f"{self.pair} - current price {current_price:.8f}, "
|
||||
f"bought at {self.open_rate:.8f} and calculated "
|
||||
f"stop loss is at: {self.initial_stop_loss:.8f} initial "
|
||||
f"stop at {self.stop_loss:.8f}. "
|
||||
f"trailing stop loss saved us: "
|
||||
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} "
|
||||
f"and max observed rate was {self.max_rate:.8f}")
|
||||
f"{self.pair} - Stoploss adjusted. current_price={current_price:.8f}, "
|
||||
f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate:.8f}, "
|
||||
f"initial_stop_loss={self.initial_stop_loss:.8f}, "
|
||||
f"stop_loss={self.stop_loss:.8f}. "
|
||||
f"Trailing stoploss saved us: "
|
||||
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
|
||||
|
||||
def update(self, order: Dict) -> None:
|
||||
"""
|
||||
@@ -331,23 +330,18 @@ class Trade(_DECL_BASE):
|
||||
self
|
||||
)
|
||||
|
||||
def calc_open_trade_price(
|
||||
self,
|
||||
fee: Optional[float] = None) -> float:
|
||||
def calc_open_trade_price(self, fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculate the open_rate including fee.
|
||||
:param fee: fee to use on the open rate (optional).
|
||||
If rate is not set self.fee will be used
|
||||
:return: Price in of the open trade incl. Fees
|
||||
"""
|
||||
|
||||
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
|
||||
fees = buy_trade * Decimal(fee or self.fee_open)
|
||||
return float(buy_trade + fees)
|
||||
|
||||
def calc_close_trade_price(
|
||||
self,
|
||||
rate: Optional[float] = None,
|
||||
def calc_close_trade_price(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculate the close_rate including fee
|
||||
@@ -357,7 +351,6 @@ class Trade(_DECL_BASE):
|
||||
If rate is not set self.close_rate will be used
|
||||
:return: Price in BTC of the open trade
|
||||
"""
|
||||
|
||||
if rate is None and not self.close_rate:
|
||||
return 0.0
|
||||
|
||||
@@ -365,9 +358,7 @@ class Trade(_DECL_BASE):
|
||||
fees = sell_trade * Decimal(fee or self.fee_close)
|
||||
return float(sell_trade - fees)
|
||||
|
||||
def calc_profit(
|
||||
self,
|
||||
rate: Optional[float] = None,
|
||||
def calc_profit(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculate the absolute profit in stake currency between Close and Open trade
|
||||
@@ -385,9 +376,7 @@ class Trade(_DECL_BASE):
|
||||
profit = close_trade_price - open_trade_price
|
||||
return float(f"{profit:.8f}")
|
||||
|
||||
def calc_profit_percent(
|
||||
self,
|
||||
rate: Optional[float] = None,
|
||||
def calc_profit_percent(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculates the profit in percentage (including fee).
|
||||
@@ -396,7 +385,6 @@ class Trade(_DECL_BASE):
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
:return: profit in percentage as float
|
||||
"""
|
||||
|
||||
open_trade_price = self.calc_open_trade_price()
|
||||
close_trade_price = self.calc_close_trade_price(
|
||||
rate=(rate or self.close_rate),
|
||||
@@ -436,8 +424,8 @@ class Trade(_DECL_BASE):
|
||||
and trade.initial_stop_loss_pct != desired_stoploss):
|
||||
# Stoploss value got changed
|
||||
|
||||
logger.info(f"Stoploss for {trade} needs adjustment.")
|
||||
logger.info(f"Stoploss for {trade} needs adjustment...")
|
||||
# Force reset of stoploss
|
||||
trade.stop_loss = None
|
||||
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||
logger.info(f"new stoploss: {trade.stop_loss}, ")
|
||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
||||
|
||||
36
freqtrade/plot/plot_utils.py
Normal file
36
freqtrade/plot/plot_utils.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.utils import setup_utils_configuration
|
||||
|
||||
|
||||
def validate_plot_args(args: Dict[str, Any]):
|
||||
if not args.get('datadir') and not args.get('config'):
|
||||
raise OperationalException(
|
||||
"You need to specify either `--datadir` or `--config` "
|
||||
"for plot-profit and plot-dataframe.")
|
||||
|
||||
|
||||
def start_plot_dataframe(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Entrypoint for dataframe plotting
|
||||
"""
|
||||
# Import here to avoid errors if plot-dependencies are not installed.
|
||||
from freqtrade.plot.plotting import load_and_plot_trades
|
||||
validate_plot_args(args)
|
||||
config = setup_utils_configuration(args, RunMode.PLOT)
|
||||
|
||||
load_and_plot_trades(config)
|
||||
|
||||
|
||||
def start_plot_profit(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Entrypoint for plot_profit
|
||||
"""
|
||||
# Import here to avoid errors if plot-dependencies are not installed.
|
||||
from freqtrade.plot.plotting import plot_profit
|
||||
validate_plot_args(args)
|
||||
config = setup_utils_configuration(args, RunMode.PLOT)
|
||||
|
||||
plot_profit(config)
|
||||
@@ -1,22 +1,60 @@
|
||||
import logging
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import (combine_tickers_with_mean,
|
||||
create_cum_profit,
|
||||
extract_trades_of_period, load_trades)
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
try:
|
||||
from plotly import tools
|
||||
from plotly.subplots import make_subplots
|
||||
from plotly.offline import plot
|
||||
import plotly.graph_objs as go
|
||||
import plotly.graph_objects as go
|
||||
except ImportError:
|
||||
logger.exception("Module plotly not found \n Please install using `pip install plotly`")
|
||||
logger.exception("Module plotly not found \n Please install using `pip3 install plotly`")
|
||||
exit(1)
|
||||
|
||||
|
||||
def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots:
|
||||
def init_plotscript(config):
|
||||
"""
|
||||
Initialize objects needed for plotting
|
||||
:return: Dict with tickers, trades and pairs
|
||||
"""
|
||||
|
||||
if "pairs" in config:
|
||||
pairs = config["pairs"]
|
||||
else:
|
||||
pairs = config["exchange"]["pair_whitelist"]
|
||||
|
||||
# Set timerange to use
|
||||
timerange = TimeRange.parse_timerange(config.get("timerange"))
|
||||
|
||||
tickers = history.load_data(
|
||||
datadir=Path(str(config.get("datadir"))),
|
||||
pairs=pairs,
|
||||
ticker_interval=config.get('ticker_interval', '5m'),
|
||||
timerange=timerange,
|
||||
)
|
||||
|
||||
trades = load_trades(config['trade_source'],
|
||||
db_url=config.get('db_url'),
|
||||
exportfilename=config.get('exportfilename'),
|
||||
)
|
||||
|
||||
return {"tickers": tickers,
|
||||
"trades": trades,
|
||||
"pairs": pairs,
|
||||
}
|
||||
|
||||
|
||||
def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots:
|
||||
"""
|
||||
Generator all the indicator selected by the user for a specific row
|
||||
:param fig: Plot figure to append to
|
||||
@@ -33,7 +71,7 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m
|
||||
mode='lines',
|
||||
name=indicator
|
||||
)
|
||||
fig.append_trace(scattergl, row, 1)
|
||||
fig.add_trace(scattergl, row, 1)
|
||||
else:
|
||||
logger.info(
|
||||
'Indicator "%s" ignored. Reason: This indicator is not found '
|
||||
@@ -44,9 +82,29 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m
|
||||
return fig
|
||||
|
||||
|
||||
def plot_trades(fig, trades: pd.DataFrame):
|
||||
def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots:
|
||||
"""
|
||||
Plot trades to "fig"
|
||||
Add profit-plot
|
||||
:param fig: Plot figure to append to
|
||||
:param row: row number for this plot
|
||||
:param data: candlestick DataFrame
|
||||
:param column: Column to use for plot
|
||||
:param name: Name to use
|
||||
:return: fig with added profit plot
|
||||
"""
|
||||
profit = go.Scattergl(
|
||||
x=data.index,
|
||||
y=data[column],
|
||||
name=name,
|
||||
)
|
||||
fig.add_trace(profit, row, 1)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
||||
"""
|
||||
Add trades to "fig"
|
||||
"""
|
||||
# Trades can be empty
|
||||
if trades is not None and len(trades) > 0:
|
||||
@@ -79,20 +137,16 @@ def plot_trades(fig, trades: pd.DataFrame):
|
||||
color='red'
|
||||
)
|
||||
)
|
||||
fig.append_trace(trade_buys, 1, 1)
|
||||
fig.append_trace(trade_sells, 1, 1)
|
||||
fig.add_trace(trade_buys, 1, 1)
|
||||
fig.add_trace(trade_sells, 1, 1)
|
||||
else:
|
||||
logger.warning("No trades found.")
|
||||
return fig
|
||||
|
||||
|
||||
def generate_graph(
|
||||
pair: str,
|
||||
data: pd.DataFrame,
|
||||
trades: pd.DataFrame = None,
|
||||
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None,
|
||||
indicators1: List[str] = [],
|
||||
indicators2: List[str] = [],
|
||||
) -> go.Figure:
|
||||
indicators2: List[str] = [],) -> go.Figure:
|
||||
"""
|
||||
Generate the graph from the data generated by Backtesting or from DB
|
||||
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
|
||||
@@ -105,7 +159,7 @@ def generate_graph(
|
||||
"""
|
||||
|
||||
# Define the graph
|
||||
fig = tools.make_subplots(
|
||||
fig = make_subplots(
|
||||
rows=3,
|
||||
cols=1,
|
||||
shared_xaxes=True,
|
||||
@@ -127,7 +181,7 @@ def generate_graph(
|
||||
close=data.close,
|
||||
name='Price'
|
||||
)
|
||||
fig.append_trace(candles, 1, 1)
|
||||
fig.add_trace(candles, 1, 1)
|
||||
|
||||
if 'buy' in data.columns:
|
||||
df_buy = data[data['buy'] == 1]
|
||||
@@ -144,7 +198,7 @@ def generate_graph(
|
||||
color='green',
|
||||
)
|
||||
)
|
||||
fig.append_trace(buys, 1, 1)
|
||||
fig.add_trace(buys, 1, 1)
|
||||
else:
|
||||
logger.warning("No buy-signals found.")
|
||||
|
||||
@@ -163,7 +217,7 @@ def generate_graph(
|
||||
color='red',
|
||||
)
|
||||
)
|
||||
fig.append_trace(sells, 1, 1)
|
||||
fig.add_trace(sells, 1, 1)
|
||||
else:
|
||||
logger.warning("No sell-signals found.")
|
||||
|
||||
@@ -182,11 +236,11 @@ def generate_graph(
|
||||
fillcolor="rgba(0,176,246,0.2)",
|
||||
line={'color': 'rgba(255,255,255,0)'},
|
||||
)
|
||||
fig.append_trace(bb_lower, 1, 1)
|
||||
fig.append_trace(bb_upper, 1, 1)
|
||||
fig.add_trace(bb_lower, 1, 1)
|
||||
fig.add_trace(bb_upper, 1, 1)
|
||||
|
||||
# Add indicators to main plot
|
||||
fig = generate_row(fig=fig, row=1, indicators=indicators1, data=data)
|
||||
fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data)
|
||||
|
||||
fig = plot_trades(fig, trades)
|
||||
|
||||
@@ -196,15 +250,64 @@ def generate_graph(
|
||||
y=data['volume'],
|
||||
name='Volume'
|
||||
)
|
||||
fig.append_trace(volume, 2, 1)
|
||||
fig.add_trace(volume, 2, 1)
|
||||
|
||||
# Add indicators to seperate row
|
||||
fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data)
|
||||
fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def generate_plot_file(fig, pair, ticker_interval) -> None:
|
||||
def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
|
||||
trades: pd.DataFrame) -> go.Figure:
|
||||
# Combine close-values for all pairs, rename columns to "pair"
|
||||
df_comb = combine_tickers_with_mean(tickers, "close")
|
||||
|
||||
# Add combined cumulative profit
|
||||
df_comb = create_cum_profit(df_comb, trades, 'cum_profit')
|
||||
|
||||
# Plot the pairs average close prices, and total profit growth
|
||||
avgclose = go.Scattergl(
|
||||
x=df_comb.index,
|
||||
y=df_comb['mean'],
|
||||
name='Avg close price',
|
||||
)
|
||||
|
||||
fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
|
||||
row_width=[1, 1, 1],
|
||||
vertical_spacing=0.05,
|
||||
subplot_titles=["AVG Close Price", "Combined Profit", "Profit per pair"])
|
||||
fig['layout'].update(title="Freqtrade Profit plot")
|
||||
fig['layout']['yaxis1'].update(title='Price')
|
||||
fig['layout']['yaxis2'].update(title='Profit')
|
||||
fig['layout']['yaxis3'].update(title='Profit')
|
||||
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
||||
|
||||
fig.add_trace(avgclose, 1, 1)
|
||||
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
|
||||
|
||||
for pair in pairs:
|
||||
profit_col = f'cum_profit_{pair}'
|
||||
df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col)
|
||||
|
||||
fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def generate_plot_filename(pair, ticker_interval) -> str:
|
||||
"""
|
||||
Generate filenames per pair/ticker_interval to be used for storing plots
|
||||
"""
|
||||
pair_name = pair.replace("/", "_")
|
||||
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html'
|
||||
|
||||
logger.info('Generate plot file for %s', pair)
|
||||
|
||||
return file_name
|
||||
|
||||
|
||||
def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False) -> None:
|
||||
"""
|
||||
Generate a plot html file from pre populated fig plotly object
|
||||
:param fig: Plotly Figure to plot
|
||||
@@ -212,12 +315,71 @@ def generate_plot_file(fig, pair, ticker_interval) -> None:
|
||||
:param ticker_interval: Used as part of the filename
|
||||
:return: None
|
||||
"""
|
||||
logger.info('Generate plot file for %s', pair)
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pair_name = pair.replace("/", "_")
|
||||
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html'
|
||||
_filename = directory.joinpath(filename)
|
||||
plot(fig, filename=str(_filename),
|
||||
auto_open=auto_open)
|
||||
logger.info(f"Stored plot as {_filename}")
|
||||
|
||||
Path("user_data/plots").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)),
|
||||
auto_open=False)
|
||||
def load_and_plot_trades(config: Dict[str, Any]):
|
||||
"""
|
||||
From configuration provided
|
||||
- Initializes plot-script
|
||||
- Get tickers data
|
||||
- Generate Dafaframes populated with indicators and signals based on configured strategy
|
||||
- Load trades excecuted during the selected period
|
||||
- Generate Plotly plot objects
|
||||
- Generate plot files
|
||||
:return: None
|
||||
"""
|
||||
strategy = StrategyResolver(config).strategy
|
||||
|
||||
plot_elements = init_plotscript(config)
|
||||
trades = plot_elements['trades']
|
||||
pair_counter = 0
|
||||
for pair, data in plot_elements["tickers"].items():
|
||||
pair_counter += 1
|
||||
logger.info("analyse pair %s", pair)
|
||||
tickers = {}
|
||||
tickers[pair] = data
|
||||
|
||||
dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair})
|
||||
trades_pair = trades.loc[trades['pair'] == pair]
|
||||
trades_pair = extract_trades_of_period(dataframe, trades_pair)
|
||||
|
||||
fig = generate_candlestick_graph(
|
||||
pair=pair,
|
||||
data=dataframe,
|
||||
trades=trades_pair,
|
||||
indicators1=config["indicators1"],
|
||||
indicators2=config["indicators2"],
|
||||
)
|
||||
|
||||
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),
|
||||
directory=config['user_data_dir'] / "plot")
|
||||
|
||||
logger.info('End of plotting process. %s plots generated', pair_counter)
|
||||
|
||||
|
||||
def plot_profit(config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Plots the total profit for all pairs.
|
||||
Note, the profit calculation isn't realistic.
|
||||
But should be somewhat proportional, and therefor useful
|
||||
in helping out to find a good algorithm.
|
||||
"""
|
||||
plot_elements = init_plotscript(config)
|
||||
trades = load_trades(config['trade_source'],
|
||||
db_url=str(config.get('db_url')),
|
||||
exportfilename=str(config.get('exportfilename')),
|
||||
)
|
||||
# Filter trades to relevant pairs
|
||||
trades = trades[trades['pair'].isin(plot_elements["pairs"])]
|
||||
|
||||
# Create an average close price of all the pairs that were involved.
|
||||
# this could be useful to gauge the overall market trend
|
||||
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], trades)
|
||||
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
||||
directory=config['user_data_dir'] / "plot", auto_open=True)
|
||||
|
||||
@@ -28,6 +28,7 @@ class ExchangeResolver(IResolver):
|
||||
except ImportError:
|
||||
logger.info(
|
||||
f"No {exchange_name} specific subclass found. Using the generic class instead.")
|
||||
if not hasattr(self, "exchange"):
|
||||
self.exchange = Exchange(config)
|
||||
|
||||
def _load_exchange(
|
||||
@@ -44,13 +45,13 @@ class ExchangeResolver(IResolver):
|
||||
|
||||
exchange = ex_class(kwargs['config'])
|
||||
if exchange:
|
||||
logger.info("Using resolved exchange %s", exchange_name)
|
||||
logger.info(f"Using resolved exchange '{exchange_name}'...")
|
||||
return exchange
|
||||
except AttributeError:
|
||||
# Pass and raise ImportError instead
|
||||
pass
|
||||
|
||||
raise ImportError(
|
||||
"Impossible to load Exchange '{}'. This class does not exist"
|
||||
" or contains Python code errors".format(exchange_name)
|
||||
f"Impossible to load Exchange '{exchange_name}'. This class does not exist "
|
||||
"or contains Python code errors."
|
||||
)
|
||||
|
||||
@@ -7,8 +7,10 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
|
||||
from freqtrade.constants import DEFAULT_HYPEROPT
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_HYPEROPT_LOSS
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
||||
from freqtrade.resolvers import IResolver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,7 +23,62 @@ class HyperOptResolver(IResolver):
|
||||
|
||||
__slots__ = ['hyperopt']
|
||||
|
||||
def __init__(self, config: Optional[Dict] = None) -> None:
|
||||
def __init__(self, config: Dict) -> None:
|
||||
"""
|
||||
Load the custom class from config parameter
|
||||
:param config: configuration dictionary
|
||||
"""
|
||||
|
||||
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
|
||||
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
|
||||
self.hyperopt = self._load_hyperopt(hyperopt_name, config,
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
||||
logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
|
||||
"Using populate_buy_trend from the strategy.")
|
||||
if not hasattr(self.hyperopt, 'populate_sell_trend'):
|
||||
logger.warning("Hyperopt class does not provide populate_sell_trend() method. "
|
||||
"Using populate_sell_trend from the strategy.")
|
||||
|
||||
def _load_hyperopt(
|
||||
self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt:
|
||||
"""
|
||||
Search and loads the specified hyperopt.
|
||||
:param hyperopt_name: name of the module to import
|
||||
:param config: configuration dictionary
|
||||
:param extra_dir: additional directory to search for the given hyperopt
|
||||
:return: HyperOpt instance or None
|
||||
"""
|
||||
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
||||
|
||||
abs_paths = [
|
||||
config['user_data_dir'].joinpath('hyperopts'),
|
||||
current_path,
|
||||
]
|
||||
|
||||
if extra_dir:
|
||||
# Add extra hyperopt directory on top of search paths
|
||||
abs_paths.insert(0, Path(extra_dir).resolve())
|
||||
|
||||
hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
|
||||
object_name=hyperopt_name, kwargs={'config': config})
|
||||
if hyperopt:
|
||||
return hyperopt
|
||||
raise OperationalException(
|
||||
f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist "
|
||||
"or contains Python code errors."
|
||||
)
|
||||
|
||||
|
||||
class HyperOptLossResolver(IResolver):
|
||||
"""
|
||||
This class contains all the logic to load custom hyperopt loss class
|
||||
"""
|
||||
|
||||
__slots__ = ['hyperoptloss']
|
||||
|
||||
def __init__(self, config: Dict = None) -> None:
|
||||
"""
|
||||
Load the custom class from config parameter
|
||||
:param config: configuration dictionary or None
|
||||
@@ -29,49 +86,44 @@ class HyperOptResolver(IResolver):
|
||||
config = config or {}
|
||||
|
||||
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
|
||||
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
|
||||
self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path'))
|
||||
hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
|
||||
self.hyperoptloss = self._load_hyperoptloss(
|
||||
hyperopt_name, config, extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
# Assign ticker_interval to be used in hyperopt
|
||||
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
|
||||
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
|
||||
|
||||
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
||||
logger.warning("Custom Hyperopt does not provide populate_buy_trend. "
|
||||
"Using populate_buy_trend from DefaultStrategy.")
|
||||
if not hasattr(self.hyperopt, 'populate_sell_trend'):
|
||||
logger.warning("Custom Hyperopt does not provide populate_sell_trend. "
|
||||
"Using populate_sell_trend from DefaultStrategy.")
|
||||
if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'):
|
||||
raise OperationalException(
|
||||
f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.")
|
||||
|
||||
def _load_hyperopt(
|
||||
self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt:
|
||||
def _load_hyperoptloss(
|
||||
self, hyper_loss_name: str, config: Dict,
|
||||
extra_dir: Optional[str] = None) -> IHyperOptLoss:
|
||||
"""
|
||||
Search and loads the specified hyperopt.
|
||||
:param hyperopt_name: name of the module to import
|
||||
Search and loads the specified hyperopt loss class.
|
||||
:param hyper_loss_name: name of the module to import
|
||||
:param config: configuration dictionary
|
||||
:param extra_dir: additional directory to search for the given hyperopt
|
||||
:return: HyperOpt instance or None
|
||||
:return: HyperOptLoss instance or None
|
||||
"""
|
||||
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
||||
|
||||
abs_paths = [
|
||||
current_path.parent.parent.joinpath('user_data/hyperopts'),
|
||||
config['user_data_dir'].joinpath('hyperopts'),
|
||||
current_path,
|
||||
]
|
||||
|
||||
if extra_dir:
|
||||
# Add extra hyperopt directory on top of search paths
|
||||
abs_paths.insert(0, Path(extra_dir))
|
||||
abs_paths.insert(0, Path(extra_dir).resolve())
|
||||
|
||||
for _path in abs_paths:
|
||||
try:
|
||||
hyperopt = self._search_object(directory=_path, object_type=IHyperOpt,
|
||||
object_name=hyperopt_name)
|
||||
if hyperopt:
|
||||
logger.info("Using resolved hyperopt %s from '%s'", hyperopt_name, _path)
|
||||
return hyperopt
|
||||
except FileNotFoundError:
|
||||
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
|
||||
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss,
|
||||
object_name=hyper_loss_name)
|
||||
if hyperoptloss:
|
||||
return hyperoptloss
|
||||
|
||||
raise ImportError(
|
||||
"Impossible to load Hyperopt '{}'. This class does not exist"
|
||||
" or contains Python code errors".format(hyperopt_name)
|
||||
raise OperationalException(
|
||||
f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist "
|
||||
"or contains Python code errors."
|
||||
)
|
||||
|
||||
@@ -7,12 +7,12 @@ import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Type, Any
|
||||
from typing import Any, List, Optional, Tuple, Type, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IResolver(object):
|
||||
class IResolver:
|
||||
"""
|
||||
This class contains all the logic to load custom classes
|
||||
"""
|
||||
@@ -29,7 +29,8 @@ class IResolver(object):
|
||||
"""
|
||||
|
||||
# Generate spec based on absolute path
|
||||
spec = importlib.util.spec_from_file_location('unknown', str(module_path))
|
||||
# Pass object_name as first argument to have logging print a reasonable name.
|
||||
spec = importlib.util.spec_from_file_location(object_name, str(module_path))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||
@@ -45,7 +46,7 @@ class IResolver(object):
|
||||
|
||||
@staticmethod
|
||||
def _search_object(directory: Path, object_type, object_name: str,
|
||||
kwargs: dict = {}) -> Optional[Any]:
|
||||
kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]:
|
||||
"""
|
||||
Search for the objectname in the given directory
|
||||
:param directory: relative or absolute directory path
|
||||
@@ -57,9 +58,33 @@ class IResolver(object):
|
||||
if not str(entry).endswith('.py'):
|
||||
logger.debug('Ignoring %s', entry)
|
||||
continue
|
||||
module_path = entry.resolve()
|
||||
obj = IResolver._get_valid_object(
|
||||
object_type, Path.resolve(directory.joinpath(entry)), object_name
|
||||
object_type, module_path, object_name
|
||||
)
|
||||
if obj:
|
||||
return obj(**kwargs)
|
||||
return (obj(**kwargs), module_path)
|
||||
return (None, None)
|
||||
|
||||
@staticmethod
|
||||
def _load_object(paths: List[Path], object_type, object_name: str,
|
||||
kwargs: dict = {}) -> Optional[Any]:
|
||||
"""
|
||||
Try to load object from path list.
|
||||
"""
|
||||
|
||||
for _path in paths:
|
||||
try:
|
||||
(module, module_path) = IResolver._search_object(directory=_path,
|
||||
object_type=object_type,
|
||||
object_name=object_name,
|
||||
kwargs=kwargs)
|
||||
if module:
|
||||
logger.info(
|
||||
f"Using resolved {object_type.__name__.lower()[1:]} {object_name} "
|
||||
f"from '{module_path}'...")
|
||||
return module
|
||||
except FileNotFoundError:
|
||||
logger.warning('Path "%s" does not exist.', _path.resolve())
|
||||
|
||||
return None
|
||||
|
||||
@@ -6,6 +6,7 @@ This module load custom hyperopts
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
from freqtrade.resolvers import IResolver
|
||||
|
||||
@@ -24,36 +25,30 @@ class PairListResolver(IResolver):
|
||||
Load the custom class from config parameter
|
||||
:param config: configuration dictionary or None
|
||||
"""
|
||||
self.pairlist = self._load_pairlist(pairlist_name, kwargs={'freqtrade': freqtrade,
|
||||
self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'freqtrade': freqtrade,
|
||||
'config': config})
|
||||
|
||||
def _load_pairlist(
|
||||
self, pairlist_name: str, kwargs: dict) -> IPairList:
|
||||
self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
|
||||
"""
|
||||
Search and loads the specified pairlist.
|
||||
:param pairlist_name: name of the module to import
|
||||
:param config: configuration dictionary
|
||||
:param extra_dir: additional directory to search for the given pairlist
|
||||
:return: PairList instance or None
|
||||
"""
|
||||
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
|
||||
|
||||
abs_paths = [
|
||||
current_path.parent.parent.joinpath('user_data/pairlist'),
|
||||
config['user_data_dir'].joinpath('pairlist'),
|
||||
current_path,
|
||||
]
|
||||
|
||||
for _path in abs_paths:
|
||||
try:
|
||||
pairlist = self._search_object(directory=_path, object_type=IPairList,
|
||||
object_name=pairlist_name,
|
||||
kwargs=kwargs)
|
||||
pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
|
||||
object_name=pairlist_name, kwargs=kwargs)
|
||||
if pairlist:
|
||||
logger.info("Using resolved pairlist %s from '%s'", pairlist_name, _path)
|
||||
return pairlist
|
||||
except FileNotFoundError:
|
||||
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
|
||||
|
||||
raise ImportError(
|
||||
"Impossible to load Pairlist '{}'. This class does not exist"
|
||||
" or contains Python code errors".format(pairlist_name)
|
||||
raise OperationalException(
|
||||
f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist "
|
||||
"or contains Python code errors."
|
||||
)
|
||||
|
||||
@@ -11,9 +11,8 @@ from inspect import getfullargspec
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade import constants, OperationalException
|
||||
from freqtrade.resolvers import IResolver
|
||||
from freqtrade.strategy import import_strategy
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -123,7 +122,7 @@ class StrategyResolver(IResolver):
|
||||
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||
|
||||
abs_paths = [
|
||||
Path.cwd().joinpath('user_data/strategies'),
|
||||
config['user_data_dir'].joinpath('strategies'),
|
||||
current_path,
|
||||
]
|
||||
|
||||
@@ -132,7 +131,7 @@ class StrategyResolver(IResolver):
|
||||
abs_paths.insert(0, Path(extra_dir).resolve())
|
||||
|
||||
if ":" in strategy_name:
|
||||
logger.info("loading base64 endocded strategy")
|
||||
logger.info("loading base64 encoded strategy")
|
||||
strat = strategy_name.split(":")
|
||||
|
||||
if len(strat) == 2:
|
||||
@@ -147,25 +146,20 @@ class StrategyResolver(IResolver):
|
||||
# register temp path with the bot
|
||||
abs_paths.insert(0, temp.resolve())
|
||||
|
||||
for _path in abs_paths:
|
||||
try:
|
||||
strategy = self._search_object(directory=_path, object_type=IStrategy,
|
||||
strategy = self._load_object(paths=abs_paths, object_type=IStrategy,
|
||||
object_name=strategy_name, kwargs={'config': config})
|
||||
if strategy:
|
||||
logger.info("Using resolved strategy %s from '%s'", strategy_name, _path)
|
||||
strategy._populate_fun_len = len(
|
||||
getfullargspec(strategy.populate_indicators).args)
|
||||
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
|
||||
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
|
||||
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
|
||||
try:
|
||||
return import_strategy(strategy, config=config)
|
||||
except TypeError as e:
|
||||
logger.warning(
|
||||
f"Impossible to load strategy '{strategy}' from {_path}. Error: {e}")
|
||||
except FileNotFoundError:
|
||||
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
|
||||
if any([x == 2 for x in [strategy._populate_fun_len,
|
||||
strategy._buy_fun_len,
|
||||
strategy._sell_fun_len]]):
|
||||
strategy.INTERFACE_VERSION = 1
|
||||
|
||||
raise ImportError(
|
||||
return strategy
|
||||
|
||||
raise OperationalException(
|
||||
f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
|
||||
" or contains Python code errors"
|
||||
"or contains Python code errors."
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ from freqtrade.constants import SUPPORTED_FIAT
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CryptoFiat(object):
|
||||
class CryptoFiat:
|
||||
"""
|
||||
Object to describe what is the price of Crypto-currency in a FIAT
|
||||
"""
|
||||
@@ -60,7 +60,7 @@ class CryptoFiat(object):
|
||||
return self._expiration - time.time() <= 0
|
||||
|
||||
|
||||
class CryptoToFiatConverter(object):
|
||||
class CryptoToFiatConverter:
|
||||
"""
|
||||
Main class to initiate Crypto to FIAT.
|
||||
This object contains a list of pair Crypto, FIAT
|
||||
@@ -104,7 +104,7 @@ class CryptoToFiatConverter(object):
|
||||
:return: float, value in fiat of the crypto-currency amount
|
||||
"""
|
||||
if crypto_symbol == fiat_symbol:
|
||||
return crypto_amount
|
||||
return float(crypto_amount)
|
||||
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
|
||||
return float(crypto_amount) * float(price)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import Dict, Any, List, Optional
|
||||
|
||||
import arrow
|
||||
import sqlalchemy as sql
|
||||
from numpy import mean, nan_to_num, NAN
|
||||
from numpy import mean, NAN
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import TemporaryError, DependencyException
|
||||
@@ -54,7 +54,7 @@ class RPCException(Exception):
|
||||
}
|
||||
|
||||
|
||||
class RPC(object):
|
||||
class RPC:
|
||||
"""
|
||||
RPC class can be used to have extra feature, like bot data, and access to DB data
|
||||
"""
|
||||
@@ -195,9 +195,9 @@ class RPC(object):
|
||||
trades = Trade.query.order_by(Trade.id).all()
|
||||
|
||||
profit_all_coin = []
|
||||
profit_all_percent = []
|
||||
profit_all_perc = []
|
||||
profit_closed_coin = []
|
||||
profit_closed_percent = []
|
||||
profit_closed_perc = []
|
||||
durations = []
|
||||
|
||||
for trade in trades:
|
||||
@@ -211,7 +211,7 @@ class RPC(object):
|
||||
if not trade.is_open:
|
||||
profit_percent = trade.calc_profit_percent()
|
||||
profit_closed_coin.append(trade.calc_profit())
|
||||
profit_closed_percent.append(profit_percent)
|
||||
profit_closed_perc.append(profit_percent)
|
||||
else:
|
||||
# Get current rate
|
||||
try:
|
||||
@@ -223,7 +223,7 @@ class RPC(object):
|
||||
profit_all_coin.append(
|
||||
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))
|
||||
)
|
||||
profit_all_percent.append(profit_percent)
|
||||
profit_all_perc.append(profit_percent)
|
||||
|
||||
best_pair = Trade.session.query(
|
||||
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
|
||||
@@ -238,7 +238,8 @@ class RPC(object):
|
||||
|
||||
# Prepare data to display
|
||||
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
||||
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
||||
profit_closed_percent = (round(mean(profit_closed_perc) * 100, 2) if profit_closed_perc
|
||||
else 0.0)
|
||||
profit_closed_fiat = self._fiat_converter.convert_amount(
|
||||
profit_closed_coin_sum,
|
||||
stake_currency,
|
||||
@@ -246,7 +247,7 @@ class RPC(object):
|
||||
) if self._fiat_converter else 0
|
||||
|
||||
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
||||
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
|
||||
profit_all_percent = round(mean(profit_all_perc) * 100, 2) if profit_all_perc else 0.0
|
||||
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||
profit_all_coin_sum,
|
||||
stake_currency,
|
||||
@@ -281,10 +282,11 @@ class RPC(object):
|
||||
rate = 1.0
|
||||
else:
|
||||
try:
|
||||
if coin in('USDT', 'USD', 'EUR'):
|
||||
rate = 1.0 / self._freqtrade.get_sell_rate('BTC/' + coin, False)
|
||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC")
|
||||
if pair.startswith("BTC"):
|
||||
rate = 1.0 / self._freqtrade.get_sell_rate(pair, False)
|
||||
else:
|
||||
rate = self._freqtrade.get_sell_rate(coin + '/BTC', False)
|
||||
rate = self._freqtrade.get_sell_rate(pair, False)
|
||||
except (TemporaryError, DependencyException):
|
||||
logger.warning(f" Could not get rate for pair {coin}.")
|
||||
continue
|
||||
@@ -292,13 +294,16 @@ class RPC(object):
|
||||
total = total + est_btc
|
||||
output.append({
|
||||
'currency': coin,
|
||||
'available': balance['free'],
|
||||
'balance': balance['total'],
|
||||
'pending': balance['used'],
|
||||
'free': balance['free'] if balance['free'] is not None else 0,
|
||||
'balance': balance['total'] if balance['total'] is not None else 0,
|
||||
'used': balance['used'] if balance['used'] is not None else 0,
|
||||
'est_btc': est_btc,
|
||||
})
|
||||
if total == 0.0:
|
||||
raise RPCException('all balances are zero')
|
||||
if self._freqtrade.config.get('dry_run', False):
|
||||
raise RPCException('Running in Dry Run, balances are not available.')
|
||||
else:
|
||||
raise RPCException('All balances are zero.')
|
||||
|
||||
symbol = fiat_display_currency
|
||||
value = self._fiat_converter.convert_amount(total, 'BTC',
|
||||
|
||||
@@ -9,7 +9,7 @@ from freqtrade.rpc import RPC, RPCMessageType
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RPCManager(object):
|
||||
class RPCManager:
|
||||
"""
|
||||
Class to manage RPC objects (Telegram, Slack, ...)
|
||||
"""
|
||||
@@ -56,7 +56,10 @@ class RPCManager(object):
|
||||
logger.info('Sending rpc message: %s', msg)
|
||||
for mod in self.registered_modules:
|
||||
logger.debug('Forwarding message to rpc.%s', mod.name)
|
||||
try:
|
||||
mod.send_msg(msg)
|
||||
except NotImplementedError:
|
||||
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
|
||||
|
||||
def startup_messages(self, config, pairlist) -> None:
|
||||
if config.get('dry_run', False):
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
This module manage Telegram communication
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, List
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from tabulate import tabulate
|
||||
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
|
||||
from telegram import ParseMode, ReplyKeyboardMarkup, Update
|
||||
from telegram.error import NetworkError, TelegramError
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
from telegram.ext import CommandHandler, Updater, CallbackContext
|
||||
|
||||
from freqtrade.__init__ import __version__
|
||||
from freqtrade.rpc import RPC, RPCException, RPCMessageType
|
||||
@@ -31,7 +31,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||
"""
|
||||
def wrapper(self, *args, **kwargs):
|
||||
""" Decorator logic """
|
||||
update = kwargs.get('update') or args[1]
|
||||
update = kwargs.get('update') or args[0]
|
||||
|
||||
# Reject unauthorized messages
|
||||
chat_id = int(self._config['telegram']['chat_id'])
|
||||
@@ -79,7 +79,8 @@ class Telegram(RPC):
|
||||
registers all known command handlers
|
||||
and starts polling for message updates
|
||||
"""
|
||||
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
||||
self._updater = Updater(token=self._config['telegram']['token'], workers=0,
|
||||
use_context=True)
|
||||
|
||||
# Register command handler and start telegram message polling
|
||||
handles = [
|
||||
@@ -96,7 +97,7 @@ class Telegram(RPC):
|
||||
CommandHandler('reload_conf', self._reload_conf),
|
||||
CommandHandler('stopbuy', self._stopbuy),
|
||||
CommandHandler('whitelist', self._whitelist),
|
||||
CommandHandler('blacklist', self._blacklist, pass_args=True),
|
||||
CommandHandler('blacklist', self._blacklist),
|
||||
CommandHandler('edge', self._edge),
|
||||
CommandHandler('help', self._help),
|
||||
CommandHandler('version', self._version),
|
||||
@@ -175,7 +176,7 @@ class Telegram(RPC):
|
||||
self._send_msg(message)
|
||||
|
||||
@authorized_only
|
||||
def _status(self, bot: Bot, update: Update) -> None:
|
||||
def _status(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /status.
|
||||
Returns the current TradeThread status
|
||||
@@ -184,11 +185,8 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Check if additional parameters are passed
|
||||
params = update.message.text.replace('/status', '').split(' ') \
|
||||
if update.message.text else []
|
||||
if 'table' in params:
|
||||
self._status_table(bot, update)
|
||||
if 'table' in context.args:
|
||||
self._status_table(update, context)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -217,16 +215,17 @@ class Telegram(RPC):
|
||||
|
||||
"*Open Order:* `{open_order}`" if r['open_order'] else ""
|
||||
]
|
||||
messages.append("\n".join(filter(None, lines)).format(**r))
|
||||
# Filter empty lines using list-comprehension
|
||||
messages.append("\n".join([l for l in lines if l]).format(**r))
|
||||
|
||||
for msg in messages:
|
||||
self._send_msg(msg, bot=bot)
|
||||
self._send_msg(msg)
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _status_table(self, bot: Bot, update: Update) -> None:
|
||||
def _status_table(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /status table.
|
||||
Returns the current TradeThread status in table format
|
||||
@@ -239,10 +238,10 @@ class Telegram(RPC):
|
||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _daily(self, bot: Bot, update: Update) -> None:
|
||||
def _daily(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /daily <n>
|
||||
Returns a daily profit (in BTC) over the last n days.
|
||||
@@ -253,8 +252,8 @@ class Telegram(RPC):
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(update.message.text.replace('/daily', '').strip())
|
||||
except (TypeError, ValueError):
|
||||
timescale = int(context.args[0])
|
||||
except (TypeError, ValueError, IndexError):
|
||||
timescale = 7
|
||||
try:
|
||||
stats = self._rpc_daily_profit(
|
||||
@@ -271,12 +270,12 @@ class Telegram(RPC):
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
||||
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _profit(self, bot: Bot, update: Update) -> None:
|
||||
def _profit(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /profit.
|
||||
Returns a cumulative profit statistics.
|
||||
@@ -316,12 +315,12 @@ class Telegram(RPC):
|
||||
f"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
||||
f"*Avg. Duration:* `{avg_duration}`\n" \
|
||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
|
||||
self._send_msg(markdown_msg, bot=bot)
|
||||
self._send_msg(markdown_msg)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _balance(self, bot: Bot, update: Update) -> None:
|
||||
def _balance(self, update: Update, context: CallbackContext) -> None:
|
||||
""" Handler for /balance """
|
||||
try:
|
||||
result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
|
||||
@@ -329,16 +328,16 @@ class Telegram(RPC):
|
||||
for currency in result['currencies']:
|
||||
if currency['est_btc'] > 0.0001:
|
||||
curr_output = "*{currency}:*\n" \
|
||||
"\t`Available: {available: .8f}`\n" \
|
||||
"\t`Available: {free: .8f}`\n" \
|
||||
"\t`Balance: {balance: .8f}`\n" \
|
||||
"\t`Pending: {pending: .8f}`\n" \
|
||||
"\t`Pending: {used: .8f}`\n" \
|
||||
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
||||
else:
|
||||
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
|
||||
|
||||
# Handle overflowing messsage length
|
||||
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
self._send_msg(output, bot=bot)
|
||||
self._send_msg(output)
|
||||
output = curr_output
|
||||
else:
|
||||
output += curr_output
|
||||
@@ -346,12 +345,12 @@ class Telegram(RPC):
|
||||
output += "\n*Estimated Value*:\n" \
|
||||
"\t`BTC: {total: .8f}`\n" \
|
||||
"\t`{symbol}: {value: .2f}`\n".format(**result)
|
||||
self._send_msg(output, bot=bot)
|
||||
self._send_msg(output)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _start(self, bot: Bot, update: Update) -> None:
|
||||
def _start(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /start.
|
||||
Starts TradeThread
|
||||
@@ -360,10 +359,10 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_start()
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _stop(self, bot: Bot, update: Update) -> None:
|
||||
def _stop(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /stop.
|
||||
Stops TradeThread
|
||||
@@ -372,10 +371,10 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_stop()
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _reload_conf(self, bot: Bot, update: Update) -> None:
|
||||
def _reload_conf(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /reload_conf.
|
||||
Triggers a config file reload
|
||||
@@ -384,10 +383,10 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_reload_conf()
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _stopbuy(self, bot: Bot, update: Update) -> None:
|
||||
def _stopbuy(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /stop_buy.
|
||||
Sets max_open_trades to 0 and gracefully sells all open trades
|
||||
@@ -396,10 +395,10 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_stopbuy()
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg))
|
||||
|
||||
@authorized_only
|
||||
def _forcesell(self, bot: Bot, update: Update) -> None:
|
||||
def _forcesell(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /forcesell <id>.
|
||||
Sells the given trade at current price
|
||||
@@ -408,16 +407,16 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
trade_id = update.message.text.replace('/forcesell', '').strip()
|
||||
trade_id = context.args[0] if len(context.args) > 0 else None
|
||||
try:
|
||||
msg = self._rpc_forcesell(trade_id)
|
||||
self._send_msg('Forcesell Result: `{result}`'.format(**msg), bot=bot)
|
||||
self._send_msg('Forcesell Result: `{result}`'.format(**msg))
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _forcebuy(self, bot: Bot, update: Update) -> None:
|
||||
def _forcebuy(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /forcebuy <asset> <price>.
|
||||
Buys a pair trade at the given or current price
|
||||
@@ -426,16 +425,15 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
message = update.message.text.replace('/forcebuy', '').strip().split()
|
||||
pair = message[0]
|
||||
price = float(message[1]) if len(message) > 1 else None
|
||||
pair = context.args[0]
|
||||
price = float(context.args[1]) if len(context.args) > 1 else None
|
||||
try:
|
||||
self._rpc_forcebuy(pair, price)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _performance(self, bot: Bot, update: Update) -> None:
|
||||
def _performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /performance.
|
||||
Shows a performance statistic from finished trades
|
||||
@@ -454,10 +452,10 @@ class Telegram(RPC):
|
||||
message = '<b>Performance:</b>\n{}'.format(stats)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _count(self, bot: Bot, update: Update) -> None:
|
||||
def _count(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /count.
|
||||
Returns the number of trades running
|
||||
@@ -474,10 +472,10 @@ class Telegram(RPC):
|
||||
logger.debug(message)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _whitelist(self, bot: Bot, update: Update) -> None:
|
||||
def _whitelist(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /whitelist
|
||||
Shows the currently active whitelist
|
||||
@@ -491,17 +489,17 @@ class Telegram(RPC):
|
||||
logger.debug(message)
|
||||
self._send_msg(message)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _blacklist(self, bot: Bot, update: Update, args: List[str]) -> None:
|
||||
def _blacklist(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /blacklist
|
||||
Shows the currently active blacklist
|
||||
"""
|
||||
try:
|
||||
|
||||
blacklist = self._rpc_blacklist(args)
|
||||
blacklist = self._rpc_blacklist(context.args)
|
||||
|
||||
message = f"Blacklist contains {blacklist['length']} pairs\n"
|
||||
message += f"`{', '.join(blacklist['blacklist'])}`"
|
||||
@@ -509,10 +507,10 @@ class Telegram(RPC):
|
||||
logger.debug(message)
|
||||
self._send_msg(message)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _edge(self, bot: Bot, update: Update) -> None:
|
||||
def _edge(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /edge
|
||||
Shows information related to Edge
|
||||
@@ -521,12 +519,12 @@ class Telegram(RPC):
|
||||
edge_pairs = self._rpc_edge()
|
||||
edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple')
|
||||
message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>'
|
||||
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _help(self, bot: Bot, update: Update) -> None:
|
||||
def _help(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /help.
|
||||
Show commands of the bot
|
||||
@@ -558,10 +556,10 @@ class Telegram(RPC):
|
||||
"*/help:* `This help message`\n" \
|
||||
"*/version:* `Show version`"
|
||||
|
||||
self._send_msg(message, bot=bot)
|
||||
self._send_msg(message)
|
||||
|
||||
@authorized_only
|
||||
def _version(self, bot: Bot, update: Update) -> None:
|
||||
def _version(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /version.
|
||||
Show version information
|
||||
@@ -569,10 +567,9 @@ class Telegram(RPC):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
||||
self._send_msg('*Version:* `{}`'.format(__version__))
|
||||
|
||||
def _send_msg(self, msg: str, bot: Bot = None,
|
||||
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||
"""
|
||||
Send given markdown message
|
||||
:param msg: message
|
||||
@@ -580,7 +577,6 @@ class Telegram(RPC):
|
||||
:param parse_mode: telegram parse mode
|
||||
:return: None
|
||||
"""
|
||||
bot = bot or self._updater.bot
|
||||
|
||||
keyboard = [['/daily', '/profit', '/balance'],
|
||||
['/status', '/status table', '/performance'],
|
||||
@@ -590,7 +586,7 @@ class Telegram(RPC):
|
||||
|
||||
try:
|
||||
try:
|
||||
bot.send_message(
|
||||
self._updater.bot.send_message(
|
||||
self._config['telegram']['chat_id'],
|
||||
text=msg,
|
||||
parse_mode=parse_mode,
|
||||
@@ -603,7 +599,7 @@ class Telegram(RPC):
|
||||
'Telegram NetworkError: %s! Trying one more time.',
|
||||
network_err.message
|
||||
)
|
||||
bot.send_message(
|
||||
self._updater.bot.send_message(
|
||||
self._config['telegram']['chat_id'],
|
||||
text=msg,
|
||||
parse_mode=parse_mode,
|
||||
|
||||
@@ -43,7 +43,9 @@ class Webhook(RPC):
|
||||
valuedict = self._config['webhook'].get('webhookbuy', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||
elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION,
|
||||
RPCMessageType.CUSTOM_NOTIFICATION,
|
||||
RPCMessageType.WARNING_NOTIFICATION):
|
||||
valuedict = self._config['webhook'].get('webhookstatus', None)
|
||||
else:
|
||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||
|
||||
@@ -25,4 +25,5 @@ class RunMode(Enum):
|
||||
BACKTEST = "backtest"
|
||||
EDGE = "edge"
|
||||
HYPEROPT = "hyperopt"
|
||||
PLOT = "plot"
|
||||
OTHER = "other" # Used for plotting scripts and test
|
||||
|
||||
@@ -1,45 +1 @@
|
||||
import logging
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
# Import Default-Strategy to have hyperopt correctly resolve
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
|
||||
"""
|
||||
Imports given Strategy instance to global scope
|
||||
of freqtrade.strategy and returns an instance of it
|
||||
"""
|
||||
|
||||
# Copy all attributes from base class and class
|
||||
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
|
||||
|
||||
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
|
||||
# `TypeError: can't pickle _abc_data objects``
|
||||
# This will only apply to python 3.7
|
||||
if sys.version_info.major == 3 and sys.version_info.minor == 7 and '_abc_impl' in comb:
|
||||
del comb['_abc_impl']
|
||||
|
||||
attr = deepcopy(comb)
|
||||
|
||||
# Adjust module name
|
||||
attr['__module__'] = 'freqtrade.strategy'
|
||||
|
||||
name = strategy.__class__.__name__
|
||||
clazz = type(name, (IStrategy,), attr)
|
||||
|
||||
logger.debug(
|
||||
'Imported strategy %s.%s as %s.%s',
|
||||
strategy.__module__, strategy.__class__.__name__,
|
||||
clazz.__module__, strategy.__class__.__name__,
|
||||
)
|
||||
|
||||
# Modify global scope to declare class
|
||||
globals()[name] = clazz
|
||||
|
||||
return clazz(config)
|
||||
from freqtrade.strategy.interface import IStrategy # noqa: F401
|
||||
|
||||
@@ -4,15 +4,18 @@ import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.indicator_helpers import fishers_inverse
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
|
||||
class DefaultStrategy(IStrategy):
|
||||
"""
|
||||
Default Strategy provided by freqtrade bot.
|
||||
You can override it with your own strategy
|
||||
Please do not modify this strategy, it's intended for internal use only.
|
||||
Please look at the SampleStrategy in the user_data/strategy directory
|
||||
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||
for samples and inspiration.
|
||||
"""
|
||||
INTERFACE_VERSION = 2
|
||||
|
||||
# Minimal ROI designed for the strategy
|
||||
minimal_roi = {
|
||||
@@ -73,67 +76,25 @@ class DefaultStrategy(IStrategy):
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
# Awesome oscillator
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
"""
|
||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
"""
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
# MFI
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
|
||||
# Minus Directional Indicator / Movement
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# Plus Directional Indicator / Movement
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
"""
|
||||
# ROC
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
"""
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi'] = fishers_inverse(dataframe['rsi'])
|
||||
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
"""
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
"""
|
||||
|
||||
# Overlap Studies
|
||||
# ------------------------------------
|
||||
|
||||
# Previous Bollinger bands
|
||||
# Because ta.BBANDS implementation is broken with small numbers, it actually
|
||||
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
|
||||
# and use middle band instead.
|
||||
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
@@ -142,88 +103,11 @@ class DefaultStrategy(IStrategy):
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
|
||||
# SAR Parabol
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# Cycle Indicator
|
||||
# ------------------------------------
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
@@ -4,9 +4,9 @@ This module defines the interface to apply for strategies
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Dict, List, NamedTuple, Tuple
|
||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
||||
import warnings
|
||||
|
||||
import arrow
|
||||
@@ -39,6 +39,7 @@ class SellType(Enum):
|
||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||
SELL_SIGNAL = "sell_signal"
|
||||
FORCE_SELL = "force_sell"
|
||||
EMERGENCY_SELL = "emergency_sell"
|
||||
NONE = ""
|
||||
|
||||
|
||||
@@ -60,6 +61,11 @@ class IStrategy(ABC):
|
||||
stoploss -> float: optimal stoploss designed for the strategy
|
||||
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||
"""
|
||||
# Strategy interface version
|
||||
# Default to version 2
|
||||
# Version 1 is the initial interface without metadata dict
|
||||
# Version 2 populate_* include metadata dict
|
||||
INTERFACE_VERSION: int = 2
|
||||
|
||||
_populate_fun_len: int = 0
|
||||
_buy_fun_len: int = 0
|
||||
@@ -107,6 +113,7 @@ class IStrategy(ABC):
|
||||
self.config = config
|
||||
# Dict to determine if analysis is necessary
|
||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||
self._pair_locked_until: Dict[str, datetime] = {}
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
@@ -154,13 +161,47 @@ class IStrategy(ABC):
|
||||
"""
|
||||
return self.__class__.__name__
|
||||
|
||||
def lock_pair(self, pair: str, until: datetime) -> None:
|
||||
"""
|
||||
Locks pair until a given timestamp happens.
|
||||
Locked pairs are not analyzed, and are prevented from opening new trades.
|
||||
:param pair: Pair to lock
|
||||
:param until: datetime in UTC until the pair should be blocked from opening new trades.
|
||||
Needs to be timezone aware `datetime.now(timezone.utc)`
|
||||
"""
|
||||
self._pair_locked_until[pair] = until
|
||||
|
||||
def is_pair_locked(self, pair: str) -> bool:
|
||||
"""
|
||||
Checks if a pair is currently locked
|
||||
"""
|
||||
if pair not in self._pair_locked_until:
|
||||
return False
|
||||
return self._pair_locked_until[pair] >= datetime.now(timezone.utc)
|
||||
|
||||
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
:param dataframe: Dataframe containing ticker data
|
||||
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||
:return: DataFrame with ticker data and indicator data
|
||||
"""
|
||||
logger.debug("TA Analysis Launched")
|
||||
dataframe = self.advise_indicators(dataframe, metadata)
|
||||
dataframe = self.advise_buy(dataframe, metadata)
|
||||
dataframe = self.advise_sell(dataframe, metadata)
|
||||
return dataframe
|
||||
|
||||
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set.
|
||||
:param dataframe: Dataframe containing ticker data
|
||||
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
|
||||
:return: DataFrame with ticker data and indicator data
|
||||
"""
|
||||
pair = str(metadata.get('pair'))
|
||||
|
||||
# Test if seen this pair and last candle before.
|
||||
@@ -168,10 +209,7 @@ class IStrategy(ABC):
|
||||
if (not self.process_only_new_candles or
|
||||
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
||||
# Defs that only make change on new candle data.
|
||||
logger.debug("TA Analysis Launched")
|
||||
dataframe = self.advise_indicators(dataframe, metadata)
|
||||
dataframe = self.advise_buy(dataframe, metadata)
|
||||
dataframe = self.advise_sell(dataframe, metadata)
|
||||
dataframe = self.analyze_ticker(dataframe, metadata)
|
||||
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
||||
else:
|
||||
logger.debug("Skipping TA Analysis for already analyzed candle")
|
||||
@@ -198,7 +236,7 @@ class IStrategy(ABC):
|
||||
return False, False
|
||||
|
||||
try:
|
||||
dataframe = self.analyze_ticker(dataframe, {'pair': pair})
|
||||
dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair})
|
||||
except ValueError as error:
|
||||
logger.warning(
|
||||
'Unable to analyze ticker for pair %s: %s',
|
||||
@@ -246,14 +284,13 @@ class IStrategy(ABC):
|
||||
sell: bool, low: float = None, high: float = None,
|
||||
force_stoploss: float = 0) -> SellCheckTuple:
|
||||
"""
|
||||
This function evaluate if on the condition required to trigger a sell has been reached
|
||||
if the threshold is reached and updates the trade record.
|
||||
This function evaluates if one of the conditions required to trigger a sell
|
||||
has been reached, which can either be a stop-loss, ROI or sell-signal.
|
||||
:param low: Only used during backtesting to simulate stoploss
|
||||
:param high: Only used during backtesting, to simulate ROI
|
||||
:param force_stoploss: Externally provided stoploss
|
||||
:return: True if trade should be sold, False otherwise
|
||||
"""
|
||||
|
||||
# Set current rate to low for backtesting sell
|
||||
current_rate = low or rate
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
@@ -265,6 +302,8 @@ class IStrategy(ABC):
|
||||
force_stoploss=force_stoploss, high=high)
|
||||
|
||||
if stoplossflag.sell_flag:
|
||||
logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
|
||||
f"sell_type={stoplossflag.sell_type}")
|
||||
return stoplossflag
|
||||
|
||||
# Set current rate to high for backtesting sell
|
||||
@@ -273,22 +312,31 @@ class IStrategy(ABC):
|
||||
experimental = self.config.get('experimental', {})
|
||||
|
||||
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
||||
logger.debug('Buy signal still active - not selling.')
|
||||
# This one is noisy, commented out
|
||||
# logger.debug(f"{trade.pair} - Buy signal still active. sell_flag=False")
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
|
||||
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
||||
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
|
||||
logger.debug('Required profit reached. Selling..')
|
||||
logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, "
|
||||
f"sell_type=SellType.ROI")
|
||||
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
|
||||
|
||||
if experimental.get('sell_profit_only', False):
|
||||
logger.debug('Checking if trade is profitable..')
|
||||
# This one is noisy, commented out
|
||||
# logger.debug(f"{trade.pair} - Checking if trade is profitable...")
|
||||
if trade.calc_profit(rate=rate) <= 0:
|
||||
# This one is noisy, commented out
|
||||
# logger.debug(f"{trade.pair} - Trade is not profitable. sell_flag=False")
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
|
||||
if sell and not buy and experimental.get('use_sell_signal', False):
|
||||
logger.debug('Sell signal received. Selling..')
|
||||
logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
|
||||
f"sell_type=SellType.SELL_SIGNAL")
|
||||
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
|
||||
|
||||
# This one is noisy, commented out...
|
||||
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
|
||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
||||
@@ -299,7 +347,6 @@ class IStrategy(ABC):
|
||||
decides to sell or not
|
||||
:param current_profit: current profit in percent
|
||||
"""
|
||||
|
||||
trailing_stop = self.config.get('trailing_stop', False)
|
||||
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
|
||||
|
||||
@@ -320,7 +367,7 @@ class IStrategy(ABC):
|
||||
if 'trailing_stop_positive' in self.config and high_profit > sl_offset:
|
||||
# Ignore mypy error check in configuration that this is a float
|
||||
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
|
||||
logger.debug(f"using positive stop loss: {stop_loss_value} "
|
||||
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
||||
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
|
||||
|
||||
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
|
||||
@@ -330,40 +377,49 @@ class IStrategy(ABC):
|
||||
(trade.stop_loss >= current_rate) and
|
||||
(not self.order_types.get('stoploss_on_exchange'))):
|
||||
|
||||
selltype = SellType.STOP_LOSS
|
||||
sell_type = SellType.STOP_LOSS
|
||||
|
||||
# If initial stoploss is not the same as current one then it is trailing.
|
||||
if trade.initial_stop_loss != trade.stop_loss:
|
||||
selltype = SellType.TRAILING_STOP_LOSS
|
||||
sell_type = SellType.TRAILING_STOP_LOSS
|
||||
logger.debug(
|
||||
f"HIT STOP: current price at {current_rate:.6f}, "
|
||||
f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, "
|
||||
f"stoploss is {trade.stop_loss:.6f}, "
|
||||
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
||||
f"trade opened at {trade.open_rate:.6f}")
|
||||
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")
|
||||
logger.debug(f"{trade.pair} - Trailing stop saved "
|
||||
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
|
||||
|
||||
logger.debug('Stop loss hit.')
|
||||
return SellCheckTuple(sell_flag=True, sell_type=selltype)
|
||||
return SellCheckTuple(sell_flag=True, sell_type=sell_type)
|
||||
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
|
||||
def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]:
|
||||
"""
|
||||
Based on trade duration defines the ROI entry that may have been reached.
|
||||
:param trade_dur: trade duration in minutes
|
||||
:return: minimal ROI entry value or None if none proper ROI entry was found.
|
||||
"""
|
||||
# Get highest entry in ROI dict where key <= trade-duration
|
||||
roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys()))
|
||||
if not roi_list:
|
||||
return None
|
||||
roi_entry = max(roi_list)
|
||||
return self.minimal_roi[roi_entry]
|
||||
|
||||
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||
"""
|
||||
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
||||
Based on trade duration, current price and ROI configuration, decides whether bot should
|
||||
sell. Requires current_profit to be in percent!!
|
||||
:return: True if bot should sell at current rate
|
||||
"""
|
||||
|
||||
# Check if time matches and current rate is above threshold
|
||||
trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||
|
||||
# Get highest entry in ROI dict where key >= trade-duration
|
||||
roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys())))
|
||||
threshold = self.minimal_roi[roi_entry]
|
||||
if current_profit > threshold:
|
||||
return True
|
||||
|
||||
trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60)
|
||||
roi = self.min_roi_reached_entry(trade_dur)
|
||||
if roi is None:
|
||||
return False
|
||||
else:
|
||||
return current_profit > roi
|
||||
|
||||
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||
# pragma pylint: disable=protected-access
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'limit'
|
||||
time_in_force = 'ioc'
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
amount=1, rate=200, time_in_force=time_in_force)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args[0][1] == order_type
|
||||
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||
assert api_mock.create_order.call_args[0][3] == 1
|
||||
assert api_mock.create_order.call_args[0][4] == 200
|
||||
assert api_mock.create_order.call_args[0][5] == {'timeInForce': 'ioc',
|
||||
'trading_agreement': 'agree'}
|
||||
|
||||
|
||||
def test_sell_kraken_trading_agreement(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'market'
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args[0][1] == order_type
|
||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||
assert api_mock.create_order.call_args[0][3] == 1
|
||||
assert api_mock.create_order.call_args[0][4] is None
|
||||
assert api_mock.create_order.call_args[0][5] == {'trading_agreement': 'agree'}
|
||||
@@ -1,528 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
from filelock import Timeout
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from freqtrade import DependencyException
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.data.history import load_tickerdata_file
|
||||
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
|
||||
from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE
|
||||
from freqtrade.optimize import setup_configuration, start_hyperopt
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def hyperopt(default_conf, mocker):
|
||||
patch_exchange(mocker)
|
||||
return Hyperopt(default_conf)
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
def create_trials(mocker, hyperopt) -> None:
|
||||
"""
|
||||
When creating trials, mock the hyperopt Trials so that *by default*
|
||||
- we don't create any pickle'd files in the filesystem
|
||||
- we might have a pickle'd file so make sure that we return
|
||||
false when looking for it
|
||||
"""
|
||||
hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
|
||||
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
||||
|
||||
|
||||
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.HYPEROPT)
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples)
|
||||
|
||||
assert 'live' not in config
|
||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'refresh_pairs' not in config
|
||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'timerange' not in config
|
||||
assert 'runmode' in config
|
||||
assert config['runmode'] == RunMode.HYPEROPT
|
||||
|
||||
|
||||
def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--datadir', '/foo/bar',
|
||||
'hyperopt',
|
||||
'--ticker-interval', '1m',
|
||||
'--timerange', ':100',
|
||||
'--refresh-pairs-cached',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--epochs', '1000',
|
||||
'--spaces', 'all',
|
||||
'--print-all'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.HYPEROPT)
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert config['runmode'] == RunMode.HYPEROPT
|
||||
|
||||
assert log_has(
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
caplog.record_tuples)
|
||||
|
||||
assert 'position_stacking' in config
|
||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'use_max_market_positions' in config
|
||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
||||
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
||||
|
||||
assert 'refresh_pairs' in config
|
||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'timerange' in config
|
||||
assert log_has(
|
||||
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
assert 'epochs' in config
|
||||
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...',
|
||||
caplog.record_tuples)
|
||||
|
||||
assert 'spaces' in config
|
||||
assert log_has(
|
||||
'Parameter -s/--spaces detected: {}'.format(config['spaces']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'print_all' in config
|
||||
assert log_has('Parameter --print-all detected ...', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
hyperopts = DefaultHyperOpts
|
||||
delattr(hyperopts, 'populate_buy_trend')
|
||||
delattr(hyperopts, 'populate_sell_trend')
|
||||
mocker.patch(
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
|
||||
MagicMock(return_value=hyperopts)
|
||||
)
|
||||
x = HyperOptResolver(default_conf, ).hyperopt
|
||||
assert not hasattr(x, 'populate_buy_trend')
|
||||
assert not hasattr(x, 'populate_sell_trend')
|
||||
assert log_has("Custom Hyperopt does not provide populate_sell_trend. "
|
||||
"Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples)
|
||||
assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
|
||||
"Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples)
|
||||
assert hasattr(x, "ticker_interval")
|
||||
|
||||
|
||||
def test_start(mocker, default_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_hyperopt(args)
|
||||
|
||||
import pprint
|
||||
pprint.pprint(caplog.record_tuples)
|
||||
|
||||
assert log_has(
|
||||
'Starting freqtrade in Hyperopt mode',
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert start_mock.call_count == 1
|
||||
|
||||
|
||||
def test_start_no_data(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_hyperopt(args)
|
||||
|
||||
import pprint
|
||||
pprint.pprint(caplog.record_tuples)
|
||||
|
||||
assert log_has('No data found. Terminating.', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_start_failure(mocker, default_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'TestStrategy',
|
||||
'hyperopt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
with pytest.raises(DependencyException):
|
||||
start_hyperopt(args)
|
||||
assert log_has(
|
||||
"Please don't use --strategy for hyperopt.",
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_start_filelock(mocker, default_conf, caplog) -> None:
|
||||
start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE))
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_hyperopt(args)
|
||||
assert log_has(
|
||||
"Another running instance of freqtrade Hyperopt detected.",
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
|
||||
|
||||
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
|
||||
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20)
|
||||
under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20)
|
||||
assert over > correct
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None:
|
||||
shorter = hyperopt.calculate_loss(1, 100, 20)
|
||||
longer = hyperopt.calculate_loss(1, 100, 30)
|
||||
assert shorter < longer
|
||||
|
||||
|
||||
def test_loss_calculation_has_limited_profit(hyperopt) -> None:
|
||||
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
|
||||
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
|
||||
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
|
||||
assert over == correct
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
||||
hyperopt.current_best_loss = 2
|
||||
hyperopt.log_results(
|
||||
{
|
||||
'loss': 1,
|
||||
'current_tries': 1,
|
||||
'total_tries': 2,
|
||||
'result': 'foo.',
|
||||
'initial_point': False
|
||||
}
|
||||
)
|
||||
out, err = capsys.readouterr()
|
||||
assert ' 2/2: foo. Objective: 1.00000' in out
|
||||
|
||||
|
||||
def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
|
||||
hyperopt.current_best_loss = 2
|
||||
hyperopt.log_results(
|
||||
{
|
||||
'loss': 3,
|
||||
}
|
||||
)
|
||||
assert caplog.record_tuples == []
|
||||
|
||||
|
||||
def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker, hyperopt)
|
||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
hyperopt.trials = trials
|
||||
hyperopt.save_trials()
|
||||
|
||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
assert log_has(
|
||||
'Saving 1 evaluations to \'{}\''.format(trials_file),
|
||||
caplog.record_tuples
|
||||
)
|
||||
mock_dump.assert_called_once()
|
||||
|
||||
|
||||
def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker, hyperopt)
|
||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
|
||||
hyperopt_trial = hyperopt.read_trials()
|
||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
assert log_has(
|
||||
'Reading Trials from \'{}\''.format(trials_file),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert hyperopt_trial == trials
|
||||
mock_load.assert_called_once()
|
||||
|
||||
|
||||
def test_roi_table_generation(hyperopt) -> None:
|
||||
params = {
|
||||
'roi_t1': 5,
|
||||
'roi_t2': 10,
|
||||
'roi_t3': 15,
|
||||
'roi_p1': 1,
|
||||
'roi_p2': 2,
|
||||
'roi_p3': 3,
|
||||
}
|
||||
|
||||
assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||
|
||||
|
||||
def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
'hyperopt_jobs': 1, })
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
|
||||
hyperopt.start()
|
||||
parallel.assert_called_once()
|
||||
assert log_has('Best result:\nfoo result\nwith values:\n', caplog.record_tuples)
|
||||
assert dumper.called
|
||||
# Should be called twice, once for tickerdata, once to save evaluations
|
||||
assert dumper.call_count == 2
|
||||
|
||||
|
||||
def test_format_results(hyperopt):
|
||||
# Test with BTC as stake_currency
|
||||
trades = [
|
||||
('ETH/BTC', 2, 2, 123),
|
||||
('LTC/BTC', 1, 1, 123),
|
||||
('XPR/BTC', -1, -2, -246)
|
||||
]
|
||||
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
result = hyperopt.format_results(df)
|
||||
assert result.find(' 66.67%')
|
||||
assert result.find('Total profit 1.00000000 BTC')
|
||||
assert result.find('2.0000Σ %')
|
||||
|
||||
# Test with EUR as stake_currency
|
||||
trades = [
|
||||
('ETH/EUR', 2, 2, 123),
|
||||
('LTC/EUR', 1, 1, 123),
|
||||
('XPR/EUR', -1, -2, -246)
|
||||
]
|
||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||
result = hyperopt.format_results(df)
|
||||
assert result.find('Total profit 1.00000000 EUR')
|
||||
|
||||
|
||||
def test_has_space(hyperopt):
|
||||
hyperopt.config.update({'spaces': ['buy', 'roi']})
|
||||
assert hyperopt.has_space('roi')
|
||||
assert hyperopt.has_space('buy')
|
||||
assert not hyperopt.has_space('stoploss')
|
||||
|
||||
hyperopt.config.update({'spaces': ['all']})
|
||||
assert hyperopt.has_space('buy')
|
||||
|
||||
|
||||
def test_populate_indicators(hyperopt) -> None:
|
||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)}
|
||||
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||
{'pair': 'UNITTEST/BTC'})
|
||||
|
||||
# Check if some indicators are generated. We will not test all of them
|
||||
assert 'adx' in dataframe
|
||||
assert 'mfi' in dataframe
|
||||
assert 'rsi' in dataframe
|
||||
|
||||
|
||||
def test_buy_strategy_generator(hyperopt) -> None:
|
||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)}
|
||||
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||
{'pair': 'UNITTEST/BTC'})
|
||||
|
||||
populate_buy_trend = hyperopt.custom_hyperopt.buy_strategy_generator(
|
||||
{
|
||||
'adx-value': 20,
|
||||
'fastd-value': 20,
|
||||
'mfi-value': 20,
|
||||
'rsi-value': 20,
|
||||
'adx-enabled': True,
|
||||
'fastd-enabled': True,
|
||||
'mfi-enabled': True,
|
||||
'rsi-enabled': True,
|
||||
'trigger': 'bb_lower'
|
||||
}
|
||||
)
|
||||
result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'})
|
||||
# Check if some indicators are generated. We will not test all of them
|
||||
assert 'buy' in result
|
||||
assert 1 in result['buy']
|
||||
|
||||
|
||||
def test_generate_optimizer(mocker, default_conf) -> None:
|
||||
default_conf.update({'config': 'config.json.example'})
|
||||
default_conf.update({'timerange': None})
|
||||
default_conf.update({'spaces': 'all'})
|
||||
default_conf.update({'hyperopt_min_trades': 1})
|
||||
|
||||
trades = [
|
||||
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||
]
|
||||
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
||||
MagicMock(return_value=backtest_result)
|
||||
)
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
||||
|
||||
optimizer_param = {
|
||||
'adx-value': 0,
|
||||
'fastd-value': 35,
|
||||
'mfi-value': 0,
|
||||
'rsi-value': 0,
|
||||
'adx-enabled': False,
|
||||
'fastd-enabled': True,
|
||||
'mfi-enabled': False,
|
||||
'rsi-enabled': False,
|
||||
'trigger': 'macd_cross_signal',
|
||||
'sell-adx-value': 0,
|
||||
'sell-fastd-value': 75,
|
||||
'sell-mfi-value': 0,
|
||||
'sell-rsi-value': 0,
|
||||
'sell-adx-enabled': False,
|
||||
'sell-fastd-enabled': True,
|
||||
'sell-mfi-enabled': False,
|
||||
'sell-rsi-enabled': False,
|
||||
'sell-trigger': 'macd_cross_signal',
|
||||
'roi_t1': 60.0,
|
||||
'roi_t2': 30.0,
|
||||
'roi_t3': 20.0,
|
||||
'roi_p1': 0.01,
|
||||
'roi_p2': 0.01,
|
||||
'roi_p3': 0.1,
|
||||
'stoploss': -0.4,
|
||||
}
|
||||
response_expected = {
|
||||
'loss': 1.9840569076926293,
|
||||
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||
'( 2.31Σ%). Avg duration 100.0 mins.',
|
||||
'params': optimizer_param
|
||||
}
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
|
||||
assert generate_optimizer_value == response_expected
|
||||
@@ -1,223 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import argparse
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.arguments import Arguments, TimeRange
|
||||
|
||||
|
||||
# Parse common command-line-arguments. Used for all tools
|
||||
def test_parse_args_none() -> None:
|
||||
arguments = Arguments([], '')
|
||||
assert isinstance(arguments, Arguments)
|
||||
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||
|
||||
|
||||
def test_parse_args_defaults() -> None:
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
assert args.config == ['config.json']
|
||||
assert args.strategy_path is None
|
||||
assert args.datadir is None
|
||||
assert args.loglevel == 0
|
||||
|
||||
|
||||
def test_parse_args_config() -> None:
|
||||
args = Arguments(['-c', '/dev/null'], '').get_parsed_arg()
|
||||
assert args.config == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null'], '').get_parsed_arg()
|
||||
assert args.config == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null',
|
||||
'--config', '/dev/zero'],
|
||||
'').get_parsed_arg()
|
||||
assert args.config == ['/dev/null', '/dev/zero']
|
||||
|
||||
|
||||
def test_parse_args_db_url() -> None:
|
||||
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
|
||||
assert args.db_url == 'sqlite:///test.sqlite'
|
||||
|
||||
|
||||
def test_parse_args_verbose() -> None:
|
||||
args = Arguments(['-v'], '').get_parsed_arg()
|
||||
assert args.loglevel == 1
|
||||
|
||||
args = Arguments(['--verbose'], '').get_parsed_arg()
|
||||
assert args.loglevel == 1
|
||||
|
||||
|
||||
def test_common_scripts_options() -> None:
|
||||
arguments = Arguments(['-p', 'ETH/BTC'], '')
|
||||
arguments.common_scripts_options()
|
||||
args = arguments.get_parsed_arg()
|
||||
assert args.pairs == 'ETH/BTC'
|
||||
|
||||
|
||||
def test_parse_args_version() -> None:
|
||||
with pytest.raises(SystemExit, match=r'0'):
|
||||
Arguments(['--version'], '').get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['-c'], '').get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_strategy() -> None:
|
||||
args = Arguments(['--strategy', 'SomeStrategy'], '').get_parsed_arg()
|
||||
assert args.strategy == 'SomeStrategy'
|
||||
|
||||
|
||||
def test_parse_args_strategy_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['--strategy'], '').get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_strategy_path() -> None:
|
||||
args = Arguments(['--strategy-path', '/some/path'], '').get_parsed_arg()
|
||||
assert args.strategy_path == '/some/path'
|
||||
|
||||
|
||||
def test_parse_args_strategy_path_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['--strategy-path'], '').get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_dynamic_whitelist() -> None:
|
||||
args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg()
|
||||
assert args.dynamic_whitelist == 20
|
||||
|
||||
|
||||
def test_parse_args_dynamic_whitelist_10() -> None:
|
||||
args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg()
|
||||
assert args.dynamic_whitelist == 10
|
||||
|
||||
|
||||
def test_parse_args_dynamic_whitelist_invalid_values() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['--dynamic-whitelist', 'abc'], '').get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_timerange_incorrect() -> None:
|
||||
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
|
||||
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
|
||||
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
|
||||
|
||||
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
|
||||
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
|
||||
timerange = Arguments.parse_timerange('20100522-20150730')
|
||||
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
||||
|
||||
# Added test for unix timestamp - BTC genesis date
|
||||
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
|
||||
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
|
||||
timerange = Arguments.parse_timerange('1231006505-1233360000')
|
||||
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
||||
|
||||
# TODO: Find solution for the following case (passing timestamp in ms)
|
||||
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
|
||||
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
|
||||
|
||||
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||
Arguments.parse_timerange('-')
|
||||
|
||||
|
||||
def test_parse_args_backtesting_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
|
||||
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['backtesting --ticker-interval', 'abc'], '').get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_backtesting_custom() -> None:
|
||||
args = [
|
||||
'-c', 'test_conf.json',
|
||||
'backtesting',
|
||||
'--live',
|
||||
'--ticker-interval', '1m',
|
||||
'--refresh-pairs-cached',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
'TestStrategy'
|
||||
]
|
||||
call_args = Arguments(args, '').get_parsed_arg()
|
||||
assert call_args.config == ['test_conf.json']
|
||||
assert call_args.live is True
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'backtesting'
|
||||
assert call_args.func is not None
|
||||
assert call_args.ticker_interval == '1m'
|
||||
assert call_args.refresh_pairs is True
|
||||
assert type(call_args.strategy_list) is list
|
||||
assert len(call_args.strategy_list) == 2
|
||||
|
||||
|
||||
def test_parse_args_hyperopt_custom() -> None:
|
||||
args = [
|
||||
'-c', 'test_conf.json',
|
||||
'hyperopt',
|
||||
'--epochs', '20',
|
||||
'--spaces', 'buy'
|
||||
]
|
||||
call_args = Arguments(args, '').get_parsed_arg()
|
||||
assert call_args.config == ['test_conf.json']
|
||||
assert call_args.epochs == 20
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'hyperopt'
|
||||
assert call_args.spaces == ['buy']
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
def test_download_data_options() -> None:
|
||||
args = [
|
||||
'--pairs-file', 'file_with_pairs',
|
||||
'--datadir', 'datadir/folder',
|
||||
'--days', '30',
|
||||
'--exchange', 'binance'
|
||||
]
|
||||
arguments = Arguments(args, '')
|
||||
arguments.common_options()
|
||||
arguments.download_data_options()
|
||||
args = arguments.parse_args()
|
||||
assert args.pairs_file == 'file_with_pairs'
|
||||
assert args.datadir == 'datadir/folder'
|
||||
assert args.days == 30
|
||||
assert args.exchange == 'binance'
|
||||
|
||||
|
||||
def test_plot_dataframe_options() -> None:
|
||||
args = [
|
||||
'--indicators1', 'sma10,sma100',
|
||||
'--indicators2', 'macd,fastd,fastk',
|
||||
'--plot-limit', '30',
|
||||
'-p', 'UNITTEST/BTC',
|
||||
]
|
||||
arguments = Arguments(args, '')
|
||||
arguments.common_scripts_options()
|
||||
arguments.plot_dataframe_options()
|
||||
pargs = arguments.parse_args(True)
|
||||
assert pargs.indicators1 == "sma10,sma100"
|
||||
assert pargs.indicators2 == "macd,fastd,fastk"
|
||||
assert pargs.plot_limit == 30
|
||||
assert pargs.pairs == "UNITTEST/BTC"
|
||||
|
||||
|
||||
def test_check_int_positive() -> None:
|
||||
|
||||
assert Arguments.check_int_positive("3") == 3
|
||||
assert Arguments.check_int_positive("1") == 1
|
||||
assert Arguments.check_int_positive("100") == 100
|
||||
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
Arguments.check_int_positive("-2")
|
||||
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
Arguments.check_int_positive("0")
|
||||
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
Arguments.check_int_positive("3.5")
|
||||
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
Arguments.check_int_positive("DeadBeef")
|
||||
@@ -1,700 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||
|
||||
import json
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft4Validator, ValidationError, validate
|
||||
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import Configuration, set_loggers
|
||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.tests.conftest import log_has, log_has_re
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def all_conf():
|
||||
config_file = Path(__file__).parents[2] / "config_full.json.example"
|
||||
print(config_file)
|
||||
configuration = Configuration(Namespace())
|
||||
conf = configuration._load_config_file(str(config_file))
|
||||
return conf
|
||||
|
||||
|
||||
def test_load_config_invalid_pair(default_conf) -> None:
|
||||
default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config_schema(default_conf)
|
||||
|
||||
|
||||
def test_load_config_missing_attributes(default_conf) -> None:
|
||||
default_conf.pop('exchange')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config_schema(default_conf)
|
||||
|
||||
|
||||
def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
||||
default_conf['stake_amount'] = 'fake'
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config_schema(default_conf)
|
||||
|
||||
|
||||
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
configuration = Configuration(Namespace())
|
||||
validated_conf = configuration._load_config_file('somefile')
|
||||
assert file_mock.call_count == 1
|
||||
assert validated_conf.items() >= default_conf.items()
|
||||
|
||||
|
||||
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||
default_conf['max_open_trades'] = 0
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf['max_open_trades'] == 0
|
||||
assert 'internals' in validated_conf
|
||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
||||
conf1 = deepcopy(default_conf)
|
||||
conf2 = deepcopy(default_conf)
|
||||
del conf1['exchange']['key']
|
||||
del conf1['exchange']['secret']
|
||||
del conf2['exchange']['name']
|
||||
conf2['exchange']['pair_whitelist'] += ['NANO/BTC']
|
||||
|
||||
config_files = [conf1, conf2]
|
||||
|
||||
configsmock = MagicMock(side_effect=config_files)
|
||||
mocker.patch('freqtrade.configuration.Configuration._load_config_file', configsmock)
|
||||
|
||||
arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ]
|
||||
args = Arguments(arg_list, '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
exchange_conf = default_conf['exchange']
|
||||
assert validated_conf['exchange']['name'] == exchange_conf['name']
|
||||
assert validated_conf['exchange']['key'] == exchange_conf['key']
|
||||
assert validated_conf['exchange']['secret'] == exchange_conf['secret']
|
||||
assert validated_conf['exchange']['pair_whitelist'] != conf1['exchange']['pair_whitelist']
|
||||
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
|
||||
|
||||
assert 'internals' in validated_conf
|
||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None:
|
||||
default_conf['max_open_trades'] = -1
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf['max_open_trades'] > 999999999
|
||||
assert validated_conf['max_open_trades'] == float('inf')
|
||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||
assert "runmode" in validated_conf
|
||||
assert validated_conf['runmode'] == RunMode.DRY_RUN
|
||||
|
||||
|
||||
def test_load_config_file_exception(mocker) -> None:
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.open',
|
||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||
)
|
||||
configuration = Configuration(Namespace())
|
||||
|
||||
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
|
||||
configuration._load_config_file('somefile')
|
||||
|
||||
|
||||
def test_load_config(default_conf, mocker) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
||||
assert validated_conf.get('strategy_path') is None
|
||||
assert 'edge' not in validated_conf
|
||||
|
||||
|
||||
def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
arglist = [
|
||||
'--dynamic-whitelist', '10',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path',
|
||||
'--db-url', 'sqlite:///someurl',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('pairlist', {}).get('method') == 'VolumePairList'
|
||||
assert validated_conf.get('pairlist', {}).get('config').get('number_assets') == 10
|
||||
assert validated_conf.get('strategy') == 'TestStrategy'
|
||||
assert validated_conf.get('strategy_path') == '/some/path'
|
||||
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
||||
|
||||
# Test conf provided db_url prod
|
||||
conf = default_conf.copy()
|
||||
conf["dry_run"] = False
|
||||
conf["db_url"] = "sqlite:///path/to/db.sqlite"
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
assert validated_conf.get('db_url') == "sqlite:///path/to/db.sqlite"
|
||||
|
||||
# Test conf provided db_url dry_run
|
||||
conf = default_conf.copy()
|
||||
conf["dry_run"] = True
|
||||
conf["db_url"] = "sqlite:///path/to/db.sqlite"
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
assert validated_conf.get('db_url') == "sqlite:///path/to/db.sqlite"
|
||||
|
||||
# Test args provided db_url prod
|
||||
conf = default_conf.copy()
|
||||
conf["dry_run"] = False
|
||||
del conf["db_url"]
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
|
||||
assert "runmode" in validated_conf
|
||||
assert validated_conf['runmode'] == RunMode.LIVE
|
||||
|
||||
# Test args provided db_url dry_run
|
||||
conf = default_conf.copy()
|
||||
conf["dry_run"] = True
|
||||
conf["db_url"] = DEFAULT_DB_PROD_URL
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL
|
||||
|
||||
|
||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
default_conf.update({
|
||||
'strategy': 'CustomStrategy',
|
||||
'strategy_path': '/tmp/strategies',
|
||||
})
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('strategy') == 'CustomStrategy'
|
||||
assert validated_conf.get('strategy_path') == '/tmp/strategies'
|
||||
|
||||
|
||||
def test_show_info(default_conf, mocker, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
arglist = [
|
||||
'--dynamic-whitelist', '10',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--db-url', 'sqlite:///tmp/testdb',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
configuration.get_config()
|
||||
|
||||
assert log_has(
|
||||
'Parameter --dynamic-whitelist has been deprecated, '
|
||||
'and will be completely replaced by the whitelist dict in the future. '
|
||||
'For now: using dynamically generated whitelist based on VolumePairList. '
|
||||
'(not applicable with Backtesting and Hyperopt)',
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
|
||||
assert log_has('Dry run is enabled', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'live' not in config
|
||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'refresh_pairs' not in config
|
||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'timerange' not in config
|
||||
assert 'export' not in config
|
||||
|
||||
|
||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', '/foo/bar',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--live',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--refresh-pairs-cached',
|
||||
'--timerange', ':100',
|
||||
'--export', '/bar/foo'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
caplog.record_tuples)
|
||||
|
||||
assert 'live' in config
|
||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'position_stacking'in config
|
||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'use_max_market_positions' in config
|
||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
||||
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
||||
|
||||
assert 'refresh_pairs'in config
|
||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||
assert 'timerange' in config
|
||||
assert log_has(
|
||||
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
assert 'export' in config
|
||||
assert log_has(
|
||||
'Parameter --export detected: {} ...'.format(config['export']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--export', '/bar/foo',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
'TestStrategy'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args, RunMode.BACKTEST)
|
||||
config = configuration.get_config()
|
||||
assert config['runmode'] == RunMode.BACKTEST
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
caplog.record_tuples)
|
||||
|
||||
assert 'strategy_list' in config
|
||||
assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
|
||||
assert 'use_max_market_positions' not in config
|
||||
|
||||
assert 'timerange' not in config
|
||||
|
||||
assert 'export' in config
|
||||
assert log_has(
|
||||
'Parameter --export detected: {} ...'.format(config['export']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
arglist = [
|
||||
'hyperopt',
|
||||
'--epochs', '10',
|
||||
'--spaces', 'all',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args, RunMode.HYPEROPT)
|
||||
config = configuration.get_config()
|
||||
|
||||
assert 'epochs' in config
|
||||
assert int(config['epochs']) == 10
|
||||
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...',
|
||||
caplog.record_tuples)
|
||||
|
||||
assert 'spaces' in config
|
||||
assert config['spaces'] == ['all']
|
||||
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
||||
assert "runmode" in config
|
||||
assert config['runmode'] == RunMode.HYPEROPT
|
||||
|
||||
|
||||
def test_check_exchange(default_conf, caplog) -> None:
|
||||
configuration = Configuration(Namespace())
|
||||
|
||||
# Test an officially supported by Freqtrade team exchange
|
||||
default_conf.get('exchange').update({'name': 'BITTREX'})
|
||||
assert configuration.check_exchange(default_conf)
|
||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||
caplog.record_tuples)
|
||||
caplog.clear()
|
||||
|
||||
# Test an officially supported by Freqtrade team exchange
|
||||
default_conf.get('exchange').update({'name': 'binance'})
|
||||
assert configuration.check_exchange(default_conf)
|
||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||
caplog.record_tuples)
|
||||
caplog.clear()
|
||||
|
||||
# Test an available exchange, supported by ccxt
|
||||
default_conf.get('exchange').update({'name': 'kraken'})
|
||||
assert configuration.check_exchange(default_conf)
|
||||
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
||||
r"by the Freqtrade development team\. .*",
|
||||
caplog.record_tuples)
|
||||
caplog.clear()
|
||||
|
||||
# Test a 'bad' exchange, which known to have serious problems
|
||||
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||
assert not configuration.check_exchange(default_conf)
|
||||
assert log_has_re(r"Exchange .* is known to not work with the bot yet\. "
|
||||
r"Use it only for development and testing purposes\.",
|
||||
caplog.record_tuples)
|
||||
caplog.clear()
|
||||
|
||||
# Test a 'bad' exchange with check_for_bad=False
|
||||
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||
assert configuration.check_exchange(default_conf, False)
|
||||
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
||||
r"by the Freqtrade development team\. .*",
|
||||
caplog.record_tuples)
|
||||
caplog.clear()
|
||||
|
||||
# Test an invalid exchange
|
||||
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||
configuration.config = default_conf
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r'.*Exchange "unknown_exchange" is not supported by ccxt '
|
||||
r'and therefore not available for the bot.*'
|
||||
):
|
||||
configuration.check_exchange(default_conf)
|
||||
|
||||
|
||||
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)))
|
||||
# Prevent setting loggers
|
||||
mocker.patch('freqtrade.configuration.set_loggers', MagicMock)
|
||||
arglist = ['-vvv']
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('verbosity') == 3
|
||||
assert log_has('Verbosity set to 3', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_set_loggers() -> None:
|
||||
# Reset Logging to Debug, otherwise this fails randomly as it's set globally
|
||||
logging.getLogger('requests').setLevel(logging.DEBUG)
|
||||
logging.getLogger("urllib3").setLevel(logging.DEBUG)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(logging.DEBUG)
|
||||
logging.getLogger('telegram').setLevel(logging.DEBUG)
|
||||
|
||||
previous_value1 = logging.getLogger('requests').level
|
||||
previous_value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
previous_value3 = logging.getLogger('telegram').level
|
||||
|
||||
set_loggers()
|
||||
|
||||
value1 = logging.getLogger('requests').level
|
||||
assert previous_value1 is not value1
|
||||
assert value1 is logging.INFO
|
||||
|
||||
value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
assert previous_value2 is not value2
|
||||
assert value2 is logging.INFO
|
||||
|
||||
value3 = logging.getLogger('telegram').level
|
||||
assert previous_value3 is not value3
|
||||
assert value3 is logging.INFO
|
||||
|
||||
set_loggers(log_level=2)
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
|
||||
set_loggers(log_level=3)
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
|
||||
|
||||
def test_set_logfile(default_conf, mocker):
|
||||
mocker.patch('freqtrade.configuration.open',
|
||||
mocker.mock_open(read_data=json.dumps(default_conf)))
|
||||
|
||||
arglist = [
|
||||
'--logfile', 'test_file.log',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf['logfile'] == "test_file.log"
|
||||
f = Path("test_file.log")
|
||||
assert f.is_file()
|
||||
f.unlink()
|
||||
|
||||
|
||||
def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('forcebuy_enable')
|
||||
assert log_has('`forcebuy` RPC message enabled.', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_validate_default_conf(default_conf) -> None:
|
||||
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
|
||||
|
||||
|
||||
def test__create_datadir(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('os.path.isdir', MagicMock(return_value=False))
|
||||
md = MagicMock()
|
||||
mocker.patch('os.makedirs', md)
|
||||
cfg = Configuration(Namespace())
|
||||
cfg._create_datadir(default_conf, '/foo/bar')
|
||||
assert md.call_args[0][0] == "/foo/bar"
|
||||
assert log_has('Created data directory: /foo/bar', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_validate_tsl(default_conf):
|
||||
default_conf['trailing_stop'] = True
|
||||
default_conf['trailing_stop_positive'] = 0
|
||||
default_conf['trailing_stop_positive_offset'] = 0
|
||||
|
||||
default_conf['trailing_only_offset_is_reached'] = True
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'The config trailing_only_offset_is_reached needs '
|
||||
'trailing_stop_positive_offset to be more than 0 in your config.'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config_consistency(default_conf)
|
||||
|
||||
default_conf['trailing_stop_positive_offset'] = 0.01
|
||||
default_conf['trailing_stop_positive'] = 0.015
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'The config trailing_stop_positive_offset needs '
|
||||
'to be greater than trailing_stop_positive_offset in your config.'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config_consistency(default_conf)
|
||||
|
||||
default_conf['trailing_stop_positive'] = 0.01
|
||||
default_conf['trailing_stop_positive_offset'] = 0.015
|
||||
Configuration(Namespace())
|
||||
configuration._validate_config_consistency(default_conf)
|
||||
|
||||
|
||||
def test_load_config_default_exchange(all_conf) -> None:
|
||||
"""
|
||||
config['exchange'] subtree has required options in it
|
||||
so it cannot be omitted in the config
|
||||
"""
|
||||
del all_conf['exchange']
|
||||
|
||||
assert 'exchange' not in all_conf
|
||||
|
||||
with pytest.raises(ValidationError,
|
||||
match=r'\'exchange\' is a required property'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config_schema(all_conf)
|
||||
|
||||
|
||||
def test_load_config_default_exchange_name(all_conf) -> None:
|
||||
"""
|
||||
config['exchange']['name'] option is required
|
||||
so it cannot be omitted in the config
|
||||
"""
|
||||
del all_conf['exchange']['name']
|
||||
|
||||
assert 'name' not in all_conf['exchange']
|
||||
|
||||
with pytest.raises(ValidationError,
|
||||
match=r'\'name\' is a required property'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config_schema(all_conf)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
|
||||
("exchange", "key", ""),
|
||||
("exchange", "secret", ""),
|
||||
("exchange", "password", ""),
|
||||
])
|
||||
def test_load_config_default_subkeys(all_conf, keys) -> None:
|
||||
"""
|
||||
Test for parameters with default values in sub-paths
|
||||
so they can be omitted in the config and the default value
|
||||
should is added to the config.
|
||||
"""
|
||||
# Get first level key
|
||||
key = keys[0]
|
||||
# get second level key
|
||||
subkey = keys[1]
|
||||
|
||||
del all_conf[key][subkey]
|
||||
|
||||
assert subkey not in all_conf[key]
|
||||
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config_schema(all_conf)
|
||||
assert subkey in all_conf[key]
|
||||
assert all_conf[key][subkey] == keys[2]
|
||||
@@ -1,189 +0,0 @@
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from plotly import tools
|
||||
import plotly.graph_objs as go
|
||||
from copy import deepcopy
|
||||
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import load_backtest_data
|
||||
from freqtrade.plot.plotting import (generate_graph, generate_plot_file,
|
||||
generate_row, plot_trades)
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
from freqtrade.tests.conftest import log_has, log_has_re
|
||||
|
||||
|
||||
def fig_generating_mock(fig, *args, **kwargs):
|
||||
""" Return Fig - used to mock generate_row and plot_trades"""
|
||||
return fig
|
||||
|
||||
|
||||
def find_trace_in_fig_data(data, search_string: str):
|
||||
matches = filter(lambda x: x.name == search_string, data)
|
||||
return next(matches)
|
||||
|
||||
|
||||
def generage_empty_figure():
|
||||
return tools.make_subplots(
|
||||
rows=3,
|
||||
cols=1,
|
||||
shared_xaxes=True,
|
||||
row_width=[1, 1, 4],
|
||||
vertical_spacing=0.0001,
|
||||
)
|
||||
|
||||
|
||||
def test_generate_row(default_conf, caplog):
|
||||
pair = "UNITTEST/BTC"
|
||||
timerange = TimeRange(None, 'line', 0, -1000)
|
||||
|
||||
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||
datadir=None, timerange=timerange)
|
||||
indicators1 = ["ema10"]
|
||||
indicators2 = ["macd"]
|
||||
|
||||
# Generate buy/sell signals and indicators
|
||||
strat = DefaultStrategy(default_conf)
|
||||
data = strat.analyze_ticker(data, {'pair': pair})
|
||||
fig = generage_empty_figure()
|
||||
|
||||
# Row 1
|
||||
fig1 = generate_row(fig=deepcopy(fig), row=1, indicators=indicators1, data=data)
|
||||
figure = fig1.layout.figure
|
||||
ema10 = find_trace_in_fig_data(figure.data, "ema10")
|
||||
assert isinstance(ema10, go.Scatter)
|
||||
assert ema10.yaxis == "y"
|
||||
|
||||
fig2 = generate_row(fig=deepcopy(fig), row=3, indicators=indicators2, data=data)
|
||||
figure = fig2.layout.figure
|
||||
macd = find_trace_in_fig_data(figure.data, "macd")
|
||||
assert isinstance(macd, go.Scatter)
|
||||
assert macd.yaxis == "y3"
|
||||
|
||||
# No indicator found
|
||||
fig3 = generate_row(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
|
||||
assert fig == fig3
|
||||
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_plot_trades(caplog):
|
||||
fig1 = generage_empty_figure()
|
||||
# nothing happens when no trades are available
|
||||
fig = plot_trades(fig1, None)
|
||||
assert fig == fig1
|
||||
assert log_has("No trades found.", caplog.record_tuples)
|
||||
pair = "ADA/BTC"
|
||||
filename = history.make_testdata_path(None) / "backtest-result_test.json"
|
||||
trades = load_backtest_data(filename)
|
||||
trades = trades.loc[trades['pair'] == pair]
|
||||
|
||||
fig = plot_trades(fig, trades)
|
||||
figure = fig1.layout.figure
|
||||
|
||||
# Check buys - color, should be in first graph, ...
|
||||
trade_buy = find_trace_in_fig_data(figure.data, "trade_buy")
|
||||
assert isinstance(trade_buy, go.Scatter)
|
||||
assert trade_buy.yaxis == 'y'
|
||||
assert len(trades) == len(trade_buy.x)
|
||||
assert trade_buy.marker.color == 'green'
|
||||
|
||||
trade_sell = find_trace_in_fig_data(figure.data, "trade_sell")
|
||||
assert isinstance(trade_sell, go.Scatter)
|
||||
assert trade_sell.yaxis == 'y'
|
||||
assert len(trades) == len(trade_sell.x)
|
||||
assert trade_sell.marker.color == 'red'
|
||||
|
||||
|
||||
def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog):
|
||||
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row',
|
||||
MagicMock(side_effect=fig_generating_mock))
|
||||
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
|
||||
MagicMock(side_effect=fig_generating_mock))
|
||||
|
||||
pair = "UNITTEST/BTC"
|
||||
timerange = TimeRange(None, 'line', 0, -1000)
|
||||
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||
datadir=None, timerange=timerange)
|
||||
data['buy'] = 0
|
||||
data['sell'] = 0
|
||||
|
||||
indicators1 = []
|
||||
indicators2 = []
|
||||
fig = generate_graph(pair=pair, data=data, trades=None,
|
||||
indicators1=indicators1, indicators2=indicators2)
|
||||
assert isinstance(fig, go.Figure)
|
||||
assert fig.layout.title.text == pair
|
||||
figure = fig.layout.figure
|
||||
|
||||
assert len(figure.data) == 2
|
||||
# Candlesticks are plotted first
|
||||
candles = find_trace_in_fig_data(figure.data, "Price")
|
||||
assert isinstance(candles, go.Candlestick)
|
||||
|
||||
volume = find_trace_in_fig_data(figure.data, "Volume")
|
||||
assert isinstance(volume, go.Bar)
|
||||
|
||||
assert row_mock.call_count == 2
|
||||
assert trades_mock.call_count == 1
|
||||
|
||||
assert log_has("No buy-signals found.", caplog.record_tuples)
|
||||
assert log_has("No sell-signals found.", caplog.record_tuples)
|
||||
|
||||
|
||||
def test_generate_graph_no_trades(default_conf, mocker):
|
||||
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row',
|
||||
MagicMock(side_effect=fig_generating_mock))
|
||||
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
|
||||
MagicMock(side_effect=fig_generating_mock))
|
||||
pair = 'UNITTEST/BTC'
|
||||
timerange = TimeRange(None, 'line', 0, -1000)
|
||||
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||
datadir=None, timerange=timerange)
|
||||
|
||||
# Generate buy/sell signals and indicators
|
||||
strat = DefaultStrategy(default_conf)
|
||||
data = strat.analyze_ticker(data, {'pair': pair})
|
||||
|
||||
indicators1 = []
|
||||
indicators2 = []
|
||||
fig = generate_graph(pair=pair, data=data, trades=None,
|
||||
indicators1=indicators1, indicators2=indicators2)
|
||||
assert isinstance(fig, go.Figure)
|
||||
assert fig.layout.title.text == pair
|
||||
figure = fig.layout.figure
|
||||
|
||||
assert len(figure.data) == 6
|
||||
# Candlesticks are plotted first
|
||||
candles = find_trace_in_fig_data(figure.data, "Price")
|
||||
assert isinstance(candles, go.Candlestick)
|
||||
|
||||
volume = find_trace_in_fig_data(figure.data, "Volume")
|
||||
assert isinstance(volume, go.Bar)
|
||||
|
||||
buy = find_trace_in_fig_data(figure.data, "buy")
|
||||
assert isinstance(buy, go.Scatter)
|
||||
# All buy-signals should be plotted
|
||||
assert int(data.buy.sum()) == len(buy.x)
|
||||
|
||||
sell = find_trace_in_fig_data(figure.data, "sell")
|
||||
assert isinstance(sell, go.Scatter)
|
||||
# All buy-signals should be plotted
|
||||
assert int(data.sell.sum()) == len(sell.x)
|
||||
|
||||
assert find_trace_in_fig_data(figure.data, "BB lower")
|
||||
assert find_trace_in_fig_data(figure.data, "BB upper")
|
||||
|
||||
assert row_mock.call_count == 2
|
||||
assert trades_mock.call_count == 1
|
||||
|
||||
|
||||
def test_generate_plot_file(mocker, caplog):
|
||||
fig = generage_empty_figure()
|
||||
plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
|
||||
generate_plot_file(fig, "UNITTEST/BTC", "5m")
|
||||
|
||||
assert plot_mock.call_count == 1
|
||||
assert plot_mock.call_args[0][0] == fig
|
||||
assert (plot_mock.call_args_list[0][1]['filename']
|
||||
== "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")
|
||||
@@ -1,42 +0,0 @@
|
||||
from freqtrade.utils import setup_utils_configuration, start_list_exchanges
|
||||
from freqtrade.tests.conftest import get_args
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def test_setup_utils_configuration():
|
||||
args = [
|
||||
'--config', 'config.json.example',
|
||||
]
|
||||
|
||||
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
|
||||
assert "exchange" in config
|
||||
assert config['exchange']['dry_run'] is True
|
||||
assert config['exchange']['key'] == ''
|
||||
assert config['exchange']['secret'] == ''
|
||||
|
||||
|
||||
def test_list_exchanges(capsys):
|
||||
|
||||
args = [
|
||||
"list-exchanges",
|
||||
]
|
||||
|
||||
start_list_exchanges(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(r"Exchanges supported by ccxt and available.*", captured.out)
|
||||
assert re.match(r".*binance,.*", captured.out)
|
||||
assert re.match(r".*bittrex,.*", captured.out)
|
||||
|
||||
# Test with --one-column
|
||||
args = [
|
||||
"list-exchanges",
|
||||
"--one-column",
|
||||
]
|
||||
|
||||
start_list_exchanges(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out)
|
||||
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^bittrex$", captured.out, re.MULTILINE)
|
||||
@@ -1,23 +1,29 @@
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from typing import Any, Dict
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.configuration import Configuration
|
||||
import arrow
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.configuration import Configuration, TimeRange
|
||||
from freqtrade.configuration.directory_operations import create_userdata_dir
|
||||
from freqtrade.data.history import refresh_backtest_ohlcv_data
|
||||
from freqtrade.exchange import available_exchanges
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_utils_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]:
|
||||
def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
|
||||
"""
|
||||
Prepare the configuration for utils subcommands
|
||||
:param args: Cli args from Arguments()
|
||||
:return: Configuration
|
||||
"""
|
||||
configuration = Configuration(args, method)
|
||||
config = configuration.load_config()
|
||||
config = configuration.get_config()
|
||||
|
||||
config['exchange']['dry_run'] = True
|
||||
# Ensure we do not use Exchange credentials
|
||||
@@ -27,15 +33,67 @@ def setup_utils_configuration(args: Namespace, method: RunMode) -> Dict[str, Any
|
||||
return config
|
||||
|
||||
|
||||
def start_list_exchanges(args: Namespace) -> None:
|
||||
def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Print available exchanges
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if args.print_one_column:
|
||||
if args['print_one_column']:
|
||||
print('\n'.join(available_exchanges()))
|
||||
else:
|
||||
print(f"Exchanges supported by ccxt and available for Freqtrade: "
|
||||
f"{', '.join(available_exchanges())}")
|
||||
|
||||
|
||||
def start_create_userdir(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Create "user_data" directory to contain user data strategies, hyperopts, ...)
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
if "user_data_dir" in args and args["user_data_dir"]:
|
||||
create_userdata_dir(args["user_data_dir"], create_dir=True)
|
||||
else:
|
||||
logger.warning("`create-userdir` requires --userdir to be set.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def start_download_data(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Download data (former download_backtest_data.py script)
|
||||
"""
|
||||
config = setup_utils_configuration(args, RunMode.OTHER)
|
||||
|
||||
timerange = TimeRange()
|
||||
if 'days' in config:
|
||||
time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d")
|
||||
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
||||
|
||||
if 'pairs' not in config:
|
||||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
"Please check the documentation on how to configure this.")
|
||||
|
||||
dl_path = Path(config['datadir'])
|
||||
logger.info(f'About to download pairs: {config["pairs"]}, '
|
||||
f'intervals: {config["timeframes"]} to {dl_path}')
|
||||
|
||||
pairs_not_available: List[str] = []
|
||||
|
||||
try:
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver(config['exchange']['name'], config).exchange
|
||||
|
||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||
exchange, pairs=config["pairs"], timeframes=config["timeframes"],
|
||||
dl_path=Path(config['datadir']), timerange=timerange, erase=config.get("erase"))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("SIGINT received, aborting ...")
|
||||
|
||||
finally:
|
||||
if pairs_not_available:
|
||||
logger.info(f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||
f"on exchange {config['exchange']['name']}.")
|
||||
|
||||
12
freqtrade/vendor/qtpylib/indicators.py
vendored
12
freqtrade/vendor/qtpylib/indicators.py
vendored
@@ -213,8 +213,7 @@ def atr(bars, window=14, exp=False):
|
||||
else:
|
||||
res = rolling_mean(tr, window)
|
||||
|
||||
res = pd.Series(res)
|
||||
return (res.shift(1) * (window - 1) + res) / window
|
||||
return pd.Series(res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
@@ -602,6 +601,14 @@ def pvt(bars):
|
||||
bars['close'].shift(1)) * bars['volume']
|
||||
return trend.cumsum()
|
||||
|
||||
|
||||
def chopiness(bars, window=14):
|
||||
atrsum = true_range(bars).rolling(window).sum()
|
||||
highs = bars['high'].rolling(window).max()
|
||||
lows = bars['low'].rolling(window).min()
|
||||
return 100 * np.log10(atrsum / (highs - lows)) / np.log10(window)
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
|
||||
@@ -629,6 +636,7 @@ PandasObject.rsi = rsi
|
||||
PandasObject.stoch = stoch
|
||||
PandasObject.zscore = zscore
|
||||
PandasObject.pvt = pvt
|
||||
PandasObject.chopiness = chopiness
|
||||
PandasObject.tdi = tdi
|
||||
PandasObject.true_range = true_range
|
||||
PandasObject.mid_price = mid_price
|
||||
|
||||
@@ -17,7 +17,7 @@ class Wallet(NamedTuple):
|
||||
total: float = 0
|
||||
|
||||
|
||||
class Wallets(object):
|
||||
class Wallets:
|
||||
|
||||
def __init__(self, config: dict, exchange: Exchange) -> None:
|
||||
self._config = config
|
||||
|
||||
@@ -4,27 +4,26 @@ Main Freqtrade worker class.
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from argparse import Namespace
|
||||
from typing import Any, Callable, Optional
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
import sdnotify
|
||||
|
||||
from freqtrade import (constants, OperationalException, TemporaryError,
|
||||
__version__)
|
||||
from freqtrade import (OperationalException, TemporaryError, __version__,
|
||||
constants)
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.state import State
|
||||
from freqtrade.rpc import RPCMessageType
|
||||
|
||||
from freqtrade.state import State
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Worker(object):
|
||||
class Worker:
|
||||
"""
|
||||
Freqtradebot worker class
|
||||
"""
|
||||
|
||||
def __init__(self, args: Namespace, config=None) -> None:
|
||||
def __init__(self, args: Dict[str, Any], config=None) -> None:
|
||||
"""
|
||||
Init all variables and objects the bot needs to work
|
||||
"""
|
||||
@@ -127,10 +126,10 @@ class Worker(object):
|
||||
time.sleep(duration)
|
||||
return result
|
||||
|
||||
def _process(self) -> bool:
|
||||
state_changed = False
|
||||
def _process(self) -> None:
|
||||
logger.debug("========================================")
|
||||
try:
|
||||
state_changed = self.freqtrade.process()
|
||||
self.freqtrade.process()
|
||||
except TemporaryError as error:
|
||||
logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...")
|
||||
time.sleep(constants.RETRY_TIMEOUT)
|
||||
@@ -143,10 +142,6 @@ class Worker(object):
|
||||
})
|
||||
logger.exception('OperationalException. Stopping trader ...')
|
||||
self.freqtrade.state = State.STOPPED
|
||||
# TODO: The return value of _process() is not used apart tests
|
||||
# and should (could) be eliminated later. See PR #1689.
|
||||
# state_changed = True
|
||||
return state_changed
|
||||
|
||||
def _reconfigure(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -11,15 +11,16 @@ nav:
|
||||
- Telegram: telegram-usage.md
|
||||
- Web Hook: webhook-config.md
|
||||
- REST API: rest-api.md
|
||||
- Data Downloading: data-download.md
|
||||
- Backtesting: backtesting.md
|
||||
- Hyperopt: hyperopt.md
|
||||
- Edge positioning: edge.md
|
||||
- Plotting: plotting.md
|
||||
- Deprecated features: deprecated.md
|
||||
- FAQ: faq.md
|
||||
- Data Analysis: data-analysis.md
|
||||
- Plotting: plotting.md
|
||||
- SQL Cheatsheet: sql_cheatsheet.md
|
||||
- Sandbox testing: sandbox-testing.md
|
||||
- Deprecated features: deprecated.md
|
||||
- Contributors guide: developer.md
|
||||
theme:
|
||||
name: material
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user