Compare commits
977 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1593847203 | ||
|
7204227022 | ||
|
dab4ab78fc | ||
|
a74b941b72 | ||
|
89bba6f776 | ||
|
82f86569ee | ||
|
2a95d6855b | ||
|
3929ad4e1f | ||
|
2f1d9696cd | ||
|
0773a65333 | ||
|
8201f70a80 | ||
|
74b2f11d4f | ||
|
514e073c57 | ||
|
59e881c59d | ||
|
2e1e080022 | ||
|
470efd6f40 | ||
|
6640f4a1b2 | ||
|
e408274fb3 | ||
|
a135eaa993 | ||
|
87ff7be550 | ||
|
7441300270 | ||
|
b4f4fae0ca | ||
|
336808ec54 | ||
|
b26faa13bd | ||
|
562e4e63de | ||
|
ad5f7e1581 | ||
|
3cf95f9f6c | ||
|
f0710cafd0 | ||
|
73fa5bae96 | ||
|
a43d436f98 | ||
|
ff5ba64385 | ||
|
7e38a07490 | ||
|
8872158a6a | ||
|
db3e789294 | ||
|
4813ff338b | ||
|
fbd7dc1d6c | ||
|
f07b26f245 | ||
|
3acc10dd48 | ||
|
e5f06c201f | ||
|
bf20cde83d | ||
|
364859394b | ||
|
657f1b6c45 | ||
|
e350bcc2ef | ||
|
c2566f2436 | ||
|
cac314d4b3 | ||
|
b116cc75c4 | ||
|
bedbd964fc | ||
|
ca4d0067e4 | ||
|
8a0d90136c | ||
|
5b680f2ece | ||
|
1bc63288a3 | ||
|
45b2d24b79 | ||
|
10ca249293 | ||
|
d6b6ded8bd | ||
|
a52366c45d | ||
|
88c91a8a54 | ||
|
14755779de | ||
|
20dd3f2d67 | ||
|
8a31b4c646 | ||
|
78cd75dfef | ||
|
b805e4e150 | ||
|
6e938b59c8 | ||
|
4f17511fdc | ||
|
d8630ef847 | ||
|
47fabca1d9 | ||
|
c48876b196 | ||
|
16e10d99b9 | ||
|
0adcee9233 | ||
|
f41c659cb2 | ||
|
4c977b2e01 | ||
|
b152585d9b | ||
|
fd22c87295 | ||
|
93b213ae0b | ||
|
30eb23e1aa | ||
|
4ec83a2c24 | ||
|
9e23ca14d1 | ||
|
ebf5738a6e | ||
|
c649f9844e | ||
|
3208f30c30 | ||
|
5e731ec278 | ||
|
e55b2a1a1c | ||
|
ed8d805797 | ||
|
0ebf2e44be | ||
|
00a95945e1 | ||
|
9d739f98ac | ||
|
2588990f4b | ||
|
271846dfb6 | ||
|
c181fac6c7 | ||
|
0ac46eddca | ||
|
c735d35265 | ||
|
e957894852 | ||
|
369335b80c | ||
|
2ebddcf45c | ||
|
8564affdf0 | ||
|
a39d51d7d0 | ||
|
750dc8bf56 | ||
|
033742b708 | ||
|
84ba431d10 | ||
|
b6e26c82ea | ||
|
e8eb968a6f | ||
|
66605a1909 | ||
|
1e61263a28 | ||
|
bd08874f1f | ||
|
ff6a3465a7 | ||
|
5b58141f6b | ||
|
bf4e9a5dbb | ||
|
837d4d82b4 | ||
|
a8ffd29e18 | ||
|
92fda0f76c | ||
|
df62dd65d3 | ||
|
d72d388726 | ||
|
fda71085e0 | ||
|
1a765f1a17 | ||
|
06ab51b53d | ||
|
7de1631045 | ||
|
4c8411e835 | ||
|
f348956e4c | ||
|
a4dfd77d23 | ||
|
89e0c76a3f | ||
|
abc504412a | ||
|
36d5bb6f99 | ||
|
ad89d19955 | ||
|
e6e35c2584 | ||
|
ace70510f3 | ||
|
a320d4ccba | ||
|
790e6146f5 | ||
|
96bd5a6dc1 | ||
|
f5d8741832 | ||
|
effcc988c1 | ||
|
7ac3fbfdc9 | ||
|
4c4134a272 | ||
|
b2682bcbf5 | ||
|
4111734637 | ||
|
6e27c47dee | ||
|
18cc3539ca | ||
|
76ad5bea0e | ||
|
13e80e449c | ||
|
023eb19615 | ||
|
3e4617be37 | ||
|
ed9ec402fd | ||
|
56de81a1f9 | ||
|
ccb41d1ef9 | ||
|
2374cda8d0 | ||
|
1b7a09c184 | ||
|
37925e7f6c | ||
|
762ae3a598 | ||
|
9f8a2acf46 | ||
|
4fdec9d6e5 | ||
|
640d58eb13 | ||
|
fa8c61382b | ||
|
b6ac898f8f | ||
|
57bcff1964 | ||
|
939a87ed2e | ||
|
16d6914b15 | ||
|
05e473642b | ||
|
0d592f6c55 | ||
|
476adf872a | ||
|
9584629f50 | ||
|
c1c49183b5 | ||
|
8069cd6689 | ||
|
1f79ca9539 | ||
|
1d8fc97053 | ||
|
19f3669fbd | ||
|
06024b0ab0 | ||
|
6e952a0aa8 | ||
|
57dee794d1 | ||
|
2c0bb71a6e | ||
|
ab8f638e44 | ||
|
d250b67f33 | ||
|
42b8241541 | ||
|
6cc98c1ea9 | ||
|
77c367ad1d | ||
|
26b3148904 | ||
|
27dc9ca799 | ||
|
63e87ef85b | ||
|
6697b677dc | ||
|
baad1a5166 | ||
|
7cf7982565 | ||
|
f3f6e9d365 | ||
|
4228137dff | ||
|
3c8d27d098 | ||
|
4c1705fb1e | ||
|
31389b38f1 | ||
|
ff7a3cc885 | ||
|
4d1488498c | ||
|
10a22e7872 | ||
|
e72b6a440b | ||
|
b5ca4b7f35 | ||
|
c49f4b73dd | ||
|
80cbf08a58 | ||
|
5e23cc719d | ||
|
0680fe2a1a | ||
|
bba5f54722 | ||
|
85c4546333 | ||
|
23b5c0e833 | ||
|
cdd1bc425b | ||
|
2a9c06c40f | ||
|
434e0234c5 | ||
|
caf415dc97 | ||
|
e9337bf56e | ||
|
bcd02a871f | ||
|
e1c14bc86c | ||
|
69c23c00e0 | ||
|
1e19d7e463 | ||
|
5e0391aa2b | ||
|
2e91ee3849 | ||
|
9b32d617db | ||
|
2ec8376af9 | ||
|
86ef32318c | ||
|
4d062d41cb | ||
|
e78e42339d | ||
|
057ab1b7a6 | ||
|
613300c61d | ||
|
7d1f66ccf8 | ||
|
a7418449f9 | ||
|
d68e6f8362 | ||
|
c34ce15b14 | ||
|
1350d2cd1a | ||
|
9db2aca791 | ||
|
80d58b7930 | ||
|
568ecc201a | ||
|
cad7ed5570 | ||
|
57bb9281f6 | ||
|
5e53e9bcaa | ||
|
edfbb56749 | ||
|
652a04ac70 | ||
|
5e9ab3e261 | ||
|
be6fd3af9a | ||
|
e272cd485c | ||
|
f27528538d | ||
|
ad35a3d7ab | ||
|
211b9cbe04 | ||
|
946b8c29d7 | ||
|
33940ae66b | ||
|
d2589c4415 | ||
|
22733e44bf | ||
|
82d4051a39 | ||
|
0664a8c0e6 | ||
|
9b23376415 | ||
|
553a1b90ba | ||
|
7ea9da9605 | ||
|
9b98e608e6 | ||
|
885edc9768 | ||
|
e1b8485b51 | ||
|
764a35d035 | ||
|
e93bbd3831 | ||
|
8938c95e00 | ||
|
00ab6f572a | ||
|
73e9cbdea1 | ||
|
78381e9e7b | ||
|
75252b6251 | ||
|
f95b0ccdab | ||
|
38f184e50d | ||
|
1f4e5b17b7 | ||
|
9ee7e28ef8 | ||
|
3ac5b91899 | ||
|
4b29c4cdbf | ||
|
89729aefe8 | ||
|
15aae8a58c | ||
|
096c69dc4f | ||
|
2c0d2c1532 | ||
|
eca8ddabe9 | ||
|
c57d5ef1cd | ||
|
6bbc0eefed | ||
|
8c5b299449 | ||
|
9806699592 | ||
|
f2e878d9ec | ||
|
628c4c996a | ||
|
642d20b2f7 | ||
|
f6a88c6e9b | ||
|
d1fa5f307b | ||
|
cd0e813a85 | ||
|
dc47a391da | ||
|
9f94678478 | ||
|
04fea69a28 | ||
|
d6a8821596 | ||
|
7617dd5029 | ||
|
e9d9df3473 | ||
|
b6ee3d99b1 | ||
|
9a2bd83827 | ||
|
f359f869ab | ||
|
d6fba7c2f3 | ||
|
498bcf213f | ||
|
33efb7ace6 | ||
|
d74ca78bd8 | ||
|
3c91ba134f | ||
|
9a83d84109 | ||
|
8ae4018e4d | ||
|
739901b606 | ||
|
03b5be91f7 | ||
|
272c977d08 | ||
|
75446d8195 | ||
|
448b09d7b6 | ||
|
704fea616b | ||
|
23665c7731 | ||
|
4025ec9900 | ||
|
6a397f579e | ||
|
c31f118d0c | ||
|
2f005d6be9 | ||
|
45f5394d79 | ||
|
7e214d8e4c | ||
|
ed10048394 | ||
|
43f2ef226c | ||
|
42b5a0977e | ||
|
3b1252207d | ||
|
4ac53f1549 | ||
|
21b807aa85 | ||
|
28e0398c68 | ||
|
637ec60644 | ||
|
11bb7e520c | ||
|
6ba9316e15 | ||
|
60e3e626e4 | ||
|
9db915853a | ||
|
5237723f22 | ||
|
eb07f1fee9 | ||
|
8d92f8b362 | ||
|
49f0a72121 | ||
|
5978b7bb93 | ||
|
e09408f9b7 | ||
|
83e596c06f | ||
|
0268bfdbd4 | ||
|
b994f5c273 | ||
|
e9de088209 | ||
|
d05db077e2 | ||
|
d2f2473070 | ||
|
47b6b56566 | ||
|
27cc73f47e | ||
|
cc91ccad3e | ||
|
0102413f58 | ||
|
665e0570ae | ||
|
9391c27b80 | ||
|
a75fb3d4be | ||
|
2d86510acf | ||
|
5c3b14069e | ||
|
fe483ad011 | ||
|
0ce070acac | ||
|
6c0a1fc42c | ||
|
d066ab2620 | ||
|
3a5bd4c03e | ||
|
93b2621651 | ||
|
6aa1ec2a4c | ||
|
cc9fc41318 | ||
|
fe40636ae1 | ||
|
577b1fd965 | ||
|
0f97a999fb | ||
|
cb6e136893 | ||
|
5c3fb4d5b3 | ||
|
7c6921c743 | ||
|
22af7f7881 | ||
|
6ffb8b7a70 | ||
|
95e725c2b6 | ||
|
b18ad7a834 | ||
|
cfa76fa600 | ||
|
d226fff111 | ||
|
0c6164df7e | ||
|
d8bc350445 | ||
|
242ff26e21 | ||
|
ab0adabd39 | ||
|
ba4db0da49 | ||
|
7aa42f8868 | ||
|
3245ebccd4 | ||
|
359b0ba088 | ||
|
5234f8bf28 | ||
|
9a3bad291a | ||
|
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 | ||
|
e8e05b6876 | ||
|
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 | ||
|
a5fb3e08f7 | ||
|
59caff8fb1 | ||
|
f825e81d0e | ||
|
7bea0007c7 | ||
|
8dd8addd3a | ||
|
e14dd4974f | ||
|
7a97995d81 | ||
|
03e60b9ea4 | ||
|
c1bc1e3137 | ||
|
73ac98da80 | ||
|
14b43b504b | ||
|
a3c605f147 | ||
|
333413d298 | ||
|
9de8d7276e | ||
|
432b106d58 | ||
|
2c7a248307 | ||
|
113947132c | ||
|
0a253d66d0 | ||
|
ae0e001187 | ||
|
eab82fdec7 | ||
|
da755d1c83 | ||
|
1b2581f0cb | ||
|
56c8bdbaa2 | ||
|
23435512c4 | ||
|
6c3a0eb1d6 | ||
|
c85cd13ca1 |
@@ -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
|
27
.travis.yml
27
.travis.yml
@@ -10,15 +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
|
||||
- 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:
|
||||
@@ -26,20 +22,25 @@ jobs:
|
||||
include:
|
||||
- stage: tests
|
||||
script:
|
||||
- pytest --random-order --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
|
||||
- freqtrade --datadir freqtrade/tests/testdata backtesting
|
||||
- freqtrade --datadir tests/testdata backtesting
|
||||
name: backtest
|
||||
- script:
|
||||
- cp config.json.example config.json
|
||||
- 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
|
||||
|
||||
@@ -55,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
|
||||
@@ -114,6 +114,6 @@ Contributors may be given commit privileges. Preference will be given to those w
|
||||
1. Access to resources for cross-platform development and testing.
|
||||
1. Time to devote to the project regularly.
|
||||
|
||||
Beeing a Committer does not grant write permission on `develop` or `master` for security reasons (Users trust FreqTrade with their Exchange API keys).
|
||||
Being a Committer does not grant write permission on `develop` or `master` for security reasons (Users trust FreqTrade with their Exchange API keys).
|
||||
|
||||
After beeing Committer for some time, a Committer may be named Core Committer and given full repository access.
|
||||
After being Committer for some time, a Committer may be named Core Committer and given full repository access.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM python:3.7.3-slim-stretch
|
||||
FROM python:3.7.5-slim-stretch
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install curl build-essential libssl-dev \
|
||||
@@ -16,9 +16,9 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt requirements-common.txt /freqtrade/
|
||||
COPY requirements.txt requirements-common.txt requirements-hyperopt.txt /freqtrade/
|
||||
RUN pip install numpy --no-cache-dir \
|
||||
&& pip install -r requirements.txt --no-cache-dir
|
||||
&& pip install -r requirements-hyperopt.txt --no-cache-dir
|
||||
|
||||
# Install and execute
|
||||
COPY . /freqtrade/
|
||||
|
@@ -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`.
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -22,7 +22,10 @@
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 9
|
||||
"order_book_max": 9,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
},
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
@@ -41,7 +44,7 @@
|
||||
"ZEC/BTC",
|
||||
"XLM/BTC",
|
||||
"NXT/BTC",
|
||||
"POWR/BTC",
|
||||
"TRX/BTC",
|
||||
"ADA/BTC",
|
||||
"XMR/BTC"
|
||||
],
|
||||
@@ -49,11 +52,6 @@
|
||||
"DOGE/BTC"
|
||||
]
|
||||
},
|
||||
"experimental": {
|
||||
"use_sell_signal": false,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
},
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
|
@@ -22,7 +22,10 @@
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 9
|
||||
"order_book_max": 9,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
@@ -51,11 +54,6 @@
|
||||
"BNB/BTC"
|
||||
]
|
||||
},
|
||||
"experimental": {
|
||||
"use_sell_signal": false,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
},
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
|
@@ -33,11 +33,15 @@
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 9
|
||||
"order_book_max": 9,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
},
|
||||
"order_types": {
|
||||
"buy": "limit",
|
||||
"sell": "limit",
|
||||
"emergencysell": "market",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": false,
|
||||
"stoploss_on_exchange_interval": 60
|
||||
@@ -74,7 +78,7 @@
|
||||
"ZEC/BTC",
|
||||
"XLM/BTC",
|
||||
"NXT/BTC",
|
||||
"POWR/BTC",
|
||||
"TRX/BTC",
|
||||
"ADA/BTC",
|
||||
"XMR/BTC"
|
||||
],
|
||||
@@ -99,11 +103,6 @@
|
||||
"max_trade_duration_minute": 1440,
|
||||
"remove_pumps": false
|
||||
},
|
||||
"experimental": {
|
||||
"use_sell_signal": false,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "your_telegram_token",
|
||||
@@ -120,7 +119,8 @@
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
"process_throttle_secs": 5,
|
||||
"heartbeat_interval": 60
|
||||
},
|
||||
"strategy": "DefaultStrategy",
|
||||
"strategy_path": "user_data/strategies/"
|
||||
|
@@ -22,7 +22,11 @@
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 9
|
||||
"order_book_max": 9,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
|
||||
},
|
||||
"exchange": {
|
||||
"name": "kraken",
|
||||
@@ -66,5 +70,6 @@
|
||||
"forcebuy_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
}
|
||||
},
|
||||
"download_trades": true
|
||||
}
|
||||
|
20
docker-compose.develop.yml
Normal file
20
docker-compose.develop.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
version: '3'
|
||||
services:
|
||||
freqtrade_develop:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: "./Dockerfile.develop"
|
||||
volumes:
|
||||
- ".:/freqtrade"
|
||||
entrypoint:
|
||||
- "freqtrade"
|
||||
|
||||
freqtrade_bash:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: "./Dockerfile.develop"
|
||||
volumes:
|
||||
- ".:/freqtrade"
|
||||
entrypoint:
|
||||
- "/bin/bash"
|
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
version: '3'
|
||||
services:
|
||||
freqtrade:
|
||||
image: freqtradeorg/freqtrade:master
|
||||
volumes:
|
||||
- "./user_data:/freqtrade/user_data"
|
||||
- "./config.json:/freqtrade/config.json"
|
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,26 +1,26 @@
|
||||
# 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` directory, 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
|
||||
@@ -33,31 +33,30 @@ freqtrade backtesting
|
||||
freqtrade backtesting --ticker-interval 1m
|
||||
```
|
||||
|
||||
#### Update cached pairs with the latest data
|
||||
|
||||
```bash
|
||||
freqtrade backtesting --refresh-pairs-cached
|
||||
```
|
||||
|
||||
#### With live data (do not alter your testdata files)
|
||||
|
||||
```bash
|
||||
freqtrade backtesting --live
|
||||
```
|
||||
|
||||
#### 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
|
||||
freqtrade backtesting --datadir freqtrade/tests/testdata-20180101
|
||||
freqtrade --datadir user_data/data/bittrex-20180101 backtesting
|
||||
```
|
||||
|
||||
#### With a (custom) strategy file
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
@@ -70,68 +69,41 @@ The exported trades can be used for [further analysis](#further-backtest-result-
|
||||
#### Exporting trades to file specifying a custom filename
|
||||
|
||||
```bash
|
||||
freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json
|
||||
freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json
|
||||
```
|
||||
|
||||
#### Running backtest with smaller testset
|
||||
#### Supplying custom fee value
|
||||
|
||||
Use the `--timerange` argument to change how much of the testset
|
||||
you want to use. The last N ticks/timeframes will be used.
|
||||
|
||||
Example:
|
||||
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
|
||||
To account for this in backtesting, you can use `--fee 0.001` to supply this value to backtesting.
|
||||
This fee must be a percentage, and will be applied twice (once for trade entry, and once for trade exit).
|
||||
|
||||
```bash
|
||||
freqtrade backtesting --timerange=-200
|
||||
freqtrade backtesting --fee 0.001
|
||||
```
|
||||
|
||||
#### 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.
|
||||
#### Running backtest with smaller testset by using timerange
|
||||
|
||||
Use the `--timerange` argument to change how much of the testset you want to use.
|
||||
|
||||
|
||||
For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata.
|
||||
|
||||
```bash
|
||||
freqtrade backtesting --timerange=20190501-
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
#### 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 directory `user_data/data/binance` and copy `pairs.json` in that directory.
|
||||
- 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 directory 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 directory, 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).
|
||||
|
||||
## Understand the backtesting result
|
||||
|
||||
The most important in the backtesting is to understand the result.
|
||||
@@ -176,11 +148,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 +163,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,22 +181,39 @@ 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
|
||||
- Sell signal sells happen at open-price of the following candle
|
||||
- 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)
|
||||
|
||||
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
|
||||
Also, keep in mind that past results don't guarantee future success.
|
||||
|
||||
In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions.
|
||||
|
||||
### 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 directory.
|
||||
|
||||
@@ -237,9 +221,9 @@ All listed Strategies need to be in the same directory.
|
||||
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,76 @@
|
||||
|
||||
This page explains the different parameters of the bot and how to run it.
|
||||
|
||||
!Note:
|
||||
!!! 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] [--db-url PATH]
|
||||
[--sd-notify]
|
||||
{backtesting,edge,hyperopt} ...
|
||||
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,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
|
||||
...
|
||||
|
||||
Free, open source crypto trading bot
|
||||
|
||||
positional arguments:
|
||||
{backtesting,edge,hyperopt}
|
||||
{backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
|
||||
backtesting Backtesting module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
create-userdir Create user-data directory.
|
||||
list-exchanges Print available exchanges.
|
||||
list-timeframes Print available ticker intervals (timeframes) for the
|
||||
exchange.
|
||||
download-data Download backtesting data.
|
||||
plot-dataframe Plot candles with indicators.
|
||||
plot-profit Generate plot showing profits.
|
||||
|
||||
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
|
||||
--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.
|
||||
--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
|
||||
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
|
||||
@@ -82,6 +96,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
|
||||
├── hyperopt_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.
|
||||
@@ -110,6 +147,7 @@ Learn more about strategy file in
|
||||
|
||||
This parameter allows you to add an additional strategy lookup path, which gets
|
||||
checked before the default locations (The passed path must be a directory!):
|
||||
|
||||
```bash
|
||||
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||
```
|
||||
@@ -136,80 +174,11 @@ Backtesting also uses the config specified via `-c/--config`.
|
||||
|
||||
```
|
||||
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||
[--max_open_trades MAX_OPEN_TRADES]
|
||||
[--stake_amount STAKE_AMOUNT] [-r] [--eps] [--dmmp]
|
||||
[-l]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export EXPORT] [--export-filename PATH]
|
||||
|
||||
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).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
--max_open_trades MAX_OPEN_TRADES
|
||||
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).
|
||||
--dmmp, --disable-max-market-positions
|
||||
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
|
||||
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
|
||||
--export EXPORT Export backtest results, argument are: trades. Example
|
||||
--export=trades
|
||||
--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-
|
||||
result.json)
|
||||
```
|
||||
|
||||
### How to use **--refresh-pairs-cached** parameter?
|
||||
|
||||
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`.
|
||||
|
||||
## Hyperopt commands
|
||||
|
||||
To optimize your strategy, you can use hyperopt parameter hyperoptimization
|
||||
to find optimal parameter values for your stategy.
|
||||
|
||||
```
|
||||
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||
[--max_open_trades INT]
|
||||
[--stake_amount STAKE_AMOUNT] [-r]
|
||||
[--customhyperopt NAME] [--hyperopt-path PATH]
|
||||
[--eps] [-e INT]
|
||||
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
||||
[--dmmp] [--print-all] [-j JOBS]
|
||||
[--random-state INT] [--min-trades INT] [--continue]
|
||||
[--hyperopt-loss NAME]
|
||||
[--max_open_trades INT]
|
||||
[--stake_amount STAKE_AMOUNT] [--fee FLOAT]
|
||||
[--eps] [--dmmp]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export EXPORT] [--export-filename PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -222,13 +191,72 @@ 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.
|
||||
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
|
||||
entry and exit).
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
--dmmp, --disable-max-market-positions
|
||||
Disable applying `max_open_trades` during backtest
|
||||
(same as setting `max_open_trades` to a very high
|
||||
number).
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
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`
|
||||
--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`
|
||||
|
||||
```
|
||||
|
||||
### Getting historic data for backtesting
|
||||
|
||||
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
|
||||
|
||||
To optimize your strategy, you can use hyperopt parameter hyperoptimization
|
||||
to find optimal parameter values for your stategy.
|
||||
|
||||
```
|
||||
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||
[--max_open_trades INT]
|
||||
[--stake_amount STAKE_AMOUNT] [--fee FLOAT]
|
||||
[--customhyperopt NAME] [--hyperopt-path PATH]
|
||||
[--eps] [-e INT]
|
||||
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
||||
[--dmmp] [--print-all] [--no-color] [--print-json]
|
||||
[-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`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
--max_open_trades INT
|
||||
Specify max_open_trades to use.
|
||||
--stake_amount STAKE_AMOUNT
|
||||
Specify stake_amount.
|
||||
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
|
||||
entry and exit).
|
||||
--customhyperopt NAME
|
||||
Specify hyperopt class name (default:
|
||||
`DefaultHyperOpts`).
|
||||
`DefaultHyperOpt`).
|
||||
--hyperopt-path PATH Specify additional lookup path for Hyperopts and
|
||||
Hyperopt Loss functions.
|
||||
--eps, --enable-position-stacking
|
||||
@@ -243,6 +271,9 @@ optional arguments:
|
||||
(same as setting `max_open_trades` to a very high
|
||||
number).
|
||||
--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.
|
||||
--print-json Print best result detailization in JSON format.
|
||||
-j JOBS, --job-workers JOBS
|
||||
The number of concurrently running jobs for
|
||||
hyperoptimization (hyperopt worker processes). If -1
|
||||
@@ -256,43 +287,43 @@ optional arguments:
|
||||
--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
|
||||
--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. (default:
|
||||
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]
|
||||
[--max_open_trades MAX_OPEN_TRADES]
|
||||
[--stake_amount STAKE_AMOUNT] [-r]
|
||||
[--stoplosses STOPLOSS_RANGE]
|
||||
[--max_open_trades INT] [--stake_amount STAKE_AMOUNT]
|
||||
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
|
||||
|
||||
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.
|
||||
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
|
||||
entry and exit).
|
||||
--stoplosses STOPLOSS_RANGE
|
||||
Defines a range of stoploss against which edge will
|
||||
assess the strategy the format is "min,max,step"
|
||||
(without any space).example:
|
||||
--stoplosses=-0.01,-0.1,-0.001
|
||||
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`
|
||||
|
||||
```
|
||||
|
||||
To understand edge and how to read the results, please read the [edge documentation](edge.md).
|
||||
|
@@ -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.
|
||||
@@ -32,32 +59,34 @@ Mandatory Parameters are marked as **Required**.
|
||||
| `unfilledtimeout.sell` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
|
||||
| `bid_strategy.ask_last_balance` | 0.0 | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance).
|
||||
| `bid_strategy.use_order_book` | false | Allows buying of pair using the rates in Order Book Bids.
|
||||
| `bid_strategy.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
|
||||
| `bid_strategy.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
|
||||
| `bid_strategy. check_depth_of_market.enabled` | false | Does not buy if the % difference of buy orders and sell orders is met in Order Book.
|
||||
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher.
|
||||
| `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks.
|
||||
| `ask_strategy.order_book_min` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||
| `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||
| `ask_strategy.use_sell_signal` | true | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `ask_strategy.sell_profit_only` | false | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `ask_strategy.ignore_roi_if_buy_signal` | false | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
|
||||
| `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
|
||||
| `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.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.
|
||||
| `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
||||
| `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).
|
||||
| `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.
|
||||
@@ -69,16 +98,16 @@ Mandatory Parameters are marked as **Required**.
|
||||
| `strategy` | DefaultStrategy | Defines Strategy class to use.
|
||||
| `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.heartbeat_interval` | 60 | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages.
|
||||
| `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`
|
||||
@@ -88,9 +117,9 @@ Values set in the configuration file always overwrite values set in the strategy
|
||||
* `process_only_new_candles`
|
||||
* `order_types`
|
||||
* `order_time_in_force`
|
||||
* `use_sell_signal` (experimental)
|
||||
* `sell_profit_only` (experimental)
|
||||
* `ignore_roi_if_buy_signal` (experimental)
|
||||
* `use_sell_signal` (ask_strategy)
|
||||
* `sell_profit_only` (ask_strategy)
|
||||
* `ignore_roi_if_buy_signal` (ask_strategy)
|
||||
|
||||
### Understand stake_amount
|
||||
|
||||
@@ -170,19 +199,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 +221,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 +234,7 @@ Configuration:
|
||||
"order_types": {
|
||||
"buy": "limit",
|
||||
"sell": "limit",
|
||||
"emergencysell": "market",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": false,
|
||||
"stoploss_on_exchange_interval": 60
|
||||
@@ -217,11 +249,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
|
||||
|
||||
|
@@ -1,42 +1,94 @@
|
||||
# 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
|
||||
|
||||
``` python
|
||||
from freqtrade.data.btanalysis import load_backtest_data
|
||||
df = load_backtest_data("user_data/backtest-result.json")
|
||||
| Task | Tool |
|
||||
--- | ---
|
||||
Bot operations | CLI
|
||||
Repetitive tasks | Shell scripts
|
||||
Data analysis & visualization | Notebook
|
||||
|
||||
# Show value-counts per pair
|
||||
df.groupby("pair")["sell_reason"].value_counts()
|
||||
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())
|
||||
```
|
||||
|
||||
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.
|
||||
### Load multiple configuration files
|
||||
|
||||
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:
|
||||
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
|
||||
from freqtrade.data.btanalysis import load_trades_from_db
|
||||
import json
|
||||
from freqtrade.configuration import Configuration
|
||||
|
||||
df = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
||||
|
||||
df.groupby("pair")["sell_reason"].value_counts()
|
||||
# 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/"
|
||||
}
|
||||
```
|
||||
|
||||
### Further Data analysis documentation
|
||||
|
||||
* [Strategy debugging](strategy_analysis_example.md) - also available as Jupyter notebook (`user_data/notebooks/strategy_analysis_example.ipynb`)
|
||||
* [Plotting](plotting.md)
|
||||
|
||||
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.
|
||||
|
88
docs/data-download.md
Normal file
88
docs/data-download.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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.
|
||||
|
||||
### Trades (tick) data
|
||||
|
||||
By default, `download-data` subcommand downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
|
||||
This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes.
|
||||
|
||||
Since this data is large by default, the files use gzip by default. They are stored in your data-directory with the naming convention of `<pair>-trades.json.gz` (`ETH_BTC-trades.json.gz`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository.
|
||||
|
||||
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally.
|
||||
|
||||
Example call:
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --dl-trades
|
||||
```
|
||||
|
||||
!!! Note
|
||||
While this method uses async calls, it will be slow, since it requires the result of the previous call to generate the next request to the exchange.
|
||||
|
||||
!!! Warning
|
||||
The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading.
|
||||
|
||||
### Historic Kraken data
|
||||
|
||||
The Kraken API does only provide 720 historic candles, which is sufficient for FreqTrade dry-run and live trade modes, but is a problem for backtesting.
|
||||
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
|
||||
|
||||
## Next step
|
||||
|
||||
Great, you now have backtest data downloaded, so you can now start [backtesting](backtesting.md) your strategy.
|
@@ -4,16 +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.
|
||||
|
||||
### the `--live` command line option
|
||||
|
||||
`--live` in the context of backtesting allows to download the latest tick data for backtesting.
|
||||
Since this only downloads one set of data (by default 500 candles) - this is not really suitable for extendet backtesting, and has therefore been deprecated.
|
||||
|
||||
This command was deprecated in `2019.6-dev` and will be removed after the next release.
|
||||
|
||||
## 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
|
||||
|
||||
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,74 @@ 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)
|
||||
|
||||
```
|
||||
|
||||
### Local docker usage
|
||||
|
||||
The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine.
|
||||
|
||||
#### Install
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [docker](https://docs.docker.com/install/)
|
||||
* [docker-compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
#### Starting the bot
|
||||
##### Use the develop dockerfile
|
||||
``` bash
|
||||
rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml
|
||||
```
|
||||
#### Docker Compose
|
||||
|
||||
##### Starting
|
||||
|
||||
``` bash
|
||||
docker-compose up
|
||||
```
|
||||

|
||||
|
||||
##### Rebuilding
|
||||
``` bash
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
##### Execing (effectively SSH into the container)
|
||||
|
||||
The `exec` command requires that the container already be running, if you want to start it
|
||||
that can be effected by `docker-compose up` or `docker-compose run freqtrade_develop`
|
||||
|
||||
``` bash
|
||||
docker-compose exec freqtrade_develop /bin/bash
|
||||
```
|
||||

|
||||
|
||||
|
||||
## Modules
|
||||
|
||||
### Dynamic Pairlist
|
||||
@@ -126,6 +189,15 @@ print(datetime.utcnow())
|
||||
The output will show the last entry from the Exchange as well as the current UTC date.
|
||||
If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above).
|
||||
|
||||
## Updating example notebooks
|
||||
|
||||
To keep the jupyter notebooks aligned with the documentation, the following should be ran after updating a example notebook.
|
||||
|
||||
``` bash
|
||||
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace user_data/notebooks/strategy_analysis_example.ipynb
|
||||
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md
|
||||
```
|
||||
|
||||
## Creating a release
|
||||
|
||||
This part of the documentation is aimed at maintainers, and shows how to create a release.
|
||||
@@ -156,8 +228,10 @@ 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 the version-number specified as tag.
|
||||
* Use "master" as reference (this step comes after the above PR is merged).
|
||||
* Use the above changelog as release comment (as codeblock)
|
||||
|
||||
|
@@ -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.
|
||||
|
10
docs/edge.md
10
docs/edge.md
@@ -234,9 +234,8 @@ An example of its output:
|
||||
|
||||
### Update cached pairs with the latest data
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
@@ -250,13 +249,10 @@ freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
|
||||
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.
|
||||
Doing `--timerange=-20190901` will get all available data until September 1st (excluding September 1st 2019).
|
||||
|
||||
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`
|
||||
|
14
docs/faq.md
14
docs/faq.md
@@ -38,18 +38,28 @@ like pauses. You can stop your bot, adjust settings and start it again.
|
||||
|
||||
### I want to improve the bot with a new strategy
|
||||
|
||||
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
||||
That's great. We have a nice backtesting and hyperoptimization 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?
|
||||
|
||||
You can use the `/forcesell all` command from Telegram.
|
||||
|
||||
### I get the message "RESTRICTED_MARKET"
|
||||
|
||||
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
|
||||
Per default Hyperopt called without the `-e`/`--epochs` command line option will only
|
||||
run 100 epochs, means 100 evals of your triggers, guards, ... Too few
|
||||
to find a great result (unless if you are very lucky), so you probably
|
||||
have to run it for 10.000 or more. But it will take an eternity to
|
||||
|
145
docs/hyperopt.md
145
docs/hyperopt.md
@@ -6,31 +6,39 @@ 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)
|
||||
Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
||||
|
||||
## Prepare Hyperopting
|
||||
|
||||
Before we start digging into Hyperopt, we recommend you to take a look at
|
||||
an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py)
|
||||
the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py).
|
||||
|
||||
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy.
|
||||
|
||||
### 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
|
||||
* 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
|
||||
|
||||
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
|
||||
|
||||
@@ -56,9 +64,9 @@ multiple guards. The constructed strategy will be something like
|
||||
"*buy exactly when close price touches lower bollinger band, BUT only if
|
||||
ADX > 10*".
|
||||
|
||||
If you have updated the buy strategy, ie. changed the contents of
|
||||
`populate_buy_trend()` method you have to update the `guards` and
|
||||
`triggers` hyperopts must use.
|
||||
If you have updated the buy strategy, i.e. changed the contents of
|
||||
`populate_buy_trend()` method, you have to update the `guards` and
|
||||
`triggers` your hyperopt must use correspondingly.
|
||||
|
||||
#### Sell optimization
|
||||
|
||||
@@ -74,7 +82,7 @@ To avoid naming collisions in the search-space, please prefix all sell-spaces wi
|
||||
#### Using ticker-interval as part of the Strategy
|
||||
|
||||
The Strategy exposes the ticker-interval as `self.ticker_interval`. The same value is available as class-attribute `HyperoptName.ticker_interval`.
|
||||
In the case of the linked sample-value this would be `SampleHyperOpts.ticker_interval`.
|
||||
In the case of the linked sample-value this would be `SampleHyperOpt.ticker_interval`.
|
||||
|
||||
## Solving a Mystery
|
||||
|
||||
@@ -159,7 +167,11 @@ By default, FreqTrade uses a loss function, which has been with freqtrade since
|
||||
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), `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns) and `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration).
|
||||
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
|
||||
|
||||
@@ -303,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,
|
||||
@@ -341,44 +355,97 @@ 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 = {
|
||||
"118": 0,
|
||||
"78": 0.0363463,
|
||||
"21": 0.0915,
|
||||
"0": 0.106
|
||||
```
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||
minimal_roi = {
|
||||
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.
|
||||
|
||||
#### 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
|
||||
|
||||
|
@@ -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?
|
||||
|
||||
|
@@ -99,8 +99,8 @@ sudo apt-get install build-essential git
|
||||
|
||||
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).
|
||||
|
||||
@@ -109,13 +109,17 @@ 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 .
|
||||
```
|
||||
|
||||
!!! 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
|
||||
|
||||
#### 1. Install TA-Lib
|
||||
@@ -175,7 +179,6 @@ cp config.json.example config.json
|
||||
|
||||
``` bash
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install -r requirements.txt
|
||||
python3 -m pip install -e .
|
||||
```
|
||||
|
||||
@@ -219,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).
|
||||
@@ -243,14 +257,12 @@ As compiling from source on windows has heavy dependencies (requires a partial v
|
||||
```cmd
|
||||
>cd \path\freqtrade-develop
|
||||
>python -m venv .env
|
||||
>cd .env\Scripts
|
||||
>activate.bat
|
||||
>cd \path\freqtrade-develop
|
||||
>.env\Scripts\activate.bat
|
||||
REM optionally install ta-lib from wheel
|
||||
REM >pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl
|
||||
>pip install -r requirements.txt
|
||||
>pip install -e .
|
||||
>python freqtrade\main.py
|
||||
>freqtrade
|
||||
```
|
||||
|
||||
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
||||
|
@@ -49,4 +49,6 @@
|
||||
</nav>
|
||||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
</header>
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
||||
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
</header>
|
||||
|
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 --datadir user_data/data/binance_save/ plot-profit -p LTC/BTC
|
||||
```
|
||||
|
@@ -1 +1,2 @@
|
||||
mkdocs-material==3.1.0
|
||||
mkdocs-material==4.4.3
|
||||
mdx_truly_sane_lists==1.2
|
||||
|
@@ -100,7 +100,6 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
||||
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `reload_conf` | | Reloads the configuration file
|
||||
| `status` | | Lists all open trades
|
||||
| `status table` | | List all open trades in a table format
|
||||
| `count` | | Displays number of trades used and available
|
||||
| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||
| `forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
|
@@ -24,7 +24,7 @@ strategy file will be updated on Github. Put your custom strategy file
|
||||
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
|
||||
|
||||
@@ -36,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
|
||||
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
|
||||
@@ -55,8 +60,7 @@ file as reference.**
|
||||
!!! Warning Using future data
|
||||
Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author
|
||||
needs to take care to avoid having the strategy utilize data from the future.
|
||||
Samples for usage of future data are `dataframe.shift(-1)`, `dataframe.resample("1h")` (this uses the left border of the interval, so moves data from an hour to the start of the hour).
|
||||
They all use data which is not available during regular operations, so these strategies will perform well during backtesting, but will fail / perform badly in dry-runs.
|
||||
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
|
||||
|
||||
### Customize Indicators
|
||||
|
||||
@@ -109,9 +113,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
|
||||
@@ -122,7 +125,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:
|
||||
@@ -134,15 +137,19 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
|
||||
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
|
||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Buying requires sellers to buy from - therefore volume needs to be > 0 (`dataframe['volume'] > 0`) to make sure that the bot does not buy/sell in no-activity periods.
|
||||
|
||||
### Sell signal rules
|
||||
|
||||
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
||||
@@ -152,7 +159,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:
|
||||
@@ -164,9 +171,10 @@ def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
|
||||
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
|
||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
@@ -220,7 +228,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).
|
||||
|
||||
@@ -242,9 +250,9 @@ Instead, have a look at the section [Storing information](#Storing-information)
|
||||
|
||||
### Storing information
|
||||
|
||||
Storing information can be accomplished by crating a new dictionary within the strategy class.
|
||||
Storing information can be accomplished by creating a new dictionary within the strategy class.
|
||||
|
||||
The name of the variable can be choosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
|
||||
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
|
||||
|
||||
```python
|
||||
class Awesomestrategy(IStrategy):
|
||||
@@ -274,27 +282,26 @@ 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).
|
||||
- `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries.
|
||||
- `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure.
|
||||
- `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
|
||||
@@ -309,8 +316,10 @@ 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
|
||||
method is used.
|
||||
|
||||
!!! 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
|
||||
|
||||
@@ -320,7 +329,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.
|
||||
@@ -342,9 +350,9 @@ def informative_pairs(self):
|
||||
As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short.
|
||||
All intervals and all pairs can be specified as long as they are available (and active) on the used exchange.
|
||||
It is however better to use resampling to longer time-intervals when possible
|
||||
to avoid hammering the exchange with too many requests and risk beeing blocked.
|
||||
to avoid hammering the exchange with too many requests and risk being blocked.
|
||||
|
||||
### Additional data - Wallets
|
||||
### Additional data (Wallets)
|
||||
|
||||
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
||||
|
||||
@@ -390,10 +398,10 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds).
|
||||
|
||||
### Where is the default strategy?
|
||||
### Where can i find a strategy template?
|
||||
|
||||
The default buy strategy is located in the file
|
||||
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
||||
The strategy template is located in the file
|
||||
[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py).
|
||||
|
||||
### Specify custom strategy location
|
||||
|
||||
@@ -403,13 +411,25 @@ If you want to use a strategy from a different directory you can pass `--strateg
|
||||
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||
```
|
||||
|
||||
### Common mistakes when developing strategies
|
||||
|
||||
Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.
|
||||
This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions.
|
||||
|
||||
The following lists some common patterns which should be avoided to prevent frustration:
|
||||
|
||||
- don't use `shift(-1)`. This uses data from the future, which is not available.
|
||||
- don't use `.iloc[-1]` or any other absolute position in the dataframe, this will be different between dry-run and backtesting.
|
||||
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
||||
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||
|
||||
### Further strategy ideas
|
||||
|
||||
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
||||
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
|
||||
|
||||
|
142
docs/strategy_analysis_example.md
Normal file
142
docs/strategy_analysis_example.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Strategy analysis example
|
||||
|
||||
Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
# 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 = Path('user_data')
|
||||
# Location of the strategy
|
||||
strategy_location = 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"
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# Load data using values set above
|
||||
from freqtrade.data.history import load_pair_history
|
||||
|
||||
candles = load_pair_history(datadir=data_location,
|
||||
ticker_interval=ticker_interval,
|
||||
pair=pair)
|
||||
|
||||
# Confirm success
|
||||
print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}")
|
||||
candles.head()
|
||||
```
|
||||
|
||||
## Load and run strategy
|
||||
* Rerun each time the strategy file is changed
|
||||
|
||||
|
||||
```python
|
||||
# Load strategy using values set above
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
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})
|
||||
df.tail()
|
||||
```
|
||||
|
||||
### Display the trade details
|
||||
|
||||
* Note that using `data.head()` would also work, however 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=False)
|
||||
data.tail()
|
||||
```
|
||||
|
||||
## Load existing objects into a Jupyter notebook
|
||||
|
||||
The following cells 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 to pandas dataframe
|
||||
|
||||
Analyze a trades dataframe (also used below for plotting)
|
||||
|
||||
|
||||
```python
|
||||
from freqtrade.data.btanalysis import load_backtest_data
|
||||
|
||||
# Load backtest results
|
||||
trades = load_backtest_data(user_data_dir / "backtest_results/backtest-result.json")
|
||||
|
||||
# Show value-counts per pair
|
||||
trades.groupby("pair")["sell_reason"].value_counts()
|
||||
```
|
||||
|
||||
### Load live trading results into a pandas dataframe
|
||||
|
||||
In case you did already some trading and want to analyze your performance
|
||||
|
||||
|
||||
```python
|
||||
from freqtrade.data.btanalysis import load_trades_from_db
|
||||
|
||||
# Fetch trades from database
|
||||
trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
||||
|
||||
# Display results
|
||||
trades.groupby("pair")["sell_reason"].value_counts()
|
||||
```
|
||||
|
||||
## Plot results
|
||||
|
||||
Freqtrade offers interactive plotting capabilities based on plotly.
|
||||
|
||||
|
||||
```python
|
||||
from freqtrade.plot.plotting import generate_candlestick_graph
|
||||
# Limit graph period to keep plotly quick and reactive
|
||||
|
||||
data_red = data['2019-06-01':'2019-06-10']
|
||||
# Generate candlestick graph
|
||||
graph = generate_candlestick_graph(pair=pair,
|
||||
data=data_red,
|
||||
trades=trades,
|
||||
indicators1=['sma20', 'ema50', 'ema55'],
|
||||
indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']
|
||||
)
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# Show graph inline
|
||||
# graph.show()
|
||||
|
||||
# Render graph in a seperate window
|
||||
graph.show(renderer="browser")
|
||||
|
||||
```
|
||||
|
||||
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.
|
13
docs/stylesheets/ft.extra.css
Normal file
13
docs/stylesheets/ft.extra.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.rst-versions {
|
||||
font-size: .7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rst-versions.rst-badge .rst-current-version {
|
||||
font-size: .7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rst-versions .rst-other-versions {
|
||||
color: white;
|
||||
}
|
126
docs/utils.md
Normal file
126
docs/utils.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Utility Subcommands
|
||||
|
||||
Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
|
||||
|
||||
## List Exchanges
|
||||
|
||||
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
||||
|
||||
```
|
||||
usage: freqtrade list-exchanges [-h] [-1] [-a]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-1, --one-column Print output in one column.
|
||||
-a, --all Print all exchanges known to the ccxt library.
|
||||
```
|
||||
|
||||
* Example: see exchanges available for the bot:
|
||||
```
|
||||
$ freqtrade list-exchanges
|
||||
Exchanges available for Freqtrade: _1btcxe, acx, allcoin, bequant, bibox, binance, binanceje, binanceus, bitbank, bitfinex, bitfinex2, bitkk, bitlish, bitmart, bittrex, bitz, bleutrade, btcalpha, btcmarkets, btcturk, buda, cex, cobinhood, coinbaseprime, coinbasepro, coinex, cointiger, coss, crex24, digifinex, dsx, dx, ethfinex, fcoin, fcoinjp, gateio, gdax, gemini, hitbtc2, huobipro, huobiru, idex, kkex, kraken, kucoin, kucoin2, kuna, lbank, mandala, mercado, oceanex, okcoincny, okcoinusd, okex, okex3, poloniex, rightbtc, theocean, tidebit, upbit, zb
|
||||
```
|
||||
|
||||
* Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade):
|
||||
```
|
||||
$ freqtrade list-exchanges -a
|
||||
All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpro, bcex, bequant, bibox, bigone, binance, binanceje, binanceus, bit2c, bitbank, bitbay, bitfinex, bitfinex2, bitflyer, bitforex, bithumb, bitkk, bitlish, bitmart, bitmex, bitso, bitstamp, bitstamp1, bittrex, bitz, bl3p, bleutrade, braziliex, btcalpha, btcbox, btcchina, btcmarkets, btctradeim, btctradeua, btcturk, buda, bxinth, cex, chilebit, cobinhood, coinbase, coinbaseprime, coinbasepro, coincheck, coinegg, coinex, coinexchange, coinfalcon, coinfloor, coingi, coinmarketcap, coinmate, coinone, coinspot, cointiger, coolcoin, coss, crex24, crypton, deribit, digifinex, dsx, dx, ethfinex, exmo, exx, fcoin, fcoinjp, flowbtc, foxbit, fybse, gateio, gdax, gemini, hitbtc, hitbtc2, huobipro, huobiru, ice3x, idex, independentreserve, indodax, itbit, kkex, kraken, kucoin, kucoin2, kuna, lakebtc, latoken, lbank, liquid, livecoin, luno, lykke, mandala, mercado, mixcoins, negociecoins, nova, oceanex, okcoincny, okcoinusd, okex, okex3, paymium, poloniex, rightbtc, southxchange, stronghold, surbitcoin, theocean, therock, tidebit, tidex, upbit, vaultoro, vbtc, virwox, xbtce, yobit, zaif, zb
|
||||
```
|
||||
|
||||
## List Timeframes
|
||||
|
||||
Use the `list-timeframes` subcommand to see the list of ticker intervals (timeframes) available for the exchange.
|
||||
|
||||
```
|
||||
usage: freqtrade list-timeframes [-h] [--exchange EXCHANGE] [-1]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
-1, --one-column Print output in one column.
|
||||
|
||||
```
|
||||
|
||||
* Example: see the timeframes for the 'binance' exchange, set in the configuration file:
|
||||
|
||||
```
|
||||
$ freqtrade -c config_binance.json list-timeframes
|
||||
...
|
||||
Timeframes available for the exchange `binance`: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
|
||||
```
|
||||
|
||||
* Example: enumerate exchanges available for Freqtrade and print timeframes supported by each of them:
|
||||
```
|
||||
$ for i in `freqtrade list-exchanges -1`; do freqtrade list-timeframes --exchange $i; done
|
||||
```
|
||||
|
||||
## List pairs/list markets
|
||||
|
||||
The `list-pairs` and `list-markets` subcommands allow to see the pairs/markets available on exchange.
|
||||
|
||||
Pairs are markets with the '/' character between the base currency part and the quote currency part in the market symbol.
|
||||
For example, in the 'ETH/BTC' pair 'ETH' is the base currency, while 'BTC' is the quote currency.
|
||||
|
||||
For pairs traded by Freqtrade the pair quote currency is defined by the value of the `stake_currency` configuration setting.
|
||||
|
||||
You can print info about any pair/market with these subcommands - and you can filter output by quote-currency using `--quote BTC`, or by base-currency using `--base ETH` options correspondingly.
|
||||
|
||||
These subcommands have same usage and same set of available options:
|
||||
|
||||
```
|
||||
usage: freqtrade list-markets [-h] [--exchange EXCHANGE] [--print-list]
|
||||
[--print-json] [-1] [--print-csv]
|
||||
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
||||
[-a]
|
||||
|
||||
usage: freqtrade list-pairs [-h] [--exchange EXCHANGE] [--print-list]
|
||||
[--print-json] [-1] [--print-csv]
|
||||
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
|
||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
--print-list Print list of pairs or market symbols. By default data
|
||||
is printed in the tabular format.
|
||||
--print-json Print list of pairs or market symbols in JSON format.
|
||||
-1, --one-column Print output in one column.
|
||||
--print-csv Print exchange pair or market data in the csv format.
|
||||
--base BASE_CURRENCY [BASE_CURRENCY ...]
|
||||
Specify base currency(-ies). Space-separated list.
|
||||
--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]
|
||||
Specify quote currency(-ies). Space-separated list.
|
||||
-a, --all Print all pairs or market symbols. By default only
|
||||
active ones are shown.
|
||||
```
|
||||
|
||||
By default, only active pairs/markets are shown. Active pairs/markets are those that can currently be traded
|
||||
on the exchange. The see the list of all pairs/markets (not only the active ones), use the `-a`/`-all` option.
|
||||
|
||||
Pairs/markets are sorted by its symbol string in the printed output.
|
||||
|
||||
### Examples
|
||||
|
||||
* Print the list of active pairs with quote currency USD on exchange, specified in the default
|
||||
configuration file (i.e. pairs on the "Bittrex" exchange) in JSON format:
|
||||
|
||||
```
|
||||
$ freqtrade list-pairs --quote USD --print-json
|
||||
```
|
||||
|
||||
* Print the list of all pairs on the exchange, specified in the `config_binance.json` configuration file
|
||||
(i.e. on the "Binance" exchange) with base currencies BTC or ETH and quote currencies USDT or USD, as the
|
||||
human-readable list with summary:
|
||||
|
||||
```
|
||||
$ freqtrade -c config_binance.json list-pairs --all --base BTC ETH --quote USDT USD --print-list
|
||||
```
|
||||
|
||||
* Print all markets on exchange "Kraken", in the tabular format:
|
||||
|
||||
```
|
||||
$ freqtrade list-markets --exchange kraken --all
|
||||
```
|
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.7'
|
||||
__version__ = '2019.10'
|
||||
|
||||
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,2 +1,4 @@
|
||||
from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401
|
||||
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
|
||||
|
@@ -2,74 +2,72 @@
|
||||
This module contains the argument manager class
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
from typing import List, NamedTuple, Optional
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
|
||||
|
||||
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir"]
|
||||
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", "refresh_pairs"]
|
||||
"max_open_trades", "stake_amount", "fee"]
|
||||
|
||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||
"live", "strategy_list", "export", "exportfilename"]
|
||||
"strategy_list", "export", "exportfilename"]
|
||||
|
||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||
"position_stacking", "epochs", "spaces",
|
||||
"use_max_market_positions", "print_all", "hyperopt_jobs",
|
||||
"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_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
|
||||
ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"]
|
||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||
|
||||
ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY +
|
||||
["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
|
||||
"trade_source", "export", "exportfilename", "timerange",
|
||||
"refresh_pairs", "live"])
|
||||
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column",
|
||||
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
|
||||
|
||||
ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY +
|
||||
["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"])
|
||||
ARGS_CREATE_USERDIR = ["user_data_dir"]
|
||||
|
||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "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 = ["download-data", "list-timeframes", "list-markets", "list-pairs",
|
||||
"plot-dataframe", "plot-profit"]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"]
|
||||
|
||||
|
||||
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):
|
||||
class Arguments:
|
||||
"""
|
||||
Arguments Class. Manage the arguments received by the cli
|
||||
"""
|
||||
def __init__(self, args: Optional[List[str]], description: str,
|
||||
no_default_config: bool = False) -> None:
|
||||
def __init__(self, args: Optional[List[str]]) -> None:
|
||||
self.args = args
|
||||
self._parsed_arg: Optional[argparse.Namespace] = None
|
||||
self.parser = argparse.ArgumentParser(description=description)
|
||||
self._no_default_config = no_default_config
|
||||
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) -> argparse.Namespace:
|
||||
def get_parsed_arg(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Return the list of arguments
|
||||
:return: List[str] List of arguments
|
||||
@@ -78,7 +76,7 @@ class Arguments(object):
|
||||
self._load_args()
|
||||
self._parsed_arg = self._parse_args()
|
||||
|
||||
return self._parsed_arg
|
||||
return vars(self._parsed_arg)
|
||||
|
||||
def _parse_args(self) -> argparse.Namespace:
|
||||
"""
|
||||
@@ -86,9 +84,16 @@ class Arguments(object):
|
||||
"""
|
||||
parsed_arg = self.parser.parse_args(self.args)
|
||||
|
||||
# When no config is provided, but a config exists, use that configuration!
|
||||
subparser = parsed_arg.subparser if 'subparser' in parsed_arg else None
|
||||
|
||||
# Workaround issue in argparse with action='append' and default value
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
if not self._no_default_config and parsed_arg.config is None:
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
if (parsed_arg.config is None
|
||||
and subparser not in NO_CONF_ALLOWED
|
||||
and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file()
|
||||
or (subparser not in NO_CONF_REQURIED))):
|
||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
@@ -106,7 +111,9 @@ class Arguments(object):
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
|
||||
from freqtrade.utils import start_list_exchanges
|
||||
from freqtrade.utils import (start_create_userdir, start_download_data,
|
||||
start_list_exchanges, start_list_timeframes,
|
||||
start_list_markets)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||
|
||||
@@ -125,6 +132,12 @@ class Arguments(object):
|
||||
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',
|
||||
@@ -133,44 +146,51 @@ class Arguments(object):
|
||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=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)
|
||||
# Add list-timeframes subcommand
|
||||
list_timeframes_cmd = subparsers.add_parser(
|
||||
'list-timeframes',
|
||||
help='Print available ticker intervals (timeframes) for the exchange.'
|
||||
)
|
||||
list_timeframes_cmd.set_defaults(func=start_list_timeframes)
|
||||
self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd)
|
||||
|
||||
# Add list-markets subcommand
|
||||
list_markets_cmd = subparsers.add_parser(
|
||||
'list-markets',
|
||||
help='Print markets on exchange.'
|
||||
)
|
||||
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
|
||||
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd)
|
||||
|
||||
# Add list-pairs subcommand
|
||||
list_pairs_cmd = subparsers.add_parser(
|
||||
'list-pairs',
|
||||
help='Print pairs on exchange.'
|
||||
)
|
||||
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
|
||||
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_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)
|
||||
|
@@ -2,9 +2,10 @@ import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange import (is_exchange_bad, is_exchange_available,
|
||||
is_exchange_officially_supported, available_exchanges)
|
||||
|
||||
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason,
|
||||
is_exchange_known_ccxt, is_exchange_bad,
|
||||
is_exchange_officially_supported)
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,28 +20,39 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||
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 is_exchange_available(exchange):
|
||||
if not exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange "{exchange}" is not supported by ccxt '
|
||||
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 available for Freqtrade: '
|
||||
f'{", ".join(available_exchanges())}'
|
||||
)
|
||||
|
||||
if not is_exchange_known_ccxt(exchange):
|
||||
raise OperationalException(
|
||||
f'Exchange "{exchange}" is not known to the ccxt library '
|
||||
f'and therefore not available for the bot.\n'
|
||||
f'The following exchanges are supported by ccxt: '
|
||||
f'The following exchanges are available for Freqtrade: '
|
||||
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
|
||||
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 '
|
||||
logger.warning(f'Exchange "{exchange}" is known to the the ccxt library, '
|
||||
f'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.')
|
||||
|
@@ -2,7 +2,6 @@
|
||||
Definition of cli arguments used in arguments.py
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
|
||||
@@ -55,7 +54,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"datadir": Arg(
|
||||
'-d', '--datadir',
|
||||
help='Path to backtest data.',
|
||||
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
|
||||
@@ -102,13 +106,6 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help='Specify stake_amount.',
|
||||
type=float,
|
||||
),
|
||||
"refresh_pairs": Arg(
|
||||
'-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',
|
||||
),
|
||||
# Backtesting
|
||||
"position_stacking": Arg(
|
||||
'--eps', '--enable-position-stacking',
|
||||
@@ -123,14 +120,9 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
action='store_false',
|
||||
default=True,
|
||||
),
|
||||
"live": Arg(
|
||||
'-l', '--live',
|
||||
help='Use live data.',
|
||||
action='store_true',
|
||||
),
|
||||
"strategy_list": Arg(
|
||||
'--strategy-list',
|
||||
help='Provide a comma-separated list of strategies to backtest. '
|
||||
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 '
|
||||
@@ -146,10 +138,14 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
'--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`',
|
||||
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
|
||||
metavar='PATH',
|
||||
default=os.path.join('user_data', 'backtest_data',
|
||||
'backtest-result.json'),
|
||||
),
|
||||
"fee": Arg(
|
||||
'--fee',
|
||||
help='Specify fee ratio. Will be applied twice (on trade entry and exit).',
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
# Edge
|
||||
"stoploss_range": Arg(
|
||||
@@ -191,6 +187,19 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
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 '
|
||||
@@ -226,20 +235,64 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
'--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. (default: `%(default)s`).',
|
||||
'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.',
|
||||
help='Print output in one column.',
|
||||
action='store_true',
|
||||
),
|
||||
"list_exchanges_all": Arg(
|
||||
'-a', '--all',
|
||||
help='Print all exchanges known to the ccxt library.',
|
||||
action='store_true',
|
||||
),
|
||||
# List pairs / markets
|
||||
"list_pairs_all": Arg(
|
||||
'-a', '--all',
|
||||
help='Print all pairs or market symbols. By default only active '
|
||||
'ones are shown.',
|
||||
action='store_true',
|
||||
),
|
||||
"print_list": Arg(
|
||||
'--print-list',
|
||||
help='Print list of pairs or market symbols. By default data is '
|
||||
'printed in the tabular format.',
|
||||
action='store_true',
|
||||
),
|
||||
"list_pairs_print_json": Arg(
|
||||
'--print-json',
|
||||
help='Print list of pairs or market symbols in JSON format.',
|
||||
action='store_true',
|
||||
default=False,
|
||||
),
|
||||
"print_csv": Arg(
|
||||
'--print-csv',
|
||||
help='Print exchange pair or market data in the csv format.',
|
||||
action='store_true',
|
||||
),
|
||||
"quote_currencies": Arg(
|
||||
'--quote',
|
||||
help='Specify quote currency(-ies). Space-separated list.',
|
||||
nargs='+',
|
||||
metavar='QUOTE_CURRENCY',
|
||||
),
|
||||
"base_currencies": Arg(
|
||||
'--base',
|
||||
help='Specify base currency(-ies). Space-separated list.',
|
||||
nargs='+',
|
||||
metavar='BASE_CURRENCY',
|
||||
),
|
||||
# Script options
|
||||
"pairs": Arg(
|
||||
'-p', '--pairs',
|
||||
help='Show profits for only these pairs. Pairs are comma-separated.',
|
||||
help='Show profits for only these pairs. Pairs are space-separated.',
|
||||
nargs='+',
|
||||
),
|
||||
# Download data
|
||||
"pairs_file": Arg(
|
||||
@@ -253,6 +306,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
),
|
||||
"download_trades": Arg(
|
||||
'--dl-trades',
|
||||
help='Download trades instead of OHLCV data. The bot will resample trades to the '
|
||||
'desired timeframe as specified as --timeframes/-t.',
|
||||
action='store_true',
|
||||
),
|
||||
"exchange": Arg(
|
||||
'--exchange',
|
||||
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
|
||||
@@ -261,9 +320,10 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"timeframes": Arg(
|
||||
'-t', '--timeframes',
|
||||
help=f'Specify which tickers to download. Space-separated list. '
|
||||
f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.',
|
||||
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(
|
||||
@@ -275,14 +335,16 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"indicators1": Arg(
|
||||
'--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',
|
||||
'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. '
|
||||
'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.',
|
||||
default='macd,macdsignal',
|
||||
'Space-separated list. Example: `fastd fastk`. Default: `%(default)s`.',
|
||||
default=['macd', 'macdsignal'],
|
||||
nargs='+',
|
||||
),
|
||||
"plot_limit": Arg(
|
||||
'--plot-limit',
|
||||
|
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."
|
||||
)
|
@@ -1,31 +1,34 @@
|
||||
"""
|
||||
This module contains the configuration class
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import warnings
|
||||
from argparse import Namespace
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
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.create_datadir import create_datadir
|
||||
from freqtrade.configuration.json_schema import validate_config_schema
|
||||
from freqtrade.configuration.config_validation import (validate_config_consistency,
|
||||
validate_config_schema)
|
||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||
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
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Configuration(object):
|
||||
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: Namespace, runmode: RunMode = None) -> None:
|
||||
def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None:
|
||||
self.args = args
|
||||
self.config: Optional[Dict[str, Any]] = None
|
||||
self.runmode = runmode
|
||||
@@ -40,46 +43,49 @@ class Configuration(object):
|
||||
|
||||
return self.config
|
||||
|
||||
def _load_config_files(self) -> Dict[str, Any]:
|
||||
@staticmethod
|
||||
def from_files(files: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Iterate through the config files passed in the args,
|
||||
loading all of them and merging their contents.
|
||||
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 self.args.config:
|
||||
logger.info('Using config: %s ...', path)
|
||||
for path in files:
|
||||
logger.info(f'Using config: {path} ...')
|
||||
|
||||
# Merge config options, overwriting old values
|
||||
config = deep_merge_dicts(self._load_config_file(path), config)
|
||||
config = deep_merge_dicts(load_config_file(path), config)
|
||||
|
||||
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:
|
||||
config = json.load(file)
|
||||
except FileNotFoundError:
|
||||
raise OperationalException(
|
||||
f'Config file "{path}" not found!'
|
||||
' Please create a config file or check whether it exists.')
|
||||
|
||||
return config
|
||||
|
||||
def _normalize_config(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Make config more canonical -- i.e. for example add missing parts that we expect
|
||||
to be normally in it...
|
||||
"""
|
||||
# Normalize config
|
||||
if 'internals' not in config:
|
||||
config['internals'] = {}
|
||||
# TODO: This can be deleted along with removal of deprecated
|
||||
# experimental settings
|
||||
if 'ask_strategy' not in config:
|
||||
config['ask_strategy'] = {}
|
||||
|
||||
# validate configuration before returning
|
||||
logger.info('Validating configuration ...')
|
||||
validate_config_schema(config)
|
||||
|
||||
return config
|
||||
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -87,15 +93,10 @@ class Configuration(object):
|
||||
:return: Configuration dictionary
|
||||
"""
|
||||
# Load all configs
|
||||
config: Dict[str, Any] = self._load_config_files()
|
||||
config: Dict[str, Any] = self.load_from_files(self.args["config"])
|
||||
|
||||
# Make resulting config more canonical
|
||||
self._normalize_config(config)
|
||||
|
||||
logger.info('Validating configuration ...')
|
||||
validate_config_schema(config)
|
||||
|
||||
self._validate_config_consistency(config)
|
||||
# Keep a copy of the original configuration file
|
||||
config['original_config'] = deepcopy(config)
|
||||
|
||||
self._process_common_options(config)
|
||||
|
||||
@@ -105,6 +106,15 @@ class Configuration(object):
|
||||
|
||||
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)
|
||||
|
||||
process_temporary_deprecated_settings(config)
|
||||
|
||||
validate_config_consistency(config)
|
||||
|
||||
return config
|
||||
|
||||
def _process_logging_options(self, config: Dict[str, Any]) -> None:
|
||||
@@ -113,33 +123,27 @@ class Configuration(object):
|
||||
the -v/--verbose, --logfile options
|
||||
"""
|
||||
# Log level
|
||||
if 'verbosity' in self.args and self.args.verbosity:
|
||||
config.update({'verbosity': self.args.verbosity})
|
||||
else:
|
||||
config.update({'verbosity': 0})
|
||||
config.update({'verbosity': self.args.get("verbosity", 0)})
|
||||
|
||||
if 'logfile' in self.args and self.args.logfile:
|
||||
config.update({'logfile': self.args.logfile})
|
||||
if 'logfile' in self.args and self.args["logfile"]:
|
||||
config.update({'logfile': self.args["logfile"]})
|
||||
|
||||
setup_logging(config)
|
||||
|
||||
def _process_strategy_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
# 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})
|
||||
|
||||
self._args_to_config(config, argname='strategy_path',
|
||||
logstring='Using additional Strategy lookup path: {}')
|
||||
|
||||
def _process_common_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
self._process_logging_options(config)
|
||||
self._process_strategy_options(config)
|
||||
|
||||
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})
|
||||
# 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):
|
||||
@@ -162,23 +166,39 @@ class Configuration(object):
|
||||
config['max_open_trades'] = float('inf')
|
||||
|
||||
# Support for sd_notify
|
||||
if 'sd_notify' in self.args and self.args.sd_notify:
|
||||
if 'sd_notify' in self.args and self.args["sd_notify"]:
|
||||
config['internals'].update({'sd_notify': True})
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
check_exchange(config)
|
||||
|
||||
def _process_datadir_options(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Extract information for sys.argv and load datadir configuration:
|
||||
the --datadir option
|
||||
Extract information for sys.argv and load directory configurations
|
||||
--user-data, --datadir
|
||||
"""
|
||||
if 'datadir' in self.args and self.args.datadir:
|
||||
config.update({'datadir': create_datadir(config, self.args.datadir)})
|
||||
else:
|
||||
config.update({'datadir': create_datadir(config, None)})
|
||||
# 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'))
|
||||
|
||||
if self.args.get('exportfilename'):
|
||||
self._args_to_config(config, argname='exportfilename',
|
||||
logstring='Storing backtest results to {} ...')
|
||||
else:
|
||||
config['exportfilename'] = (config['user_data_dir']
|
||||
/ 'backtest_results/backtest-result.json')
|
||||
|
||||
def _process_optimize_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
# This will override the strategy configuration
|
||||
@@ -186,19 +206,15 @@ class Configuration(object):
|
||||
logstring='Parameter -i/--ticker-interval detected ... '
|
||||
'Using ticker_interval: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='live',
|
||||
logstring='Parameter -l/--live detected ...',
|
||||
deprecated_msg='--live will be removed soon.')
|
||||
|
||||
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:
|
||||
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})
|
||||
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:
|
||||
@@ -208,16 +224,17 @@ class Configuration(object):
|
||||
logstring='Parameter --stake_amount detected, '
|
||||
'overriding stake_amount to: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='fee',
|
||||
logstring='Parameter --fee detected, '
|
||||
'setting fee to: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='timerange',
|
||||
logstring='Parameter --timerange detected: {} ...')
|
||||
|
||||
self._process_datadir_options(config)
|
||||
|
||||
self._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)
|
||||
logstring='Using strategy list of {} strategies', logfun=len)
|
||||
|
||||
self._args_to_config(config, argname='ticker_interval',
|
||||
logstring='Overriding ticker interval with Command line argument')
|
||||
@@ -225,20 +242,17 @@ class Configuration(object):
|
||||
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)
|
||||
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)
|
||||
logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"])
|
||||
|
||||
# Hyperopt section
|
||||
self._args_to_config(config, argname='hyperopt',
|
||||
logstring='Using Hyperopt file {}')
|
||||
logstring='Using Hyperopt class name: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_path',
|
||||
logstring='Using additional Hyperopt lookup path: {}')
|
||||
@@ -254,6 +268,15 @@ class Configuration(object):
|
||||
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: {}')
|
||||
|
||||
@@ -267,7 +290,7 @@ class Configuration(object):
|
||||
logstring='Hyperopt continue: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_loss',
|
||||
logstring='Using loss function: {}')
|
||||
logstring='Using Hyperopt loss class name: {}')
|
||||
|
||||
def _process_plot_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
@@ -285,44 +308,26 @@ class Configuration(object):
|
||||
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: {}')
|
||||
self._args_to_config(config, argname='download_trades',
|
||||
logstring='Detected --dl-trades: {}')
|
||||
|
||||
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("Runmode set to {self.runmode}.")
|
||||
logger.info(f"Runmode set to {self.runmode}.")
|
||||
|
||||
config.update({'runmode': self.runmode})
|
||||
|
||||
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 _args_to_config(self, config: Dict[str, Any], argname: str,
|
||||
logstring: str, logfun: Optional[Callable] = None,
|
||||
deprecated_msg: Optional[str] = None) -> None:
|
||||
@@ -335,12 +340,49 @@ class Configuration(object):
|
||||
sample: logfun=len (prints the length of the found
|
||||
configuration instead of the content)
|
||||
"""
|
||||
if argname in self.args and getattr(self.args, argname):
|
||||
if (argname in self.args and self.args[argname] is not None
|
||||
and self.args[argname] is not False):
|
||||
|
||||
config.update({argname: getattr(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()
|
||||
|
@@ -1,20 +0,0 @@
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str:
|
||||
|
||||
folder = Path(datadir) if datadir else Path('user_data/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)
|
59
freqtrade/configuration/deprecated_settings.py
Normal file
59
freqtrade/configuration/deprecated_settings.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Functions to handle deprecated settings
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_conflicting_settings(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str):
|
||||
section1_config = config.get(section1, {})
|
||||
section2_config = config.get(section2, {})
|
||||
if name1 in section1_config and name2 in section2_config:
|
||||
raise OperationalException(
|
||||
f"Conflicting settings `{section1}.{name1}` and `{section2}.{name2}` "
|
||||
"(DEPRECATED) detected in the configuration file. "
|
||||
"This deprecated setting will be removed in the next versions of Freqtrade. "
|
||||
f"Please delete it from your configuration and use the `{section1}.{name1}` "
|
||||
"setting instead."
|
||||
)
|
||||
|
||||
|
||||
def process_deprecated_setting(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str):
|
||||
section2_config = config.get(section2, {})
|
||||
|
||||
if name2 in section2_config:
|
||||
logger.warning(
|
||||
"DEPRECATED: "
|
||||
f"The `{section2}.{name2}` setting is deprecated and "
|
||||
"will be removed in the next versions of Freqtrade. "
|
||||
f"Please use the `{section1}.{name1}` setting in your configuration instead."
|
||||
)
|
||||
section1_config = config.get(section1, {})
|
||||
section1_config[name1] = section2_config[name2]
|
||||
|
||||
|
||||
def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
|
||||
check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
|
||||
'experimental', 'use_sell_signal')
|
||||
check_conflicting_settings(config, 'ask_strategy', 'sell_profit_only',
|
||||
'experimental', 'sell_profit_only')
|
||||
check_conflicting_settings(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
|
||||
'experimental', 'ignore_roi_if_buy_signal')
|
||||
|
||||
process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal',
|
||||
'experimental', 'use_sell_signal')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only',
|
||||
'experimental', 'sell_profit_only')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
|
||||
'experimental', 'ignore_roi_if_buy_signal')
|
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
|
@@ -1,53 +0,0 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from jsonschema import Draft4Validator, validators
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
|
||||
from freqtrade import constants
|
||||
|
||||
|
||||
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
|
||||
)
|
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
|
75
freqtrade/configuration/timerange.py
Normal file
75
freqtrade/configuration/timerange.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
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{13})$', (None, 'date')),
|
||||
(r'^(\d{13})-$', ('date', None)),
|
||||
(r'^(\d{13})-(\d{13})$', ('date', 'date')),
|
||||
]
|
||||
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
|
||||
elif len(starts) == 13:
|
||||
start = int(starts) // 1000
|
||||
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
|
||||
elif len(stops) == 13:
|
||||
stop = int(stops) // 1000
|
||||
else:
|
||||
stop = int(stops)
|
||||
return TimeRange(stype[0], stype[1], start, stop)
|
||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
@@ -5,13 +5,12 @@ 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 = 'DefaultHyperOpt'
|
||||
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
|
||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||
@@ -23,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',
|
||||
@@ -39,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',
|
||||
@@ -101,7 +114,10 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'use_order_book': {'type': 'boolean'},
|
||||
'order_book_min': {'type': 'number', 'minimum': 1},
|
||||
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}
|
||||
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50},
|
||||
'use_sell_signal': {'type': 'boolean'},
|
||||
'sell_profit_only': {'type': 'boolean'},
|
||||
'ignore_roi_if_buy_signal': {'type': 'boolean'}
|
||||
}
|
||||
},
|
||||
'order_types': {
|
||||
@@ -109,6 +125,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'}
|
||||
@@ -130,7 +147,8 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'use_sell_signal': {'type': 'boolean'},
|
||||
'sell_profit_only': {'type': 'boolean'},
|
||||
'ignore_roi_if_buy_signal_true': {'type': 'boolean'}
|
||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
||||
'block_bad_exchanges': {'type': 'boolean'}
|
||||
}
|
||||
},
|
||||
'pairlist': {
|
||||
@@ -248,6 +266,6 @@ CONF_SCHEMA = {
|
||||
'stake_amount',
|
||||
'dry_run',
|
||||
'bid_strategy',
|
||||
'telegram'
|
||||
'unfilledtimeout',
|
||||
]
|
||||
}
|
||||
|
@@ -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'
|
||||
]
|
||||
|
@@ -30,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)
|
||||
@@ -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,
|
||||
(round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2)
|
||||
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,16 +112,16 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
||||
return trades
|
||||
|
||||
|
||||
def load_trades(config) -> pd.DataFrame:
|
||||
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 config["trade_source"] == "DB":
|
||||
return load_trades_from_db(config["db_url"])
|
||||
elif config["trade_source"] == "file":
|
||||
return load_backtest_data(Path(config["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:
|
||||
@@ -139,14 +150,21 @@ def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "c
|
||||
return df_comb
|
||||
|
||||
|
||||
def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame:
|
||||
def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
||||
timeframe: 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)
|
||||
:param col_name: Column name that will be assigned the results
|
||||
:param timeframe: Timeframe used during the operations
|
||||
:return: Returns df with one additional column, col_name, containing the cumulative profit.
|
||||
"""
|
||||
df[col_name] = trades.set_index('close_time')['profitperc'].cumsum()
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
ticker_minutes = timeframe_to_minutes(timeframe)
|
||||
# Resample to ticker_interval to make sure trades match candles
|
||||
_trades_sum = trades.resample(f'{ticker_minutes}min', on='close_time')[['profitperc']].sum()
|
||||
df.loc[:, col_name] = _trades_sum.cumsum()
|
||||
# Set first value to 0
|
||||
df.loc[df.iloc[0].name, col_name] = 0
|
||||
# FFill to get continuous
|
||||
|
@@ -114,3 +114,25 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
||||
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
|
||||
# logger.info('order book %s', frame )
|
||||
return frame
|
||||
|
||||
|
||||
def trades_to_ohlcv(trades: list, timeframe: str) -> list:
|
||||
"""
|
||||
Converts trades list to ohlcv list
|
||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||
:param timeframe: Ticker timeframe to resample data to
|
||||
:return: ohlcv timeframe as list (as returned by ccxt.fetch_ohlcv)
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
ticker_minutes = timeframe_to_minutes(timeframe)
|
||||
df = pd.DataFrame(trades)
|
||||
df['datetime'] = pd.to_datetime(df['datetime'])
|
||||
df = df.set_index('datetime')
|
||||
|
||||
df_new = df['price'].resample(f'{ticker_minutes}min').ohlc()
|
||||
df_new['volume'] = df['amount'].resample(f'{ticker_minutes}min').sum()
|
||||
df_new['date'] = df_new.index.astype("int64") // 10 ** 6
|
||||
# Drop 0 volume rows
|
||||
df_new = df_new.dropna()
|
||||
columns = ["date", "open", "high", "low", "close", "volume"]
|
||||
return list(zip(*[df_new[x].values.tolist() for x in columns]))
|
||||
|
@@ -6,7 +6,7 @@ Common Interface for bot and strategy to access data.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
@@ -17,7 +17,7 @@ from freqtrade.state import RunMode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataProvider():
|
||||
class DataProvider:
|
||||
|
||||
def __init__(self, config: dict, exchange: Exchange) -> None:
|
||||
self._config = config
|
||||
@@ -44,36 +44,55 @@ class DataProvider():
|
||||
|
||||
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 market(self, pair: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Return market data for the pair
|
||||
:param pair: Pair to get the data for
|
||||
:return: Market data dict from ccxt or None if market info is not available for the pair
|
||||
"""
|
||||
return self._exchange.markets.get(pair)
|
||||
|
||||
def ticker(self, pair: str):
|
||||
"""
|
||||
Return last ticker data
|
||||
@@ -81,9 +100,9 @@ class DataProvider():
|
||||
# TODO: Implement me
|
||||
pass
|
||||
|
||||
def orderbook(self, pair: str, maximum: int):
|
||||
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
|
||||
"""
|
||||
return latest orderbook data
|
||||
fetch 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.
|
||||
|
@@ -17,7 +17,7 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade import OperationalException, misc
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv
|
||||
from freqtrade.exchange import Exchange, timeframe_to_minutes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -33,20 +33,12 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||
start_index = 0
|
||||
stop_index = len(tickerlist)
|
||||
|
||||
if timerange.starttype == 'line':
|
||||
stop_index = timerange.startts
|
||||
if timerange.starttype == 'index':
|
||||
start_index = timerange.startts
|
||||
elif timerange.starttype == 'date':
|
||||
if timerange.starttype == 'date':
|
||||
while (start_index < len(tickerlist) and
|
||||
tickerlist[start_index][0] < timerange.startts * 1000):
|
||||
start_index += 1
|
||||
|
||||
if timerange.stoptype == 'line':
|
||||
start_index = len(tickerlist) + timerange.stopts
|
||||
if timerange.stoptype == 'index':
|
||||
stop_index = timerange.stopts
|
||||
elif timerange.stoptype == 'date':
|
||||
if timerange.stoptype == 'date':
|
||||
while (stop_index > 0 and
|
||||
tickerlist[stop_index-1][0] > timerange.stopts * 1000):
|
||||
stop_index -= 1
|
||||
@@ -57,28 +49,67 @@ 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,
|
||||
timerange: Optional[TimeRange] = None) -> Optional[list]:
|
||||
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_trades_file(datadir: Path, pair: str,
|
||||
timerange: Optional[TimeRange] = None) -> List[Dict]:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
:return: tradelist or empty list if unsuccesful
|
||||
"""
|
||||
filename = pair_trades_filename(datadir, pair)
|
||||
tradesdata = misc.file_load_json(filename)
|
||||
if not tradesdata:
|
||||
return []
|
||||
|
||||
return tradesdata
|
||||
|
||||
|
||||
def store_trades_file(datadir: Path, pair: str,
|
||||
data: list, is_zip: bool = True):
|
||||
"""
|
||||
Stores tickerdata to file
|
||||
"""
|
||||
filename = pair_trades_filename(datadir, pair)
|
||||
misc.file_dump_json(filename, data, is_zip=is_zip)
|
||||
|
||||
|
||||
def _validate_pairdata(pair, pairdata, timerange: TimeRange):
|
||||
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
||||
logger.warning('Missing data at start for pair %s, data starts at %s',
|
||||
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
if timerange.stoptype == 'date' and pairdata[-1][0] < timerange.stopts * 1000:
|
||||
logger.warning('Missing data at end for pair %s, data ends at %s',
|
||||
pair, arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
|
||||
|
||||
def load_pair_history(pair: str,
|
||||
ticker_interval: str,
|
||||
datadir: Optional[Path],
|
||||
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
||||
datadir: Path,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
refresh_pairs: bool = False,
|
||||
exchange: Optional[Exchange] = None,
|
||||
fill_up_missing: bool = True,
|
||||
@@ -109,79 +140,69 @@ def load_pair_history(pair: str,
|
||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||
|
||||
if pairdata:
|
||||
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
||||
logger.warning('Missing data at start for pair %s, data starts at %s',
|
||||
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
if timerange.stoptype == 'date' and pairdata[-1][0] < timerange.stopts * 1000:
|
||||
logger.warning('Missing data at end for pair %s, data ends at %s',
|
||||
pair,
|
||||
arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
if timerange:
|
||||
_validate_pairdata(pair, pairdata, timerange)
|
||||
return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair,
|
||||
fill_missing=fill_up_missing,
|
||||
drop_incomplete=drop_incomplete)
|
||||
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),
|
||||
timerange: Optional[TimeRange] = None,
|
||||
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,
|
||||
datadir=datadir, timerange=timerange,
|
||||
refresh_pairs=refresh_pairs,
|
||||
exchange=exchange,
|
||||
fill_up_missing=fill_up_missing)
|
||||
if hist is not None:
|
||||
result[pair] = hist
|
||||
for pair in pairs:
|
||||
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
||||
datadir=datadir, timerange=timerange,
|
||||
refresh_pairs=refresh_pairs,
|
||||
exchange=exchange,
|
||||
fill_up_missing=fill_up_missing)
|
||||
if hist is not None:
|
||||
result[pair] = hist
|
||||
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,
|
||||
timerange: Optional[TimeRange]) -> Tuple[List[Any],
|
||||
Optional[int]]:
|
||||
def pair_trades_filename(datadir: Path, pair: str) -> Path:
|
||||
pair_s = pair.replace("/", "_")
|
||||
filename = datadir.joinpath(f'{pair_s}-trades.json.gz')
|
||||
return filename
|
||||
|
||||
|
||||
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 whether data from an before the stored data will be
|
||||
downloaded.
|
||||
If that's the case then what's available should be completely overwritten.
|
||||
Only used by download_pair_history().
|
||||
"""
|
||||
|
||||
since_ms = None
|
||||
@@ -195,12 +216,11 @@ 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)
|
||||
# remove the last item, could be incomplete candle
|
||||
if data:
|
||||
data.pop()
|
||||
# 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()
|
||||
else:
|
||||
data = []
|
||||
|
||||
@@ -215,7 +235,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 +259,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,
|
||||
since_ms=since_ms if since_ms
|
||||
else
|
||||
int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000)
|
||||
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)
|
||||
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 +291,121 @@ 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: Optional[TimeRange] = None,
|
||||
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 download_trades_history(datadir: Path,
|
||||
exchange: Exchange,
|
||||
pair: str,
|
||||
timerange: Optional[TimeRange] = None) -> bool:
|
||||
"""
|
||||
Download trade history from the exchange.
|
||||
Appends to previously downloaded trades data.
|
||||
"""
|
||||
try:
|
||||
|
||||
since = timerange.startts * 1000 if timerange and timerange.starttype == 'date' else None
|
||||
|
||||
trades = load_trades_file(datadir, pair)
|
||||
|
||||
from_id = trades[-1]['id'] if trades else None
|
||||
|
||||
logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None')
|
||||
logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None')
|
||||
|
||||
new_trades = exchange.get_historic_trades(pair=pair,
|
||||
since=since if since else
|
||||
int(arrow.utcnow().shift(
|
||||
days=-30).float_timestamp) * 1000,
|
||||
# until=xxx,
|
||||
from_id=from_id,
|
||||
)
|
||||
trades.extend(new_trades[1])
|
||||
store_trades_file(datadir, pair, trades)
|
||||
|
||||
logger.debug("New Start: %s", trades[0]['datetime'])
|
||||
logger.debug("New End: %s", trades[-1]['datetime'])
|
||||
logger.info(f"New Amount of trades: {len(trades)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Failed to download historic trades for pair: "{pair}". '
|
||||
f'Error: {e}'
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
||||
timerange: TimeRange, erase=False) -> List[str]:
|
||||
"""
|
||||
Refresh stored trades data.
|
||||
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
|
||||
|
||||
dl_file = pair_trades_filename(datadir, pair)
|
||||
if erase and dl_file.exists():
|
||||
logger.info(
|
||||
f'Deleting existing data for pair {pair}.')
|
||||
dl_file.unlink()
|
||||
|
||||
logger.info(f'Downloading trades for pair {pair}.')
|
||||
download_trades_history(datadir=datadir, exchange=exchange,
|
||||
pair=pair,
|
||||
timerange=timerange)
|
||||
return pairs_not_available
|
||||
|
||||
|
||||
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
|
||||
datadir: Path, timerange: TimeRange, erase=False) -> None:
|
||||
"""
|
||||
Convert stored trades data to ohlcv data
|
||||
"""
|
||||
for pair in pairs:
|
||||
trades = load_trades_file(datadir, pair)
|
||||
for timeframe in timeframes:
|
||||
ohlcv_file = pair_data_filename(datadir, pair, timeframe)
|
||||
if erase and ohlcv_file.exists():
|
||||
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
||||
ohlcv_file.unlink()
|
||||
ohlcv = trades_to_ohlcv(trades, timeframe)
|
||||
# Store ohlcv
|
||||
store_tickerdata_file(datadir, pair, timeframe, data=ohlcv)
|
||||
|
||||
|
||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||
"""
|
||||
Get the maximum timeframe for the given backtest data
|
||||
|
@@ -10,7 +10,7 @@ import utils_find_1st as utf1st
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import constants, OperationalException
|
||||
from freqtrade.configuration import Arguments, TimeRange
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.strategy.interface import SellType
|
||||
|
||||
@@ -28,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
|
||||
@@ -75,10 +75,12 @@ 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()
|
||||
if config.get('fee'):
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee()
|
||||
|
||||
def calculate(self) -> bool:
|
||||
pairs = self.config['exchange']['pair_whitelist']
|
||||
@@ -93,7 +95,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,16 @@
|
||||
from freqtrade.exchange.exchange import Exchange # noqa: F401
|
||||
from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401
|
||||
is_exchange_available,
|
||||
from freqtrade.exchange.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS # noqa: F401
|
||||
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
|
||||
is_exchange_bad,
|
||||
is_exchange_known_ccxt,
|
||||
is_exchange_officially_supported,
|
||||
ccxt_exchanges,
|
||||
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.exchange import (market_is_active, # noqa: F401
|
||||
symbol_is_pair)
|
||||
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__)
|
||||
@@ -12,6 +16,8 @@ class Binance(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
}
|
||||
|
||||
def get_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
@@ -25,3 +31,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,
|
||||
@@ -21,10 +22,91 @@ from freqtrade import (DependencyException, InvalidOrderException,
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
|
||||
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",
|
||||
"hitbtc": "This API cannot be used with Freqtrade. "
|
||||
"Use `hitbtc2` exchange id to access this exchange.",
|
||||
**dict.fromkeys([
|
||||
'adara',
|
||||
'anxpro',
|
||||
'bigone',
|
||||
'coinbase',
|
||||
'coinexchange',
|
||||
'coinmarketcap',
|
||||
'lykke',
|
||||
'xbtce',
|
||||
], "Does not provide timeframes. ccxt fetchOHLCV: False"),
|
||||
**dict.fromkeys([
|
||||
'bcex',
|
||||
'bit2c',
|
||||
'bitbay',
|
||||
'bitflyer',
|
||||
'bitforex',
|
||||
'bithumb',
|
||||
'bitso',
|
||||
'bitstamp1',
|
||||
'bl3p',
|
||||
'braziliex',
|
||||
'btcbox',
|
||||
'btcchina',
|
||||
'btctradeim',
|
||||
'btctradeua',
|
||||
'bxinth',
|
||||
'chilebit',
|
||||
'coincheck',
|
||||
'coinegg',
|
||||
'coinfalcon',
|
||||
'coinfloor',
|
||||
'coingi',
|
||||
'coinmate',
|
||||
'coinone',
|
||||
'coinspot',
|
||||
'coolcoin',
|
||||
'crypton',
|
||||
'deribit',
|
||||
'exmo',
|
||||
'exx',
|
||||
'flowbtc',
|
||||
'foxbit',
|
||||
'fybse',
|
||||
# 'hitbtc',
|
||||
'ice3x',
|
||||
'independentreserve',
|
||||
'indodax',
|
||||
'itbit',
|
||||
'lakebtc',
|
||||
'latoken',
|
||||
'liquid',
|
||||
'livecoin',
|
||||
'luno',
|
||||
'mixcoins',
|
||||
'negociecoins',
|
||||
'nova',
|
||||
'paymium',
|
||||
'southxchange',
|
||||
'stronghold',
|
||||
'surbitcoin',
|
||||
'therock',
|
||||
'tidex',
|
||||
'vaultoro',
|
||||
'vbtc',
|
||||
'virwox',
|
||||
'yobit',
|
||||
'zaif',
|
||||
], "Does not provide timeframes. ccxt fetchOHLCV: emulated"),
|
||||
}
|
||||
|
||||
MAP_EXCHANGE_CHILDCLASS = {
|
||||
'binanceus': 'binance',
|
||||
'binanceje': 'binance',
|
||||
}
|
||||
|
||||
|
||||
def retrier_async(f):
|
||||
@@ -63,9 +145,11 @@ def retrier(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
class Exchange(object):
|
||||
class Exchange:
|
||||
|
||||
_config: Dict = {}
|
||||
|
||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||
_params: Dict = {}
|
||||
|
||||
# Dict to specify which options each exchange implements
|
||||
@@ -76,10 +160,13 @@ class Exchange(object):
|
||||
"order_time_in_force": ["gtc"],
|
||||
"ohlcv_candle_limit": 500,
|
||||
"ohlcv_partial_candle": True,
|
||||
"trades_pagination": "time", # Possible are "time" or "id"
|
||||
"trades_pagination_arg": "since",
|
||||
|
||||
}
|
||||
_ft_has: Dict = {}
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
def __init__(self, config: dict, validate: bool = True) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
it does basic validation whether the specified exchange and pairs are valid.
|
||||
@@ -119,6 +206,9 @@ class Exchange(object):
|
||||
self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit']
|
||||
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
|
||||
|
||||
self._trades_pagination = self._ft_has['trades_pagination']
|
||||
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
|
||||
|
||||
# Initialize ccxt objects
|
||||
self._api = self._init_ccxt(
|
||||
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
|
||||
@@ -127,20 +217,21 @@ class Exchange(object):
|
||||
|
||||
logger.info('Using Exchange "%s"', self.name)
|
||||
|
||||
if validate:
|
||||
# Check if timeframe is available
|
||||
self.validate_timeframes(config.get('ticker_interval'))
|
||||
|
||||
# Initial markets load
|
||||
self._load_markets()
|
||||
|
||||
# Check if all pairs are available
|
||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||
self.validate_ordertypes(config.get('order_types', {}))
|
||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||
|
||||
# Converts the interval provided in minutes in config to seconds
|
||||
self.markets_refresh_interval: int = exchange_config.get(
|
||||
"markets_refresh_interval", 60) * 60
|
||||
# Initial markets load
|
||||
self._load_markets()
|
||||
|
||||
# Check if all pairs are available
|
||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||
self.validate_ordertypes(config.get('order_types', {}))
|
||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||
|
||||
if config.get('ticker_interval'):
|
||||
# Check if timeframe is available
|
||||
self.validate_timeframes(config['ticker_interval'])
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
@@ -159,7 +250,7 @@ class Exchange(object):
|
||||
# Find matching class for the given exchange name
|
||||
name = exchange_config['name']
|
||||
|
||||
if not is_exchange_available(name, ccxt_module):
|
||||
if not is_exchange_known_ccxt(name, ccxt_module):
|
||||
raise OperationalException(f'Exchange {name} is not supported by ccxt')
|
||||
|
||||
ex_config = {
|
||||
@@ -193,6 +284,10 @@ class Exchange(object):
|
||||
"""exchange ccxt id"""
|
||||
return self._api.id
|
||||
|
||||
@property
|
||||
def timeframes(self) -> List[str]:
|
||||
return list((self._api.timeframes or {}).keys())
|
||||
|
||||
@property
|
||||
def markets(self) -> Dict:
|
||||
"""exchange ccxt markets"""
|
||||
@@ -201,6 +296,28 @@ class Exchange(object):
|
||||
self._load_markets()
|
||||
return self._api.markets
|
||||
|
||||
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
|
||||
pairs_only: bool = False, active_only: bool = False) -> Dict:
|
||||
"""
|
||||
Return exchange ccxt markets, filtered out by base currency and quote currency
|
||||
if this was requested in parameters.
|
||||
|
||||
TODO: consider moving it to the Dataprovider
|
||||
"""
|
||||
markets = self.markets
|
||||
if not markets:
|
||||
raise OperationalException("Markets were not loaded.")
|
||||
|
||||
if base_currencies:
|
||||
markets = {k: v for k, v in markets.items() if v['base'] in base_currencies}
|
||||
if quote_currencies:
|
||||
markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies}
|
||||
if pairs_only:
|
||||
markets = {k: v for k, v in markets.items() if symbol_is_pair(v['symbol'])}
|
||||
if active_only:
|
||||
markets = {k: v for k, v in markets.items() if market_is_active(v)}
|
||||
return markets
|
||||
|
||||
def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
|
||||
if pair_interval in self._klines:
|
||||
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
|
||||
@@ -260,7 +377,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
|
||||
@@ -269,6 +386,12 @@ 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:
|
||||
"""
|
||||
@@ -279,7 +402,7 @@ class Exchange(object):
|
||||
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:
|
||||
def validate_timeframes(self, timeframe: Optional[str]) -> None:
|
||||
"""
|
||||
Checks if ticker interval from config is a supported timeframe on the exchange
|
||||
"""
|
||||
@@ -292,10 +415,9 @@ class Exchange(object):
|
||||
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:
|
||||
if timeframe and (timeframe not in self.timeframes):
|
||||
raise OperationalException(
|
||||
f'Invalid ticker {timeframe}, this Exchange supports {timeframes}')
|
||||
f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}")
|
||||
|
||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||
"""
|
||||
@@ -309,7 +431,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:
|
||||
@@ -355,7 +477,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,
|
||||
@@ -365,11 +487,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:
|
||||
@@ -380,6 +503,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,
|
||||
@@ -397,12 +522,12 @@ 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'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'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
@@ -439,30 +564,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:
|
||||
@@ -535,19 +644,24 @@ class Exchange(object):
|
||||
logger.info("returning cached ticker-data for %s", pair)
|
||||
return self._cached_ticker[pair]
|
||||
|
||||
def get_history(self, pair: str, ticker_interval: str,
|
||||
since_ms: int) -> List:
|
||||
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,
|
||||
since_ms=since_ms))
|
||||
self._async_get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval,
|
||||
since_ms=since_ms))
|
||||
|
||||
async def _async_get_history(self, pair: str,
|
||||
ticker_interval: str,
|
||||
since_ms: int) -> List:
|
||||
async def _async_get_historic_ohlcv(self, pair: str,
|
||||
ticker_interval: str,
|
||||
since_ms: int) -> List:
|
||||
|
||||
one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit
|
||||
logger.debug(
|
||||
@@ -573,7 +687,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))
|
||||
|
||||
@@ -621,7 +738,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:
|
||||
@@ -658,6 +775,154 @@ class Exchange(object):
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e
|
||||
|
||||
@retrier_async
|
||||
async def _async_fetch_trades(self, pair: str,
|
||||
since: Optional[int] = None,
|
||||
params: Optional[dict] = None) -> List[Dict]:
|
||||
"""
|
||||
Asyncronously gets trade history using fetch_trades.
|
||||
Handles exchange errors, does one call to the exchange.
|
||||
:param pair: Pair to fetch trade data for
|
||||
:param since: Since as integer timestamp in milliseconds
|
||||
returns: List of dicts containing trades
|
||||
"""
|
||||
try:
|
||||
# fetch trades asynchronously
|
||||
if params:
|
||||
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
|
||||
trades = await self._api_async.fetch_trades(pair, params=params, limit=1000)
|
||||
else:
|
||||
logger.debug(
|
||||
"Fetching trades for pair %s, since %s %s...",
|
||||
pair, since,
|
||||
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
|
||||
)
|
||||
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
|
||||
return trades
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching historical trade data.'
|
||||
f'Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f'Could not fetch trade data. Msg: {e}') from e
|
||||
|
||||
async def _async_get_trade_history_id(self, pair: str,
|
||||
until: int,
|
||||
since: Optional[int] = None,
|
||||
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]:
|
||||
"""
|
||||
Asyncronously gets trade history using fetch_trades
|
||||
use this when exchange uses id-based iteration (check `self._trades_pagination`)
|
||||
:param pair: Pair to fetch trade data for
|
||||
:param since: Since as integer timestamp in milliseconds
|
||||
:param until: Until as integer timestamp in milliseconds
|
||||
:param from_id: Download data starting with ID (if id is known). Ignores "since" if set.
|
||||
returns tuple: (pair, trades-list)
|
||||
"""
|
||||
|
||||
trades: List[Dict] = []
|
||||
|
||||
if not from_id:
|
||||
# Fetch first elements using timebased method to get an ID to paginate on
|
||||
# Depending on the Exchange, this can introduce a drift at the start of the interval
|
||||
# of up to an hour.
|
||||
# e.g. Binance returns the "last 1000" candles within a 1h time interval
|
||||
# - so we will miss the first trades.
|
||||
t = await self._async_fetch_trades(pair, since=since)
|
||||
from_id = t[-1]['id']
|
||||
trades.extend(t[:-1])
|
||||
while True:
|
||||
t = await self._async_fetch_trades(pair,
|
||||
params={self._trades_pagination_arg: from_id})
|
||||
if len(t):
|
||||
# Skip last id since its the key for the next call
|
||||
trades.extend(t[:-1])
|
||||
if from_id == t[-1]['id'] or t[-1]['timestamp'] > until:
|
||||
logger.debug(f"Stopping because from_id did not change. "
|
||||
f"Reached {t[-1]['timestamp']} > {until}")
|
||||
# Reached the end of the defined-download period - add last trade as well.
|
||||
trades.extend(t[-1:])
|
||||
break
|
||||
|
||||
from_id = t[-1]['id']
|
||||
else:
|
||||
break
|
||||
|
||||
return (pair, trades)
|
||||
|
||||
async def _async_get_trade_history_time(self, pair: str, until: int,
|
||||
since: Optional[int] = None) -> Tuple[str, List]:
|
||||
"""
|
||||
Asyncronously gets trade history using fetch_trades,
|
||||
when the exchange uses time-based iteration (check `self._trades_pagination`)
|
||||
:param pair: Pair to fetch trade data for
|
||||
:param since: Since as integer timestamp in milliseconds
|
||||
:param until: Until as integer timestamp in milliseconds
|
||||
returns tuple: (pair, trades-list)
|
||||
"""
|
||||
|
||||
trades: List[Dict] = []
|
||||
while True:
|
||||
t = await self._async_fetch_trades(pair, since=since)
|
||||
if len(t):
|
||||
since = t[-1]['timestamp']
|
||||
trades.extend(t)
|
||||
# Reached the end of the defined-download period
|
||||
if until and t[-1]['timestamp'] > until:
|
||||
logger.debug(
|
||||
f"Stopping because until was reached. {t[-1]['timestamp']} > {until}")
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
return (pair, trades)
|
||||
|
||||
async def _async_get_trade_history(self, pair: str,
|
||||
since: Optional[int] = None,
|
||||
until: Optional[int] = None,
|
||||
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]:
|
||||
"""
|
||||
Async wrapper handling downloading trades using either time or id based methods.
|
||||
"""
|
||||
|
||||
if self._trades_pagination == 'time':
|
||||
return await self._async_get_trade_history_time(
|
||||
pair=pair, since=since,
|
||||
until=until or ccxt.Exchange.milliseconds())
|
||||
elif self._trades_pagination == 'id':
|
||||
return await self._async_get_trade_history_id(
|
||||
pair=pair, since=since,
|
||||
until=until or ccxt.Exchange.milliseconds(), from_id=from_id
|
||||
)
|
||||
else:
|
||||
raise OperationalException(f"Exchange {self.name} does use neither time, "
|
||||
f"nor id based pagination")
|
||||
|
||||
def get_historic_trades(self, pair: str,
|
||||
since: Optional[int] = None,
|
||||
until: Optional[int] = None,
|
||||
from_id: Optional[str] = None) -> Tuple[str, 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: Timestamp in milliseconds to get history from
|
||||
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
|
||||
:param from_id: Download data starting with ID (if id is known)
|
||||
:returns List of tickers
|
||||
"""
|
||||
if not self.exchange_has("fetchTrades"):
|
||||
raise OperationalException("This exchange does not suport downloading Trades.")
|
||||
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
self._async_get_trade_history(pair=pair, since=since,
|
||||
until=until, from_id=from_id))
|
||||
|
||||
@retrier
|
||||
def cancel_order(self, order_id: str, pair: str) -> None:
|
||||
if self._config['dry_run']:
|
||||
@@ -677,8 +942,13 @@ class Exchange(object):
|
||||
@retrier
|
||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
return order
|
||||
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:
|
||||
@@ -719,7 +989,8 @@ 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
|
||||
@@ -747,20 +1018,35 @@ class Exchange(object):
|
||||
raise OperationalException(e) from e
|
||||
|
||||
|
||||
def is_exchange_bad(exchange: str) -> bool:
|
||||
return exchange in ['bitmex', 'bitstamp']
|
||||
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_known_ccxt(exchange_name: str, ccxt_module=None) -> bool:
|
||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||
|
||||
|
||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
return exchange_name in ['bittrex', 'binance']
|
||||
|
||||
|
||||
def ccxt_exchanges(ccxt_module=None) -> List[str]:
|
||||
"""
|
||||
Return the list of all exchanges known to ccxt
|
||||
"""
|
||||
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||
|
||||
|
||||
def available_exchanges(ccxt_module=None) -> List[str]:
|
||||
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||
"""
|
||||
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
||||
"""
|
||||
exchanges = ccxt_exchanges(ccxt_module)
|
||||
return [x for x in exchanges if not is_exchange_bad(x)]
|
||||
|
||||
|
||||
def timeframe_to_seconds(ticker_interval: str) -> int:
|
||||
@@ -774,13 +1060,66 @@ 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)
|
||||
|
||||
|
||||
def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency: str = None):
|
||||
"""
|
||||
Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the
|
||||
quote currency separated by '/' character. If base_currency and/or quote_currency is passed,
|
||||
it also checks that the symbol contains appropriate base and/or quote currency part before
|
||||
and after the separating character correspondingly.
|
||||
"""
|
||||
symbol_parts = market_symbol.split('/')
|
||||
return (len(symbol_parts) == 2 and
|
||||
(symbol_parts[0] == base_currency if base_currency else len(symbol_parts[0]) > 0) and
|
||||
(symbol_parts[1] == quote_currency if quote_currency else len(symbol_parts[1]) > 0))
|
||||
|
||||
|
||||
def market_is_active(market):
|
||||
"""
|
||||
Return True if the market is active.
|
||||
"""
|
||||
# "It's active, if the active flag isn't explicitly set to false. If it's missing or
|
||||
# true then it's true. If it's undefined, then it's most likely true, but not 100% )"
|
||||
# See https://github.com/ccxt/ccxt/issues/4874,
|
||||
# https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520
|
||||
return market.get('active', True) is not False
|
||||
|
@@ -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,37 @@ logger = logging.getLogger(__name__)
|
||||
class Kraken(Exchange):
|
||||
|
||||
_params: Dict = {"trading_agreement": "agree"}
|
||||
_ft_has: Dict = {
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "since",
|
||||
}
|
||||
|
||||
@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
|
||||
|
@@ -1,34 +1,36 @@
|
||||
"""
|
||||
Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
||||
"""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from math import isclose
|
||||
from os import getpid
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from freqtrade import (DependencyException, OperationalException, InvalidOrderException,
|
||||
__version__, constants, persistence)
|
||||
from freqtrade import (DependencyException, InvalidOrderException, __version__,
|
||||
constants, persistence)
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
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.exchange import timeframe_to_minutes, timeframe_to_next_date
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import (ExchangeResolver, PairListResolver,
|
||||
StrategyResolver)
|
||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver
|
||||
from freqtrade.state import State
|
||||
from freqtrade.strategy.interface import SellType, IStrategy
|
||||
from freqtrade.strategy.interface import IStrategy, SellType
|
||||
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.
|
||||
@@ -49,9 +51,14 @@ class FreqtradeBot(object):
|
||||
# Init objects
|
||||
self.config = config
|
||||
|
||||
self._heartbeat_msg = 0
|
||||
|
||||
self.heartbeat_interval = self.config.get('internals', {}).get('heartbeat_interval', 60)
|
||||
|
||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||
|
||||
self.rpc: RPCManager = RPCManager(self)
|
||||
# Check config consistency here since strategies can set certain options
|
||||
validate_config_consistency(config)
|
||||
|
||||
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
|
||||
|
||||
@@ -79,6 +86,13 @@ class FreqtradeBot(object):
|
||||
initial_state = self.config.get('initial_state')
|
||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||
|
||||
# RPC runs in separate threads, can start handling external commands just after
|
||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||
# so anything in the Freqtradebot instance should be ready (initialized), including
|
||||
# the initial state of the bot.
|
||||
# Keep this at the end of this initialization method.
|
||||
self.rpc: RPCManager = RPCManager(self)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Cleanup pending resources on an already stopped bot
|
||||
@@ -99,13 +113,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()
|
||||
@@ -131,19 +144,21 @@ class FreqtradeBot(object):
|
||||
self.strategy.informative_pairs())
|
||||
|
||||
# First process current opened trades
|
||||
for trade in trades:
|
||||
state_changed |= self.process_maybe_execute_sell(trade)
|
||||
self.process_maybe_execute_sells(trades)
|
||||
|
||||
# Then looking for buy opportunities
|
||||
if len(trades) < self.config['max_open_trades']:
|
||||
state_changed = self.process_maybe_execute_buy()
|
||||
self.process_maybe_execute_buys()
|
||||
|
||||
if 'unfilledtimeout' in self.config:
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
Trade.session.flush()
|
||||
|
||||
return state_changed
|
||||
if (self.heartbeat_interval
|
||||
and (arrow.utcnow().timestamp - self._heartbeat_msg > self.heartbeat_interval)):
|
||||
logger.info(f"Bot heartbeat. PID={getpid()}")
|
||||
self._heartbeat_msg = arrow.utcnow().timestamp
|
||||
|
||||
def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]):
|
||||
"""
|
||||
@@ -209,7 +224,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,17 +268,17 @@ 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)
|
||||
|
||||
if not whitelist:
|
||||
logger.warning("Whitelist is empty.")
|
||||
logger.info("Active pair whitelist is empty.")
|
||||
return False
|
||||
|
||||
# Remove currently opened and latest pairs from whitelist
|
||||
@@ -273,18 +288,25 @@ class FreqtradeBot(object):
|
||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||
|
||||
if not whitelist:
|
||||
logger.info("No currency pair in whitelist, but checking to sell open trades.")
|
||||
logger.info("No currency pair in active pair whitelist, "
|
||||
"but checking to sell open trades.")
|
||||
return False
|
||||
|
||||
buycount = 0
|
||||
# running get_signal on historical data fetched
|
||||
for _pair in whitelist:
|
||||
(buy, sell) = self.strategy.get_signal(
|
||||
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
|
||||
if self.strategy.is_pair_locked(_pair):
|
||||
logger.info(f"Pair {_pair} is currently locked.")
|
||||
continue
|
||||
|
||||
if buy and not sell:
|
||||
(buy, sell) = self.strategy.get_signal(
|
||||
_pair, self.strategy.ticker_interval,
|
||||
self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
|
||||
|
||||
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 +316,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 +361,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,56 +446,48 @@ class FreqtradeBot(object):
|
||||
|
||||
return True
|
||||
|
||||
def process_maybe_execute_buy(self) -> bool:
|
||||
def process_maybe_execute_buys(self) -> None:
|
||||
"""
|
||||
Tries to execute a buy trade in a safe way
|
||||
:return: True if executed
|
||||
Tries to execute buy orders for trades in a safe way
|
||||
"""
|
||||
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.debug('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:
|
||||
def process_maybe_execute_sells(self, trades: List[Any]) -> None:
|
||||
"""
|
||||
Tries to execute a sell trade
|
||||
:return: True if executed
|
||||
Tries to execute sell orders for trades in a safe way
|
||||
"""
|
||||
try:
|
||||
self.update_trade_state(trade)
|
||||
result = False
|
||||
for trade in trades:
|
||||
try:
|
||||
self.update_trade_state(trade)
|
||||
|
||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open:
|
||||
result = self.handle_stoploss_on_exchange(trade)
|
||||
if result:
|
||||
self.wallets.update()
|
||||
return result
|
||||
|
||||
if trade.is_open and trade.open_order_id is None:
|
||||
if (self.strategy.order_types.get('stoploss_on_exchange') and
|
||||
self.handle_stoploss_on_exchange(trade)):
|
||||
result = True
|
||||
continue
|
||||
# Check if we can sell our current pair
|
||||
result = self.handle_trade(trade)
|
||||
if trade.open_order_id is None and self.handle_trade(trade):
|
||||
result = True
|
||||
|
||||
# Updating wallets if any trade occured
|
||||
if result:
|
||||
self.wallets.update()
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to sell trade: %s', exception)
|
||||
|
||||
return result
|
||||
# Updating wallets if any trade occured
|
||||
if result:
|
||||
self.wallets.update()
|
||||
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to sell trade: %s', exception)
|
||||
return False
|
||||
|
||||
def get_real_amount(self, trade: Trade, order: Dict) -> float:
|
||||
def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float:
|
||||
"""
|
||||
Get real amount for the trade
|
||||
Necessary for exchanges which charge fees in base currency (e.g. binance)
|
||||
"""
|
||||
order_amount = order['amount']
|
||||
if order_amount is None:
|
||||
order_amount = order['amount']
|
||||
# Only run for closed orders
|
||||
if trade.fee_open == 0 or order['status'] == 'open':
|
||||
return order_amount
|
||||
@@ -507,9 +522,9 @@ class FreqtradeBot(object):
|
||||
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")
|
||||
raise DependencyException("Half bought? Amounts don't match")
|
||||
real_amount = amount - fee_abs
|
||||
if fee_abs != 0:
|
||||
logger.info(f"Applying fee on amount for {trade} "
|
||||
@@ -532,12 +547,12 @@ class FreqtradeBot(object):
|
||||
# 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
|
||||
|
||||
except OperationalException as exception:
|
||||
except DependencyException as exception:
|
||||
logger.warning("Could not update trade amount: %s", exception)
|
||||
|
||||
trade.update(order)
|
||||
@@ -571,18 +586,20 @@ class FreqtradeBot(object):
|
||||
:return: True if trade has been sold, False otherwise
|
||||
"""
|
||||
if not trade.is_open:
|
||||
raise ValueError(f'Attempt to handle closed trade: {trade}')
|
||||
raise DependencyException(f'Attempt to handle closed trade: {trade}')
|
||||
|
||||
logger.debug('Handling %s ...', trade)
|
||||
|
||||
(buy, sell) = (False, False)
|
||||
experimental = self.config.get('experimental', {})
|
||||
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
||||
|
||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||
|
||||
if (config_ask_strategy.get('use_sell_signal', True) or
|
||||
config_ask_strategy.get('ignore_roi_if_buy_signal')):
|
||||
(buy, sell) = self.strategy.get_signal(
|
||||
trade.pair, self.strategy.ticker_interval,
|
||||
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
|
||||
|
||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||
if config_ask_strategy.get('use_order_book', False):
|
||||
logger.info('Using order book for selling...')
|
||||
# logger.debug('Order book %s',orderBook)
|
||||
@@ -608,6 +625,33 @@ class FreqtradeBot(object):
|
||||
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
|
||||
@@ -626,53 +670,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,7 +721,7 @@ class FreqtradeBot(object):
|
||||
if trade.stop_loss > float(order['info']['stopPrice']):
|
||||
# we check if the update is neccesary
|
||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat:
|
||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||
# cancelling the current stoploss on exchange first
|
||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
|
||||
'in order to add another one ...', order['id'])
|
||||
@@ -706,16 +731,13 @@ 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 "
|
||||
f"for pair {trade.pair}.")
|
||||
# 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_and_execute_sell(self, trade: Trade, sell_rate: float,
|
||||
buy: bool, sell: bool) -> bool:
|
||||
@@ -741,8 +763,8 @@ class FreqtradeBot(object):
|
||||
"""
|
||||
buy_timeout = self.config['unfilledtimeout']['buy']
|
||||
sell_timeout = self.config['unfilledtimeout']['sell']
|
||||
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
|
||||
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
|
||||
buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
|
||||
sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
|
||||
|
||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||
try:
|
||||
@@ -766,21 +788,18 @@ class FreqtradeBot(object):
|
||||
self.wallets.update()
|
||||
continue
|
||||
|
||||
# Handle cancelled on exchange
|
||||
if order['status'] == 'canceled':
|
||||
if order['side'] == 'buy':
|
||||
self.handle_buy_order_full_cancel(trade, "canceled on Exchange")
|
||||
elif order['side'] == 'sell':
|
||||
self.handle_timedout_limit_sell(trade, order)
|
||||
self.wallets.update()
|
||||
# Check if order is still actually open
|
||||
elif order['status'] == 'open':
|
||||
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
|
||||
self.handle_timedout_limit_buy(trade, order)
|
||||
self.wallets.update()
|
||||
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
|
||||
self.handle_timedout_limit_sell(trade, order)
|
||||
self.wallets.update()
|
||||
if ((order['side'] == 'buy' and order['status'] == 'canceled')
|
||||
or (order['status'] == 'open'
|
||||
and order['side'] == 'buy' and ordertime < buy_timeout_threshold)):
|
||||
|
||||
self.handle_timedout_limit_buy(trade, order)
|
||||
self.wallets.update()
|
||||
|
||||
elif ((order['side'] == 'sell' and order['status'] == 'canceled')
|
||||
or (order['status'] == 'open'
|
||||
and order['side'] == 'sell' and ordertime < sell_timeout_threshold)):
|
||||
self.handle_timedout_limit_sell(trade, order)
|
||||
self.wallets.update()
|
||||
|
||||
def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None:
|
||||
"""Close trade in database and send message"""
|
||||
@@ -796,16 +815,33 @@ class FreqtradeBot(object):
|
||||
"""Buy timeout - cancel order
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
if order['remaining'] == order['amount']:
|
||||
reason = "cancelled due to timeout"
|
||||
if order['status'] != 'canceled':
|
||||
corder = self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
else:
|
||||
# Order was cancelled already, so we can reuse the existing dict
|
||||
corder = order
|
||||
reason = "canceled on Exchange"
|
||||
|
||||
if corder['remaining'] == corder['amount']:
|
||||
# if trade is not partially completed, just delete the trade
|
||||
self.handle_buy_order_full_cancel(trade, "cancelled due to timeout")
|
||||
self.handle_buy_order_full_cancel(trade, reason)
|
||||
return True
|
||||
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
# and close the order
|
||||
trade.amount = order['amount'] - order['remaining']
|
||||
trade.amount = corder['amount'] - corder['remaining']
|
||||
trade.stake_amount = trade.amount * trade.open_rate
|
||||
# verify if fees were taken from amount to avoid problems during selling
|
||||
try:
|
||||
new_amount = self.get_real_amount(trade, corder, trade.amount)
|
||||
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
||||
trade.amount = new_amount
|
||||
# Fee was applied, so set to 0
|
||||
trade.fee_open = 0
|
||||
except DependencyException as e:
|
||||
logger.warning("Could not update trade amount: %s", e)
|
||||
|
||||
trade.open_order_id = None
|
||||
logger.info('Partial buy order timeout for %s.', trade)
|
||||
self.rpc.send_msg({
|
||||
@@ -868,20 +904,32 @@ class FreqtradeBot(object):
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||
|
||||
# Execute sell and update trade record
|
||||
order_id = self.exchange.sell(pair=str(trade.pair),
|
||||
ordertype=self.strategy.order_types[sell_type],
|
||||
amount=trade.amount, rate=limit,
|
||||
time_in_force=self.strategy.order_time_in_force['sell']
|
||||
)['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")
|
||||
|
||||
trade.open_order_id = order_id
|
||||
# Execute sell and update trade record
|
||||
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']
|
||||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
@@ -898,7 +946,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,
|
||||
|
@@ -1,40 +0,0 @@
|
||||
from math import cos, exp, pi, sqrt
|
||||
|
||||
import numpy as np
|
||||
import talib as ta
|
||||
from pandas import Series
|
||||
|
||||
|
||||
def went_up(series: Series) -> bool:
|
||||
return series > series.shift(1)
|
||||
|
||||
|
||||
def went_down(series: Series) -> bool:
|
||||
return series < series.shift(1)
|
||||
|
||||
|
||||
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
|
||||
magic = pi * sqrt(2) / smoothing
|
||||
a1 = exp(-magic)
|
||||
coeff2 = 2 * a1 * cos(magic)
|
||||
coeff3 = -a1 * a1
|
||||
coeff1 = (1 - coeff2 - coeff3) / 2
|
||||
|
||||
filtered = series.copy()
|
||||
|
||||
for i in range(2, len(series)):
|
||||
filtered.iloc[i] = coeff1 * (series.iloc[i] + series.iloc[i-1]) + \
|
||||
coeff2 * filtered.iloc[i-1] + coeff3 * filtered.iloc[i-2]
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def fishers_inverse(series: Series, smoothing: float = 0) -> np.ndarray:
|
||||
""" Does a smoothed fishers inverse transformation.
|
||||
Can be used with any oscillator that goes from 0 to 100 like RSI or MFI """
|
||||
v1 = 0.1 * (series - 50)
|
||||
if smoothing > 0:
|
||||
v2 = ta.WMA(v1.values, timeperiod=smoothing)
|
||||
else:
|
||||
v2 = v1
|
||||
return (np.exp(2 * v2)-1) / (np.exp(2 * v2) + 1)
|
@@ -11,7 +11,6 @@ if sys.version_info < (3, 6):
|
||||
|
||||
# flake8: noqa E402
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from typing import Any, List
|
||||
|
||||
from freqtrade import OperationalException
|
||||
@@ -31,16 +30,13 @@ def main(sysargv: List[str] = None) -> None:
|
||||
return_code: Any = 1
|
||||
worker = None
|
||||
try:
|
||||
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,11 +5,12 @@ import gzip
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing.io import IO
|
||||
|
||||
import numpy as np
|
||||
import rapidjson
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -39,7 +40,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
||||
return dates.dt.to_pydatetime()
|
||||
|
||||
|
||||
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
|
||||
@@ -49,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:
|
||||
@@ -60,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,
|
||||
@@ -71,8 +72,10 @@ def json_load(datafile):
|
||||
|
||||
def file_load_json(file):
|
||||
|
||||
gzipfile = file.with_suffix(file.suffix + '.gz')
|
||||
|
||||
if file.suffix != ".gz":
|
||||
gzipfile = file.with_suffix(file.suffix + '.gz')
|
||||
else:
|
||||
gzipfile = file
|
||||
# Try gzip file first, otherwise regular json file.
|
||||
if gzipfile.is_file():
|
||||
logger.debug('Loading ticker data from file %s', gzipfile)
|
||||
@@ -113,3 +116,14 @@ 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()}
|
||||
|
||||
|
||||
def plural(num, singular: str, plural: str = None) -> str:
|
||||
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
||||
|
@@ -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.configuration 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
|
||||
|
||||
@@ -62,9 +63,12 @@ class Backtesting(object):
|
||||
self.config['exchange']['uid'] = ''
|
||||
self.config['dry_run'] = True
|
||||
self.strategylist: List[IStrategy] = []
|
||||
|
||||
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
|
||||
self.fee = self.exchange.get_fee()
|
||||
|
||||
if config.get('fee'):
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee()
|
||||
|
||||
if self.config.get('runmode') != RunMode.HYPEROPT:
|
||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||
@@ -80,6 +84,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 +98,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
|
||||
@@ -144,8 +149,8 @@ class Backtesting(object):
|
||||
len(results[results.profit_abs < 0])
|
||||
])
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(tabular_data, headers=headers, # type: ignore
|
||||
floatfmt=floatfmt, tablefmt="pipe")
|
||||
return tabulate(tabular_data, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||
|
||||
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
||||
"""
|
||||
@@ -183,10 +188,10 @@ class Backtesting(object):
|
||||
len(results[results.profit_abs < 0])
|
||||
])
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(tabular_data, headers=headers, # type: ignore
|
||||
floatfmt=floatfmt, tablefmt="pipe")
|
||||
return tabulate(tabular_data, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||
|
||||
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 +202,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 +220,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 +240,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:
|
||||
@@ -263,6 +270,11 @@ class Backtesting(object):
|
||||
# - (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)
|
||||
|
||||
# Use the maximum between closerate and low as we
|
||||
# cannot sell outside of a candle.
|
||||
# Applies when using {"xx": -1} as roi to force sells after xx minutes
|
||||
closerate = max(closerate, sell_row.low)
|
||||
else:
|
||||
# This should not be reached...
|
||||
closerate = sell_row.open
|
||||
@@ -285,23 +297,25 @@ 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,
|
||||
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||
open_time=buy_row.date,
|
||||
close_time=sell_row.date,
|
||||
trade_duration=int((
|
||||
sell_row.date - buy_row.date).total_seconds() // 60),
|
||||
open_index=buy_row.Index,
|
||||
close_index=sell_row.Index,
|
||||
open_at_end=True,
|
||||
open_rate=buy_row.open,
|
||||
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
|
||||
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,
|
||||
close_time=sell_row.date,
|
||||
trade_duration=int((
|
||||
sell_row.date - buy_row.date).total_seconds() // 60),
|
||||
open_index=buy_row.Index,
|
||||
close_index=sell_row.Index,
|
||||
open_at_end=True,
|
||||
open_rate=buy_row.open,
|
||||
close_rate=sell_row.open,
|
||||
sell_reason=SellType.FORCE_SELL
|
||||
)
|
||||
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:
|
||||
@@ -373,11 +387,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:
|
||||
@@ -398,16 +416,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:
|
||||
@@ -452,7 +467,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}")
|
||||
|
@@ -5,46 +5,57 @@ from typing import Any, Callable, Dict, List
|
||||
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
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 DefaultHyperOpts(IHyperOpt):
|
||||
class DefaultHyperOpt(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'])
|
||||
@@ -80,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'),
|
||||
@@ -97,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'])
|
||||
@@ -140,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'),
|
||||
@@ -156,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[
|
||||
(
|
||||
@@ -211,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[
|
||||
(
|
||||
@@ -223,4 +198,5 @@ class DefaultHyperOpts(IHyperOpt):
|
||||
(dataframe['fastd'] > 54)
|
||||
),
|
||||
'sell'] = 1
|
||||
|
||||
return dataframe
|
||||
|
@@ -9,14 +9,14 @@ from tabulate import tabulate
|
||||
from freqtrade import constants
|
||||
from freqtrade.edge import Edge
|
||||
|
||||
from freqtrade.configuration 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
|
||||
@@ -68,8 +69,8 @@ class EdgeCli(object):
|
||||
])
|
||||
|
||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||
return tabulate(tabular_data, headers=headers, # type: ignore
|
||||
floatfmt=floatfmt, tablefmt="pipe")
|
||||
return tabulate(tabular_data, headers=headers,
|
||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||
|
||||
def start(self) -> None:
|
||||
result = self.edge.calculate()
|
||||
|
@@ -5,23 +5,29 @@ This module contains the hyperopt logic
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
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.configuration 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
|
||||
# Import IHyperOptLoss to allow users import from this file
|
||||
# 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
|
||||
|
||||
@@ -30,13 +36,15 @@ 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
|
||||
|
||||
@@ -45,13 +53,21 @@ 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
|
||||
|
||||
self.backtesting = Backtesting(self.config)
|
||||
|
||||
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
|
||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||
|
||||
self.total_tries = config.get('epochs', 0)
|
||||
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
|
||||
|
||||
if not self.config.get('hyperopt_continue'):
|
||||
@@ -60,36 +76,52 @@ class Hyperopt(Backtesting):
|
||||
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.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
if 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.populate_sell_trend # type: ignore
|
||||
|
||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||
# 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),
|
||||
self.position_stacking = self.config.get('position_stacking', False)
|
||||
|
||||
if self.has_space('sell'):
|
||||
# Make sure use_sell_signal is enabled
|
||||
if 'ask_strategy' not in self.config:
|
||||
self.config['ask_strategy'] = {}
|
||||
self.config['ask_strategy']['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 [TICKERDATA_PICKLE, TRIALSDATA_PICKLE]:
|
||||
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):
|
||||
@@ -106,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:
|
||||
@@ -124,87 +156,137 @@ 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']
|
||||
)
|
||||
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)
|
||||
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()
|
||||
)
|
||||
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']
|
||||
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}'
|
||||
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_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 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)
|
||||
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)
|
||||
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(TICKERDATA_PICKLE)
|
||||
processed = load(self.tickerdata_pickle)
|
||||
|
||||
min_date, max_date = get_timeframe(processed)
|
||||
|
||||
results = self.backtest(
|
||||
results = self.backtesting.backtest(
|
||||
{
|
||||
'stake_amount': self.config['stake_amount'],
|
||||
'processed': processed,
|
||||
@@ -214,9 +296,10 @@ class Hyperopt(Backtesting):
|
||||
'end_date': max_date,
|
||||
}
|
||||
)
|
||||
result_explanation = self.format_results(results)
|
||||
results_explanation = self.format_results(results)
|
||||
|
||||
trade_count = len(results.index)
|
||||
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)
|
||||
@@ -226,7 +309,8 @@ class Hyperopt(Backtesting):
|
||||
return {
|
||||
'loss': MAX_LOSS,
|
||||
'params': params,
|
||||
'result': result_explanation,
|
||||
'results_explanation': results_explanation,
|
||||
'total_profit': total_profit,
|
||||
}
|
||||
|
||||
loss = self.calculate_loss(results=results, trade_count=trade_count,
|
||||
@@ -235,12 +319,13 @@ class Hyperopt(Backtesting):
|
||||
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
|
||||
@@ -253,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,
|
||||
@@ -263,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.',
|
||||
@@ -277,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
|
||||
)
|
||||
|
||||
@@ -301,47 +401,44 @@ class Hyperopt(Backtesting):
|
||||
(max_date - min_date).days
|
||||
)
|
||||
|
||||
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']
|
||||
|
@@ -39,7 +39,7 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
||||
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.
|
||||
sharp_ratio = -20.
|
||||
|
||||
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
|
||||
return -sharp_ratio
|
||||
|
@@ -8,6 +8,9 @@ import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
from freqtrade.exchange import market_is_active
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -77,7 +80,7 @@ class IPairList(ABC):
|
||||
continue
|
||||
# Check if market is active
|
||||
market = markets[pair]
|
||||
if not market['active']:
|
||||
if not market_is_active(market):
|
||||
logger.info(f"Ignoring {pair} from whitelist. Market is not active.")
|
||||
continue
|
||||
sanitized_whitelist.add(pair)
|
||||
|
@@ -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,24 +330,19 @@ 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,
|
||||
fee: Optional[float] = None) -> float:
|
||||
def calc_close_trade_price(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculate the close_rate including fee
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
@@ -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,10 +358,8 @@ 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,
|
||||
fee: Optional[float] = None) -> float:
|
||||
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
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
@@ -385,10 +376,8 @@ 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,
|
||||
fee: Optional[float] = None) -> float:
|
||||
def calc_profit_percent(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculates the profit in percentage (including fee).
|
||||
:param rate: rate to compare with (optional).
|
||||
@@ -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,15 +1,14 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.configuration import Arguments
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import (combine_tickers_with_mean,
|
||||
create_cum_profit, load_trades)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
create_cum_profit,
|
||||
extract_trades_of_period, load_trades)
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,46 +18,39 @@ try:
|
||||
from plotly.offline import plot
|
||||
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 init_plotscript(config):
|
||||
"""
|
||||
Initialize objects needed for plotting
|
||||
:return: Dict with tickers, trades, pairs and strategy
|
||||
:return: Dict with tickers, trades and pairs
|
||||
"""
|
||||
exchange: Optional[Exchange] = None
|
||||
|
||||
# Exchange is only needed when downloading data!
|
||||
if config.get("live", False) or config.get("refresh_pairs", False):
|
||||
exchange = ExchangeResolver(config.get('exchange', {}).get('name'),
|
||||
config).exchange
|
||||
|
||||
strategy = StrategyResolver(config).strategy
|
||||
if "pairs" in config:
|
||||
pairs = config["pairs"].split(',')
|
||||
pairs = config["pairs"]
|
||||
else:
|
||||
pairs = config["exchange"]["pair_whitelist"]
|
||||
|
||||
# Set timerange to use
|
||||
timerange = Arguments.parse_timerange(config.get("timerange"))
|
||||
timerange = TimeRange.parse_timerange(config.get("timerange"))
|
||||
|
||||
tickers = history.load_data(
|
||||
datadir=Path(str(config.get("datadir"))),
|
||||
pairs=pairs,
|
||||
ticker_interval=config['ticker_interval'],
|
||||
refresh_pairs=config.get('refresh_pairs', False),
|
||||
ticker_interval=config.get('ticker_interval', '5m'),
|
||||
timerange=timerange,
|
||||
exchange=exchange,
|
||||
live=config.get("live", False),
|
||||
)
|
||||
|
||||
trades = load_trades(config)
|
||||
trades = load_trades(config['trade_source'],
|
||||
db_url=config.get('db_url'),
|
||||
exportfilename=config.get('exportfilename'),
|
||||
)
|
||||
|
||||
return {"tickers": tickers,
|
||||
"trades": trades,
|
||||
"pairs": pairs,
|
||||
"strategy": strategy,
|
||||
}
|
||||
|
||||
|
||||
@@ -72,14 +64,13 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_
|
||||
"""
|
||||
for indicator in indicators:
|
||||
if indicator in data:
|
||||
# TODO: Figure out why scattergl causes problems
|
||||
scattergl = go.Scatter(
|
||||
scatter = go.Scatter(
|
||||
x=data['date'],
|
||||
y=data[indicator].values,
|
||||
mode='lines',
|
||||
name=indicator
|
||||
)
|
||||
fig.add_trace(scattergl, row, 1)
|
||||
fig.add_trace(scatter, row, 1)
|
||||
else:
|
||||
logger.info(
|
||||
'Indicator "%s" ignored. Reason: This indicator is not found '
|
||||
@@ -100,7 +91,7 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub
|
||||
:param name: Name to use
|
||||
:return: fig with added profit plot
|
||||
"""
|
||||
profit = go.Scattergl(
|
||||
profit = go.Scatter(
|
||||
x=data.index,
|
||||
y=data[column],
|
||||
name=name,
|
||||
@@ -229,23 +220,27 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
|
||||
else:
|
||||
logger.warning("No sell-signals found.")
|
||||
|
||||
# TODO: Figure out why scattergl causes problems plotly/plotly.js#2284
|
||||
if 'bb_lowerband' in data and 'bb_upperband' in data:
|
||||
bb_lower = go.Scattergl(
|
||||
bb_lower = go.Scatter(
|
||||
x=data.date,
|
||||
y=data.bb_lowerband,
|
||||
name='BB lower',
|
||||
showlegend=False,
|
||||
line={'color': 'rgba(255,255,255,0)'},
|
||||
)
|
||||
bb_upper = go.Scattergl(
|
||||
bb_upper = go.Scatter(
|
||||
x=data.date,
|
||||
y=data.bb_upperband,
|
||||
name='BB upper',
|
||||
name='Bollinger Band',
|
||||
fill="tonexty",
|
||||
fillcolor="rgba(0,176,246,0.2)",
|
||||
line={'color': 'rgba(255,255,255,0)'},
|
||||
)
|
||||
fig.add_trace(bb_lower, 1, 1)
|
||||
fig.add_trace(bb_upper, 1, 1)
|
||||
if 'bb_upperband' in indicators1 and 'bb_lowerband' in indicators1:
|
||||
indicators1.remove('bb_upperband')
|
||||
indicators1.remove('bb_lowerband')
|
||||
|
||||
# Add indicators to main plot
|
||||
fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data)
|
||||
@@ -256,40 +251,49 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
|
||||
volume = go.Bar(
|
||||
x=data['date'],
|
||||
y=data['volume'],
|
||||
name='Volume'
|
||||
)
|
||||
name='Volume',
|
||||
marker_color='DarkSlateGrey',
|
||||
marker_line_color='DarkSlateGrey'
|
||||
)
|
||||
fig.add_trace(volume, 2, 1)
|
||||
|
||||
# Add indicators to seperate row
|
||||
# Add indicators to separate row
|
||||
fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
|
||||
trades: pd.DataFrame) -> go.Figure:
|
||||
trades: pd.DataFrame, timeframe: str) -> 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')
|
||||
df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe)
|
||||
|
||||
# Plot the pairs average close prices, and total profit growth
|
||||
avgclose = go.Scattergl(
|
||||
avgclose = go.Scatter(
|
||||
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])
|
||||
fig['layout'].update(title="Profit plot")
|
||||
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)
|
||||
df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col, timeframe)
|
||||
|
||||
fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
|
||||
|
||||
@@ -308,7 +312,7 @@ def generate_plot_filename(pair, ticker_interval) -> str:
|
||||
return file_name
|
||||
|
||||
|
||||
def store_plot_file(fig, filename: str, auto_open: bool = False) -> None:
|
||||
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
|
||||
@@ -316,8 +320,71 @@ def store_plot_file(fig, filename: str, auto_open: bool = False) -> None:
|
||||
:param ticker_interval: Used as part of the filename
|
||||
:return: None
|
||||
"""
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Path("user_data/plots").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
plot(fig, filename=str(Path('user_data/plots').joinpath(filename)),
|
||||
_filename = directory.joinpath(filename)
|
||||
plot(fig, filename=str(_filename),
|
||||
auto_open=auto_open)
|
||||
logger.info(f"Stored plot as {_filename}")
|
||||
|
||||
|
||||
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, config.get('ticker_interval', '5m'))
|
||||
store_plot_file(fig, filename='freqtrade-profit-plot.html',
|
||||
directory=config['user_data_dir'] / "plot", auto_open=True)
|
||||
|
@@ -3,7 +3,7 @@ This module loads custom exchanges
|
||||
"""
|
||||
import logging
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS
|
||||
import freqtrade.exchange as exchanges
|
||||
from freqtrade.resolvers import IResolver
|
||||
|
||||
@@ -17,19 +17,22 @@ class ExchangeResolver(IResolver):
|
||||
|
||||
__slots__ = ['exchange']
|
||||
|
||||
def __init__(self, exchange_name: str, config: dict) -> None:
|
||||
def __init__(self, exchange_name: str, config: dict, validate: bool = True) -> None:
|
||||
"""
|
||||
Load the custom class from config parameter
|
||||
:param config: configuration dictionary
|
||||
"""
|
||||
# Map exchange name to avoid duplicate classes for identical exchanges
|
||||
exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name)
|
||||
exchange_name = exchange_name.title()
|
||||
try:
|
||||
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config})
|
||||
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config,
|
||||
'validate': validate})
|
||||
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)
|
||||
self.exchange = Exchange(config, validate=validate)
|
||||
|
||||
def _load_exchange(
|
||||
self, exchange_name: str, kwargs: dict) -> Exchange:
|
||||
@@ -43,7 +46,7 @@ class ExchangeResolver(IResolver):
|
||||
try:
|
||||
ex_class = getattr(exchanges, exchange_name)
|
||||
|
||||
exchange = ex_class(kwargs['config'])
|
||||
exchange = ex_class(**kwargs)
|
||||
if exchange:
|
||||
logger.info(f"Using resolved exchange '{exchange_name}'...")
|
||||
return exchange
|
||||
|
@@ -31,39 +31,32 @@ class HyperOptResolver(IResolver):
|
||||
|
||||
# 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'))
|
||||
|
||||
# Assign ticker_interval to be used in hyperopt
|
||||
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
|
||||
self.hyperopt = self._load_hyperopt(hyperopt_name, config,
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
||||
logger.warning("Custom Hyperopt does not provide populate_buy_trend. "
|
||||
"Using populate_buy_trend from DefaultStrategy.")
|
||||
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("Custom Hyperopt does not provide populate_sell_trend. "
|
||||
"Using populate_sell_trend from DefaultStrategy.")
|
||||
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, extra_dir: Optional[str] = None) -> IHyperOpt:
|
||||
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 = [
|
||||
Path.cwd().joinpath('user_data/hyperopts'),
|
||||
current_path,
|
||||
]
|
||||
|
||||
if extra_dir:
|
||||
# Add extra hyperopt directory on top of search paths
|
||||
abs_paths.insert(0, Path(extra_dir).resolve())
|
||||
abs_paths = self.build_search_paths(config, current_path=current_path,
|
||||
user_subdir='hyperopts', extra_dir=extra_dir)
|
||||
|
||||
hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
|
||||
object_name=hyperopt_name)
|
||||
object_name=hyperopt_name, kwargs={'config': config})
|
||||
if hyperopt:
|
||||
return hyperopt
|
||||
raise OperationalException(
|
||||
@@ -79,7 +72,7 @@ class HyperOptLossResolver(IResolver):
|
||||
|
||||
__slots__ = ['hyperoptloss']
|
||||
|
||||
def __init__(self, config: Optional[Dict] = None) -> None:
|
||||
def __init__(self, config: Dict = None) -> None:
|
||||
"""
|
||||
Load the custom class from config parameter
|
||||
:param config: configuration dictionary or None
|
||||
@@ -89,7 +82,7 @@ class HyperOptLossResolver(IResolver):
|
||||
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
|
||||
hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
|
||||
self.hyperoptloss = self._load_hyperoptloss(
|
||||
hyperopt_name, extra_dir=config.get('hyperopt_path'))
|
||||
hyperopt_name, config, extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
# Assign ticker_interval to be used in hyperopt
|
||||
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
|
||||
@@ -99,23 +92,19 @@ class HyperOptLossResolver(IResolver):
|
||||
f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.")
|
||||
|
||||
def _load_hyperoptloss(
|
||||
self, hyper_loss_name: str, extra_dir: Optional[str] = None) -> IHyperOptLoss:
|
||||
self, hyper_loss_name: str, config: Dict,
|
||||
extra_dir: Optional[str] = None) -> IHyperOptLoss:
|
||||
"""
|
||||
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: HyperOptLoss instance or None
|
||||
"""
|
||||
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
||||
|
||||
abs_paths = [
|
||||
Path.cwd().joinpath('user_data/hyperopts'),
|
||||
current_path,
|
||||
]
|
||||
|
||||
if extra_dir:
|
||||
# Add extra hyperopt directory on top of search paths
|
||||
abs_paths.insert(0, Path(extra_dir).resolve())
|
||||
abs_paths = self.build_search_paths(config, current_path=current_path,
|
||||
user_subdir='hyperopts', extra_dir=extra_dir)
|
||||
|
||||
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss,
|
||||
object_name=hyper_loss_name)
|
||||
|
@@ -7,29 +7,44 @@ import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Tuple, Type, Union
|
||||
from typing import Any, List, Optional, Tuple, Union, Generator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IResolver(object):
|
||||
class IResolver:
|
||||
"""
|
||||
This class contains all the logic to load custom classes
|
||||
"""
|
||||
|
||||
def build_search_paths(self, config, current_path: Path, user_subdir: str,
|
||||
extra_dir: Optional[str] = None) -> List[Path]:
|
||||
|
||||
abs_paths = [
|
||||
config['user_data_dir'].joinpath(user_subdir),
|
||||
current_path,
|
||||
]
|
||||
|
||||
if extra_dir:
|
||||
# Add extra directory to the top of the search paths
|
||||
abs_paths.insert(0, Path(extra_dir).resolve())
|
||||
|
||||
return abs_paths
|
||||
|
||||
@staticmethod
|
||||
def _get_valid_object(object_type, module_path: Path,
|
||||
object_name: str) -> Optional[Type[Any]]:
|
||||
object_name: str) -> Generator[Any, None, None]:
|
||||
"""
|
||||
Returns the first object with matching object_type and object_name in the path given.
|
||||
Generator returning objects with matching object_type and object_name in the path given.
|
||||
:param object_type: object_type (class)
|
||||
:param module_path: absolute path to the module
|
||||
:param object_name: Class name of the object
|
||||
:return: class or None
|
||||
:return: generator containing matching objects
|
||||
"""
|
||||
|
||||
# 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
|
||||
@@ -41,7 +56,7 @@ class IResolver(object):
|
||||
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
||||
if object_name == name and object_type in obj.__bases__
|
||||
)
|
||||
return next(valid_objects_gen, None)
|
||||
return valid_objects_gen
|
||||
|
||||
@staticmethod
|
||||
def _search_object(directory: Path, object_type, object_name: str,
|
||||
@@ -57,10 +72,10 @@ class IResolver(object):
|
||||
if not str(entry).endswith('.py'):
|
||||
logger.debug('Ignoring %s', entry)
|
||||
continue
|
||||
module_path = Path.resolve(directory.joinpath(entry))
|
||||
obj = IResolver._get_valid_object(
|
||||
object_type, module_path, object_name
|
||||
)
|
||||
module_path = entry.resolve()
|
||||
|
||||
obj = next(IResolver._get_valid_object(object_type, module_path, object_name), None)
|
||||
|
||||
if obj:
|
||||
return (obj(**kwargs), module_path)
|
||||
return (None, None)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# pragma pylint: disable=attribute-defined-outside-init
|
||||
|
||||
"""
|
||||
This module load custom hyperopts
|
||||
This module load custom pairlists
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PairListResolver(IResolver):
|
||||
"""
|
||||
This class contains all the logic to load custom hyperopt class
|
||||
This class contains all the logic to load custom PairList class
|
||||
"""
|
||||
|
||||
__slots__ = ['pairlist']
|
||||
@@ -25,23 +25,22 @@ 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,
|
||||
'config': config})
|
||||
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 = [
|
||||
Path.cwd().joinpath('user_data/pairlist'),
|
||||
current_path,
|
||||
]
|
||||
abs_paths = self.build_search_paths(config, current_path=current_path,
|
||||
user_subdir='pairlist', extra_dir=None)
|
||||
|
||||
pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
|
||||
object_name=pairlist_name, kwargs=kwargs)
|
||||
|
@@ -13,7 +13,6 @@ from typing import Dict, Optional
|
||||
|
||||
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__)
|
||||
@@ -39,13 +38,13 @@ class StrategyResolver(IResolver):
|
||||
config=config,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
|
||||
# make sure experimental dict is available
|
||||
if 'experimental' not in config:
|
||||
config['experimental'] = {}
|
||||
# make sure ask_strategy dict is available
|
||||
if 'ask_strategy' not in config:
|
||||
config['ask_strategy'] = {}
|
||||
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
# (Attribute name, default, experimental)
|
||||
# (Attribute name, default, ask_strategy)
|
||||
attributes = [("minimal_roi", {"0": 10.0}, False),
|
||||
("ticker_interval", None, False),
|
||||
("stoploss", None, False),
|
||||
@@ -58,20 +57,20 @@ class StrategyResolver(IResolver):
|
||||
("order_time_in_force", None, False),
|
||||
("stake_currency", None, False),
|
||||
("stake_amount", None, False),
|
||||
("use_sell_signal", False, True),
|
||||
("use_sell_signal", True, True),
|
||||
("sell_profit_only", False, True),
|
||||
("ignore_roi_if_buy_signal", False, True),
|
||||
]
|
||||
for attribute, default, experimental in attributes:
|
||||
if experimental:
|
||||
self._override_attribute_helper(config['experimental'], attribute, default)
|
||||
for attribute, default, ask_strategy in attributes:
|
||||
if ask_strategy:
|
||||
self._override_attribute_helper(config['ask_strategy'], attribute, default)
|
||||
else:
|
||||
self._override_attribute_helper(config, attribute, default)
|
||||
|
||||
# Loop this list again to have output combined
|
||||
for attribute, _, exp in attributes:
|
||||
if exp and attribute in config['experimental']:
|
||||
logger.info("Strategy using %s: %s", attribute, config['experimental'][attribute])
|
||||
if exp and attribute in config['ask_strategy']:
|
||||
logger.info("Strategy using %s: %s", attribute, config['ask_strategy'][attribute])
|
||||
elif attribute in config:
|
||||
logger.info("Strategy using %s: %s", attribute, config[attribute])
|
||||
|
||||
@@ -96,7 +95,10 @@ class StrategyResolver(IResolver):
|
||||
logger.info("Override strategy '%s' with value in config file: %s.",
|
||||
attribute, config[attribute])
|
||||
elif hasattr(self.strategy, attribute):
|
||||
config[attribute] = getattr(self.strategy, attribute)
|
||||
val = getattr(self.strategy, attribute)
|
||||
# None's cannot exist in the config, so do not copy them
|
||||
if val is not None:
|
||||
config[attribute] = val
|
||||
# Explicitly check for None here as other "falsy" values are possible
|
||||
elif default is not None:
|
||||
setattr(self.strategy, attribute, default)
|
||||
@@ -122,14 +124,8 @@ class StrategyResolver(IResolver):
|
||||
"""
|
||||
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||
|
||||
abs_paths = [
|
||||
Path.cwd().joinpath('user_data/strategies'),
|
||||
current_path,
|
||||
]
|
||||
|
||||
if extra_dir:
|
||||
# Add extra strategy directory on top of search paths
|
||||
abs_paths.insert(0, Path(extra_dir).resolve())
|
||||
abs_paths = self.build_search_paths(config, current_path=current_path,
|
||||
user_subdir='strategies', extra_dir=extra_dir)
|
||||
|
||||
if ":" in strategy_name:
|
||||
logger.info("loading base64 encoded strategy")
|
||||
@@ -153,13 +149,12 @@ class StrategyResolver(IResolver):
|
||||
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)
|
||||
if any([x == 2 for x in [strategy._populate_fun_len,
|
||||
strategy._buy_fun_len,
|
||||
strategy._sell_fun_len]]):
|
||||
strategy.INTERFACE_VERSION = 1
|
||||
|
||||
try:
|
||||
return import_strategy(strategy, config=config)
|
||||
except TypeError as e:
|
||||
logger.warning(
|
||||
f"Impossible to load strategy '{strategy_name}'. "
|
||||
f"Error: {e}")
|
||||
return strategy
|
||||
|
||||
raise OperationalException(
|
||||
f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
|
||||
|
@@ -2,7 +2,7 @@ import logging
|
||||
import threading
|
||||
from datetime import date, datetime
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Dict
|
||||
from typing import Dict, Callable, Any
|
||||
|
||||
from arrow import Arrow
|
||||
from flask import Flask, jsonify, request
|
||||
@@ -34,41 +34,45 @@ class ArrowJSONEncoder(JSONEncoder):
|
||||
return JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
# Type should really be Callable[[ApiServer, Any], Any], but that will create a circular dependency
|
||||
def require_login(func: Callable[[Any, Any], Any]):
|
||||
|
||||
def func_wrapper(obj, *args, **kwargs):
|
||||
|
||||
auth = request.authorization
|
||||
if auth and obj.check_auth(auth.username, auth.password):
|
||||
return func(obj, *args, **kwargs)
|
||||
else:
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
return func_wrapper
|
||||
|
||||
|
||||
# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
|
||||
def rpc_catch_errors(func: Callable[[Any], Any]):
|
||||
|
||||
def func_wrapper(obj, *args, **kwargs):
|
||||
|
||||
try:
|
||||
return func(obj, *args, **kwargs)
|
||||
except RPCException as e:
|
||||
logger.exception("API Error calling %s: %s", func.__name__, e)
|
||||
return obj.rest_error(f"Error querying {func.__name__}: {e}")
|
||||
|
||||
return func_wrapper
|
||||
|
||||
|
||||
class ApiServer(RPC):
|
||||
"""
|
||||
This class runs api server and provides rpc.rpc functionality to it
|
||||
|
||||
This class starts a none blocking thread the api server runs within
|
||||
This class starts a non-blocking thread the api server runs within
|
||||
"""
|
||||
|
||||
def rpc_catch_errors(func):
|
||||
|
||||
def func_wrapper(self, *args, **kwargs):
|
||||
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except RPCException as e:
|
||||
logger.exception("API Error calling %s: %s", func.__name__, e)
|
||||
return self.rest_error(f"Error querying {func.__name__}: {e}")
|
||||
|
||||
return func_wrapper
|
||||
|
||||
def check_auth(self, username, password):
|
||||
return (username == self._config['api_server'].get('username') and
|
||||
password == self._config['api_server'].get('password'))
|
||||
|
||||
def require_login(func):
|
||||
|
||||
def func_wrapper(self, *args, **kwargs):
|
||||
|
||||
auth = request.authorization
|
||||
if auth and self.check_auth(auth.username, auth.password):
|
||||
return func(self, *args, **kwargs)
|
||||
else:
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
return func_wrapper
|
||||
|
||||
def __init__(self, freqtrade) -> None:
|
||||
"""
|
||||
Init the api server, and init the super class RPC
|
||||
|
@@ -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,
|
||||
@@ -293,9 +294,9 @@ 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:
|
||||
|
@@ -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, ...)
|
||||
"""
|
||||
@@ -18,7 +18,7 @@ class RPCManager(object):
|
||||
self.registered_modules: List[RPC] = []
|
||||
|
||||
# Enable telegram
|
||||
if freqtrade.config['telegram'].get('enabled', False):
|
||||
if freqtrade.config.get('telegram', {}).get('enabled', False):
|
||||
logger.info('Enabling rpc.telegram ...')
|
||||
from freqtrade.rpc.telegram import Telegram
|
||||
self.registered_modules.append(Telegram(freqtrade))
|
||||
@@ -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)
|
||||
mod.send_msg(msg)
|
||||
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:
|
||||
@@ -221,13 +219,13 @@ class Telegram(RPC):
|
||||
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
|
||||
@@ -240,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.
|
||||
@@ -254,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(
|
||||
@@ -272,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.
|
||||
@@ -317,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', ''))
|
||||
@@ -330,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
|
||||
@@ -347,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
|
||||
@@ -361,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
|
||||
@@ -373,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
|
||||
@@ -385,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
|
||||
@@ -397,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
|
||||
@@ -409,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
|
||||
@@ -427,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
|
||||
@@ -455,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
|
||||
@@ -475,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
|
||||
@@ -492,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'])}`"
|
||||
@@ -510,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
|
||||
@@ -522,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
|
||||
@@ -559,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
|
||||
@@ -570,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
|
||||
@@ -581,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'],
|
||||
@@ -591,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,
|
||||
@@ -604,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,7 +4,7 @@ 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, Optional, Tuple
|
||||
import warnings
|
||||
@@ -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
|
||||
@@ -72,8 +78,8 @@ class IStrategy(ABC):
|
||||
|
||||
# trailing stoploss
|
||||
trailing_stop: bool = False
|
||||
trailing_stop_positive: float
|
||||
trailing_stop_positive_offset: float
|
||||
trailing_stop_positive: Optional[float] = None
|
||||
trailing_stop_positive_offset: float = 0.0
|
||||
trailing_only_offset_is_reached = False
|
||||
|
||||
# associated ticker interval
|
||||
@@ -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,30 +302,41 @@ 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
|
||||
current_rate = high or rate
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
experimental = self.config.get('experimental', {})
|
||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||
|
||||
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
||||
logger.debug('Buy signal still active - not selling.')
|
||||
if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False):
|
||||
# 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..')
|
||||
if config_ask_strategy.get('sell_profit_only', False):
|
||||
# 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..')
|
||||
|
||||
if sell and not buy and config_ask_strategy.get('use_sell_signal', True):
|
||||
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,28 +347,24 @@ 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
|
||||
|
||||
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
|
||||
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
|
||||
|
||||
if trailing_stop:
|
||||
if self.trailing_stop:
|
||||
# trailing stoploss handling
|
||||
sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0
|
||||
tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False)
|
||||
sl_offset = self.trailing_stop_positive_offset
|
||||
|
||||
# Make sure current_profit is calculated using high for backtesting.
|
||||
high_profit = current_profit if not high else trade.calc_profit_percent(high)
|
||||
|
||||
# Don't update stoploss if trailing_only_offset_is_reached is true.
|
||||
if not (tsl_only_offset and high_profit < sl_offset):
|
||||
if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):
|
||||
# Specific handling for trailing_stop_positive
|
||||
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} "
|
||||
if self.trailing_stop_positive is not None and high_profit > sl_offset:
|
||||
stop_loss_value = self.trailing_stop_positive
|
||||
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,20 +374,20 @@ 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"stop loss is {trade.stop_loss:.6f}, "
|
||||
f"initial stop loss was at {trade.initial_stop_loss:.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)
|
||||
|
||||
|
@@ -1,69 +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.options = {}
|
||||
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.options = {}
|
||||
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,235 +0,0 @@
|
||||
|
||||
# --- Do not remove these libs ---
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from pandas import DataFrame
|
||||
# --------------------------------
|
||||
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
import numpy # noqa
|
||||
|
||||
|
||||
# This class is a sample. Feel free to customize it.
|
||||
class TestStrategyLegacy(IStrategy):
|
||||
"""
|
||||
This is a test strategy using the legacy function headers, which will be
|
||||
removed in a future update.
|
||||
Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py
|
||||
for a uptodate version of this template.
|
||||
|
||||
"""
|
||||
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||
minimal_roi = {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
# This attribute will be overridden if the config file contains "stoploss"
|
||||
stoploss = -0.10
|
||||
|
||||
# Optimal ticker interval for the strategy
|
||||
ticker_interval = '5m'
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
# ------------------------------------
|
||||
|
||||
# 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)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
|
||||
# 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
|
||||
# ------------------------------------
|
||||
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
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) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user