Compare commits
1357 Commits
feat/convo
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
0afd5a7385 | ||
|
2131205db6 | ||
|
b2b19915e6 | ||
|
bba6f8e133 | ||
|
a6d2233b95 | ||
|
9857675a5e | ||
|
4ab047dfa7 | ||
|
476ed938f5 | ||
|
40ffac9de0 | ||
|
b892d373cd | ||
|
c3647e49ad | ||
|
37ed37dc76 | ||
|
5cb688c112 | ||
|
3e394d0612 | ||
|
c4c2298686 | ||
|
8564dc10b2 | ||
|
3fb892fcb8 | ||
|
9968348324 | ||
|
fa293c54f8 | ||
|
95449ca886 | ||
|
70fa4a53cd | ||
|
467c63ff01 | ||
|
b8a9c200fe | ||
|
7c10af65a1 | ||
|
e2cd23b1d2 | ||
|
0d408d3d43 | ||
|
2309197771 | ||
|
66fe9abce0 | ||
|
200c18f3e4 | ||
|
351b5f6e65 | ||
|
605cc20a21 | ||
|
f73d2a5371 | ||
|
485a074674 | ||
|
865cf5232b | ||
|
95a24c3133 | ||
|
6833059c70 | ||
|
3833dc0b78 | ||
|
e0d3c771db | ||
|
5a18ab0784 | ||
|
1d66f82b1d | ||
|
2e765fe6d1 | ||
|
21ea02bbcf | ||
|
2ea0157197 | ||
|
03352f3b62 | ||
|
26eb4f7fe6 | ||
|
7e1f3aa545 | ||
|
14532e3a56 | ||
|
a449f7c78c | ||
|
8854ef8cba | ||
|
526943f29e | ||
|
df51111c33 | ||
|
dd8900a1c6 | ||
|
5404905d28 | ||
|
bed51fa790 | ||
|
f5a5c2d6b9 | ||
|
a102cfdfc9 | ||
|
be72670ca2 | ||
|
cf2cb94f8d | ||
|
fa3a81b022 | ||
|
7ff30c6df8 | ||
|
7751768b2e | ||
|
9c2cdd4fb9 | ||
|
69b9b35a08 | ||
|
c2c97d9f78 | ||
|
48d3c8e62e | ||
|
ac817b7808 | ||
|
4d4f4bf23e | ||
|
c083723698 | ||
|
f8d89c46e5 | ||
|
1952e453bb | ||
|
77985fa591 | ||
|
a75d891007 | ||
|
dae3f72be7 | ||
|
f03a99918a | ||
|
a655524221 | ||
|
26738370c7 | ||
|
fe02f611fb | ||
|
1b10a3a2bf | ||
|
92a060c5b4 | ||
|
096fd1916c | ||
|
fb09a16127 | ||
|
6b204c97ed | ||
|
0c4574b3b7 | ||
|
d9d9993179 | ||
|
7b494c8333 | ||
|
bc9454e0f9 | ||
|
36a0a14a23 | ||
|
c137666230 | ||
|
7fed0782d5 | ||
|
bd3b70293f | ||
|
30fc24bd8c | ||
|
7e3de178e1 | ||
|
0c9c9fff0e | ||
|
b96f6670e3 | ||
|
6e02743256 | ||
|
2b4fa92d09 | ||
|
be250230b6 | ||
|
5d33ffc015 | ||
|
b48498f27f | ||
|
e582d8bacb | ||
|
ff40ee655b | ||
|
57deaad806 | ||
|
7779b82277 | ||
|
2bd2058afa | ||
|
bf7936b0af | ||
|
8236bbfd48 | ||
|
4dc13ac16a | ||
|
eb5423469a | ||
|
43496d7929 | ||
|
92c70b6b90 | ||
|
77897c7d6b | ||
|
531861573a | ||
|
c9b904eb0e | ||
|
372f1cb37f | ||
|
a3acdd5240 | ||
|
e6a125719e | ||
|
78a1551798 | ||
|
6f79d14c9c | ||
|
28d8722fa7 | ||
|
2715b2ccf0 | ||
|
2ea575cb31 | ||
|
1b31c54162 | ||
|
e289c10b6c | ||
|
26ed1ca07c | ||
|
b1e20bcd1e | ||
|
12a73bc151 | ||
|
19e112f399 | ||
|
cccf4f305b | ||
|
dc7e834911 | ||
|
a630799984 | ||
|
916e1bbc7c | ||
|
631cb44f5c | ||
|
367186cc34 | ||
|
92f34f262e | ||
|
5e13b48648 | ||
|
6dfb1a1d14 | ||
|
f8330800d1 | ||
|
3ec7c72da1 | ||
|
355fde3bca | ||
|
fa7c29fe9f | ||
|
861c577138 | ||
|
e062a74e70 | ||
|
c330c493d5 | ||
|
8a49d62068 | ||
|
a642524928 | ||
|
eb96490c99 | ||
|
6282b42741 | ||
|
513df4515b | ||
|
411e21f430 | ||
|
f0b5f95fd6 | ||
|
736c396d98 | ||
|
5a7ca35c6b | ||
|
077a947972 | ||
|
8ac3a94358 | ||
|
dfbebdea9b | ||
|
b795a70102 | ||
|
026b6a39a9 | ||
|
2860e817bd | ||
|
19b78fbc22 | ||
|
cde432fef0 | ||
|
8ae44c204e | ||
|
ed0e7ead31 | ||
|
3928051baf | ||
|
e35c85000e | ||
|
3cabcabcbd | ||
|
85776db692 | ||
|
ce81af08d8 | ||
|
5aa6c1dfae | ||
|
4f4dfa2a59 | ||
|
90669e0ba9 | ||
|
bc9f6d30c1 | ||
|
4ae2333306 | ||
|
8c63e3dc4f | ||
|
b0dddd35ca | ||
|
96ba75179b | ||
|
2589717375 | ||
|
bc0816aa66 | ||
|
1743ad7946 | ||
|
9367cbcfd3 | ||
|
43a7b9236b | ||
|
4891174a71 | ||
|
8845f765db | ||
|
7e11bce4f4 | ||
|
8955e09175 | ||
|
d13ea71a58 | ||
|
b72f61080b | ||
|
75c31cc8cc | ||
|
1b3d9efedd | ||
|
2f8f60373e | ||
|
55781e7f10 | ||
|
72284317c2 | ||
|
80a27bc0db | ||
|
1c9abd9e35 | ||
|
c14ac8a205 | ||
|
b09fb5826f | ||
|
fb1541bdf6 | ||
|
444d18aa39 | ||
|
91ab4abba8 | ||
|
16057da6cc | ||
|
d97500581d | ||
|
f1e831a7b8 | ||
|
31a396bc25 | ||
|
7cdcd97c26 | ||
|
73b59df77b | ||
|
86aef7cf9d | ||
|
159090c0e7 | ||
|
0cb28f3d82 | ||
|
d0d0cbe1d1 | ||
|
02078456fc | ||
|
01dfb1cba8 | ||
|
ee205ddc86 | ||
|
298f5685ee | ||
|
486d8a48a0 | ||
|
d426077445 | ||
|
9aa455fcd4 | ||
|
d9c8b322ce | ||
|
68154a1f52 | ||
|
f7c1ee6d3e | ||
|
9c6a49436b | ||
|
75464c22f5 | ||
|
cdd44a4005 | ||
|
34313a7af6 | ||
|
4053ee4581 | ||
|
56170dba19 | ||
|
79a2de7a64 | ||
|
c0a57d352f | ||
|
cbdd86d777 | ||
|
281dd7785e | ||
|
ad58bac810 | ||
|
8928d3616a | ||
|
e8cffeeffd | ||
|
76d289f0ce | ||
|
245ae99273 | ||
|
70ad7b42b1 | ||
|
0ece73578c | ||
|
b317524ed7 | ||
|
8903ba5d89 | ||
|
469166636c | ||
|
eabd321281 | ||
|
45c6ae446f | ||
|
952e641213 | ||
|
c44b5b1b3a | ||
|
fc8625c5c5 | ||
|
150c5510c7 | ||
|
36a005754a | ||
|
479aafc331 | ||
|
bdf19f1d66 | ||
|
8cf3e9f91b | ||
|
ebebcb886c | ||
|
36c45fd14f | ||
|
0128b63c1c | ||
|
e16db814fa | ||
|
f81e3d8667 | ||
|
b9c7d338b3 | ||
|
4f93106755 | ||
|
02bccd0097 | ||
|
1ba01746a0 | ||
|
83a7d888bc | ||
|
eba82360fa | ||
|
3fa23860c0 | ||
|
a80afc8f1b | ||
|
97339e14cf | ||
|
443263803c | ||
|
9906e7d646 | ||
|
e8f040bfbd | ||
|
97c420b2df | ||
|
a4b617e482 | ||
|
7b5e322ef2 | ||
|
c06cd38951 | ||
|
0a55753faf | ||
|
6b4d9f97c1 | ||
|
639987cbab | ||
|
56c2aa89bc | ||
|
bf4aa91aab | ||
|
500c401b75 | ||
|
81a2cbb4eb | ||
|
0510cf4491 | ||
|
68728409aa | ||
|
c00ffcee59 | ||
|
9aec1ddb17 | ||
|
d98890f32e | ||
|
f659f8e309 | ||
|
54db239175 | ||
|
601c37f862 | ||
|
501e746c52 | ||
|
d04146d1b1 | ||
|
ea08931ab3 | ||
|
ddd1b5c0ff | ||
|
e08d8190ae | ||
|
fbf7049ac5 | ||
|
2a1a8c0e64 | ||
|
833aaf8e10 | ||
|
566346dd87 | ||
|
d0a33d2ee7 | ||
|
fab505be1b | ||
|
2f386913ac | ||
|
4f4bfdac4d | ||
|
8b6ea32c4c | ||
|
ff497d5c90 | ||
|
c05db6742c | ||
|
75f75f3881 | ||
|
a4e4310d40 | ||
|
dfc3524334 | ||
|
a0913588b8 | ||
|
c56b344077 | ||
|
cb1f971d4b | ||
|
78e64be04e | ||
|
3175121030 | ||
|
8d649988ca | ||
|
ec7e7e744b | ||
|
5c754eb4d3 | ||
|
54d8aa7782 | ||
|
4690244673 | ||
|
2de5a59d89 | ||
|
98685f1c98 | ||
|
88e93b4902 | ||
|
dcca51985d | ||
|
21f2f67ffa | ||
|
c78342b194 | ||
|
29b9be9bd0 | ||
|
4543a1fe02 | ||
|
fc7c8cce3c | ||
|
7d1559f319 | ||
|
a43502093d | ||
|
47e84ad106 | ||
|
5ade5777e8 | ||
|
fb0e824a83 | ||
|
a7c7f720c0 | ||
|
224f289ec8 | ||
|
d84ece7258 | ||
|
6c5dc7e0a9 | ||
|
df20757d21 | ||
|
a2ce288241 | ||
|
ce3efa8f00 | ||
|
c92f28bf6f | ||
|
222ecdecd2 | ||
|
1c11a5f048 | ||
|
903a1dc3e5 | ||
|
6f9a8a089c | ||
|
8bee499328 | ||
|
236499a195 | ||
|
3d91dd8a98 | ||
|
9ccc3e52ec | ||
|
f5f151fcc5 | ||
|
7aa56adf15 | ||
|
719faab4b8 | ||
|
9f477aa3c9 | ||
|
61ac36c576 | ||
|
366c148c10 | ||
|
bf3f2e4de4 | ||
|
763f4f4a3e | ||
|
4925d8f580 | ||
|
b1f88e8861 | ||
|
a49f62eecb | ||
|
62c8dd98d5 | ||
|
b0a7b64d44 | ||
|
d808dd49e8 | ||
|
818d2bf92a | ||
|
f98a12c26c | ||
|
477dc50425 | ||
|
fab9ff1294 | ||
|
1c91b4427b | ||
|
244662b1a4 | ||
|
186fe5933b | ||
|
8ab35bbaf3 | ||
|
9044052b4e | ||
|
209811d23a | ||
|
764d5507a3 | ||
|
628f6b8b7c | ||
|
0d3de07012 | ||
|
db0f449d93 | ||
|
774eacc561 | ||
|
e3e4fbd5ba | ||
|
b7709126f9 | ||
|
4cfbc55d34 | ||
|
00054dcfde | ||
|
9d6e973e5b | ||
|
6ed337faa3 | ||
|
e579ff9532 | ||
|
ae361e1d5d | ||
|
8865af9104 | ||
|
aa54b77702 | ||
|
8073989c98 | ||
|
d45599ca3b | ||
|
b469addffb | ||
|
47ab285252 | ||
|
95ff59a21c | ||
|
7e08e3a59a | ||
|
4550447409 | ||
|
8f29312c9e | ||
|
5c280d5649 | ||
|
b23cea6e59 | ||
|
487469680f | ||
|
8fd13933c3 | ||
|
cf70deaf8d | ||
|
3d31eca365 | ||
|
d723979c42 | ||
|
366740885a | ||
|
918889a2bd | ||
|
1947fab3d7 | ||
|
cdb97e64ab | ||
|
daa59f6248 | ||
|
9c8c30b0e8 | ||
|
f3a1177bad | ||
|
ad5afd3047 | ||
|
458bfcc89b | ||
|
d4122c36ac | ||
|
0e663a5bf8 | ||
|
562efd1841 | ||
|
7baa2b9005 | ||
|
10c5adfa50 | ||
|
44c4729a9d | ||
|
dc6af9a1a7 | ||
|
82707be7d0 | ||
|
b800f27092 | ||
|
31daf72cc6 | ||
|
22ebf04daa | ||
|
52a091e063 | ||
|
d7ea750823 | ||
|
b6096efadd | ||
|
b927c9dc01 | ||
|
523a58d3d6 | ||
|
fbca8e6587 | ||
|
a10f78e3ef | ||
|
8987e5f108 | ||
|
f584edf809 | ||
|
f5848ea891 | ||
|
b5c4f9ebe2 | ||
|
0911cd72a2 | ||
|
d2a412d2c6 | ||
|
cb086f79ff | ||
|
d9bff68501 | ||
|
5bfee44bba | ||
|
0012fe36ca | ||
|
d186f8f1e1 | ||
|
cb17b36981 | ||
|
aa283a0447 | ||
|
f9fdf1c31b | ||
|
1cf0e7be24 | ||
|
82cb107520 | ||
|
b23841fbfe | ||
|
8726a4645d | ||
|
59d2ff3ffa | ||
|
39c651e40c | ||
|
a2336f256b | ||
|
a76ca771f8 | ||
|
f722823b0d | ||
|
a3988f56b2 | ||
|
5a467eb969 | ||
|
5f8202e1b5 | ||
|
bfc7f48f17 | ||
|
5b2a291109 | ||
|
d3a3ddbc61 | ||
|
8a9f2aedbb | ||
|
e88a0d5248 | ||
|
2ef11faba7 | ||
|
c9eee2944b | ||
|
6f962362f2 | ||
|
ba5de0cd00 | ||
|
3081b9402b | ||
|
30fd1e742e | ||
|
4d8e3c25bd | ||
|
1132fa6093 | ||
|
29dfb5c169 | ||
|
d10ee0979a | ||
|
0318486bee | ||
|
85e345fc48 | ||
|
1597c3aa89 | ||
|
7d26df01b8 | ||
|
c8296ccb2d | ||
|
8d60327d60 | ||
|
04564dc134 | ||
|
6161b858c4 | ||
|
1921a07b89 | ||
|
b65ade51be | ||
|
dfbb2e2b35 | ||
|
1805db2b07 | ||
|
76fbec0c17 | ||
|
29d337fa02 | ||
|
2c7ae756f5 | ||
|
d9dc831772 | ||
|
4241bff32a | ||
|
5dd60eda36 | ||
|
8acdd0b47c | ||
|
125085fbaf | ||
|
7eedcb9c14 | ||
|
e6e747bcd8 | ||
|
348a08f1c4 | ||
|
b1ac2bf515 | ||
|
b710bdaf6c | ||
|
27fa297209 | ||
|
85e64cd121 | ||
|
0d876d7a89 | ||
|
69e5377f3d | ||
|
532ecaf2c8 | ||
|
d779d60812 | ||
|
c4a80e33ea | ||
|
cab1b750b3 | ||
|
9d285e3dc0 | ||
|
fff08f737f | ||
|
ca789b3282 | ||
|
11eea9b4e1 | ||
|
de015a2d7e | ||
|
4cfc7e4427 | ||
|
0a525c6d32 | ||
|
ae8c426025 | ||
|
0fe72510d5 | ||
|
fecb9db072 | ||
|
30ac648539 | ||
|
25fd4a04d6 | ||
|
9750e9ca4e | ||
|
a57b033745 | ||
|
48e16f6aba | ||
|
d1d9e25c2e | ||
|
57969f8b01 | ||
|
8484427cf8 | ||
|
f4c17be8de | ||
|
0bdd238d7f | ||
|
1bb697e58c | ||
|
b072fae507 | ||
|
9fa6bfa655 | ||
|
da44b39423 | ||
|
d0d6f53dec | ||
|
751b205618 | ||
|
d80760d20c | ||
|
5dd919b7ad | ||
|
108a578772 | ||
|
9444bbb6f3 | ||
|
7c0c98a368 | ||
|
c1d395a7d8 | ||
|
3f6795962f | ||
|
60e651b481 | ||
|
548db18857 | ||
|
aec11618ce | ||
|
f0cbb4f949 | ||
|
027e023443 | ||
|
51c15d894b | ||
|
b262f0b374 | ||
|
a3dee9350f | ||
|
d0045673fa | ||
|
d92971cca1 | ||
|
87b7513401 | ||
|
c03c3a5706 | ||
|
9573974c47 | ||
|
6e9ff5fdd8 | ||
|
022f85095e | ||
|
6a0848a3a9 | ||
|
13376fdad8 | ||
|
5b0c143713 | ||
|
5d0e14b564 | ||
|
38050b5346 | ||
|
b1a5776f14 | ||
|
7a7f16b658 | ||
|
684d310ea0 | ||
|
49bfa556bf | ||
|
e228733f1a | ||
|
103bd9e2f2 | ||
|
ba38a826e9 | ||
|
8103656ae1 | ||
|
b980f45b2b | ||
|
b4b8dde4fb | ||
|
59d57d3466 | ||
|
f0f72fdd33 | ||
|
388dfec50b | ||
|
874413ccc5 | ||
|
4a35d32b6a | ||
|
a1166b1077 | ||
|
e5c9cde36f | ||
|
b5f55c9b14 | ||
|
7c09c01788 | ||
|
0f914cf2bd | ||
|
d175ab495b | ||
|
f2f4158974 | ||
|
764001a4c2 | ||
|
b65cff0adc | ||
|
db4f4498dc | ||
|
c2c039151c | ||
|
8765e3a4d6 | ||
|
f6b3998bbd | ||
|
0691bbaad9 | ||
|
101d9ab87f | ||
|
65a5cf64df | ||
|
608a7c2d38 | ||
|
e59eaf33e0 | ||
|
47b66f3220 | ||
|
491f49388c | ||
|
bb116456a9 | ||
|
13b1a3e737 | ||
|
98791752a9 | ||
|
0bd9b00132 | ||
|
39a658eac2 | ||
|
3c019e0e16 | ||
|
41e27ba621 | ||
|
3a9d83f86c | ||
|
9d455f58b1 | ||
|
829e10ff87 | ||
|
b62830031f | ||
|
a553a9923a | ||
|
a629d455fb | ||
|
feabed30a3 | ||
|
2ca8b0b12e | ||
|
bcdf4e0fe8 | ||
|
78e5ec13bb | ||
|
8b51f5f563 | ||
|
756c284ecd | ||
|
d1b2e38ae9 | ||
|
dd10dec73d | ||
|
f822f1795a | ||
|
386915378b | ||
|
2f1c5cf143 | ||
|
3706d28125 | ||
|
0707e70183 | ||
|
bebee15d10 | ||
|
5660036f47 | ||
|
262f03bc92 | ||
|
244fd0e731 | ||
|
fe6af0ef5d | ||
|
fd63f50221 | ||
|
5c13fbb0b8 | ||
|
9a5b090894 | ||
|
5a3f23f00c | ||
|
8b347dfdcf | ||
|
deca5479f0 | ||
|
2ea71d466c | ||
|
200f5ac157 | ||
|
9e77effacb | ||
|
f5f883202d | ||
|
594757d27d | ||
|
fed5d87cfd | ||
|
adf5b7f233 | ||
|
1b4c831469 | ||
|
78e7ab92d8 | ||
|
6e45e998ac | ||
|
a75e9f193f | ||
|
0899e5cb83 | ||
|
39331b59ed | ||
|
65d1598a90 | ||
|
46b987042b | ||
|
75d1dd2793 | ||
|
e5c68661fe | ||
|
e482feed7d | ||
|
87fe4108a2 | ||
|
02c831a4e7 | ||
|
bcd416c83d | ||
|
1d5608d627 | ||
|
79a14bcbe7 | ||
|
81bc515e5d | ||
|
201522f1b1 | ||
|
44b1005077 | ||
|
48b21d00d2 | ||
|
e83eefb71d | ||
|
345a47ede7 | ||
|
03d41bdf46 | ||
|
05f3884722 | ||
|
aaa0f49f31 | ||
|
303c628998 | ||
|
8cab2e85be | ||
|
a4423778d5 | ||
|
2a7f86bfb4 | ||
|
7add902bc7 | ||
|
cc78054b8c | ||
|
533f97f080 | ||
|
5b0bc5bbc5 | ||
|
6f7ab97fc3 | ||
|
27676f4aa2 | ||
|
79dc972e5a | ||
|
66c2e145cb | ||
|
d3d7cb1b14 | ||
|
e88bb4e05c | ||
|
7e7ae144a9 | ||
|
305eda74e2 | ||
|
84d905a648 | ||
|
32ce819889 | ||
|
26315b6bc2 | ||
|
d014e4590e | ||
|
c8a4a773ee | ||
|
ff3aa7c1a9 | ||
|
84b8cee004 | ||
|
6d9e50d60c | ||
|
be352ae014 | ||
|
563742f13c | ||
|
dc2cfee056 | ||
|
c6455c4131 | ||
|
7bcae7b665 | ||
|
3471f5204b | ||
|
521025037d | ||
|
ac2a2512ef | ||
|
607d90ca5d | ||
|
cb80d7c26f | ||
|
c283e22325 | ||
|
5ac4b81a5d | ||
|
34c42be74f | ||
|
659140e190 | ||
|
63e5d33028 | ||
|
2fed924a0d | ||
|
7d906fd4c2 | ||
|
6b829d839b | ||
|
bf968a9fd8 | ||
|
cdc96136bc | ||
|
23a71680de | ||
|
150b7f9c87 | ||
|
b8f011a2ab | ||
|
9633081c31 | ||
|
b4ea37d598 | ||
|
549a0e1c44 | ||
|
2bc9413be1 | ||
|
e6766b9b82 | ||
|
75bc5809a9 | ||
|
0f878daa98 | ||
|
01d51aa979 | ||
|
f8fa5bd969 | ||
|
18bbfa10e5 | ||
|
ff1258fd20 | ||
|
e56bf067c4 | ||
|
3fbbc57a37 | ||
|
070a7efd73 | ||
|
986bc63e54 | ||
|
2b5c11c7b4 | ||
|
62e120a602 | ||
|
48ecc7f6dc | ||
|
43962476aa | ||
|
a4a3d27ac6 | ||
|
f4bd424226 | ||
|
af137188f4 | ||
|
fd4e27d889 | ||
|
2261cbd92e | ||
|
3033e27466 | ||
|
352f4962da | ||
|
789c867c8f | ||
|
4f794aae61 | ||
|
bf6560e45b | ||
|
ccf4fbed60 | ||
|
250faf012d | ||
|
3a9ffdf135 | ||
|
ec1991d165 | ||
|
4e1f5354fe | ||
|
0cd28e2cab | ||
|
eb08ef6ced | ||
|
a4e69574d3 | ||
|
c85fc6c8ca | ||
|
f19128ad21 | ||
|
2ef656fac0 | ||
|
e9c64c5839 | ||
|
8927a92eaf | ||
|
b0ec35d526 | ||
|
5fb539190d | ||
|
f89b63b0c5 | ||
|
2c0fbd8500 | ||
|
ade64f25d3 | ||
|
72af1912ca | ||
|
08ca0f7c0f | ||
|
bcef00edee | ||
|
06edc5c044 | ||
|
c4ec4db050 | ||
|
31c7b3e136 | ||
|
22700527ac | ||
|
9600039686 | ||
|
1a74ede126 | ||
|
07c886a2b1 | ||
|
b73089deb8 | ||
|
35fe37199d | ||
|
351c5fbf7f | ||
|
f68543b151 | ||
|
be85ef2707 | ||
|
b6a741b421 | ||
|
36d65e00f9 | ||
|
a2e1389943 | ||
|
8ef110cc5f | ||
|
de7d274fcf | ||
|
7c10921564 | ||
|
a11f081d2d | ||
|
020c9a5cec | ||
|
ecff21ac21 | ||
|
3397e47ccf | ||
|
6e55a873b3 | ||
|
bddec476f9 | ||
|
cdd324d0a9 | ||
|
ce7d24f529 | ||
|
a0e2f98086 | ||
|
69d5459460 | ||
|
aafaff877b | ||
|
9061c04f1d | ||
|
9a2f6d2416 | ||
|
b5121d3f4c | ||
|
f3a6897870 | ||
|
f16fd0ad23 | ||
|
f681ee7942 | ||
|
50a9df9b29 | ||
|
2a87ad044d | ||
|
a573976406 | ||
|
0b5b8e4c97 | ||
|
ee158c1f55 | ||
|
bf242ac4a2 | ||
|
a800c19c14 | ||
|
d14283b0e7 | ||
|
9faa926803 | ||
|
48c331785c | ||
|
bbb62c8a4b | ||
|
b05999f6d5 | ||
|
ee209e3b44 | ||
|
69a63975c1 | ||
|
a3cc001f1b | ||
|
b3fbb263ce | ||
|
b95ff827d3 | ||
|
a3b4678ad6 | ||
|
a2759b495b | ||
|
bedd3688d0 | ||
|
c229ba97a9 | ||
|
07e6932a17 | ||
|
0713fc6a6a | ||
|
73992dde8d | ||
|
42c76d9e0c | ||
|
45e24d21d3 | ||
|
f440d66210 | ||
|
8873a565ee | ||
|
154b6711b3 | ||
|
4fc0edb8b7 | ||
|
d47d8c135b | ||
|
22cbc16238 | ||
|
eab724fe54 | ||
|
8d156b2770 | ||
|
3d22ad36b8 | ||
|
102c1e799c | ||
|
980ffa6bfb | ||
|
997df2032e | ||
|
d19ee9c95f | ||
|
e2d81b0ce0 | ||
|
c15e10fe1f | ||
|
2b0e281113 | ||
|
67a2cd7086 | ||
|
5a61e076d7 | ||
|
953be8a7f8 | ||
|
8c0c2496c2 | ||
|
e8dc3dd59a | ||
|
81619fb4a0 | ||
|
82dad7ab17 | ||
|
a6adcb485e | ||
|
be335c401d | ||
|
b6eb1f9395 | ||
|
7f5a624cfd | ||
|
b215329456 | ||
|
c6601cbd89 | ||
|
f96cb47727 | ||
|
365522f5c8 | ||
|
8dde7ab6b8 | ||
|
e964377edf | ||
|
d904e91663 | ||
|
61ba1a0dc7 | ||
|
48d78d8df9 | ||
|
797993d0b7 | ||
|
79d279b99b | ||
|
a577d6ab36 | ||
|
389e576b3e | ||
|
47f47a33e3 | ||
|
b8a527e4a0 | ||
|
3497de3dd5 | ||
|
cf9e99b8e1 | ||
|
2738c37845 | ||
|
c4fc811619 | ||
|
a9241f61f9 | ||
|
e38e41ab97 | ||
|
e3f0e66b9a | ||
|
b80d196d56 | ||
|
c61995aad9 | ||
|
34711eb683 | ||
|
5ed06cd79b | ||
|
a7fec1f871 | ||
|
801714a588 | ||
|
0dd2472385 | ||
|
e569f6f6df | ||
|
5da60b718d | ||
|
55850a5ccd | ||
|
7991124794 | ||
|
02c0f91f4d | ||
|
3fd6d72984 | ||
|
3c4ff2e037 | ||
|
ef1738fbf6 | ||
|
618eb951d3 | ||
|
330461cf1e | ||
|
e95eb220c5 | ||
|
c093934c24 | ||
|
b7787a9846 | ||
|
b4c3e1fd58 | ||
|
300e9acd37 | ||
|
36f95fb35d | ||
|
ccb4efbe88 | ||
|
1d6738778b | ||
|
ba7883f549 | ||
|
ceaaac6c3a | ||
|
21618594b2 | ||
|
8c9de445e7 | ||
|
d8583ab6e6 | ||
|
7569e72f55 | ||
|
1e12e888d1 | ||
|
fb742361e9 | ||
|
322d4b5351 | ||
|
c1a34396d0 | ||
|
72a98943b1 | ||
|
9bb376296d | ||
|
839215c437 | ||
|
8a0fabed0e | ||
|
680136f57d | ||
|
448505fbfb | ||
|
50d3b7bdef | ||
|
42f07e6ec2 | ||
|
6012a55828 | ||
|
9cfbb21cd7 | ||
|
bbc663fce1 | ||
|
1c47c118d6 | ||
|
daafc1c90f | ||
|
bd2839fa40 | ||
|
e291d1bb17 | ||
|
1bdc0e3917 | ||
|
152aa994a6 | ||
|
baf2090f9e | ||
|
592eebe516 | ||
|
8b307357f3 | ||
|
d27d5624e0 | ||
|
5073c780d8 | ||
|
2c1457fb95 | ||
|
1dc3c58775 | ||
|
410324ac19 | ||
|
9e619ecc50 | ||
|
03302fa0b0 | ||
|
c43e857cbc | ||
|
c855e2d79c | ||
|
a704c43402 | ||
|
2b09f01293 | ||
|
5a7008f377 | ||
|
cd6602882c | ||
|
c3ef8ebb10 | ||
|
b5c0daa069 | ||
|
da0ac8190f | ||
|
3cb9cc63b3 | ||
|
e77c16d510 | ||
|
f57394c1ce | ||
|
f22f613b24 | ||
|
2593a929d4 | ||
|
786f746958 | ||
|
c4482d56ab | ||
|
411ad5641a | ||
|
0dd852516a | ||
|
2fea23d31a | ||
|
ede79590da | ||
|
fee7b792e1 | ||
|
507d3d6d9b | ||
|
25dfbb5a08 | ||
|
9286cbed86 | ||
|
c1e528e116 | ||
|
f6ba0fe6ae | ||
|
7294db81e2 | ||
|
adf29fe1d7 | ||
|
f7f936c14f | ||
|
d1b069abfb | ||
|
7029b9602c | ||
|
020dc3c6e1 | ||
|
aa15837589 | ||
|
fa033965c8 | ||
|
08ede37795 | ||
|
8665d0866d | ||
|
1431f7cc3e | ||
|
73ef1d5191 | ||
|
8647c0192c | ||
|
2333dbae40 | ||
|
bd913bc24d | ||
|
9652c00acb | ||
|
c12fb1a49c | ||
|
25fa6bee74 | ||
|
051c3be99e | ||
|
3a83427f92 | ||
|
c14553bacb | ||
|
c2b33a0f58 | ||
|
7a18e96042 | ||
|
f681ce9139 | ||
|
31745a9dc2 | ||
|
93ce963e9b | ||
|
752110a268 | ||
|
d05ecd630f | ||
|
34e7433844 | ||
|
a7b030fff9 | ||
|
3192af8df8 | ||
|
63c732a560 | ||
|
6c0fa0dc1f | ||
|
078b430828 | ||
|
b0720fdcf5 | ||
|
1e43154bc5 | ||
|
228fc757e9 | ||
|
7fc39eafbd | ||
|
3397225df2 | ||
|
14d9789f1e | ||
|
d3fbd41f59 | ||
|
b80c9dfd1e | ||
|
5ef6ea4d91 | ||
|
73414e0fbd | ||
|
673f5c325c | ||
|
b104b54e6a | ||
|
13f6529cca | ||
|
95987663f4 | ||
|
0642a2768e | ||
|
58ad5a683a | ||
|
79d0fd937c | ||
|
741d2db334 | ||
|
795934116d | ||
|
2bf4cf7d5a | ||
|
8108a48f39 | ||
|
bb355cfac5 | ||
|
80bb120026 | ||
|
89eb1b0084 | ||
|
1211b72255 | ||
|
772800bf74 | ||
|
865d678304 | ||
|
28e51e2dfb | ||
|
58d48e79da | ||
|
a5d87859dc | ||
|
6e22607387 | ||
|
dbddc4c8aa | ||
|
20093ea090 | ||
|
81349c2a03 | ||
|
07c391322e | ||
|
a398f4730b | ||
|
a27e63a547 | ||
|
cd2a41e76e | ||
|
892fb77ec3 | ||
|
634b80f0e7 | ||
|
2298656e45 | ||
|
3216a05a9e | ||
|
da0992f859 | ||
|
25f89ac194 | ||
|
00fa904422 | ||
|
4aaa439221 | ||
|
c8ecedf6d5 | ||
|
6a4fc33c30 | ||
|
7092212ed5 | ||
|
7713f343a9 | ||
|
98dcab49ab | ||
|
b4fcda2c11 | ||
|
92a5efad0e | ||
|
b193d8418d | ||
|
f46b62f1a7 | ||
|
394a973bbb | ||
|
48ae248d2d | ||
|
5e10bb2cca | ||
|
75804a7f85 | ||
|
81eb9ebc6e | ||
|
8cfa5934db | ||
|
813724bd82 | ||
|
05dc29e60b | ||
|
41d4e516f1 | ||
|
3ab40358a2 | ||
|
8de10e3746 | ||
|
d7bd9de60e | ||
|
d0ad822034 | ||
|
7f4883008f | ||
|
a77fdb1594 | ||
|
a4b2dc30b4 | ||
|
8dce617ada | ||
|
283c1968bf | ||
|
76c4b2a975 | ||
|
7c2bfae92e | ||
|
0296061e49 | ||
|
d226f9706b | ||
|
7f61fdd9a3 | ||
|
77bb6561d5 | ||
|
178a4c8867 | ||
|
6fd9690477 | ||
|
7785809f4a | ||
|
59e6f19dd8 | ||
|
dc7b8ac7ba | ||
|
d24fce83d2 | ||
|
9b97ddd0f7 | ||
|
fc9e0ede0b | ||
|
270eed7e14 | ||
|
ab12aace5f | ||
|
5e64980319 | ||
|
b0f1d914c8 | ||
|
ce323e66ac | ||
|
e14f2cc275 | ||
|
5d4a247fa0 | ||
|
cbcee02ded | ||
|
1fc97a8008 | ||
|
9d1cf040f0 | ||
|
4ea8962ca2 | ||
|
47b50a8a29 | ||
|
c93b265ec8 | ||
|
0be0ef9e77 | ||
|
0d1172ca43 | ||
|
e43b9b65fa | ||
|
b024fafaf8 | ||
|
5b3304189c | ||
|
183bf6819f | ||
|
5ad664aaca | ||
|
9cb7d6c26e | ||
|
5d45adb37d | ||
|
bfd7803fd8 | ||
|
ee7b505dcb | ||
|
b1bfd76741 | ||
|
518e8d24dc | ||
|
1cf69f139c | ||
|
1a533668b5 | ||
|
192f75254f | ||
|
9d647fd193 | ||
|
ec5d464ff2 | ||
|
684de1937a | ||
|
08748dd021 | ||
|
4abf06119b | ||
|
534aa8f7ff | ||
|
00dbc195ac | ||
|
f677dea6a4 | ||
|
2241f24290 | ||
|
a261ee327d | ||
|
67495530b7 | ||
|
6fc3d0e5e1 | ||
|
93aff9325e | ||
|
a61274ae18 | ||
|
811f13e09a | ||
|
30bc45a1ba | ||
|
fbdda8cd15 | ||
|
3e5ca0438f | ||
|
3ca2dfc079 | ||
|
d59c48c638 | ||
|
0aca0d20d9 | ||
|
8abe1e1c2e | ||
|
bd7eeb8701 | ||
|
8a5aef20aa | ||
|
7de72a2425 | ||
|
43b49fef4f | ||
|
25fd1ea639 | ||
|
3b69745c3b | ||
|
79fe8fd85b | ||
|
d32d70d2ea | ||
|
c198ca2967 | ||
|
2f0eb95d03 | ||
|
7d27afd4b8 | ||
|
ad49541947 | ||
|
305b067e48 | ||
|
fd694f14c2 | ||
|
10d8b016e4 | ||
|
f77dffc951 | ||
|
24ace646c3 | ||
|
464cb4761c | ||
|
550ab2b8e8 | ||
|
8d4f7341c9 | ||
|
34dbe9deaa | ||
|
f958459a84 | ||
|
1d5440ff71 | ||
|
c7f485687f | ||
|
8c3ac56bc5 | ||
|
7bf531c8b8 | ||
|
c1042996db | ||
|
6198b21001 | ||
|
d3b1aa7f01 | ||
|
157bf962f7 | ||
|
86ba7dae92 | ||
|
8b456441a9 | ||
|
349d67f582 | ||
|
329d95366a | ||
|
feb6accc6c | ||
|
4435c4fd0d | ||
|
787d292ba0 | ||
|
d82264ced9 | ||
|
abdeb72eb0 | ||
|
d91ac8b669 | ||
|
bdf6537c60 | ||
|
4bac66ff0e | ||
|
75b0a3e63d | ||
|
92800930e9 | ||
|
5257e8b3ed | ||
|
e55638ed03 | ||
|
ed55296d20 | ||
|
ed99e7f857 | ||
|
8e5b4750d6 | ||
|
6470635753 | ||
|
7a43f37eb7 | ||
|
f2fa476dc6 | ||
|
ed2b1b1ed1 | ||
|
801ab39a24 | ||
|
3cbe51c3ca | ||
|
1c5e172683 | ||
|
38a780ef63 | ||
|
dc25668468 | ||
|
ce661cb58b | ||
|
5fd85368a9 | ||
|
c384d1357e | ||
|
6f031f005d | ||
|
63db1fd894 | ||
|
314c0925bf | ||
|
73114b93c2 | ||
|
71ec32ac9e | ||
|
697fad0ac4 | ||
|
0817e1698f | ||
|
91d8370909 | ||
|
2c430c806c | ||
|
61d7129d7c | ||
|
52dfb0452c | ||
|
72f9c248f5 | ||
|
df25dbc048 | ||
|
a712c5d42c | ||
|
e89609dc3a | ||
|
5bb1f4a845 | ||
|
d1a0ae45e8 | ||
|
724465c798 | ||
|
488b4512e0 | ||
|
d304f95c13 | ||
|
66f7c91357 | ||
|
762dd4f024 | ||
|
a51e44eea3 | ||
|
74b924471a | ||
|
cd7bd9bf9a | ||
|
6498e352c1 | ||
|
97e8bb09e8 | ||
|
82218d01f4 | ||
|
5188464fc0 | ||
|
c8aa7720a2 | ||
|
b39fc6b924 | ||
|
b2bab68fba | ||
|
798438df9d | ||
|
499cc5bae1 | ||
|
a6356c2821 | ||
|
c6f045afa9 | ||
|
2e30bdb9b2 | ||
|
9a46613975 | ||
|
2b89f643b7 | ||
|
c78b2080cc | ||
|
6ef15802eb | ||
|
973cfd0182 | ||
|
f0bd6b9589 | ||
|
2805e83c9f | ||
|
8e8f71ade5 | ||
|
149539d3f9 | ||
|
5cb8fe1a50 | ||
|
c52910f28b | ||
|
6434bf6745 | ||
|
32bbe603cb | ||
|
6f7eb71bbb | ||
|
d5b516842c | ||
|
f21185d1c4 | ||
|
02eb00fa33 | ||
|
c2936d551b | ||
|
4d112def17 | ||
|
126b8dac07 | ||
|
70e9fa6136 | ||
|
cd4faa9c59 | ||
|
8227b4aafe | ||
|
62c4675e29 | ||
|
cb66663fd2 | ||
|
55001bf321 | ||
|
6f2c3e2528 | ||
|
2d6ca5c8bf | ||
|
20901c833a | ||
|
8a37eba0d9 | ||
|
882e68c68b | ||
|
6a15a9b412 | ||
|
1cef40a134 | ||
|
e881175cc4 | ||
|
63f114395a | ||
|
aaeeb86622 | ||
|
19913e8dc5 | ||
|
d60b38dad2 | ||
|
d01def3c61 | ||
|
faab4b2342 | ||
|
c5b246af80 | ||
|
9296ad23d9 | ||
|
00112d81d2 | ||
|
9a556d2639 | ||
|
18709406c5 | ||
|
9ea8792d3c | ||
|
3993bd7c1c | ||
|
e0f60e175f | ||
|
b1bf6d8dc9 | ||
|
6353f3ac1a | ||
|
7a5439321c | ||
|
ce13ce4b10 | ||
|
4601705814 | ||
|
524da3c7ab | ||
|
ad0d7c9a9e | ||
|
2a7369b56a | ||
|
73792fd6ce | ||
|
70531224e6 | ||
|
07606a9e23 | ||
|
6d9f1fafb7 | ||
|
256fac2a2b | ||
|
5dbd5c235a | ||
|
3012c55ec5 | ||
|
a119fbd895 | ||
|
ebf60d85da | ||
|
43f5a16006 | ||
|
cc30210b3f | ||
|
095bedf54e | ||
|
4bad2b5c04 | ||
|
5b9e3af276 | ||
|
5405d8fa6f | ||
|
a276ef4b06 | ||
|
ec3d49ce4c | ||
|
86b30d2d66 | ||
|
2711605df6 | ||
|
daf7653988 | ||
|
cc0d8fa590 | ||
|
0c8d657d92 | ||
|
fa87e08071 | ||
|
7216d140de | ||
|
06225b9501 | ||
|
d86885c7f9 | ||
|
b61fc161bf | ||
|
6380c3d462 | ||
|
bb33b96ba7 | ||
|
1f4cc145c4 | ||
|
eda72ef26c | ||
|
a439488b74 | ||
|
bad6fe77d3 | ||
|
cb81613aa5 | ||
|
329a0a3f45 | ||
|
c293401b22 | ||
|
e604047158 | ||
|
a8c9aa01fb | ||
|
7727f31507 | ||
|
dde363343c | ||
|
439914caef | ||
|
e4284f4e7b | ||
|
36948e2a74 | ||
|
c9bc91c75b | ||
|
6fa3db3a1d | ||
|
cd1b8b9cee | ||
|
9e20d13e50 | ||
|
1d5c66da3b | ||
|
7f3524949c | ||
|
d52c1c7554 | ||
|
6f92c58e33 | ||
|
f6b90595fa | ||
|
66412bfa58 | ||
|
7efcbbb457 | ||
|
da2747d487 | ||
|
b144a6357d | ||
|
547a75d9c1 | ||
|
607d5b2f8f | ||
|
48160f3fe9 | ||
|
199fd2d074 | ||
|
89c7c2fec6 | ||
|
611e35ed81 | ||
|
f410b1b14d | ||
|
77826ebf78 | ||
|
5c571f565f | ||
|
178e5a195a | ||
|
9adce8d167 | ||
|
ec7d663496 | ||
|
a56465e049 | ||
|
851d1e9da1 | ||
|
59cfde3767 | ||
|
c53ff94b8e | ||
|
03256fc776 | ||
|
19b3669d97 | ||
|
6841bdaa81 | ||
|
8e101a9f1c | ||
|
0680ca2fe8 | ||
|
d0456b698c | ||
|
f3085443d5 | ||
|
958a4565db | ||
|
2403a03fcb |
57
.github/workflows/ci.yml
vendored
57
.github/workflows/ci.yml
vendored
@ -16,15 +16,16 @@ on:
|
|||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
permissions:
|
||||||
|
repository-projects: read
|
||||||
jobs:
|
jobs:
|
||||||
build_linux:
|
build_linux:
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-18.04, ubuntu-20.04, ubuntu-22.04 ]
|
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -90,14 +91,14 @@ jobs:
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
|
||||||
run: |
|
|
||||||
flake8
|
|
||||||
|
|
||||||
- name: Sort imports (isort)
|
- name: Sort imports (isort)
|
||||||
run: |
|
run: |
|
||||||
isort --check .
|
isort --check .
|
||||||
|
|
||||||
|
- name: Run Ruff
|
||||||
|
run: |
|
||||||
|
ruff check --format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts tests
|
mypy freqtrade scripts tests
|
||||||
@ -115,7 +116,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest ]
|
os: [ macos-latest ]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -148,6 +149,19 @@ jobs:
|
|||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: |
|
||||||
brew update
|
brew update
|
||||||
|
# homebrew fails to update python due to unlinking failures
|
||||||
|
# https://github.com/actions/runner-images/issues/6817
|
||||||
|
rm /usr/local/bin/2to3 || true
|
||||||
|
rm /usr/local/bin/2to3-3.11 || true
|
||||||
|
rm /usr/local/bin/idle3 || true
|
||||||
|
rm /usr/local/bin/idle3.11 || true
|
||||||
|
rm /usr/local/bin/pydoc3 || true
|
||||||
|
rm /usr/local/bin/pydoc3.11 || true
|
||||||
|
rm /usr/local/bin/python3 || true
|
||||||
|
rm /usr/local/bin/python3.11 || true
|
||||||
|
rm /usr/local/bin/python3-config || true
|
||||||
|
rm /usr/local/bin/python3.11-config || true
|
||||||
|
|
||||||
brew install hdf5 c-blosc
|
brew install hdf5 c-blosc
|
||||||
python -m pip install --upgrade pip wheel
|
python -m pip install --upgrade pip wheel
|
||||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||||
@ -173,14 +187,14 @@ jobs:
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
|
||||||
run: |
|
|
||||||
flake8
|
|
||||||
|
|
||||||
- name: Sort imports (isort)
|
- name: Sort imports (isort)
|
||||||
run: |
|
run: |
|
||||||
isort --check .
|
isort --check .
|
||||||
|
|
||||||
|
- name: Run Ruff
|
||||||
|
run: |
|
||||||
|
ruff check --format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts
|
||||||
@ -199,7 +213,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest ]
|
os: [ windows-latest ]
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -235,9 +249,9 @@ jobs:
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
- name: Run Ruff
|
||||||
run: |
|
run: |
|
||||||
flake8
|
ruff check --format=github .
|
||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
@ -308,7 +322,6 @@ jobs:
|
|||||||
build_linux_online:
|
build_linux_online:
|
||||||
# Run pytest with "live" checks
|
# Run pytest with "live" checks
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
# permissions:
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@ -347,6 +360,8 @@ jobs:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
- name: Tests incl. ccxt compatibility tests
|
- name: Tests incl. ccxt compatibility tests
|
||||||
|
env:
|
||||||
|
CI_WEB_PROXY: http://152.67.78.211:13128
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
||||||
|
|
||||||
@ -410,7 +425,7 @@ jobs:
|
|||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@v1.6.4
|
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
@ -418,7 +433,7 @@ jobs:
|
|||||||
repository_url: https://test.pypi.org/legacy/
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.6.4
|
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
@ -451,12 +466,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and test and push docker images
|
- name: Build and test and push docker images
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: freqtradeorg/freqtrade
|
|
||||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
||||||
run: |
|
run: |
|
||||||
build_helpers/publish_docker_multi.sh
|
build_helpers/publish_docker_multi.sh
|
||||||
|
|
||||||
deploy_arm:
|
deploy_arm:
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
needs: [ deploy ]
|
needs: [ deploy ]
|
||||||
# Only run on 64bit machines
|
# Only run on 64bit machines
|
||||||
runs-on: [self-hosted, linux, ARM64]
|
runs-on: [self-hosted, linux, ARM64]
|
||||||
@ -479,8 +495,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and test and push docker images
|
- name: Build and test and push docker images
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: freqtradeorg/freqtrade
|
|
||||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
||||||
|
GHCR_USERNAME: ${{ github.actor }}
|
||||||
|
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
build_helpers/publish_docker_arm64.sh
|
build_helpers/publish_docker_arm64.sh
|
||||||
|
|
||||||
|
@ -2,33 +2,40 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/pycqa/flake8
|
||||||
rev: "4.0.1"
|
rev: "6.0.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v0.942"
|
rev: "v1.0.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.2.1
|
- types-cachetools==5.3.0.5
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.28.11.5
|
- types-requests==2.28.11.17
|
||||||
- types-tabulate==0.9.0.0
|
- types-tabulate==0.9.0.2
|
||||||
- types-python-dateutil==2.8.19.4
|
- types-python-dateutil==2.8.19.12
|
||||||
|
- SQLAlchemy==2.0.9
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: "5.10.1"
|
rev: "5.12.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
|
# Ruff version.
|
||||||
|
rev: 'v0.0.255'
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v2.4.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: |
|
exclude: |
|
||||||
|
@ -45,16 +45,17 @@ pytest tests/test_<file_name>.py::test_<method_name>
|
|||||||
|
|
||||||
### 2. Test if your code is PEP8 compliant
|
### 2. Test if your code is PEP8 compliant
|
||||||
|
|
||||||
#### Run Flake8
|
#### Run Ruff
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flake8 freqtrade tests scripts
|
ruff .
|
||||||
```
|
```
|
||||||
|
|
||||||
We receive a lot of code that fails the `flake8` checks.
|
We receive a lot of code that fails the `ruff` checks.
|
||||||
To help with that, we encourage you to install the git pre-commit
|
To help with that, we encourage you to install the git pre-commit
|
||||||
hook that will warn you when you try to commit code that fails these checks.
|
hook that will warn you when you try to commit code that fails these checks.
|
||||||
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
|
||||||
|
you can manually run pre-commit with `pre-commit run -a`.
|
||||||
|
|
||||||
##### Additional styles applied
|
##### Additional styles applied
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10.7-slim-bullseye as base
|
FROM python:3.10.11-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
|
# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg)
|
||||||
|
|
||||||
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
|
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
|
||||||
|
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
|
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
|
||||||
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
@ -39,6 +40,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
|||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [X] [OKX](https://okx.com/)
|
- [X] [OKX](https://okx.com/)
|
||||||
|
- [X] [Bybit](https://bybit.com/)
|
||||||
|
|
||||||
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in.
|
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in.
|
||||||
|
|
||||||
@ -163,6 +165,10 @@ first. If it hasn't been reported, please
|
|||||||
ensure you follow the template guide so that the team can assist you as
|
ensure you follow the template guide so that the team can assist you as
|
||||||
quickly as possible.
|
quickly as possible.
|
||||||
|
|
||||||
|
For every [issue](https://github.com/freqtrade/freqtrade/issues/new/choose) created, kindly follow up and mark satisfaction or reminder to close issue when equilibrium ground is reached.
|
||||||
|
|
||||||
|
--Maintain github's [community policy](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct)--
|
||||||
|
|
||||||
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
||||||
|
|
||||||
Have you a great idea to improve the bot you want to share? Please,
|
Have you a great idea to improve the bot you want to share? Please,
|
||||||
|
BIN
build_helpers/TA_Lib-0.4.25-cp311-cp311-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.25-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
@ -8,8 +8,8 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
|||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib \
|
cd ta-lib \
|
||||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
|
&& curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
|
||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
&& curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
|
||||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make
|
&& make
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
|
@ -14,5 +14,8 @@ if ($pyv -eq '3.9') {
|
|||||||
if ($pyv -eq '3.10') {
|
if ($pyv -eq '3.10') {
|
||||||
pip install build_helpers\TA_Lib-0.4.25-cp310-cp310-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.25-cp310-cp310-win_amd64.whl
|
||||||
}
|
}
|
||||||
|
if ($pyv -eq '3.11') {
|
||||||
|
pip install build_helpers\TA_Lib-0.4.25-cp311-cp311-win_amd64.whl
|
||||||
|
}
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
@ -8,12 +8,17 @@ import yaml
|
|||||||
|
|
||||||
pre_commit_file = Path('.pre-commit-config.yaml')
|
pre_commit_file = Path('.pre-commit-config.yaml')
|
||||||
require_dev = Path('requirements-dev.txt')
|
require_dev = Path('requirements-dev.txt')
|
||||||
|
require = Path('requirements.txt')
|
||||||
|
|
||||||
with require_dev.open('r') as rfile:
|
with require_dev.open('r') as rfile:
|
||||||
requirements = rfile.readlines()
|
requirements = rfile.readlines()
|
||||||
|
|
||||||
|
with require.open('r') as rfile:
|
||||||
|
requirements.extend(rfile.readlines())
|
||||||
|
|
||||||
# Extract types only
|
# Extract types only
|
||||||
type_reqs = [r.strip('\n') for r in requirements if r.startswith('types-')]
|
type_reqs = [r.strip('\n') for r in requirements if r.startswith(
|
||||||
|
'types-') or r.startswith('SQLAlchemy')]
|
||||||
|
|
||||||
with pre_commit_file.open('r') as file:
|
with pre_commit_file.open('r') as file:
|
||||||
f = yaml.load(file, Loader=yaml.FullLoader)
|
f = yaml.load(file, Loader=yaml.FullLoader)
|
||||||
|
@ -3,18 +3,22 @@
|
|||||||
# Use BuildKit, otherwise building on ARM fails
|
# Use BuildKit, otherwise building on ARM fails
|
||||||
export DOCKER_BUILDKIT=1
|
export DOCKER_BUILDKIT=1
|
||||||
|
|
||||||
|
IMAGE_NAME=freqtradeorg/freqtrade
|
||||||
|
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
||||||
|
GHCR_IMAGE_NAME=ghcr.io/freqtrade/freqtrade
|
||||||
|
|
||||||
# Replace / with _ to create a valid tag
|
# Replace / with _ to create a valid tag
|
||||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||||
TAG_PLOT=${TAG}_plot
|
TAG_PLOT=${TAG}_plot
|
||||||
TAG_FREQAI=${TAG}_freqai
|
TAG_FREQAI=${TAG}_freqai
|
||||||
TAG_FREQAI_RL=${TAG_FREQAI}rl
|
TAG_FREQAI_RL=${TAG_FREQAI}rl
|
||||||
|
TAG_FREQAI_TORCH=${TAG_FREQAI}torch
|
||||||
TAG_PI="${TAG}_pi"
|
TAG_PI="${TAG}_pi"
|
||||||
|
|
||||||
TAG_ARM=${TAG}_arm
|
TAG_ARM=${TAG}_arm
|
||||||
TAG_PLOT_ARM=${TAG_PLOT}_arm
|
TAG_PLOT_ARM=${TAG_PLOT}_arm
|
||||||
TAG_FREQAI_ARM=${TAG_FREQAI}_arm
|
TAG_FREQAI_ARM=${TAG_FREQAI}_arm
|
||||||
TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm
|
TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm
|
||||||
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
|
||||||
|
|
||||||
echo "Running for ${TAG}"
|
echo "Running for ${TAG}"
|
||||||
|
|
||||||
@ -38,13 +42,13 @@ if [ $? -ne 0 ]; then
|
|||||||
echo "failed building multiarch images"
|
echo "failed building multiarch images"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
||||||
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
||||||
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
||||||
|
|
||||||
# Tag image for upload and next build step
|
# Tag image for upload and next build step
|
||||||
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
||||||
|
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
|
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
|
|
||||||
|
|
||||||
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||||
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
||||||
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
||||||
@ -59,7 +63,6 @@ fi
|
|||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
|
||||||
# docker push ${IMAGE_NAME}
|
|
||||||
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
|
||||||
@ -70,25 +73,47 @@ docker push ${CACHE_IMAGE}:$TAG_ARM
|
|||||||
# Otherwise installation might fail.
|
# Otherwise installation might fail.
|
||||||
echo "create manifests"
|
echo "create manifests"
|
||||||
|
|
||||||
docker manifest create --amend ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
docker manifest create ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG}
|
docker manifest push -p ${IMAGE_NAME}:${TAG}
|
||||||
|
|
||||||
docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT}
|
docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
|
||||||
|
|
||||||
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} ${CACHE_IMAGE}:${TAG_FREQAI}
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
|
||||||
|
|
||||||
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} ${CACHE_IMAGE}:${TAG_FREQAI_RL}
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
|
||||||
|
|
||||||
|
# Create special Torch tag - which is identical to the RL tag.
|
||||||
|
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_TORCH} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
|
||||||
|
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_TORCH}
|
||||||
|
|
||||||
|
# copy images to ghcr.io
|
||||||
|
|
||||||
|
alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane"
|
||||||
|
mkdir .crane
|
||||||
|
chmod a+rwx .crane
|
||||||
|
|
||||||
|
echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
|
||||||
|
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL}
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_TORCH}
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI}
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT}
|
||||||
|
crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG}
|
||||||
|
|
||||||
# Tag as latest for develop builds
|
# Tag as latest for develop builds
|
||||||
if [ "${TAG}" = "develop" ]; then
|
if [ "${TAG}" = "develop" ]; then
|
||||||
|
echo 'Tagging image as latest'
|
||||||
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
||||||
docker manifest push -p ${IMAGE_NAME}:latest
|
docker manifest push -p ${IMAGE_NAME}:latest
|
||||||
|
|
||||||
|
crane copy ${IMAGE_NAME}:latest ${GHCR_IMAGE_NAME}:latest
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
rm -rf .crane
|
||||||
|
|
||||||
# Cleanup old images from arm64 node.
|
# Cleanup old images from arm64 node.
|
||||||
docker image prune -a --force --filter "until=24h"
|
docker image prune -a --force --filter "until=24h"
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# The below assumes a correctly setup docker buildx environment
|
# The below assumes a correctly setup docker buildx environment
|
||||||
|
|
||||||
|
IMAGE_NAME=freqtradeorg/freqtrade
|
||||||
|
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
||||||
# Replace / with _ to create a valid tag
|
# Replace / with _ to create a valid tag
|
||||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||||
TAG_PLOT=${TAG}_plot
|
TAG_PLOT=${TAG}_plot
|
||||||
@ -11,7 +13,6 @@ TAG_PI="${TAG}_pi"
|
|||||||
|
|
||||||
PI_PLATFORM="linux/arm/v7"
|
PI_PLATFORM="linux/arm/v7"
|
||||||
echo "Running for ${TAG}"
|
echo "Running for ${TAG}"
|
||||||
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
|
||||||
CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache
|
CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache
|
||||||
|
|
||||||
# Add commit and commit_message to docker container
|
# Add commit and commit_message to docker container
|
||||||
@ -26,7 +27,10 @@ if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
|
|||||||
--cache-to=type=registry,ref=${CACHE_TAG} \
|
--cache-to=type=registry,ref=${CACHE_TAG} \
|
||||||
-f docker/Dockerfile.armhf \
|
-f docker/Dockerfile.armhf \
|
||||||
--platform ${PI_PLATFORM} \
|
--platform ${PI_PLATFORM} \
|
||||||
-t ${IMAGE_NAME}:${TAG_PI} --push .
|
-t ${IMAGE_NAME}:${TAG_PI} \
|
||||||
|
--push \
|
||||||
|
--provenance=false \
|
||||||
|
.
|
||||||
else
|
else
|
||||||
echo "event ${GITHUB_EVENT_NAME}: building with cache"
|
echo "event ${GITHUB_EVENT_NAME}: building with cache"
|
||||||
# Build regular image
|
# Build regular image
|
||||||
@ -35,12 +39,16 @@ else
|
|||||||
|
|
||||||
# Pull last build to avoid rebuilding the whole image
|
# Pull last build to avoid rebuilding the whole image
|
||||||
# docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG}
|
# docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG}
|
||||||
|
# disable provenance due to https://github.com/docker/buildx/issues/1509
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--cache-from=type=registry,ref=${CACHE_TAG} \
|
--cache-from=type=registry,ref=${CACHE_TAG} \
|
||||||
--cache-to=type=registry,ref=${CACHE_TAG} \
|
--cache-to=type=registry,ref=${CACHE_TAG} \
|
||||||
-f docker/Dockerfile.armhf \
|
-f docker/Dockerfile.armhf \
|
||||||
--platform ${PI_PLATFORM} \
|
--platform ${PI_PLATFORM} \
|
||||||
-t ${IMAGE_NAME}:${TAG_PI} --push .
|
-t ${IMAGE_NAME}:${TAG_PI} \
|
||||||
|
--push \
|
||||||
|
--provenance=false \
|
||||||
|
.
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@ -50,9 +58,9 @@ fi
|
|||||||
# Tag image for upload and next build step
|
# Tag image for upload and next build step
|
||||||
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
||||||
|
|
||||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai .
|
||||||
docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl .
|
||||||
|
|
||||||
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
||||||
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI
|
||||||
@ -68,12 +76,10 @@ fi
|
|||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
|
||||||
docker push ${CACHE_IMAGE}
|
docker push ${CACHE_IMAGE}:$TAG
|
||||||
docker push ${CACHE_IMAGE}:$TAG_PLOT
|
docker push ${CACHE_IMAGE}:$TAG_PLOT
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI
|
||||||
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL
|
||||||
docker push ${CACHE_IMAGE}:$TAG
|
|
||||||
|
|
||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
|
||||||
|
Binary file not shown.
@ -59,20 +59,6 @@
|
|||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"}
|
{"method": "StaticPairList"}
|
||||||
],
|
],
|
||||||
"edge": {
|
|
||||||
"enabled": false,
|
|
||||||
"process_throttle_secs": 3600,
|
|
||||||
"calculate_since_number_of_days": 7,
|
|
||||||
"allowed_risk": 0.01,
|
|
||||||
"stoploss_range_min": -0.01,
|
|
||||||
"stoploss_range_max": -0.1,
|
|
||||||
"stoploss_range_step": -0.01,
|
|
||||||
"minimum_winrate": 0.60,
|
|
||||||
"minimum_expectancy": 0.20,
|
|
||||||
"min_trade_number": 10,
|
|
||||||
"max_trade_duration_minute": 1440,
|
|
||||||
"remove_pumps": false
|
|
||||||
},
|
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
|
@ -56,20 +56,6 @@
|
|||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"}
|
{"method": "StaticPairList"}
|
||||||
],
|
],
|
||||||
"edge": {
|
|
||||||
"enabled": false,
|
|
||||||
"process_throttle_secs": 3600,
|
|
||||||
"calculate_since_number_of_days": 7,
|
|
||||||
"allowed_risk": 0.01,
|
|
||||||
"stoploss_range_min": -0.01,
|
|
||||||
"stoploss_range_max": -0.1,
|
|
||||||
"stoploss_range_step": -0.01,
|
|
||||||
"minimum_winrate": 0.60,
|
|
||||||
"minimum_expectancy": 0.20,
|
|
||||||
"min_trade_number": 10,
|
|
||||||
"max_trade_duration_minute": 1440,
|
|
||||||
"remove_pumps": false
|
|
||||||
},
|
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
"ccxt_config": {},
|
"ccxt_config": {},
|
||||||
"ccxt_async_config": {},
|
"ccxt_async_config": {},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"1INCH/USDT",
|
"1INCH/USDT:USDT",
|
||||||
"ALGO/USDT"
|
"ALGO/USDT:USDT"
|
||||||
],
|
],
|
||||||
"pair_blacklist": []
|
"pair_blacklist": []
|
||||||
},
|
},
|
||||||
@ -48,7 +48,7 @@
|
|||||||
],
|
],
|
||||||
"freqai": {
|
"freqai": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"purge_old_models": true,
|
"purge_old_models": 2,
|
||||||
"train_period_days": 15,
|
"train_period_days": 15,
|
||||||
"backtest_period_days": 7,
|
"backtest_period_days": 7,
|
||||||
"live_retrain_hours": 0,
|
"live_retrain_hours": 0,
|
||||||
@ -60,8 +60,8 @@
|
|||||||
"1h"
|
"1h"
|
||||||
],
|
],
|
||||||
"include_corr_pairlist": [
|
"include_corr_pairlist": [
|
||||||
"BTC/USDT",
|
"BTC/USDT:USDT",
|
||||||
"ETH/USDT"
|
"ETH/USDT:USDT"
|
||||||
],
|
],
|
||||||
"label_period_candles": 20,
|
"label_period_candles": 20,
|
||||||
"include_shifted_candles": 2,
|
"include_shifted_candles": 2,
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
"force_entry": "market",
|
"force_entry": "market",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": false,
|
"stoploss_on_exchange": false,
|
||||||
|
"stoploss_price_type": "last",
|
||||||
"stoploss_on_exchange_interval": 60,
|
"stoploss_on_exchange_interval": 60,
|
||||||
"stoploss_on_exchange_limit_ratio": 0.99
|
"stoploss_on_exchange_limit_ratio": 0.99
|
||||||
},
|
},
|
||||||
|
@ -64,20 +64,6 @@
|
|||||||
"pairlists": [
|
"pairlists": [
|
||||||
{"method": "StaticPairList"}
|
{"method": "StaticPairList"}
|
||||||
],
|
],
|
||||||
"edge": {
|
|
||||||
"enabled": false,
|
|
||||||
"process_throttle_secs": 3600,
|
|
||||||
"calculate_since_number_of_days": 7,
|
|
||||||
"allowed_risk": 0.01,
|
|
||||||
"stoploss_range_min": -0.01,
|
|
||||||
"stoploss_range_max": -0.1,
|
|
||||||
"stoploss_range_step": -0.01,
|
|
||||||
"minimum_winrate": 0.60,
|
|
||||||
"minimum_expectancy": 0.20,
|
|
||||||
"min_trade_number": 10,
|
|
||||||
"max_trade_duration_minute": 1440,
|
|
||||||
"remove_pumps": false
|
|
||||||
},
|
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.9.12-slim-bullseye as base
|
FROM python:3.9.16-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
@ -32,7 +32,7 @@ To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-an
|
|||||||
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
|
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4
|
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4 5
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will read from the last backtesting results. The `--analysis-groups` option is
|
This command will read from the last backtesting results. The `--analysis-groups` option is
|
||||||
@ -43,6 +43,7 @@ ranging from the simplest (0) to the most detailed per pair, per buy and per sel
|
|||||||
* 2: profit summaries grouped by enter_tag and exit_tag
|
* 2: profit summaries grouped by enter_tag and exit_tag
|
||||||
* 3: profit summaries grouped by pair and enter_tag
|
* 3: profit summaries grouped by pair and enter_tag
|
||||||
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||||
|
* 5: profit summaries grouped by exit_tag
|
||||||
|
|
||||||
More options are available by running with the `-h` option.
|
More options are available by running with the `-h` option.
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ This function needs to return a floating point number (`float`). Smaller numbers
|
|||||||
|
|
||||||
## Overriding pre-defined spaces
|
## Overriding pre-defined spaces
|
||||||
|
|
||||||
To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows:
|
To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`, `max_open_trades_space`), define a nested class called Hyperopt and define the required spaces as follows:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
|
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
|
||||||
@ -123,6 +123,12 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
Categorical([True, False], name='trailing_only_offset_is_reached'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Define a custom max_open_trades space
|
||||||
|
def max_open_trades_space(self) -> List[Dimension]:
|
||||||
|
return [
|
||||||
|
Integer(-1, 10, name='max_open_trades'),
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -192,7 +192,7 @@ $RepeatedMsgReduction on
|
|||||||
|
|
||||||
### Logging to journald
|
### Logging to journald
|
||||||
|
|
||||||
This needs the `systemd` python package installed as the dependency, which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows.
|
This needs the `cysystemd` python package installed as dependency (`pip install cysystemd`), which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows.
|
||||||
|
|
||||||
To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format:
|
To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format:
|
||||||
|
|
||||||
|
BIN
docs/assets/freqai_pytorch-diagram.png
Normal file
BIN
docs/assets/freqai_pytorch-diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -274,19 +274,20 @@ A backtesting result will look like that:
|
|||||||
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
||||||
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
||||||
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||||
========================================================= EXIT REASON STATS ==========================================================
|
|
||||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
|
||||||
|:-------------------|--------:|------:|-------:|--------:|
|
|
||||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
|
||||||
| stop_loss | 166 | 0 | 0 | 166 |
|
|
||||||
| exit_signal | 56 | 36 | 0 | 20 |
|
|
||||||
| force_exit | 2 | 0 | 0 | 2 |
|
|
||||||
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
||||||
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
||||||
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
|:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
||||||
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||||
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||||
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||||
|
==================== EXIT REASON STATS ====================
|
||||||
|
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||||
|
|:-------------------|--------:|------:|-------:|--------:|
|
||||||
|
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||||
|
| stop_loss | 166 | 0 | 0 | 166 |
|
||||||
|
| exit_signal | 56 | 36 | 0 | 20 |
|
||||||
|
| force_exit | 2 | 0 | 0 | 2 |
|
||||||
|
|
||||||
================== SUMMARY METRICS ==================
|
================== SUMMARY METRICS ==================
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|-----------------------------+---------------------|
|
|-----------------------------+---------------------|
|
||||||
@ -300,7 +301,11 @@ A backtesting result will look like that:
|
|||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
|
| Sortino | 1.88 |
|
||||||
|
| Sharpe | 2.97 |
|
||||||
|
| Calmar | 6.29 |
|
||||||
| Profit factor | 1.11 |
|
| Profit factor | 1.11 |
|
||||||
|
| Expectancy | -0.15 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
@ -400,7 +405,11 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
|
| Sortino | 1.88 |
|
||||||
|
| Sharpe | 2.97 |
|
||||||
|
| Calmar | 6.29 |
|
||||||
| Profit factor | 1.11 |
|
| Profit factor | 1.11 |
|
||||||
|
| Expectancy | -0.15 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
@ -447,6 +456,9 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Absolute profit`: Profit made in stake currency.
|
- `Absolute profit`: Profit made in stake currency.
|
||||||
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
||||||
- `CAGR %`: Compound annual growth rate.
|
- `CAGR %`: Compound annual growth rate.
|
||||||
|
- `Sortino`: Annualized Sortino ratio.
|
||||||
|
- `Sharpe`: Annualized Sharpe ratio.
|
||||||
|
- `Calmar`: Annualized Calmar ratio.
|
||||||
- `Profit factor`: profit / loss.
|
- `Profit factor`: profit / loss.
|
||||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||||
|
@ -12,6 +12,9 @@ This page provides you some basic concepts on how Freqtrade works and operates.
|
|||||||
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
||||||
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
||||||
* **Market order**: Guaranteed to fill, may move price depending on the order size.
|
* **Market order**: Guaranteed to fill, may move price depending on the order size.
|
||||||
|
* **Current Profit**: Currently pending (unrealized) profit for this trade. This is mainly used throughout the bot and UI.
|
||||||
|
* **Realized Profit**: Already realized profit. Only relevant in combination with [partial exits](strategy-callbacks.md#adjust-trade-position) - which also explains the calculation logic for this.
|
||||||
|
* **Total Profit**: Combined realized and unrealized profit. The relative number (%) is calculated against the total investment in this trade.
|
||||||
|
|
||||||
## Fee handling
|
## Fee handling
|
||||||
|
|
||||||
@ -57,10 +60,10 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
|
|
||||||
* Load historic data for configured pairlist.
|
* Load historic data for configured pairlist.
|
||||||
* Calls `bot_start()` once.
|
* Calls `bot_start()` once.
|
||||||
* Calls `bot_loop_start()` once.
|
|
||||||
* Calculate indicators (calls `populate_indicators()` once per pair).
|
* Calculate indicators (calls `populate_indicators()` once per pair).
|
||||||
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
|
* Calls `bot_loop_start()` strategy callback.
|
||||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
||||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||||
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
||||||
@ -75,3 +78,7 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument.
|
Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument.
|
||||||
|
|
||||||
|
!!! Warning "Callback call frequency"
|
||||||
|
Backtesting will call each callback at max. once per candle (`--timeframe-detail` modifies this behavior to once per detailed candle).
|
||||||
|
Most callbacks will be called once per iteration in live (usually every ~5s) - which can cause backtesting mismatches.
|
||||||
|
@ -11,7 +11,7 @@ Per default, the bot loads the configuration from the `config.json` file, locate
|
|||||||
|
|
||||||
You can specify a different configuration file used by the bot with the `-c/--config` command-line option.
|
You can specify a different configuration file used by the bot with the `-c/--config` command-line option.
|
||||||
|
|
||||||
If you used the [Quick start](installation.md/#quick-start) method for installing
|
If you used the [Quick start](docker_quickstart.md#docker-quick-start) method for installing
|
||||||
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
|
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
|
||||||
|
|
||||||
If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file.
|
If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file.
|
||||||
@ -134,7 +134,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
|
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Positive integer or -1.
|
||||||
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
||||||
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
||||||
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
||||||
@ -263,6 +263,7 @@ Values set in the configuration file always overwrite values set in the strategy
|
|||||||
* `minimal_roi`
|
* `minimal_roi`
|
||||||
* `timeframe`
|
* `timeframe`
|
||||||
* `stoploss`
|
* `stoploss`
|
||||||
|
* `max_open_trades`
|
||||||
* `trailing_stop`
|
* `trailing_stop`
|
||||||
* `trailing_stop_positive`
|
* `trailing_stop_positive`
|
||||||
* `trailing_stop_positive_offset`
|
* `trailing_stop_positive_offset`
|
||||||
@ -665,7 +666,7 @@ You should also make sure to read the [Exchanges](exchanges.md) section of the d
|
|||||||
### Using proxy with Freqtrade
|
### Using proxy with Freqtrade
|
||||||
|
|
||||||
To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values.
|
To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values.
|
||||||
This will have the proxy settings applied to everything (telegram, coingecko, ...) except exchange requests.
|
This will have the proxy settings applied to everything (telegram, coingecko, ...) **except** for exchange requests.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
export HTTP_PROXY="http://addr:port"
|
export HTTP_PROXY="http://addr:port"
|
||||||
@ -687,6 +688,7 @@ To use a proxy for exchange connections - you will have to define the proxies as
|
|||||||
"https": "http://addr:port"
|
"https": "http://addr:port"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -74,3 +74,8 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re
|
|||||||
* `webhooksell`, `webhookexit` -> `exit`
|
* `webhooksell`, `webhookexit` -> `exit`
|
||||||
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
|
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
|
||||||
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
|
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
|
||||||
|
|
||||||
|
|
||||||
|
## Removal of `populate_any_indicators`
|
||||||
|
|
||||||
|
version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details.
|
||||||
|
@ -24,7 +24,7 @@ This will spin up a local server (usually on port 8000) so you can see if everyt
|
|||||||
To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
||||||
Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`.
|
Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`.
|
||||||
|
|
||||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
This will install all required tools for development, including `pytest`, `ruff`, `mypy`, and `coveralls`.
|
||||||
|
|
||||||
Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing.
|
Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing.
|
||||||
This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine.
|
This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine.
|
||||||
@ -363,7 +363,7 @@ from pathlib import Path
|
|||||||
exchange = ccxt.binance({
|
exchange = ccxt.binance({
|
||||||
'apiKey': '<apikey>',
|
'apiKey': '<apikey>',
|
||||||
'secret': '<secret>'
|
'secret': '<secret>'
|
||||||
'options': {'defaultType': 'future'}
|
'options': {'defaultType': 'swap'}
|
||||||
})
|
})
|
||||||
_ = exchange.load_markets()
|
_ = exchange.load_markets()
|
||||||
|
|
||||||
|
@ -75,6 +75,25 @@ Binance has been split into 2, and users must use the correct ccxt exchange ID f
|
|||||||
* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`.
|
* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`.
|
||||||
* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`.
|
* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`.
|
||||||
|
|
||||||
|
### Binance RSA keys
|
||||||
|
|
||||||
|
Freqtrade supports binance RSA API keys.
|
||||||
|
|
||||||
|
We recommend to use them as environment variable.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
export FREQTRADE__EXCHANGE__SECRET="$(cat ./rsa_binance.private)"
|
||||||
|
```
|
||||||
|
|
||||||
|
They can however also be configured via configuration file. Since json doesn't support multi-line strings, you'll have to replace all newlines with `\n` to have a valid json file.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
// ...
|
||||||
|
"key": "<someapikey>",
|
||||||
|
"secret": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBABACAFQA<...>s8KX8=\n-----END PRIVATE KEY-----"
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
### Binance Futures
|
### Binance Futures
|
||||||
|
|
||||||
Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders.
|
Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders.
|
||||||
@ -224,8 +243,8 @@ OKX requires a passphrase for each api key, you will therefore need to add this
|
|||||||
OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
|
OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
|
||||||
|
|
||||||
!!! Warning "Futures"
|
!!! Warning "Futures"
|
||||||
OKX Futures has the concept of "position mode" - which can be Net or long/short (hedge mode).
|
OKX Futures has the concept of "position mode" - which can be "Buy/Sell" or long/short (hedge mode).
|
||||||
Freqtrade supports both modes (we recommend to use net mode) - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades.
|
Freqtrade supports both modes (we recommend to use Buy/Sell mode) - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades.
|
||||||
OKX also only provides MARK candles for the past ~3 months. Backtesting futures prior to that date will therefore lead to slight deviations, as funding-fees cannot be calculated correctly without this data.
|
OKX also only provides MARK candles for the past ~3 months. Backtesting futures prior to that date will therefore lead to slight deviations, as funding-fees cannot be calculated correctly without this data.
|
||||||
|
|
||||||
## Gate.io
|
## Gate.io
|
||||||
@ -236,6 +255,18 @@ OKX requires a passphrase for each api key, you will therefore need to add this
|
|||||||
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
||||||
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
||||||
|
|
||||||
|
## Bybit
|
||||||
|
|
||||||
|
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.
|
||||||
|
Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures.
|
||||||
|
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors.
|
||||||
|
|
||||||
|
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
|
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||||
|
|
||||||
## All exchanges
|
## All exchanges
|
||||||
|
|
||||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||||
|
24
docs/faq.md
24
docs/faq.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Supported Markets
|
## Supported Markets
|
||||||
|
|
||||||
Freqtrade supports spot trading only.
|
Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges.
|
||||||
|
|
||||||
### Can my bot open short positions?
|
### Can my bot open short positions?
|
||||||
|
|
||||||
@ -248,8 +248,26 @@ The Edge module is mostly a result of brainstorming of [@mishaker](https://githu
|
|||||||
You can find further info on expectancy, win rate, risk management and position size in the following sources:
|
You can find further info on expectancy, win rate, risk management and position size in the following sources:
|
||||||
|
|
||||||
- https://www.tradeciety.com/ultimate-math-guide-for-traders/
|
- https://www.tradeciety.com/ultimate-math-guide-for-traders/
|
||||||
- http://www.vantharp.com/tharp-concepts/expectancy.asp
|
|
||||||
- https://samuraitradingacademy.com/trading-expectancy/
|
- https://samuraitradingacademy.com/trading-expectancy/
|
||||||
- https://www.learningmarkets.com/determining-expectancy-in-your-trading/
|
- https://www.learningmarkets.com/determining-expectancy-in-your-trading/
|
||||||
- http://www.lonestocktrader.com/make-money-trading-positive-expectancy/
|
- https://www.lonestocktrader.com/make-money-trading-positive-expectancy/
|
||||||
- https://www.babypips.com/trading/trade-expectancy-matter
|
- https://www.babypips.com/trading/trade-expectancy-matter
|
||||||
|
|
||||||
|
## Official channels
|
||||||
|
|
||||||
|
Freqtrade is using exclusively the following official channels:
|
||||||
|
|
||||||
|
* [Freqtrade discord server](https://discord.gg/p7nuUNVfP7)
|
||||||
|
* [Freqtrade documentation (https://freqtrade.io)](https://freqtrade.io)
|
||||||
|
* [Freqtrade github organization](https://github.com/freqtrade)
|
||||||
|
|
||||||
|
Nobody affiliated with the freqtrade project will ask you about your exchange keys or anything else exposing your funds to exploitation.
|
||||||
|
Should you be asked to expose your exchange keys or send funds to some random wallet, then please don't follow these instructions.
|
||||||
|
|
||||||
|
Failing to follow these guidelines will not be responsibility of freqtrade.
|
||||||
|
|
||||||
|
## "Freqtrade token"
|
||||||
|
|
||||||
|
Freqtrade does not have a Crypto token offering.
|
||||||
|
|
||||||
|
Token offerings you find on the internet referring Freqtrade, FreqAI or freqUI must be considered to be a scam, trying to exploit freqtrade's popularity for their own, nefarious gains.
|
||||||
|
@ -9,7 +9,7 @@ FreqAI is configured through the typical [Freqtrade config file](configuration.m
|
|||||||
```json
|
```json
|
||||||
"freqai": {
|
"freqai": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"purge_old_models": true,
|
"purge_old_models": 2,
|
||||||
"train_period_days": 30,
|
"train_period_days": 30,
|
||||||
"backtest_period_days": 7,
|
"backtest_period_days": 7,
|
||||||
"identifier" : "unique-id",
|
"identifier" : "unique-id",
|
||||||
@ -43,116 +43,113 @@ The FreqAI strategy requires including the following lines of code in the standa
|
|||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
# the model will return all labels created by user in `populate_any_indicators`
|
# the model will return all labels created by user in `set_freqai_labels()`
|
||||||
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
# (& appended targets), an indication of whether or not the prediction should be accepted,
|
||||||
# the target mean/std values for each of the labels created by user in
|
# the target mean/std values for each of the labels created by user in
|
||||||
# `populate_any_indicators()` for each training period.
|
# `feature_engineering_*` for each training period.
|
||||||
|
|
||||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_any_indicators(
|
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
|
||||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Function designed to automatically generate, name and merge features
|
*Only functional with FreqAI enabled strategies*
|
||||||
from user indicated timeframes in the configuration file. User controls the indicators
|
This function will automatically expand the defined features on the config defined
|
||||||
passed to the training/prediction by prepending indicators with `'%-' + pair `
|
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
|
||||||
(see convention below). I.e. user should not prepend any supporting metrics
|
`include_corr_pairs`. In other words, a single feature defined in this function
|
||||||
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
|
will automatically expand to a total of
|
||||||
model.
|
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
|
||||||
:param pair: pair to be used as informative
|
`include_corr_pairs` numbers of features added to the model.
|
||||||
:param df: strategy dataframe which will receive merges from informatives
|
|
||||||
:param tf: timeframe of the dataframe which will modify the feature names
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
:param informative: the dataframe associated with the informative pair
|
|
||||||
|
:param df: strategy dataframe which will receive the features
|
||||||
|
:param period: period of the indicator - usage example:
|
||||||
|
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if informative is None:
|
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||||
|
|
||||||
# first loop is automatically duplicating indicators for time periods
|
return dataframe
|
||||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
|
||||||
t = int(t)
|
|
||||||
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
|
||||||
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
|
||||||
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
|
|
||||||
|
|
||||||
indicators = [col for col in informative if col.startswith("%")]
|
def feature_engineering_expand_basic(self, dataframe, **kwargs):
|
||||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
"""
|
||||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
*Only functional with FreqAI enabled strategies*
|
||||||
if n == 0:
|
This function will automatically expand the defined features on the config defined
|
||||||
continue
|
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||||
informative_shift = informative[indicators].shift(n)
|
In other words, a single feature defined in this function
|
||||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
will automatically expand to a total of
|
||||||
informative = pd.concat((informative, informative_shift), axis=1)
|
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
|
||||||
|
numbers of features added to the model.
|
||||||
|
|
||||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
Features defined here will *not* be automatically duplicated on user defined
|
||||||
skip_columns = [
|
`indicator_periods_candles`
|
||||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
|
||||||
]
|
|
||||||
df = df.drop(columns=skip_columns)
|
|
||||||
|
|
||||||
# Add generalized indicators here (because in live, it will call this
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
# function to populate indicators during training). Notice how we ensure not to
|
|
||||||
# add them multiple times
|
|
||||||
if set_generalized_indicators:
|
|
||||||
|
|
||||||
# user adds targets here by prepending them with &- (see convention below)
|
:param df: strategy dataframe which will receive the features
|
||||||
# If user wishes to use multiple targets, a multioutput prediction model
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
# needs to be used such as templates/CatboostPredictionMultiModel.py
|
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
|
||||||
df["&-s_close"] = (
|
"""
|
||||||
df["close"]
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
|
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||||
|
dataframe["%-raw_price"] = dataframe["close"]
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def feature_engineering_standard(self, dataframe, **kwargs):
|
||||||
|
"""
|
||||||
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
This optional function will be called once with the dataframe of the base timeframe.
|
||||||
|
This is the final function to be called, which means that the dataframe entering this
|
||||||
|
function will contain all the features and columns created by all other
|
||||||
|
freqai_feature_engineering_* functions.
|
||||||
|
|
||||||
|
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
|
||||||
|
This function is a good place for any feature that should not be auto-expanded upon
|
||||||
|
(e.g. day of the week).
|
||||||
|
|
||||||
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the features
|
||||||
|
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||||
|
"""
|
||||||
|
dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||||
|
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def set_freqai_targets(self, dataframe, **kwargs):
|
||||||
|
"""
|
||||||
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
Required function to set the targets for the model.
|
||||||
|
All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the targets
|
||||||
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||||
|
"""
|
||||||
|
dataframe["&-s_close"] = (
|
||||||
|
dataframe["close"]
|
||||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||||
.mean()
|
.mean()
|
||||||
/ df["close"]
|
/ dataframe["close"]
|
||||||
- 1
|
- 1
|
||||||
)
|
)
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice how the `populate_any_indicators()` is where [features](freqai-feature-engineering.md#feature-engineering) and labels/targets are added. A full example strategy is available in `templates/FreqaiExampleStrategy.py`.
|
Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`.
|
||||||
|
|
||||||
Notice also the location of the labels under `if set_generalized_indicators:` at the bottom of the example. This is where single features and labels/targets should be added to the feature set to avoid duplication of them from various configuration parameters that multiply the feature set, such as `include_timeframes`.
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The `self.freqai.start()` function cannot be called outside the `populate_indicators()`.
|
The `self.freqai.start()` function cannot be called outside the `populate_indicators()`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Features **must** be defined in `populate_any_indicators()`. Defining FreqAI features in `populate_indicators()`
|
Features **must** be defined in `feature_engineering_*()`. Defining FreqAI features in `populate_indicators()`
|
||||||
will cause the algorithm to fail in live/dry mode. In order to add generalized features that are not associated with a specific pair or timeframe, the following structure inside `populate_any_indicators()` should be used
|
will cause the algorithm to fail in live/dry mode. In order to add generalized features that are not associated with a specific pair or timeframe, you should use `feature_engineering_standard()`
|
||||||
(as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`):
|
(as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`).
|
||||||
|
|
||||||
```python
|
|
||||||
def populate_any_indicators(self, pair, df, tf, informative=None, set_generalized_indicators=False):
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
# Add generalized indicators here (because in live, it will call only this function to populate
|
|
||||||
# indicators for retraining). Notice how we ensure not to add them multiple times by associating
|
|
||||||
# these generalized indicators to the basepair/timeframe
|
|
||||||
if set_generalized_indicators:
|
|
||||||
df['%-day_of_week'] = (df["date"].dt.dayofweek + 1) / 7
|
|
||||||
df['%-hour_of_day'] = (df['date'].dt.hour + 1) / 25
|
|
||||||
|
|
||||||
# user adds targets here by prepending them with &- (see convention below)
|
|
||||||
# If user wishes to use multiple targets, a multioutput prediction model
|
|
||||||
# needs to be used such as templates/CatboostPredictionMultiModel.py
|
|
||||||
df["&-s_close"] = (
|
|
||||||
df["close"]
|
|
||||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
|
||||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
|
||||||
.mean()
|
|
||||||
/ df["close"]
|
|
||||||
- 1
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Please see the example script located in `freqtrade/templates/FreqaiExampleStrategy.py` for a full example of `populate_any_indicators()`.
|
|
||||||
|
|
||||||
## Important dataframe key patterns
|
## Important dataframe key patterns
|
||||||
|
|
||||||
@ -160,18 +157,18 @@ Below are the values you can expect to include/use inside a typical strategy dat
|
|||||||
|
|
||||||
| DataFrame Key | Description |
|
| DataFrame Key | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| `df['&*']` | Any dataframe column prepended with `&` in `populate_any_indicators()` is treated as a training target (label) inside FreqAI (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. FreqAI makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`. <br> **Datatype:** Depends on the output of the model.
|
| `df['&*']` | Any dataframe column prepended with `&` in `set_freqai_targets()` is treated as a training target (label) inside FreqAI (typically following the naming convention `&-s*`). For example, to predict the close price 40 candles into the future, you would set `df['&-s_close'] = df['close'].shift(-self.freqai_info["feature_parameters"]["label_period_candles"])` with `"label_period_candles": 40` in the config. FreqAI makes the predictions and gives them back under the same key (`df['&-s_close']`) to be used in `populate_entry/exit_trend()`. <br> **Datatype:** Depends on the output of the model.
|
||||||
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
| `df['&*_std/mean']` | Standard deviation and mean values of the defined labels during training (or live tracking with `fit_live_predictions_candles`). Commonly used to understand the rarity of a prediction (use the z-score as shown in `templates/FreqaiExampleStrategy.py` and explained [here](#creating-a-dynamic-target-threshold) to evaluate how often a particular prediction was observed during training or historically with `fit_live_predictions_candles`). <br> **Datatype:** Float.
|
||||||
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
| `df['do_predict']` | Indication of an outlier data point. The return value is integer between -2 and 2, which lets you know if the prediction is trustworthy or not. `do_predict==1` means that the prediction is trustworthy. If the Dissimilarity Index (DI, see details [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di)) of the input data point is above the threshold defined in the config, FreqAI will subtract 1 from `do_predict`, resulting in `do_predict==0`. If `use_SVM_to_remove_outliers()` is active, the Support Vector Machine (SVM, see details [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm)) may also detect outliers in training and prediction data. In this case, the SVM will also subtract 1 from `do_predict`. If the input data point was considered an outlier by the SVM but not by the DI, or vice versa, the result will be `do_predict==0`. If both the DI and the SVM considers the input data point to be an outlier, the result will be `do_predict==-1`. As with the SVM, if `use_DBSCAN_to_remove_outliers` is active, DBSCAN (see details [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan)) may also detect outliers and subtract 1 from `do_predict`. Hence, if both the SVM and DBSCAN are active and identify a datapoint that was above the DI threshold as an outlier, the result will be `do_predict==-2`. A particular case is when `do_predict == 2`, which means that the model has expired due to exceeding `expired_hours`. <br> **Datatype:** Integer between -2 and 2.
|
||||||
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
| `df['DI_values']` | Dissimilarity Index (DI) values are proxies for the level of confidence FreqAI has in the prediction. A lower DI means the prediction is close to the training data, i.e., higher prediction confidence. See details about the DI [here](freqai-feature-engineering.md#identifying-outliers-with-the-dissimilarity-index-di). <br> **Datatype:** Float.
|
||||||
| `df['%*']` | Any dataframe column prepended with `%` in `populate_any_indicators()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`. <br> **Datatype:** Depends on the output of the model.
|
| `df['%*']` | Any dataframe column prepended with `%` in `feature_engineering_*()` is treated as a training feature. For example, you can include the RSI in the training feature set (similar to in `templates/FreqaiExampleStrategy.py`) by setting `df['%-rsi']`. See more details on how this is done [here](freqai-feature-engineering.md). <br> **Note:** Since the number of features prepended with `%` can multiply very quickly (10s of thousands of features are easily engineered using the multiplictative functionality of, e.g., `include_shifted_candles` and `include_timeframes` as described in the [parameter table](freqai-parameter-table.md)), these features are removed from the dataframe that is returned from FreqAI to the strategy. To keep a particular type of feature for plotting purposes, you would prepend it with `%%`. <br> **Datatype:** Depends on the output of the model.
|
||||||
|
|
||||||
## Setting the `startup_candle_count`
|
## Setting the `startup_candle_count`
|
||||||
|
|
||||||
The `startup_candle_count` in the FreqAI strategy needs to be set up in the same way as in the standard Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., Ta-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`.
|
The `startup_candle_count` in the FreqAI strategy needs to be set up in the same way as in the standard Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., TA-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
There are instances where the Ta-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean:
|
There are instances where the TA-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean:
|
||||||
|
|
||||||
```
|
```
|
||||||
2022-08-31 15:14:04 - freqtrade.freqai.data_kitchen - INFO - dropped 0 training points due to NaNs in populated dataset 4319.
|
2022-08-31 15:14:04 - freqtrade.freqai.data_kitchen - INFO - dropped 0 training points due to NaNs in populated dataset 4319.
|
||||||
@ -208,7 +205,7 @@ All of the aforementioned model libraries implement gradient boosted decision tr
|
|||||||
* LightGBM: https://lightgbm.readthedocs.io/en/v3.3.2/#
|
* LightGBM: https://lightgbm.readthedocs.io/en/v3.3.2/#
|
||||||
* XGBoost: https://xgboost.readthedocs.io/en/stable/#
|
* XGBoost: https://xgboost.readthedocs.io/en/stable/#
|
||||||
|
|
||||||
There are also numerous online articles describing and comparing the algorithms. Some relatively light-weight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model.
|
There are also numerous online articles describing and comparing the algorithms. Some relatively lightweight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model.
|
||||||
|
|
||||||
Apart from the models already available in FreqAI, it is also possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to customize various aspects of the training procedures. You can place custom FreqAI models in `user_data/freqaimodels` - and freqtrade will pick them up from there based on the provided `--freqaimodel` name - which has to correspond to the class name of your custom model.
|
Apart from the models already available in FreqAI, it is also possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to customize various aspects of the training procedures. You can place custom FreqAI models in `user_data/freqaimodels` - and freqtrade will pick them up from there based on the provided `--freqaimodel` name - which has to correspond to the class name of your custom model.
|
||||||
Make sure to use unique names to avoid overriding built-in models.
|
Make sure to use unique names to avoid overriding built-in models.
|
||||||
@ -240,19 +237,160 @@ df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'do
|
|||||||
df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down'])
|
df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down'])
|
||||||
```
|
```
|
||||||
|
|
||||||
### Convolutional Neural Network model
|
## PyTorch Module
|
||||||
|
|
||||||
The `CNNPredictionModel` is a non-linear regression based on `Tensorflow` which follows very similar configuration to the other regressors. Feature engineering and label creation remains the same as highlighted [here](#building-a-freqai-strategy) and [here](#setting-model-targets). Control of the model is focused in the `model_training_parameters` configuration dictionary, which accepts any hyperparameter available to the CNN `fit()` function of Tensorflow [more here](https://www.tensorflow.org/api_docs/python/tf/keras/Model#fit). For example, this is where the `epochs` and `batch_size` are controlled:
|
### Quick start
|
||||||
|
|
||||||
```json
|
The easiest way to quickly run a pytorch model is with the following command (for regression task):
|
||||||
"model_training_parameters" : {
|
|
||||||
"batch_size": 64,
|
```bash
|
||||||
"epochs": 10,
|
freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates
|
||||||
"verbose": "auto",
|
|
||||||
"shuffle": false,
|
|
||||||
"workers": 1,
|
|
||||||
"use_multiprocessing": false
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Running the `CNNPredictionModel` is the same as other regressors: `--freqaimodel CNNPredictionModel`.
|
!!! note "Installation/docker"
|
||||||
|
The PyTorch module requires large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]?".
|
||||||
|
Users who prefer docker should ensure they use the docker image appended with `_freqaitorch`.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
#### Model
|
||||||
|
|
||||||
|
You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks.
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
class LogisticRegression(nn.Module):
|
||||||
|
def __init__(self, input_size: int):
|
||||||
|
super().__init__()
|
||||||
|
# Define your layers
|
||||||
|
self.linear = nn.Linear(input_size, 1)
|
||||||
|
self.activation = nn.Sigmoid()
|
||||||
|
|
||||||
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||||
|
# Define the forward pass
|
||||||
|
out = self.linear(x)
|
||||||
|
out = self.activation(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
class MyCoolPyTorchClassifier(BasePyTorchClassifier):
|
||||||
|
"""
|
||||||
|
This is a custom IFreqaiModel showing how a user might setup their own
|
||||||
|
custom Neural Network architecture for their training.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_convertor(self) -> PyTorchDataConvertor:
|
||||||
|
return DefaultPyTorchDataConvertor(target_tensor_type=torch.float)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
config = self.freqai_info.get("model_training_parameters", {})
|
||||||
|
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||||
|
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||||
|
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||||
|
|
||||||
|
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
|
"""
|
||||||
|
User sets up the training and test data to fit their desired model here
|
||||||
|
:param data_dictionary: the dictionary holding all data for train, test,
|
||||||
|
labels, weights
|
||||||
|
:param dk: The datakitchen object for the current coin/model
|
||||||
|
"""
|
||||||
|
|
||||||
|
class_names = self.get_class_names()
|
||||||
|
self.convert_label_column_to_int(data_dictionary, dk, class_names)
|
||||||
|
n_features = data_dictionary["train_features"].shape[-1]
|
||||||
|
model = LogisticRegression(
|
||||||
|
input_dim=n_features
|
||||||
|
)
|
||||||
|
model.to(self.device)
|
||||||
|
optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate)
|
||||||
|
criterion = torch.nn.CrossEntropyLoss()
|
||||||
|
init_model = self.get_init_model(dk.pair)
|
||||||
|
trainer = PyTorchModelTrainer(
|
||||||
|
model=model,
|
||||||
|
optimizer=optimizer,
|
||||||
|
criterion=criterion,
|
||||||
|
model_meta_data={"class_names": class_names},
|
||||||
|
device=self.device,
|
||||||
|
init_model=init_model,
|
||||||
|
data_convertor=self.data_convertor,
|
||||||
|
**self.trainer_kwargs,
|
||||||
|
)
|
||||||
|
trainer.fit(data_dictionary, self.splits)
|
||||||
|
return trainer
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Trainer
|
||||||
|
|
||||||
|
The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop:
|
||||||
|
Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer.
|
||||||
|
|
||||||
|
In addition, the trainer is responsible for the following:
|
||||||
|
- saving and loading the model
|
||||||
|
- converting the data from `pandas.DataFrame` to `torch.Tensor`.
|
||||||
|
|
||||||
|
#### Integration with Freqai module
|
||||||
|
|
||||||
|
Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy.
|
||||||
|
From top to bottom:
|
||||||
|
|
||||||
|
1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device` attribute used by children classes. Sets `model_type` attribute used by the parent class.
|
||||||
|
2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed.
|
||||||
|
3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects.
|
||||||
|
|
||||||
|
![image](assets/freqai_pytorch-diagram.png)
|
||||||
|
|
||||||
|
#### Full example
|
||||||
|
|
||||||
|
Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PyTorchMLPRegressor(BasePyTorchRegressor):
|
||||||
|
def __init__(self, **kwargs) -> None:
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
config = self.freqai_info.get("model_training_parameters", {})
|
||||||
|
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||||
|
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||||
|
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||||
|
|
||||||
|
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||||
|
n_features = data_dictionary["train_features"].shape[-1]
|
||||||
|
model = PyTorchMLPModel(
|
||||||
|
input_dim=n_features,
|
||||||
|
output_dim=1,
|
||||||
|
**self.model_kwargs
|
||||||
|
)
|
||||||
|
model.to(self.device)
|
||||||
|
optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate)
|
||||||
|
criterion = torch.nn.MSELoss()
|
||||||
|
init_model = self.get_init_model(dk.pair)
|
||||||
|
trainer = PyTorchModelTrainer(
|
||||||
|
model=model,
|
||||||
|
optimizer=optimizer,
|
||||||
|
criterion=criterion,
|
||||||
|
device=self.device,
|
||||||
|
init_model=init_model,
|
||||||
|
target_tensor_type=torch.float,
|
||||||
|
**self.trainer_kwargs,
|
||||||
|
)
|
||||||
|
trainer.fit(data_dictionary)
|
||||||
|
return trainer
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel`, where the former implements the `predict` method that is suitable for our regression task, and the latter implements the train method.
|
||||||
|
|
||||||
|
??? Note "Setting Class Names for Classifiers"
|
||||||
|
When using classifiers, the user must declare the class names (or targets) by overriding the `IFreqaiModel.class_names` attribute. This is achieved by setting `self.freqai.class_names` in the FreqAI strategy inside the `set_freqai_targets` method.
|
||||||
|
|
||||||
|
For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows:
|
||||||
|
```python
|
||||||
|
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
|
||||||
|
self.freqai.class_names = ["down", "up"]
|
||||||
|
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
|
||||||
|
dataframe["close"], 'up', 'down')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py).
|
||||||
|
@ -2,96 +2,150 @@
|
|||||||
|
|
||||||
## Defining the features
|
## Defining the features
|
||||||
|
|
||||||
Low level feature engineering is performed in the user strategy within a function called `populate_any_indicators()`. That function sets the `base features` such as, `RSI`, `MFI`, `EMA`, `SMA`, time of day, volume, etc. The `base features` can be custom indicators or they can be imported from any technical-analysis library that you can find. One important syntax rule is that all `base features` string names are prepended with `%-{pair}`, while labels/targets are prepended with `&`.
|
Low level feature engineering is performed in the user strategy within a set of functions called `feature_engineering_*`. These function set the `base features` such as, `RSI`, `MFI`, `EMA`, `SMA`, time of day, volume, etc. The `base features` can be custom indicators or they can be imported from any technical-analysis library that you can find. FreqAI is equipped with a set of functions to simplify rapid large-scale feature engineering:
|
||||||
|
|
||||||
!!! Note
|
| Function | Description |
|
||||||
Adding the full pair string, e.g. XYZ/USD, in the feature name enables improved performance for dataframe caching on the backend. If you decide *not* to add the full pair string in the feature string, FreqAI will operate in a reduced performance mode.
|
|---------------|-------------|
|
||||||
|
| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||||
|
| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`.
|
||||||
|
| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week).
|
||||||
|
| `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||||
|
|
||||||
Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the FreqAI config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles."
|
Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the FreqAI config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles."
|
||||||
|
|
||||||
It is advisable to start from the template `populate_any_indicators()` in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy:
|
It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_any_indicators(
|
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
||||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Function designed to automatically generate, name, and merge features
|
*Only functional with FreqAI enabled strategies*
|
||||||
from user-indicated timeframes in the configuration file. The user controls the indicators
|
This function will automatically expand the defined features on the config defined
|
||||||
passed to the training/prediction by prepending indicators with `'%-' + pair `
|
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
|
||||||
(see convention below). I.e., the user should not prepend any supporting metrics
|
`include_corr_pairs`. In other words, a single feature defined in this function
|
||||||
(e.g., bb_lowerband below) with % unless they explicitly want to pass that metric to the
|
will automatically expand to a total of
|
||||||
model.
|
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
|
||||||
:param pair: pair to be used as informative
|
`include_corr_pairs` numbers of features added to the model.
|
||||||
:param df: strategy dataframe which will receive merges from informatives
|
|
||||||
:param tf: timeframe of the dataframe which will modify the feature names
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
:param informative: the dataframe associated with the informative pair
|
|
||||||
|
Access metadata such as the current pair/timeframe/period with:
|
||||||
|
|
||||||
|
`metadata["pair"]` `metadata["tf"]` `metadata["period"]`
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the features
|
||||||
|
:param period: period of the indicator - usage example:
|
||||||
|
:param metadata: metadata of current pair
|
||||||
|
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if informative is None:
|
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||||
# first loop is automatically duplicating indicators for time periods
|
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
|
||||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||||
t = int(t)
|
|
||||||
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
|
||||||
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
|
||||||
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
|
|
||||||
|
|
||||||
bollinger = qtpylib.bollinger_bands(
|
bollinger = qtpylib.bollinger_bands(
|
||||||
qtpylib.typical_price(informative), window=t, stds=2.2
|
qtpylib.typical_price(dataframe), window=period, stds=2.2
|
||||||
)
|
)
|
||||||
informative[f"{pair}bb_lowerband-period_{t}"] = bollinger["lower"]
|
dataframe["bb_lowerband-period"] = bollinger["lower"]
|
||||||
informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"]
|
dataframe["bb_middleband-period"] = bollinger["mid"]
|
||||||
informative[f"{pair}bb_upperband-period_{t}"] = bollinger["upper"]
|
dataframe["bb_upperband-period"] = bollinger["upper"]
|
||||||
|
|
||||||
informative[f"%-{pair}bb_width-period_{t}"] = (
|
dataframe["%-bb_width-period"] = (
|
||||||
informative[f"{pair}bb_upperband-period_{t}"]
|
dataframe["bb_upperband-period"]
|
||||||
- informative[f"{pair}bb_lowerband-period_{t}"]
|
- dataframe["bb_lowerband-period"]
|
||||||
) / informative[f"{pair}bb_middleband-period_{t}"]
|
) / dataframe["bb_middleband-period"]
|
||||||
informative[f"%-{pair}close-bb_lower-period_{t}"] = (
|
dataframe["%-close-bb_lower-period"] = (
|
||||||
informative["close"] / informative[f"{pair}bb_lowerband-period_{t}"]
|
dataframe["close"] / dataframe["bb_lowerband-period"]
|
||||||
)
|
)
|
||||||
|
|
||||||
informative[f"%-{pair}relative_volume-period_{t}"] = (
|
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||||
informative["volume"] / informative["volume"].rolling(t).mean()
|
|
||||||
|
dataframe["%-relative_volume-period"] = (
|
||||||
|
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
|
||||||
)
|
)
|
||||||
|
|
||||||
indicators = [col for col in informative if col.startswith("%")]
|
return dataframe
|
||||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
|
||||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
|
||||||
if n == 0:
|
|
||||||
continue
|
|
||||||
informative_shift = informative[indicators].shift(n)
|
|
||||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
|
||||||
informative = pd.concat((informative, informative_shift), axis=1)
|
|
||||||
|
|
||||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs):
|
||||||
skip_columns = [
|
"""
|
||||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
*Only functional with FreqAI enabled strategies*
|
||||||
]
|
This function will automatically expand the defined features on the config defined
|
||||||
df = df.drop(columns=skip_columns)
|
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||||
|
In other words, a single feature defined in this function
|
||||||
|
will automatically expand to a total of
|
||||||
|
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
|
||||||
|
numbers of features added to the model.
|
||||||
|
|
||||||
# Add generalized indicators here (because in live, it will call this
|
Features defined here will *not* be automatically duplicated on user defined
|
||||||
# function to populate indicators during training). Notice how we ensure not to
|
`indicator_periods_candles`
|
||||||
# add them multiple times
|
|
||||||
if set_generalized_indicators:
|
|
||||||
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
|
||||||
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
|
||||||
|
|
||||||
# user adds targets here by prepending them with &- (see convention below)
|
Access metadata such as the current pair/timeframe with:
|
||||||
# If user wishes to use multiple targets, a multioutput prediction model
|
|
||||||
# needs to be used such as templates/CatboostPredictionMultiModel.py
|
`metadata["pair"]` `metadata["tf"]`
|
||||||
df["&-s_close"] = (
|
|
||||||
df["close"]
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the features
|
||||||
|
:param metadata: metadata of current pair
|
||||||
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
|
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
|
||||||
|
"""
|
||||||
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
|
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||||
|
dataframe["%-raw_price"] = dataframe["close"]
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def feature_engineering_standard(self, dataframe, metadata, **kwargs):
|
||||||
|
"""
|
||||||
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
This optional function will be called once with the dataframe of the base timeframe.
|
||||||
|
This is the final function to be called, which means that the dataframe entering this
|
||||||
|
function will contain all the features and columns created by all other
|
||||||
|
freqai_feature_engineering_* functions.
|
||||||
|
|
||||||
|
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
|
||||||
|
This function is a good place for any feature that should not be auto-expanded upon
|
||||||
|
(e.g. day of the week).
|
||||||
|
|
||||||
|
Access metadata such as the current pair with:
|
||||||
|
|
||||||
|
`metadata["pair"]`
|
||||||
|
|
||||||
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the features
|
||||||
|
:param metadata: metadata of current pair
|
||||||
|
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||||
|
"""
|
||||||
|
dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||||
|
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def set_freqai_targets(self, dataframe, metadata, **kwargs):
|
||||||
|
"""
|
||||||
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
Required function to set the targets for the model.
|
||||||
|
All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||||
|
|
||||||
|
Access metadata such as the current pair with:
|
||||||
|
|
||||||
|
`metadata["pair"]`
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the targets
|
||||||
|
:param metadata: metadata of current pair
|
||||||
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||||
|
"""
|
||||||
|
dataframe["&-s_close"] = (
|
||||||
|
dataframe["close"]
|
||||||
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||||
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||||
.mean()
|
.mean()
|
||||||
/ df["close"]
|
/ dataframe["close"]
|
||||||
- 1
|
- 1
|
||||||
)
|
)
|
||||||
|
|
||||||
return df
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
In the presented example, the user does not wish to pass the `bb_lowerband` as a feature to the model,
|
In the presented example, the user does not wish to pass the `bb_lowerband` as a feature to the model,
|
||||||
@ -118,15 +172,28 @@ After having defined the `base features`, the next step is to expand upon them u
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `include_timeframes` in the config above are the timeframes (`tf`) of each call to `populate_any_indicators()` in the strategy. In the presented case, the user is asking for the `5m`, `15m`, and `4h` timeframes of the `rsi`, `mfi`, `roc`, and `bb_width` to be included in the feature set.
|
The `include_timeframes` in the config above are the timeframes (`tf`) of each call to `feature_engineering_expand_*()` in the strategy. In the presented case, the user is asking for the `5m`, `15m`, and `4h` timeframes of the `rsi`, `mfi`, `roc`, and `bb_width` to be included in the feature set.
|
||||||
|
|
||||||
You can ask for each of the defined features to be included also for informative pairs using the `include_corr_pairlist`. This means that the feature set will include all the features from `populate_any_indicators` on all the `include_timeframes` for each of the correlated pairs defined in the config (`ETH/USD`, `LINK/USD`, and `BNB/USD` in the presented example).
|
You can ask for each of the defined features to be included also for informative pairs using the `include_corr_pairlist`. This means that the feature set will include all the features from `feature_engineering_expand_*()` on all the `include_timeframes` for each of the correlated pairs defined in the config (`ETH/USD`, `LINK/USD`, and `BNB/USD` in the presented example).
|
||||||
|
|
||||||
`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells FreqAI to include the past 2 candles for each of the features in the feature set.
|
`include_shifted_candles` indicates the number of previous candles to include in the feature set. For example, `include_shifted_candles: 2` tells FreqAI to include the past 2 candles for each of the features in the feature set.
|
||||||
|
|
||||||
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `populate_any_indicators()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles`
|
||||||
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
$= 3 * 3 * 3 * 2 * 2 = 108$.
|
||||||
|
|
||||||
|
|
||||||
|
### Gain finer control over `feature_engineering_*` functions with `metadata`
|
||||||
|
|
||||||
|
All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs):
|
||||||
|
if metadata["tf"] == "1h":
|
||||||
|
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will block `ta.ROC()` from being added to any timeframes other than `"1h"`.
|
||||||
|
|
||||||
### Returning additional info from training
|
### Returning additional info from training
|
||||||
|
|
||||||
Important metrics can be returned to the strategy at the end of each model training by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside the custom prediction model class.
|
Important metrics can be returned to the strategy at the end of each model training by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside the custom prediction model class.
|
||||||
@ -167,7 +234,7 @@ This will perform PCA on the features and reduce their dimensionality so that th
|
|||||||
|
|
||||||
## Inlier metric
|
## Inlier metric
|
||||||
|
|
||||||
The `inlier_metric` is a metric aimed at quantifying how similar a the features of a data point are to the most recent historic data points.
|
The `inlier_metric` is a metric aimed at quantifying how similar the features of a data point are to the most recent historical data points.
|
||||||
|
|
||||||
You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5.
|
You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5.
|
||||||
|
|
||||||
|
@ -15,10 +15,9 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `identifier` | **Required.** <br> A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data. <br> **Datatype:** String.
|
| `identifier` | **Required.** <br> A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data. <br> **Datatype:** String.
|
||||||
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> **Datatype:** Float > 0. <br> Default: `0` (models retrain as often as possible).
|
| `live_retrain_hours` | Frequency of retraining during dry/live runs. <br> **Datatype:** Float > 0. <br> Default: `0` (models retrain as often as possible).
|
||||||
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: `0` (models never expire).
|
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: `0` (models never expire).
|
||||||
| `purge_old_models` | Delete obsolete models. <br> **Datatype:** Boolean. <br> Default: `False` (all historic models remain on disk).
|
| `purge_old_models` | Number of models to keep on disk (not relevant to backtesting). Default is 2, which means that dry/live runs will keep the latest 2 models on disk. Setting to 0 keeps all models. This parameter also accepts a boolean to maintain backwards compatibility. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||||
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
|
||||||
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
|
||||||
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
|
|
||||||
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
|
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
|
||||||
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
|
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
|
||||||
@ -29,12 +28,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| | **Feature parameters within the `freqai.feature_parameters` sub dictionary**
|
| | **Feature parameters within the `freqai.feature_parameters` sub dictionary**
|
||||||
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
|
| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md). <br> **Datatype:** Dictionary.
|
||||||
| `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
|
| `include_timeframes` | A list of timeframes that all indicators in `feature_engineering_expand_*()` will be created for. The list is added as features to the base indicators dataset. <br> **Datatype:** List of timeframes (strings).
|
||||||
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings).
|
| `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `feature_engineering_expand_*()` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset. <br> **Datatype:** List of assets (strings).
|
||||||
| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators` (see `templates/FreqaiExampleStrategy.py` for detailed usage). You can create custom labels and choose whether to make use of this parameter or not. <br> **Datatype:** Positive integer.
|
| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `feature_engineering_expand_all()` (see `templates/FreqaiExampleStrategy.py` for detailed usage). You can create custom labels and choose whether to make use of this parameter or not. <br> **Datatype:** Positive integer.
|
||||||
| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
|
| `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle. <br> **Datatype:** Positive integer.
|
||||||
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
|
| `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)). <br> **Datatype:** Positive float (typically < 1).
|
||||||
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer.
|
| `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `feature_engineering_*()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN. <br> **Datatype:** Positive integer.
|
||||||
| `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers.
|
| `indicator_periods_candles` | Time periods to calculate indicators for. The indicators are added to the base indicator dataset. <br> **Datatype:** List of positive integers.
|
||||||
| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis) <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `principal_component_analysis` | Automatically reduce the dimensionality of the data set using Principal Component Analysis. See details about how it works [here](#reducing-data-dimensionality-with-principal-component-analysis) <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`.
|
| `plot_feature_importances` | Create a feature importance plot for each model for the top/bottom `plot_feature_importances` number of features. Plot is stored in `user_data/models/<identifier>/sub-train-<COIN>_<timestamp>.html`. <br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
@ -46,13 +45,15 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
|
| `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation). <br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset. <br> **Datatype:** Float. <br> Default: `30`.
|
||||||
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
|
||||||
|
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
|
| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.<br> **Datatype:** Integer. <br> Default: `0`.
|
||||||
|
|
||||||
### Data split parameters
|
### Data split parameters
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| | **Data split parameters within the `freqai.data_split_parameters` sub dictionary**
|
| | **Data split parameters within the `freqai.data_split_parameters` sub dictionary**
|
||||||
| `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
|
| `data_split_parameters` | Include any additional parameters available from scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). <br> **Datatype:** Dictionary.
|
||||||
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
|
| `test_size` | The fraction of data that should be used for testing instead of training. <br> **Datatype:** Positive float < 1.
|
||||||
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`.
|
| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`. <br> **Datatype:** Boolean. <br> Defaut: `False`.
|
||||||
|
|
||||||
@ -83,12 +84,34 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
| `add_state_info` | Tell FreqAI to include state information in the feature set for training and inferencing. The current state variables include trade duration, current profit, trade position. This is only available in dry/live runs, and is automatically switched to false for backtesting. <br> **Datatype:** bool. <br> Default: `False`.
|
| `add_state_info` | Tell FreqAI to include state information in the feature set for training and inferencing. The current state variables include trade duration, current profit, trade position. This is only available in dry/live runs, and is automatically switched to false for backtesting. <br> **Datatype:** bool. <br> Default: `False`.
|
||||||
| `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[<shared layers>, dict(vf=[<non-shared value network layers>], pi=[<non-shared policy network layers>])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each.
|
| `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[<shared layers>, dict(vf=[<non-shared value network layers>], pi=[<non-shared policy network layers>])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each.
|
||||||
| `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting. <br> **Datatype:** bool. <br> Default: `False`.
|
| `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting. <br> **Datatype:** bool. <br> Default: `False`.
|
||||||
|
| `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases) <br> **Datatype:** Boolean. <br> **Default:** `False`
|
||||||
|
|
||||||
|
### PyTorch parameters
|
||||||
|
|
||||||
|
#### general
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| | **Model training parameters within the `freqai.model_training_parameters` sub dictionary**
|
||||||
|
| `learning_rate` | Learning rate to be passed to the optimizer. <br> **Datatype:** float. <br> Default: `3e-4`.
|
||||||
|
| `model_kwargs` | Parameters to be passed to the model class. <br> **Datatype:** dict. <br> Default: `{}`.
|
||||||
|
| `trainer_kwargs` | Parameters to be passed to the trainer class. <br> **Datatype:** dict. <br> Default: `{}`.
|
||||||
|
|
||||||
|
#### trainer_kwargs
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary**
|
||||||
|
| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. <br> **Datatype:** int. <br> Default: `100`.
|
||||||
|
| `batch_size` | The size of the batches to use during training.. <br> **Datatype:** int. <br> Default: `64`.
|
||||||
|
| `max_n_eval_batches` | The maximum number batches to use for evaluation.. <br> **Datatype:** int, optional. <br> Default: `None`.
|
||||||
|
|
||||||
|
|
||||||
### Additional parameters
|
### Additional parameters
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
| | **Extraneous parameters**
|
| | **Extraneous parameters**
|
||||||
| `freqai.keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag should be activated so that the model save/loading follows Keras standards. If the the provided `CNNPredictionModel` is used, then this is handled automatically. <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards. <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
| `freqai.conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
| `freqai.conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. <br> **Datatype:** Integer. <br> Default: `2`.
|
||||||
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
| `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI). <br> **Datatype:** Boolean. <br> Default: `False`.
|
||||||
|
@ -24,7 +24,7 @@ The framework is built on stable_baselines3 (torch) and OpenAI gym for the base
|
|||||||
|
|
||||||
### Important considerations
|
### Important considerations
|
||||||
|
|
||||||
As explained above, the agent is "trained" in an artificial trading "environment". In our case, that environment may seem quite similar to a real Freqtrade backtesting environment, but it is *NOT*. In fact, the RL training environment is much more simplified. It does not incorporate any of the complicated strategy logic, such as callbacks like `custom_exit`, `custom_stoploss`, leverage controls, etc. The RL environment is instead a very "raw" representation of the true market, where the agent has free-will to learn the policy (read: stoploss, take profit, etc.) which is enforced by the `calculate_reward()`. Thus, it is important to consider that the agent training environment is not identical to the real world.
|
As explained above, the agent is "trained" in an artificial trading "environment". In our case, that environment may seem quite similar to a real Freqtrade backtesting environment, but it is *NOT*. In fact, the RL training environment is much more simplified. It does not incorporate any of the complicated strategy logic, such as callbacks like `custom_exit`, `custom_stoploss`, leverage controls, etc. The RL environment is instead a very "raw" representation of the true market, where the agent has free will to learn the policy (read: stoploss, take profit, etc.) which is enforced by the `calculate_reward()`. Thus, it is important to consider that the agent training environment is not identical to the real world.
|
||||||
|
|
||||||
## Running Reinforcement Learning
|
## Running Reinforcement Learning
|
||||||
|
|
||||||
@ -34,65 +34,36 @@ Setting up and running a Reinforcement Learning model is the same as running a R
|
|||||||
freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json
|
freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `populate_any_indicators` as a typical Regressor:
|
where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_any_indicators(
|
def set_freqai_targets(self, dataframe, **kwargs):
|
||||||
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
"""
|
||||||
):
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
Required function to set the targets for the model.
|
||||||
|
All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||||
|
|
||||||
if informative is None:
|
More details about feature engineering available:
|
||||||
informative = self.dp.get_pair_dataframe(pair, tf)
|
|
||||||
|
|
||||||
# first loop is automatically duplicating indicators for time periods
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
||||||
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
|
||||||
|
|
||||||
t = int(t)
|
|
||||||
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
|
||||||
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
|
||||||
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, window=t)
|
|
||||||
|
|
||||||
# The following raw price values are necessary for RL models
|
|
||||||
informative[f"%-{pair}raw_close"] = informative["close"]
|
|
||||||
informative[f"%-{pair}raw_open"] = informative["open"]
|
|
||||||
informative[f"%-{pair}raw_high"] = informative["high"]
|
|
||||||
informative[f"%-{pair}raw_low"] = informative["low"]
|
|
||||||
|
|
||||||
indicators = [col for col in informative if col.startswith("%")]
|
|
||||||
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
|
||||||
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
|
||||||
if n == 0:
|
|
||||||
continue
|
|
||||||
informative_shift = informative[indicators].shift(n)
|
|
||||||
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
|
||||||
informative = pd.concat((informative, informative_shift), axis=1)
|
|
||||||
|
|
||||||
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
|
||||||
skip_columns = [
|
|
||||||
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
|
||||||
]
|
|
||||||
df = df.drop(columns=skip_columns)
|
|
||||||
|
|
||||||
# Add generalized indicators here (because in live, it will call this
|
|
||||||
# function to populate indicators during training). Notice how we ensure not to
|
|
||||||
# add them multiple times
|
|
||||||
if set_generalized_indicators:
|
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the targets
|
||||||
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||||
|
"""
|
||||||
# For RL, there are no direct targets to set. This is filler (neutral)
|
# For RL, there are no direct targets to set. This is filler (neutral)
|
||||||
# until the agent sends an action.
|
# until the agent sends an action.
|
||||||
df["&-action"] = 0
|
dataframe["&-action"] = 0
|
||||||
|
|
||||||
return df
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
def feature_engineering_standard(self, dataframe, **kwargs):
|
||||||
# The following features are necessary for RL models
|
# The following features are necessary for RL models
|
||||||
informative[f"%-{pair}raw_close"] = informative["close"]
|
dataframe[f"%-raw_close"] = dataframe["close"]
|
||||||
informative[f"%-{pair}raw_open"] = informative["open"]
|
dataframe[f"%-raw_open"] = dataframe["open"]
|
||||||
informative[f"%-{pair}raw_high"] = informative["high"]
|
dataframe[f"%-raw_high"] = dataframe["high"]
|
||||||
informative[f"%-{pair}raw_low"] = informative["low"]
|
dataframe[f"%-raw_low"] = dataframe["low"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action.
|
||||||
@ -204,10 +175,23 @@ As you begin to modify the strategy and the prediction model, you will quickly r
|
|||||||
pnl = self.get_unrealized_profit()
|
pnl = self.get_unrealized_profit()
|
||||||
|
|
||||||
factor = 100
|
factor = 100
|
||||||
|
|
||||||
|
pair = self.pair.replace(':', '')
|
||||||
|
|
||||||
|
# you can use feature values from dataframe
|
||||||
|
# Assumes the shifted RSI indicator has been generated in the strategy.
|
||||||
|
rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_"
|
||||||
|
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
||||||
|
|
||||||
# reward agent for entering trades
|
# reward agent for entering trades
|
||||||
if action in (Actions.Long_enter.value, Actions.Short_enter.value) \
|
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
|
||||||
and self._position == Positions.Neutral:
|
and self._position == Positions.Neutral):
|
||||||
return 25
|
if rsi_now < 40:
|
||||||
|
factor = 40 / rsi_now
|
||||||
|
else:
|
||||||
|
factor = 1
|
||||||
|
return 25 * factor
|
||||||
|
|
||||||
# discourage agent from not entering trades
|
# discourage agent from not entering trades
|
||||||
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
if action == Actions.Neutral.value and self._position == Positions.Neutral:
|
||||||
return -1
|
return -1
|
||||||
@ -264,23 +248,22 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard
|
|||||||
"""
|
"""
|
||||||
def calculate_reward(self, action: int) -> float:
|
def calculate_reward(self, action: int) -> float:
|
||||||
if not self._is_valid(action):
|
if not self._is_valid(action):
|
||||||
self.tensorboard_log("is_valid")
|
self.tensorboard_log("invalid")
|
||||||
return -2
|
return -2
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter.
|
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)`. In this case the metric values are not incremented.
|
||||||
|
|
||||||
|
|
||||||
### Choosing a base environment
|
### Choosing a base environment
|
||||||
|
|
||||||
FreqAI provides two base environments, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 4 or 5 actions. In the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Meanwhile, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
|
FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include:
|
||||||
|
|
||||||
* the actions available in the `calculate_reward`
|
* the actions available in the `calculate_reward`
|
||||||
* the actions consumed by the user strategy
|
* the actions consumed by the user strategy
|
||||||
|
|
||||||
Both of the FreqAI provided environments inherit from an action/position agnostic environment object called the `BaseEnvironment`, which contains all shared logic. The architecture is designed to be easily customized. The simplest customization is the `calculate_reward()` (see details [here](#creating-a-custom-reward-function)). However, the customizations can be further extended into any of the functions inside the environment. You can do this by simply overriding those functions inside your `MyRLEnv` in the prediction model file. Or for more advanced customizations, it is encouraged to create an entirely new environment inherited from `BaseEnvironment`.
|
All of the FreqAI provided environments inherit from an action/position agnostic environment object called the `BaseEnvironment`, which contains all shared logic. The architecture is designed to be easily customized. The simplest customization is the `calculate_reward()` (see details [here](#creating-a-custom-reward-function)). However, the customizations can be further extended into any of the functions inside the environment. You can do this by simply overriding those functions inside your `MyRLEnv` in the prediction model file. Or for more advanced customizations, it is encouraged to create an entirely new environment inherited from `BaseEnvironment`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
FreqAI does not provide by default, a long-only training environment. However, creating one should be as simple as copy-pasting one of the built in environments and removing the `short` actions (and all associated references to those).
|
Only the `Base3ActionRLEnv` can do long-only training/trading (set the user strategy attribute `can_short = False`).
|
||||||
|
@ -67,6 +67,10 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to-
|
|||||||
*want* to retrain a new model with the same config file, you should simply change the `identifier`.
|
*want* to retrain a new model with the same config file, you should simply change the `identifier`.
|
||||||
This way, you can return to using any model you wish by simply specifying the `identifier`.
|
This way, you can return to using any model you wish by simply specifying the `identifier`.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Backtesting calls `set_freqai_targets()` one time for each backtest window (where the number of windows is the full backtest timerange divided by the `backtest_period_days` parameter). Doing this means that the targets simulate dry/live behavior without look ahead bias. However, the definition of the features in `feature_engineering_*()` is performed once on the entire backtest timerange. This means that you should be sure that features do look-ahead into the future.
|
||||||
|
More details about look-ahead bias can be found in [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Saving prediction data
|
### Saving prediction data
|
||||||
@ -116,7 +120,7 @@ In the presented example config, the user will only allow predictions on models
|
|||||||
|
|
||||||
Model training parameters are unique to the selected machine learning library. FreqAI allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement.
|
Model training parameters are unique to the selected machine learning library. FreqAI allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement.
|
||||||
|
|
||||||
Data split parameters are defined in `data_split_parameters` which can be any parameters associated with Scikit-learn's `train_test_split()` function. `train_test_split()` has a parameters called `shuffle` which allows to shuffle the data or keep it unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data. More details about these parameters can be found the [Scikit-learn website](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
|
Data split parameters are defined in `data_split_parameters` which can be any parameters associated with scikit-learn's `train_test_split()` function. `train_test_split()` has a parameters called `shuffle` which allows to shuffle the data or keep it unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data. More details about these parameters can be found the [scikit-learn website](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
|
||||||
|
|
||||||
The FreqAI specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future.
|
The FreqAI specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future.
|
||||||
|
|
||||||
@ -124,6 +128,9 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number
|
|||||||
|
|
||||||
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
|
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
|
||||||
|
|
||||||
|
???+ danger "Continual learning enforces a constant parameter space"
|
||||||
|
Since `continual_learning` means that the model parameter space *cannot* change between trainings, `principal_component_analysis` is automatically disabled when `continual_learning` is enabled. Hint: PCA changes the parameter space and the number of features, learn more about PCA [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis).
|
||||||
|
|
||||||
## Hyperopt
|
## Hyperopt
|
||||||
|
|
||||||
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):
|
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):
|
||||||
@ -135,7 +142,7 @@ freqtrade hyperopt --hyperopt-loss SharpeHyperOptLoss --strategy FreqaiExampleSt
|
|||||||
`hyperopt` requires you to have the data pre-downloaded in the same fashion as if you were doing [backtesting](#backtesting). In addition, you must consider some restrictions when trying to hyperopt FreqAI strategies:
|
`hyperopt` requires you to have the data pre-downloaded in the same fashion as if you were doing [backtesting](#backtesting). In addition, you must consider some restrictions when trying to hyperopt FreqAI strategies:
|
||||||
|
|
||||||
- The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI.
|
- The `--analyze-per-epoch` hyperopt parameter is not compatible with FreqAI.
|
||||||
- It's not possible to hyperopt indicators in the `populate_any_indicators()` function. This means that you cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md#running-hyperopt-with-smaller-search-space).
|
- It's not possible to hyperopt indicators in the `feature_engineering_*()` and `set_freqai_targets()` functions. This means that you cannot optimize model parameters using hyperopt. Apart from this exception, it is possible to optimize all other [spaces](hyperopt.md#running-hyperopt-with-smaller-search-space).
|
||||||
- The backtesting instructions also apply to hyperopt.
|
- The backtesting instructions also apply to hyperopt.
|
||||||
|
|
||||||
The best method for combining hyperopt and FreqAI is to focus on hyperopting entry/exit thresholds/criteria. You need to focus on hyperopting parameters that are not used in your features. For example, you should not try to hyperopt rolling window lengths in the feature creation, or any part of the FreqAI config which changes predictions. In order to efficiently hyperopt the FreqAI strategy, FreqAI stores predictions as dataframes and reuses them. Hence the requirement to hyperopt entry/exit thresholds/criteria only.
|
The best method for combining hyperopt and FreqAI is to focus on hyperopting entry/exit thresholds/criteria. You need to focus on hyperopting parameters that are not used in your features. For example, you should not try to hyperopt rolling window lengths in the feature creation, or any part of the FreqAI config which changes predictions. In order to efficiently hyperopt the FreqAI strategy, FreqAI stores predictions as dataframes and reuses them. Hence the requirement to hyperopt entry/exit thresholds/criteria only.
|
||||||
@ -161,20 +168,3 @@ tensorboard --logdir user_data/models/unique-id
|
|||||||
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if you wish to view the output in your browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
|
where `unique-id` is the `identifier` set in the `freqai` configuration file. This command must be run in a separate shell if you wish to view the output in your browser at 127.0.0.1:6060 (6060 is the default port used by Tensorboard).
|
||||||
|
|
||||||
![tensorboard](assets/tensorboard.jpg)
|
![tensorboard](assets/tensorboard.jpg)
|
||||||
|
|
||||||
## Setting up a follower
|
|
||||||
|
|
||||||
You can indicate to the bot that it should not train models, but instead should look for models trained by a leader with a specific `identifier` by defining:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"freqai": {
|
|
||||||
"enabled": true,
|
|
||||||
"follow_mode": true,
|
|
||||||
"identifier": "example",
|
|
||||||
"feature_parameters": {
|
|
||||||
// leader bots feature_parameters inserted here
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, the user has a leader bot with the `"identifier": "example"`. The leader bot is already running or is launched simultaneously with the follower. The follower will load models created by the leader and inference them to obtain predictions instead of training its own models. The user will also need to duplicate the `feature_parameters` parameters from from the leaders freqai configuration file into the freqai section of the followers config.
|
|
||||||
|
@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, the FreqAI aims to be a sand-box for easily deploying robust machine-learning libraries on real-time data ([details])(#freqai-position-in-open-source-machine-learning-landscape).
|
FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
FreqAI is, and always will be, a not-for-profit, open-source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/latest/freqai/).
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
|
|
||||||
@ -19,7 +22,7 @@ Features include:
|
|||||||
* **Automatic data download** - Compute timeranges for data downloads and update historic data (in live deployments)
|
* **Automatic data download** - Compute timeranges for data downloads and update historic data (in live deployments)
|
||||||
* **Cleaning of incoming data** - Handle NaNs safely before training and model inferencing
|
* **Cleaning of incoming data** - Handle NaNs safely before training and model inferencing
|
||||||
* **Dimensionality reduction** - Reduce the size of the training data via [Principal Component Analysis](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis)
|
* **Dimensionality reduction** - Reduce the size of the training data via [Principal Component Analysis](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis)
|
||||||
* **Deploying bot fleets** - Set one bot to train models while a fleet of [follower bots](freqai-running.md#setting-up-a-follower) inference the models and handle trades
|
* **Deploying bot fleets** - Set one bot to train models while a fleet of [consumers](producer-consumer.md) use signals.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@ -68,14 +71,32 @@ pip install -r requirements-freqai.txt
|
|||||||
!!! Note
|
!!! Note
|
||||||
Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since it does not provide wheels for this platform.
|
Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since it does not provide wheels for this platform.
|
||||||
|
|
||||||
|
!!! Note "python 3.11"
|
||||||
|
Some dependencies (Catboost, Torch) currently don't support python 3.11. Freqtrade therefore only supports python 3.10 for these models/dependencies.
|
||||||
|
Tests involving these dependencies are skipped on 3.11.
|
||||||
|
|
||||||
### Usage with docker
|
### Usage with docker
|
||||||
|
|
||||||
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker-compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
||||||
|
|
||||||
|
|
||||||
### FreqAI position in open-source machine learning landscape
|
### FreqAI position in open-source machine learning landscape
|
||||||
|
|
||||||
Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data-exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data-collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data.
|
Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data.
|
||||||
|
|
||||||
|
### Citing FreqAI
|
||||||
|
|
||||||
|
FreqAI is [published in the Journal of Open Source Software](https://joss.theoj.org/papers/10.21105/joss.04864). If you find FreqAI useful in your research, please use the following citation:
|
||||||
|
|
||||||
|
```bibtex
|
||||||
|
@article{Caulk2022,
|
||||||
|
doi = {10.21105/joss.04864},
|
||||||
|
url = {https://doi.org/10.21105/joss.04864},
|
||||||
|
year = {2022}, publisher = {The Open Journal},
|
||||||
|
volume = {7}, number = {80}, pages = {4864},
|
||||||
|
author = {Robert A. Caulk and Elin Törnquist and Matthias Voppichler and Andrew R. Lawless and Ryan McMullan and Wagner Costa Santos and Timothy C. Pogue and Johan van der Vlugt and Stefan P. Gehring and Pascal Schmidt},
|
||||||
|
title = {FreqAI: generalizing adaptive modeling for chaotic time-series market forecasts},
|
||||||
|
journal = {Journal of Open Source Software} }
|
||||||
|
```
|
||||||
|
|
||||||
## Common pitfalls
|
## Common pitfalls
|
||||||
|
|
||||||
@ -99,6 +120,8 @@ Code review and software architecture brainstorming:
|
|||||||
|
|
||||||
Software development:
|
Software development:
|
||||||
Wagner Costa @wagnercosta
|
Wagner Costa @wagnercosta
|
||||||
|
Emre Suzen @aemr3
|
||||||
|
Timothy Pogue @wizrds
|
||||||
|
|
||||||
Beta testing and bug reporting:
|
Beta testing and bug reporting:
|
||||||
Stefan Gehring @bloodhunter4rc, @longyu, Andrew Lawless @paranoidandy, Pascal Schmidt @smidelis, Ryan McMullan @smarmau, Juha Nykänen @suikula, Johan van der Vlugt @jooopiert, Richárd Józsa @richardjosza, Timothy Pogue @wizrds
|
Stefan Gehring @bloodhunter4rc, @longyu, Andrew Lawless @paranoidandy, Pascal Schmidt @smidelis, Ryan McMullan @smarmau, Juha Nykänen @suikula, Johan van der Vlugt @jooopiert, Richárd Józsa @richardjosza
|
||||||
|
@ -50,7 +50,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
[--eps] [--dmmp] [--enable-protections]
|
[--eps] [--dmmp] [--enable-protections]
|
||||||
[--dry-run-wallet DRY_RUN_WALLET]
|
[--dry-run-wallet DRY_RUN_WALLET]
|
||||||
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
|
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
|
||||||
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
|
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]]
|
||||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||||
[--random-state INT] [--min-trades INT]
|
[--random-state INT] [--min-trades INT]
|
||||||
[--hyperopt-loss NAME] [--disable-param-export]
|
[--hyperopt-loss NAME] [--disable-param-export]
|
||||||
@ -96,7 +96,7 @@ optional arguments:
|
|||||||
Specify detail timeframe for backtesting (`1m`, `5m`,
|
Specify detail timeframe for backtesting (`1m`, `5m`,
|
||||||
`30m`, `1h`, `1d`).
|
`30m`, `1h`, `1d`).
|
||||||
-e INT, --epochs INT Specify number of epochs (default: 100).
|
-e INT, --epochs INT Specify number of epochs (default: 100).
|
||||||
--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]
|
--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]
|
||||||
Specify which parameters to hyperopt. Space-separated
|
Specify which parameters to hyperopt. Space-separated
|
||||||
list.
|
list.
|
||||||
--print-all Print all results, not only the best ones.
|
--print-all Print all results, not only the best ones.
|
||||||
@ -180,6 +180,7 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid
|
|||||||
* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
|
* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
|
||||||
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
||||||
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
|
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
|
||||||
|
* `max_open_trades_space` - for custom max_open_trades optimization (if you need the ranges for the max_open_trades parameter in the optimization hyperspace that differ from default)
|
||||||
|
|
||||||
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
|
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
|
||||||
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy.
|
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy.
|
||||||
@ -365,7 +366,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
timeframe = '15m'
|
timeframe = '15m'
|
||||||
minimal_roi = {
|
minimal_roi = {
|
||||||
"0": 0.10
|
"0": 0.10
|
||||||
},
|
}
|
||||||
# Define the parameter spaces
|
# Define the parameter spaces
|
||||||
buy_ema_short = IntParameter(3, 50, default=5)
|
buy_ema_short = IntParameter(3, 50, default=5)
|
||||||
buy_ema_long = IntParameter(15, 200, default=50)
|
buy_ema_long = IntParameter(15, 200, default=50)
|
||||||
@ -643,6 +644,7 @@ Legal values are:
|
|||||||
* `roi`: just optimize the minimal profit table for your strategy
|
* `roi`: just optimize the minimal profit table for your strategy
|
||||||
* `stoploss`: search for the best stoploss value
|
* `stoploss`: search for the best stoploss value
|
||||||
* `trailing`: search for the best trailing stop values
|
* `trailing`: search for the best trailing stop values
|
||||||
|
* `trades`: search for the best max open trades values
|
||||||
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
|
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
|
||||||
* `default`: `all` except `trailing` and `protection`
|
* `default`: `all` except `trailing` and `protection`
|
||||||
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||||
@ -916,5 +918,5 @@ Once the optimized strategy has been implemented into your strategy, you should
|
|||||||
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||||
|
|
||||||
Should results not match, please double-check to make sure you transferred all conditions correctly.
|
Should results not match, please double-check to make sure you transferred all conditions correctly.
|
||||||
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
|
Pay special care to the stoploss, max_open_trades and trailing stoploss parameters, as these are often set in configuration files, which override changes to the strategy.
|
||||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
|
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss`, `max_open_trades` or `trailing_stop`).
|
||||||
|
@ -23,6 +23,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
|||||||
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
||||||
* [`VolumePairList`](#volume-pair-list)
|
* [`VolumePairList`](#volume-pair-list)
|
||||||
* [`ProducerPairList`](#producerpairlist)
|
* [`ProducerPairList`](#producerpairlist)
|
||||||
|
* [`RemotePairList`](#remotepairlist)
|
||||||
* [`AgeFilter`](#agefilter)
|
* [`AgeFilter`](#agefilter)
|
||||||
* [`OffsetFilter`](#offsetfilter)
|
* [`OffsetFilter`](#offsetfilter)
|
||||||
* [`PerformanceFilter`](#performancefilter)
|
* [`PerformanceFilter`](#performancefilter)
|
||||||
@ -173,6 +174,48 @@ You can limit the length of the pairlist with the optional parameter `number_ass
|
|||||||
`ProducerPairList` can also be used multiple times in sequence, combining the pairs from multiple producers.
|
`ProducerPairList` can also be used multiple times in sequence, combining the pairs from multiple producers.
|
||||||
Obviously in complex such configurations, the Producer may not provide data for all pairs, so the strategy must be fit for this.
|
Obviously in complex such configurations, the Producer may not provide data for all pairs, so the strategy must be fit for this.
|
||||||
|
|
||||||
|
#### RemotePairList
|
||||||
|
|
||||||
|
It allows the user to fetch a pairlist from a remote server or a locally stored json file within the freqtrade directory, enabling dynamic updates and customization of the trading pairlist.
|
||||||
|
|
||||||
|
The RemotePairList is defined in the pairlists section of the configuration settings. It uses the following configuration options:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlists": [
|
||||||
|
{
|
||||||
|
"method": "RemotePairList",
|
||||||
|
"pairlist_url": "https://example.com/pairlist",
|
||||||
|
"number_assets": 10,
|
||||||
|
"refresh_period": 1800,
|
||||||
|
"keep_pairlist_on_failure": true,
|
||||||
|
"read_timeout": 60,
|
||||||
|
"bearer_token": "my-bearer-token"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The `pairlist_url` option specifies the URL of the remote server where the pairlist is located, or the path to a local file (if file:/// is prepended). This allows the user to use either a remote server or a local file as the source for the pairlist.
|
||||||
|
|
||||||
|
The user is responsible for providing a server or local file that returns a JSON object with the following structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pairs": ["XRP/USDT", "ETH/USDT", "LTC/USDT"],
|
||||||
|
"refresh_period": 1800,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `pairs` property should contain a list of strings with the trading pairs to be used by the bot. The `refresh_period` property is optional and specifies the number of seconds that the pairlist should be cached before being refreshed.
|
||||||
|
|
||||||
|
The optional `keep_pairlist_on_failure` specifies whether the previous received pairlist should be used if the remote server is not reachable or returns an error. The default value is true.
|
||||||
|
|
||||||
|
The optional `read_timeout` specifies the maximum amount of time (in seconds) to wait for a response from the remote source, The default value is 60.
|
||||||
|
|
||||||
|
The optional `bearer_token` will be included in the requests Authorization Header.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
In case of a server error the last received pairlist will be kept if `keep_pairlist_on_failure` is set to true, when set to false a empty pairlist is returned.
|
||||||
|
|
||||||
#### AgeFilter
|
#### AgeFilter
|
||||||
|
|
||||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
||||||
|
@ -149,7 +149,7 @@ The below example assumes a timeframe of 1 hour:
|
|||||||
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
|
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
|
||||||
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
|
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
|
||||||
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
|
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
|
||||||
* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
* Locks all pairs that had 2 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
||||||
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
|
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
![freqtrade](assets/freqtrade_poweredby.svg)
|
![freqtrade](assets/freqtrade_poweredby.svg)
|
||||||
|
|
||||||
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
|
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
|
||||||
|
[![DOI](https://joss.theoj.org/papers/10.21105/joss.04864/status.svg)](https://doi.org/10.21105/joss.04864)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
|||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [X] [OKX](https://okx.com/)
|
- [X] [OKX](https://okx.com/)
|
||||||
|
- [X] [Bybit](https://bybit.com/)
|
||||||
|
|
||||||
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
||||||
|
|
||||||
|
@ -30,6 +30,12 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
|||||||
!!! Warning "Up-to-date clock"
|
!!! Warning "Up-to-date clock"
|
||||||
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
||||||
|
|
||||||
|
!!! Error "Running setup.py install for gym did not run successfully."
|
||||||
|
If you get an error related with gym we suggest you to downgrade setuptools it to version 65.5.0 you can do it with the following command:
|
||||||
|
```bash
|
||||||
|
pip install setuptools==65.5.0
|
||||||
|
```
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@ -284,10 +290,8 @@ cd freqtrade
|
|||||||
|
|
||||||
#### Freqtrade install: Conda Environment
|
#### Freqtrade install: Conda Environment
|
||||||
|
|
||||||
Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conda env create -n freqtrade-conda -f environment.yml
|
conda create --name freqtrade python=3.10
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note "Creating Conda Environment"
|
!!! Note "Creating Conda Environment"
|
||||||
@ -296,12 +300,9 @@ conda env create -n freqtrade-conda -f environment.yml
|
|||||||
```bash
|
```bash
|
||||||
# choose your own packages
|
# choose your own packages
|
||||||
conda env create -n [name of the environment] [python version] [packages]
|
conda env create -n [name of the environment] [python version] [packages]
|
||||||
|
|
||||||
# point to file with packages
|
|
||||||
conda env create -n [name of the environment] -f [file]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Enter/exit freqtrade-conda environment
|
#### Enter/exit freqtrade environment
|
||||||
|
|
||||||
To check available environments, type
|
To check available environments, type
|
||||||
|
|
||||||
@ -313,7 +314,7 @@ Enter installed environment
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# enter conda environment
|
# enter conda environment
|
||||||
conda activate freqtrade-conda
|
conda activate freqtrade
|
||||||
|
|
||||||
# exit conda environment - don't do it now
|
# exit conda environment - don't do it now
|
||||||
conda deactivate
|
conda deactivate
|
||||||
@ -323,6 +324,7 @@ Install last python dependencies with pip
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
|
python3 -m pip install -r requirements.txt
|
||||||
python3 -m pip install -e .
|
python3 -m pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -330,7 +332,7 @@ Patch conda libta-lib (Linux only)
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ensure that the environment is active!
|
# Ensure that the environment is active!
|
||||||
conda activate freqtrade-conda
|
conda activate freqtrade
|
||||||
|
|
||||||
cd build_helpers
|
cd build_helpers
|
||||||
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
|
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
|
||||||
@ -349,8 +351,8 @@ conda env list
|
|||||||
# activate base environment
|
# activate base environment
|
||||||
conda activate
|
conda activate
|
||||||
|
|
||||||
# activate freqtrade-conda environment
|
# activate freqtrade environment
|
||||||
conda activate freqtrade-conda
|
conda activate freqtrade
|
||||||
|
|
||||||
#deactivate any conda environments
|
#deactivate any conda environments
|
||||||
conda deactivate
|
conda deactivate
|
||||||
|
@ -67,8 +67,6 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
|
|||||||
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future).
|
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future).
|
||||||
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||||
|
|
||||||
Binance is currently still an exception to this naming scheme, where pairs are named `ETH/USDT` also for futures markets, but will be aligned as soon as CCXT is ready.
|
|
||||||
|
|
||||||
### Margin mode
|
### Margin mode
|
||||||
|
|
||||||
On top of `trading_mode` - you will also have to configure your `margin_mode`.
|
On top of `trading_mode` - you will also have to configure your `margin_mode`.
|
||||||
@ -92,6 +90,8 @@ One account is used to share collateral between markets (trading pairs). Margin
|
|||||||
"margin_mode": "cross"
|
"margin_mode": "cross"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Please read the [exchange specific notes](exchanges.md) for exchanges that support this mode and how they differ.
|
||||||
|
|
||||||
## Set leverage to use
|
## Set leverage to use
|
||||||
|
|
||||||
Different strategies and risk profiles will require different levels of leverage.
|
Different strategies and risk profiles will require different levels of leverage.
|
||||||
|
@ -11,9 +11,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" {{ hidden }}>
|
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" {{ hidden }}>
|
||||||
<div class="md-sidebar__scrollwrap">
|
<div class="md-sidebar__scrollwrap">
|
||||||
<div id="widget-wrapper">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="md-sidebar__inner">
|
<div class="md-sidebar__inner">
|
||||||
{% include "partials/nav.html" %}
|
{% include "partials/nav.html" %}
|
||||||
</div>
|
</div>
|
||||||
@ -44,25 +41,4 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
||||||
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<!-- Load binance SDK -->
|
|
||||||
<script async defer src="https://public.bnbstatic.com/static/js/broker-sdk/broker-sdk@1.0.0.min.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.onload = function () {
|
|
||||||
var sidebar = document.getElementById('widget-wrapper')
|
|
||||||
var newDiv = document.createElement("div");
|
|
||||||
newDiv.id = "widget";
|
|
||||||
try {
|
|
||||||
sidebar.prepend(newDiv);
|
|
||||||
|
|
||||||
window.binanceBrokerPortalSdk.initBrokerSDK('#widget', {
|
|
||||||
apiHost: 'https://www.binance.com',
|
|
||||||
brokerId: 'R4BD3S82',
|
|
||||||
slideTime: 4e4,
|
|
||||||
});
|
|
||||||
} catch(err) {
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -42,14 +42,14 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect
|
|||||||
| `producers` | **Required.** List of producers <br> **Datatype:** Array.
|
| `producers` | **Required.** List of producers <br> **Datatype:** Array.
|
||||||
| `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.<br> **Datatype:** string
|
| `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.<br> **Datatype:** string
|
||||||
| `producers.host` | **Required.** The hostname or IP address from your producer.<br> **Datatype:** string
|
| `producers.host` | **Required.** The hostname or IP address from your producer.<br> **Datatype:** string
|
||||||
| `producers.port` | **Required.** The port matching the above host.<br> **Datatype:** string
|
| `producers.port` | **Required.** The port matching the above host.<br>*Defaults to `8080`.*<br> **Datatype:** Integer
|
||||||
| `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.<br> **Datatype:** string
|
| `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.<br> **Datatype:** string
|
||||||
| `producers.ws_token` | **Required.** `ws_token` as configured on the producer.<br> **Datatype:** string
|
| `producers.ws_token` | **Required.** `ws_token` as configured on the producer.<br> **Datatype:** string
|
||||||
| | **Optional settings**
|
| | **Optional settings**
|
||||||
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
|
| `wait_timeout` | Timeout until we ping again if no message is received. <br>*Defaults to `300`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `wait_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `ping_timeout` | Ping timeout <br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `sleep_time` | Sleep time before retrying to connect.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
||||||
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `10`.*<br> **Datatype:** Integer - in seconds.
|
| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.<br>*Defaults to `False`.*<br> **Datatype:** Boolean.
|
||||||
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
|
| `message_size_limit` | Size limit per message<br>*Defaults to `8`.*<br> **Datatype:** Integer - Megabytes.
|
||||||
|
|
||||||
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
|
Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
markdown==3.3.7
|
markdown==3.3.7
|
||||||
mkdocs==1.4.2
|
mkdocs==1.4.2
|
||||||
mkdocs-material==8.5.11
|
mkdocs-material==9.1.6
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==9.9
|
pymdown-extensions==9.11
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
@ -9,9 +9,6 @@ This same command can also be used to update freqUI, should there be a new relea
|
|||||||
|
|
||||||
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
|
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
|
||||||
|
|
||||||
!!! info "Alpha release"
|
|
||||||
FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose).
|
|
||||||
|
|
||||||
!!! Note "developers"
|
!!! Note "developers"
|
||||||
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
|
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
|
||||||
|
|
||||||
@ -163,7 +160,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||||||
| `strategy <strategy>` | Get specific Strategy content. **Alpha**
|
| `strategy <strategy>` | Get specific Strategy content. **Alpha**
|
||||||
| `available_pairs` | List available backtest data. **Alpha**
|
| `available_pairs` | List available backtest data. **Alpha**
|
||||||
| `version` | Show version.
|
| `version` | Show version.
|
||||||
| `sysinfo` | Show informations about the system load.
|
| `sysinfo` | Show information about the system load.
|
||||||
| `health` | Show bot health (last bot loop).
|
| `health` | Show bot health (last bot loop).
|
||||||
|
|
||||||
!!! Warning "Alpha status"
|
!!! Warning "Alpha status"
|
||||||
@ -192,6 +189,11 @@ blacklist
|
|||||||
|
|
||||||
:param add: List of coins to add (example: "BNB/BTC")
|
:param add: List of coins to add (example: "BNB/BTC")
|
||||||
|
|
||||||
|
cancel_open_order
|
||||||
|
Cancel open order for trade.
|
||||||
|
|
||||||
|
:param trade_id: Cancels open orders for this trade.
|
||||||
|
|
||||||
count
|
count
|
||||||
Return the amount of open trades.
|
Return the amount of open trades.
|
||||||
|
|
||||||
@ -274,7 +276,6 @@ reload_config
|
|||||||
Reload configuration.
|
Reload configuration.
|
||||||
|
|
||||||
show_config
|
show_config
|
||||||
|
|
||||||
Returns part of the configuration, relevant for trading operations.
|
Returns part of the configuration, relevant for trading operations.
|
||||||
|
|
||||||
start
|
start
|
||||||
@ -320,6 +321,7 @@ version
|
|||||||
whitelist
|
whitelist
|
||||||
Show the current whitelist.
|
Show the current whitelist.
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Message WebSocket
|
### Message WebSocket
|
||||||
|
@ -23,10 +23,22 @@ These modes can be configured with these values:
|
|||||||
'stoploss_on_exchange_limit_ratio': 0.99
|
'stoploss_on_exchange_limit_ratio': 0.99
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
Stoploss on exchange is only supported for the following exchanges, and not all exchanges support both stop-limit and stop-market.
|
||||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
|
The Order-type will be ignored if only one mode is available.
|
||||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
|
||||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
| Exchange | stop-loss type |
|
||||||
|
|----------|-------------|
|
||||||
|
| Binance | limit |
|
||||||
|
| Binance Futures | market, limit |
|
||||||
|
| Huobi | limit |
|
||||||
|
| kraken | market, limit |
|
||||||
|
| Gate | limit |
|
||||||
|
| Okx | limit |
|
||||||
|
| Kucoin | stop-limit, stop-market|
|
||||||
|
|
||||||
|
!!! Note "Tight stoploss"
|
||||||
|
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
|
||||||
|
If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work.
|
||||||
|
|
||||||
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
||||||
|
|
||||||
@ -52,6 +64,18 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would
|
|||||||
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
|
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
|
||||||
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
||||||
|
|
||||||
|
### stoploss_price_type
|
||||||
|
|
||||||
|
!!! Warning "Only applies to futures"
|
||||||
|
`stoploss_price_type` only applies to futures markets (on exchanges where it's available).
|
||||||
|
Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected.
|
||||||
|
Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports.
|
||||||
|
|
||||||
|
Stoploss on exchange on futures markets can trigger on different price types.
|
||||||
|
The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index".
|
||||||
|
|
||||||
|
Acceptable values for this setting are `"last"`, `"mark"` and `"index"` - which freqtrade will transfer automatically to the corresponding API type, and place the [stoploss on exchange](#stoploss_on_exchange-and-stoploss_on_exchange_limit_ratio) order correspondingly.
|
||||||
|
|
||||||
### force_exit
|
### force_exit
|
||||||
|
|
||||||
`force_exit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API.
|
`force_exit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API.
|
||||||
|
@ -80,7 +80,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
## Enter Tag
|
## Enter Tag
|
||||||
|
|
||||||
When your strategy has multiple buy signals, you can name the signal that triggered.
|
When your strategy has multiple buy signals, you can name the signal that triggered.
|
||||||
Then you can access you buy signal on `custom_exit`
|
Then you can access your buy signal on `custom_exit`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
@ -51,7 +51,8 @@ During hyperopt, this runs only once at startup.
|
|||||||
|
|
||||||
## Bot loop start
|
## Bot loop start
|
||||||
|
|
||||||
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5
|
||||||
|
seconds, unless configured differently) or once per candle in backtest/hyperopt mode.
|
||||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
@ -61,11 +62,12 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
def bot_loop_start(self, **kwargs) -> None:
|
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Called at the start of the bot iteration (one loop).
|
Called at the start of the bot iteration (one loop).
|
||||||
Might be used to perform pair-independent tasks
|
Might be used to perform pair-independent tasks
|
||||||
(e.g. gather some remote resource for comparison)
|
(e.g. gather some remote resource for comparison)
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
"""
|
"""
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
@ -316,11 +318,11 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
# evaluate highest to lowest, so that highest possible stop is used
|
# evaluate highest to lowest, so that highest possible stop is used
|
||||||
if current_profit > 0.40:
|
if current_profit > 0.40:
|
||||||
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
elif current_profit > 0.25:
|
elif current_profit > 0.25:
|
||||||
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
elif current_profit > 0.20:
|
elif current_profit > 0.20:
|
||||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
|
|
||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
return 1
|
return 1
|
||||||
@ -659,6 +661,7 @@ Position adjustments will always be applied in the direction of the trade, so a
|
|||||||
|
|
||||||
!!! Warning "Backtesting"
|
!!! Warning "Backtesting"
|
||||||
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
|
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected.
|
||||||
|
This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
@ -827,7 +830,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||||
if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10) > trade.open_date_utc:
|
if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10)) > trade.open_date_utc:
|
||||||
# just cancel the order if it has been filled more than half of the amount
|
# just cancel the order if it has been filled more than half of the amount
|
||||||
if order.filled > order.remaining:
|
if order.filled > order.remaining:
|
||||||
return None
|
return None
|
||||||
|
@ -881,7 +881,7 @@ All columns of the informative dataframe will be available on the returning data
|
|||||||
|
|
||||||
### *stoploss_from_open()*
|
### *stoploss_from_open()*
|
||||||
|
|
||||||
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price.
|
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the entry point instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point.
|
||||||
|
|
||||||
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
|
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
|
||||||
|
|
||||||
@ -889,6 +889,8 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
|||||||
|
|
||||||
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
|
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
|
||||||
|
|
||||||
|
This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%).
|
||||||
|
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
|
||||||
@ -907,7 +909,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
|||||||
|
|
||||||
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||||
if current_profit > 0.10:
|
if current_profit > 0.10:
|
||||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@ -954,12 +956,14 @@ In some situations it may be confusing to deal with stops relative to current ra
|
|||||||
|
|
||||||
## Additional data (Wallets)
|
## Additional data (Wallets)
|
||||||
|
|
||||||
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
The strategy provides access to the `wallets` object. This contains the current balances on the exchange.
|
||||||
|
|
||||||
!!! Note
|
!!! Note "Backtesting / Hyperopt"
|
||||||
Wallets is not available during backtesting / hyperopt.
|
Wallets behaves differently depending on the function it's called.
|
||||||
|
Within `populate_*()` methods, it'll return the full wallet as configured.
|
||||||
|
Within [callbacks](strategy-callbacks.md), you'll get the wallet state corresponding to the actual simulated wallet at that point in the simulation process.
|
||||||
|
|
||||||
Please always check if `Wallets` is available to avoid failures during backtesting.
|
Please always check if `wallets` is available to avoid failures during backtesting.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
if self.wallets:
|
if self.wallets:
|
||||||
@ -989,38 +993,18 @@ from freqtrade.persistence import Trade
|
|||||||
The following example queries for the current pair and trades from today, however other filters can easily be added.
|
The following example queries for the current pair and trades from today, however other filters can easily be added.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
trades = Trade.get_trades_proxy(pair=metadata['pair'],
|
||||||
trades = Trade.get_trades([Trade.pair == metadata['pair'],
|
open_date=datetime.now(timezone.utc) - timedelta(days=1),
|
||||||
Trade.open_date > datetime.utcnow() - timedelta(days=1),
|
is_open=False,
|
||||||
Trade.is_open.is_(False),
|
|
||||||
]).order_by(Trade.close_date).all()
|
]).order_by(Trade.close_date).all()
|
||||||
# Summarize profit for this pair.
|
# Summarize profit for this pair.
|
||||||
curdayprofit = sum(trade.close_profit for trade in trades)
|
curdayprofit = sum(trade.close_profit for trade in trades)
|
||||||
```
|
```
|
||||||
|
|
||||||
Get amount of stake_currency currently invested in Trades:
|
For a full list of available methods, please consult the [Trade object](trade-object.md) documentation.
|
||||||
|
|
||||||
``` python
|
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
|
||||||
total_stakes = Trade.total_open_trades_stakes()
|
|
||||||
```
|
|
||||||
|
|
||||||
Retrieve performance per pair.
|
|
||||||
Returns a List of dicts per pair.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
|
||||||
performance = Trade.get_overall_performance()
|
|
||||||
```
|
|
||||||
|
|
||||||
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
|
|
||||||
|
|
||||||
``` json
|
|
||||||
{"pair": "ETH/BTC", "profit": 0.015, "count": 5}
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Trade history is not available during backtesting or hyperopt.
|
Trade history is not available in `populate_*` methods during backtesting or hyperopt, and will result in empty results.
|
||||||
|
|
||||||
## Prevent trades from happening for a specific pair
|
## Prevent trades from happening for a specific pair
|
||||||
|
|
||||||
@ -1057,10 +1041,9 @@ from datetime import timedelta, datetime, timezone
|
|||||||
# Within populate indicators (or populate_buy):
|
# Within populate indicators (or populate_buy):
|
||||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
# fetch closed trades for the last 2 days
|
# fetch closed trades for the last 2 days
|
||||||
trades = Trade.get_trades([Trade.pair == metadata['pair'],
|
trades = Trade.get_trades_proxy(
|
||||||
Trade.open_date > datetime.utcnow() - timedelta(days=2),
|
pair=metadata['pair'], is_open=False,
|
||||||
Trade.is_open.is_(False),
|
open_date=datetime.now(timezone.utc) - timedelta(days=2))
|
||||||
]).all()
|
|
||||||
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy
|
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy
|
||||||
sumprofit = sum(trade.close_profit for trade in trades)
|
sumprofit = sum(trade.close_profit for trade in trades)
|
||||||
if sumprofit < 0:
|
if sumprofit < 0:
|
||||||
|
@ -80,6 +80,7 @@ from freqtrade.resolvers import StrategyResolver
|
|||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
strategy = StrategyResolver.load_strategy(config)
|
strategy = StrategyResolver.load_strategy(config)
|
||||||
strategy.dp = DataProvider(config, None, None)
|
strategy.dp = DataProvider(config, None, None)
|
||||||
|
strategy.ft_bot_start()
|
||||||
|
|
||||||
# Generate buy/sell signals using strategy
|
# Generate buy/sell signals using strategy
|
||||||
df = strategy.analyze_ticker(candles, {'pair': pair})
|
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||||
|
@ -477,3 +477,254 @@ after:
|
|||||||
"ignore_buying_expired_candle_after": 120
|
"ignore_buying_expired_candle_after": 120
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## FreqAI strategy
|
||||||
|
|
||||||
|
The `populate_any_indicators()` method has been split into `feature_engineering_expand_all()`, `feature_engineering_expand_basic()`, `feature_engineering_standard()` and`set_freqai_targets()`.
|
||||||
|
|
||||||
|
For each new function, the pair (and timeframe where necessary) will be automatically added to the column.
|
||||||
|
As such, the definition of features becomes much simpler with the new logic.
|
||||||
|
|
||||||
|
For a full explanation of each method, please go to the corresponding [freqAI documentation page](freqai-feature-engineering.md#defining-the-features)
|
||||||
|
|
||||||
|
``` python linenums="1" hl_lines="12-37 39-42 63-65 67-75"
|
||||||
|
|
||||||
|
def populate_any_indicators(
|
||||||
|
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||||
|
):
|
||||||
|
|
||||||
|
if informative is None:
|
||||||
|
informative = self.dp.get_pair_dataframe(pair, tf)
|
||||||
|
|
||||||
|
# first loop is automatically duplicating indicators for time periods
|
||||||
|
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
|
||||||
|
|
||||||
|
t = int(t)
|
||||||
|
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
|
||||||
|
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
|
||||||
|
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
|
||||||
|
informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
|
||||||
|
informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
|
||||||
|
|
||||||
|
bollinger = qtpylib.bollinger_bands(
|
||||||
|
qtpylib.typical_price(informative), window=t, stds=2.2
|
||||||
|
)
|
||||||
|
informative[f"{pair}bb_lowerband-period_{t}"] = bollinger["lower"]
|
||||||
|
informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"]
|
||||||
|
informative[f"{pair}bb_upperband-period_{t}"] = bollinger["upper"]
|
||||||
|
|
||||||
|
informative[f"%-{pair}bb_width-period_{t}"] = (
|
||||||
|
informative[f"{pair}bb_upperband-period_{t}"]
|
||||||
|
- informative[f"{pair}bb_lowerband-period_{t}"]
|
||||||
|
) / informative[f"{pair}bb_middleband-period_{t}"]
|
||||||
|
informative[f"%-{pair}close-bb_lower-period_{t}"] = (
|
||||||
|
informative["close"] / informative[f"{pair}bb_lowerband-period_{t}"]
|
||||||
|
)
|
||||||
|
|
||||||
|
informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
|
||||||
|
|
||||||
|
informative[f"%-{pair}relative_volume-period_{t}"] = (
|
||||||
|
informative["volume"] / informative["volume"].rolling(t).mean()
|
||||||
|
) # (1)
|
||||||
|
|
||||||
|
informative[f"%-{pair}pct-change"] = informative["close"].pct_change()
|
||||||
|
informative[f"%-{pair}raw_volume"] = informative["volume"]
|
||||||
|
informative[f"%-{pair}raw_price"] = informative["close"]
|
||||||
|
# (2)
|
||||||
|
|
||||||
|
indicators = [col for col in informative if col.startswith("%")]
|
||||||
|
# This loop duplicates and shifts all indicators to add a sense of recency to data
|
||||||
|
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
|
||||||
|
if n == 0:
|
||||||
|
continue
|
||||||
|
informative_shift = informative[indicators].shift(n)
|
||||||
|
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
|
||||||
|
informative = pd.concat((informative, informative_shift), axis=1)
|
||||||
|
|
||||||
|
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
|
||||||
|
skip_columns = [
|
||||||
|
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
|
||||||
|
]
|
||||||
|
df = df.drop(columns=skip_columns)
|
||||||
|
|
||||||
|
# Add generalized indicators here (because in live, it will call this
|
||||||
|
# function to populate indicators during training). Notice how we ensure not to
|
||||||
|
# add them multiple times
|
||||||
|
if set_generalized_indicators:
|
||||||
|
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
||||||
|
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
||||||
|
# (3)
|
||||||
|
|
||||||
|
# user adds targets here by prepending them with &- (see convention below)
|
||||||
|
df["&-s_close"] = (
|
||||||
|
df["close"]
|
||||||
|
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||||
|
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||||
|
.mean()
|
||||||
|
/ df["close"]
|
||||||
|
- 1
|
||||||
|
) # (4)
|
||||||
|
|
||||||
|
return df
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Features - Move to `feature_engineering_expand_all`
|
||||||
|
2. Basic features, not expanded across `include_periods_candles` - move to`feature_engineering_expand_basic()`.
|
||||||
|
3. Standard features which should not be expanded - move to `feature_engineering_standard()`.
|
||||||
|
4. Targets - Move this part to `set_freqai_targets()`.
|
||||||
|
|
||||||
|
### freqai - feature engineering expand all
|
||||||
|
|
||||||
|
Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed.
|
||||||
|
|
||||||
|
``` python linenums="1"
|
||||||
|
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
|
||||||
|
"""
|
||||||
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
This function will automatically expand the defined features on the config defined
|
||||||
|
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
|
||||||
|
`include_corr_pairs`. In other words, a single feature defined in this function
|
||||||
|
will automatically expand to a total of
|
||||||
|
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
|
||||||
|
`include_corr_pairs` numbers of features added to the model.
|
||||||
|
|
||||||
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
|
|
||||||
|
More details on how these config defined parameters accelerate feature engineering
|
||||||
|
in the documentation at:
|
||||||
|
|
||||||
|
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
||||||
|
|
||||||
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the features
|
||||||
|
:param period: period of the indicator - usage example:
|
||||||
|
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||||
|
"""
|
||||||
|
|
||||||
|
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||||
|
|
||||||
|
bollinger = qtpylib.bollinger_bands(
|
||||||
|
qtpylib.typical_price(dataframe), window=period, stds=2.2
|
||||||
|
)
|
||||||
|
dataframe["bb_lowerband-period"] = bollinger["lower"]
|
||||||
|
dataframe["bb_middleband-period"] = bollinger["mid"]
|
||||||
|
dataframe["bb_upperband-period"] = bollinger["upper"]
|
||||||
|
|
||||||
|
dataframe["%-bb_width-period"] = (
|
||||||
|
dataframe["bb_upperband-period"]
|
||||||
|
- dataframe["bb_lowerband-period"]
|
||||||
|
) / dataframe["bb_middleband-period"]
|
||||||
|
dataframe["%-close-bb_lower-period"] = (
|
||||||
|
dataframe["close"] / dataframe["bb_lowerband-period"]
|
||||||
|
)
|
||||||
|
|
||||||
|
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||||
|
|
||||||
|
dataframe["%-relative_volume-period"] = (
|
||||||
|
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Freqai - feature engineering basic
|
||||||
|
|
||||||
|
Basic features. Make sure to remove the `{pair}` part from your features.
|
||||||
|
|
||||||
|
``` python linenums="1"
|
||||||
|
def feature_engineering_expand_basic(self, dataframe, **kwargs):
|
||||||
|
"""
|
||||||
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
This function will automatically expand the defined features on the config defined
|
||||||
|
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
|
||||||
|
In other words, a single feature defined in this function
|
||||||
|
will automatically expand to a total of
|
||||||
|
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
|
||||||
|
numbers of features added to the model.
|
||||||
|
|
||||||
|
Features defined here will *not* be automatically duplicated on user defined
|
||||||
|
`indicator_periods_candles`
|
||||||
|
|
||||||
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
|
|
||||||
|
More details on how these config defined parameters accelerate feature engineering
|
||||||
|
in the documentation at:
|
||||||
|
|
||||||
|
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
|
||||||
|
|
||||||
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the features
|
||||||
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
|
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
|
||||||
|
"""
|
||||||
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
|
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||||
|
dataframe["%-raw_price"] = dataframe["close"]
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
### FreqAI - feature engineering standard
|
||||||
|
|
||||||
|
``` python linenums="1"
|
||||||
|
def feature_engineering_standard(self, dataframe, **kwargs):
|
||||||
|
"""
|
||||||
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
This optional function will be called once with the dataframe of the base timeframe.
|
||||||
|
This is the final function to be called, which means that the dataframe entering this
|
||||||
|
function will contain all the features and columns created by all other
|
||||||
|
freqai_feature_engineering_* functions.
|
||||||
|
|
||||||
|
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
|
||||||
|
This function is a good place for any feature that should not be auto-expanded upon
|
||||||
|
(e.g. day of the week).
|
||||||
|
|
||||||
|
All features must be prepended with `%` to be recognized by FreqAI internals.
|
||||||
|
|
||||||
|
More details about feature engineering available:
|
||||||
|
|
||||||
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the features
|
||||||
|
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
|
||||||
|
"""
|
||||||
|
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||||
|
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
### FreqAI - set Targets
|
||||||
|
|
||||||
|
Targets now get their own, dedicated method.
|
||||||
|
|
||||||
|
``` python linenums="1"
|
||||||
|
def set_freqai_targets(self, dataframe, **kwargs):
|
||||||
|
"""
|
||||||
|
*Only functional with FreqAI enabled strategies*
|
||||||
|
Required function to set the targets for the model.
|
||||||
|
All targets must be prepended with `&` to be recognized by the FreqAI internals.
|
||||||
|
|
||||||
|
More details about feature engineering available:
|
||||||
|
|
||||||
|
https://www.freqtrade.io/en/latest/freqai-feature-engineering
|
||||||
|
|
||||||
|
:param df: strategy dataframe which will receive the targets
|
||||||
|
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
|
||||||
|
"""
|
||||||
|
dataframe["&-s_close"] = (
|
||||||
|
dataframe["close"]
|
||||||
|
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||||
|
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
|
||||||
|
.mean()
|
||||||
|
/ dataframe["close"]
|
||||||
|
- 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
@ -11,18 +11,3 @@
|
|||||||
.rst-versions .rst-other-versions {
|
.rst-versions .rst-other-versions {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#widget-wrapper {
|
|
||||||
height: calc(220px * 0.5625 + 18px);
|
|
||||||
width: 220px;
|
|
||||||
margin: 0 auto 16px auto;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--md-code-bg-color);
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: calc(76.25em - 1px)) {
|
|
||||||
#widget-wrapper { display: none; }
|
|
||||||
}
|
|
||||||
|
@ -152,7 +152,7 @@ You can create your own keyboard in `config.json`:
|
|||||||
!!! Note "Supported Commands"
|
!!! Note "Supported Commands"
|
||||||
Only the following commands are allowed. Command arguments are not supported!
|
Only the following commands are allowed. Command arguments are not supported!
|
||||||
|
|
||||||
`/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`
|
`/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||||
|
|
||||||
## Telegram commands
|
## Telegram commands
|
||||||
|
|
||||||
@ -162,26 +162,34 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
|
| **System commands**
|
||||||
| `/start` | Starts the trader
|
| `/start` | Starts the trader
|
||||||
| `/stop` | Stops the trader
|
| `/stop` | Stops the trader
|
||||||
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
| `/reload_config` | Reloads the configuration file
|
| `/reload_config` | Reloads the configuration file
|
||||||
| `/show_config` | Shows part of the current configuration with relevant settings to operation
|
| `/show_config` | Shows part of the current configuration with relevant settings to operation
|
||||||
| `/logs [limit]` | Show last log messages.
|
| `/logs [limit]` | Show last log messages.
|
||||||
|
| `/help` | Show help message
|
||||||
|
| `/version` | Show version
|
||||||
|
| **Status** |
|
||||||
| `/status` | Lists all open trades
|
| `/status` | Lists all open trades
|
||||||
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
|
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
|
||||||
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
||||||
| `/trades [limit]` | List all recently closed trades in a table format.
|
| `/trades [limit]` | List all recently closed trades in a table format.
|
||||||
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
|
||||||
| `/count` | Displays number of trades used and available
|
| `/count` | Displays number of trades used and available
|
||||||
| `/locks` | Show currently locked pairs.
|
| `/locks` | Show currently locked pairs.
|
||||||
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
|
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
|
||||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
| `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. If no direction is provided, the currently set direction will be displayed.
|
||||||
|
| **Modify Trade states** |
|
||||||
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||||
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
||||||
| `/fx` | alias for `/forceexit`
|
| `/fx` | alias for `/forceexit`
|
||||||
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
|
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
|
||||||
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
|
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
|
||||||
|
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||||
|
| `/cancel_open_order <trade_id> | /coo <trade_id>` | Cancel an open order for a trade.
|
||||||
|
| **Metrics** |
|
||||||
|
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||||
| `/performance` | Show performance of each finished trade grouped by pair
|
| `/performance` | Show performance of each finished trade grouped by pair
|
||||||
| `/balance` | Show account balance per currency
|
| `/balance` | Show account balance per currency
|
||||||
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
||||||
@ -193,8 +201,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing.
|
| `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing.
|
||||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
| `/edge` | Show validated pairs by Edge if it is enabled.
|
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||||
| `/help` | Show help message
|
|
||||||
| `/version` | Show version
|
|
||||||
|
|
||||||
## Telegram commands in action
|
## Telegram commands in action
|
||||||
|
|
||||||
@ -236,7 +243,7 @@ Enter Tag is configurable via Strategy.
|
|||||||
> **Enter Tag:** Awesome Long Signal
|
> **Enter Tag:** Awesome Long Signal
|
||||||
> **Open Rate:** `0.00007489`
|
> **Open Rate:** `0.00007489`
|
||||||
> **Current Rate:** `0.00007489`
|
> **Current Rate:** `0.00007489`
|
||||||
> **Current Profit:** `12.95%`
|
> **Unrealized Profit:** `12.95%`
|
||||||
> **Stoploss:** `0.00007389 (-0.02%)`
|
> **Stoploss:** `0.00007389 (-0.02%)`
|
||||||
|
|
||||||
### /status table
|
### /status table
|
||||||
@ -272,6 +279,7 @@ Return a summary of your profit/loss and performance.
|
|||||||
> ∙ `33.095 EUR`
|
> ∙ `33.095 EUR`
|
||||||
>
|
>
|
||||||
> **Total Trade Count:** `138`
|
> **Total Trade Count:** `138`
|
||||||
|
> **Bot started:** `2022-07-11 18:40:44`
|
||||||
> **First Trade opened:** `3 days ago`
|
> **First Trade opened:** `3 days ago`
|
||||||
> **Latest Trade opened:** `2 minutes ago`
|
> **Latest Trade opened:** `2 minutes ago`
|
||||||
> **Avg. Duration:** `2:33:45`
|
> **Avg. Duration:** `2:33:45`
|
||||||
@ -285,6 +293,7 @@ The relative profit of `15.2 Σ%` is be based on the starting capital - so in th
|
|||||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||||
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
||||||
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||||
|
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
|
||||||
|
|
||||||
### /forceexit <trade_id>
|
### /forceexit <trade_id>
|
||||||
|
|
||||||
@ -410,3 +419,27 @@ ARDR/ETH 0.366667 0.143059 -0.01
|
|||||||
### /version
|
### /version
|
||||||
|
|
||||||
> **Version:** `0.14.3`
|
> **Version:** `0.14.3`
|
||||||
|
|
||||||
|
### /marketdir
|
||||||
|
|
||||||
|
If a market direction is provided the command updates the user managed variable that represents the current market direction.
|
||||||
|
This variable is not set to any valid market direction on bot startup and must be set by the user. The example below is for `/marketdir long`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Successfully updated marketdirection from none to long.
|
||||||
|
```
|
||||||
|
|
||||||
|
If no market direction is provided the command outputs the currently set market directions. The example below is for `/marketdir`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Currently set marketdirection: even
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the market direction in your strategy via `self.market_direction`.
|
||||||
|
|
||||||
|
!!! Warning "Bot restarts"
|
||||||
|
Please note that the market direction is not persisted, and will be reset after a bot restart/reload.
|
||||||
|
|
||||||
|
!!! Danger "Backtesting"
|
||||||
|
As this value/variable is intended to be changed manually in dry/live trading.
|
||||||
|
Strategies using `market_direction` will probably not produce reliable, reproducible results (changes to this variable will not be reflected for backtesting). Use at your own risk.
|
||||||
|
148
docs/trade-object.md
Normal file
148
docs/trade-object.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# Trade Object
|
||||||
|
|
||||||
|
## Trade
|
||||||
|
|
||||||
|
A position freqtrade enters is stored in a `Trade` object - which is persisted to the database.
|
||||||
|
It's a core concept of freqtrade - and something you'll come across in many sections of the documentation, which will most likely point you to this location.
|
||||||
|
|
||||||
|
It will be passed to the strategy in many [strategy callbacks](strategy-callbacks.md). The object passed to the strategy cannot be modified directly. Indirect modifications may occur based on callback results.
|
||||||
|
|
||||||
|
## Trade - Available attributes
|
||||||
|
|
||||||
|
The following attributes / properties are available for each individual trade - and can be used with `trade.<property>` (e.g. `trade.pair`).
|
||||||
|
|
||||||
|
| Attribute | DataType | Description |
|
||||||
|
|------------|-------------|-------------|
|
||||||
|
`pair`| string | Pair of this trade
|
||||||
|
`is_open`| boolean | Is the trade currently open, or has it been concluded
|
||||||
|
`open_rate`| float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments)
|
||||||
|
`close_rate`| float | Close rate - only set when is_open = False
|
||||||
|
`stake_amount`| float | Amount in Stake (or Quote) currency.
|
||||||
|
`amount`| float | Amount in Asset / Base currency that is currently owned.
|
||||||
|
`open_date`| datetime | Timestamp when trade was opened **use `open_date_utc` instead**
|
||||||
|
`open_date_utc`| datetime | Timestamp when trade was opened - in UTC
|
||||||
|
`close_date`| datetime | Timestamp when trade was closed **use `close_date_utc` instead**
|
||||||
|
`close_date_utc`| datetime | Timestamp when trade was closed - in UTC
|
||||||
|
`close_profit`| float | Relative profit at the time of trade closure. `0.01` == 1%
|
||||||
|
`close_profit_abs`| float | Absolute profit (in stake currency) at the time of trade closure.
|
||||||
|
`leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets.
|
||||||
|
`enter_tag`| string | Tag provided on entry via the `enter_tag` column in the dataframe
|
||||||
|
`is_short` | boolean | True for short trades, False otherwise
|
||||||
|
`orders` | Order[] | List of order objects attached to this trade (includes both filled and cancelled orders)
|
||||||
|
`date_last_filled_utc` | datetime | Time of the last filled order
|
||||||
|
`entry_side` | "buy" / "sell" | Order Side the trade was entered
|
||||||
|
`exit_side` | "buy" / "sell" | Order Side that will result in a trade exit / position reduction.
|
||||||
|
`trade_direction` | "long" / "short" | Trade direction in text - long or short.
|
||||||
|
`nr_of_successful_entries` | int | Number of successful (filled) entry orders
|
||||||
|
`nr_of_successful_exits` | int | Number of successful (filled) exit orders
|
||||||
|
|
||||||
|
## Class methods
|
||||||
|
|
||||||
|
The following are class methods - which return generic information, and usually result in an explicit query against the database.
|
||||||
|
They can be used as `Trade.<method>` - e.g. `open_trades = Trade.get_open_trade_count()`
|
||||||
|
|
||||||
|
!!! Warning "Backtesting/hyperopt"
|
||||||
|
Most methods will work in both backtesting / hyperopt and live/dry modes.
|
||||||
|
During backtesting, it's limited to usage in [strategy callbacks](strategy-callbacks.md). Usage in `populate_*()` methods is not supported and will result in wrong results.
|
||||||
|
|
||||||
|
### get_trades_proxy
|
||||||
|
|
||||||
|
When your strategy needs some information on existing (open or close) trades - it's best to use `Trade.get_trades_proxy()`.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
# ...
|
||||||
|
trade_hist = Trade.get_trades_proxy(pair='ETH/USDT', is_open=False, open_date=current_date - timedelta(days=2))
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
`get_trades_proxy()` supports the following keyword arguments. All arguments are optional - calling `get_trades_proxy()` without arguments will return a list of all trades in the database.
|
||||||
|
|
||||||
|
* `pair` e.g. `pair='ETH/USDT'`
|
||||||
|
* `is_open` e.g. `is_open=False`
|
||||||
|
* `open_date` e.g. `open_date=current_date - timedelta(days=2)`
|
||||||
|
* `close_date` e.g. `close_date=current_date - timedelta(days=5)`
|
||||||
|
|
||||||
|
### get_open_trade_count
|
||||||
|
|
||||||
|
Get the number of currently open trades
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
# ...
|
||||||
|
open_trades = Trade.get_open_trade_count()
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_total_closed_profit
|
||||||
|
|
||||||
|
Retrieve the total profit the bot has generated so far.
|
||||||
|
Aggregates `close_profit_abs` for all closed trades.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
# ...
|
||||||
|
profit = Trade.get_total_closed_profit()
|
||||||
|
```
|
||||||
|
|
||||||
|
### total_open_trades_stakes
|
||||||
|
|
||||||
|
Retrieve the total stake_amount that's currently in trades.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
# ...
|
||||||
|
profit = Trade.total_open_trades_stakes()
|
||||||
|
```
|
||||||
|
|
||||||
|
### get_overall_performance
|
||||||
|
|
||||||
|
Retrieve the overall performance - similar to the `/performance` telegram command.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
# ...
|
||||||
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
|
performance = Trade.get_overall_performance()
|
||||||
|
```
|
||||||
|
|
||||||
|
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{"pair": "ETH/BTC", "profit": 0.015, "count": 5}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Order Object
|
||||||
|
|
||||||
|
An `Order` object represents an order on the exchange (or a simulated order in dry-run mode).
|
||||||
|
An `Order` object will always be tied to it's corresponding [`Trade`](#trade-object), and only really makes sense in the context of a trade.
|
||||||
|
|
||||||
|
### Order - Available attributes
|
||||||
|
|
||||||
|
an Order object is typically attached to a trade.
|
||||||
|
Most properties here can be None as they are dependant on the exchange response.
|
||||||
|
|
||||||
|
| Attribute | DataType | Description |
|
||||||
|
|------------|-------------|-------------|
|
||||||
|
`trade` | Trade | Trade object this order is attached to
|
||||||
|
`ft_pair` | string | Pair this order is for
|
||||||
|
`ft_is_open` | boolean | is the order filled?
|
||||||
|
`order_type` | string | Order type as defined on the exchange - usually market, limit or stoploss
|
||||||
|
`status` | string | Status as defined by ccxt. Usually open, closed, expired or canceled
|
||||||
|
`side` | string | Buy or Sell
|
||||||
|
`price` | float | Price the order was placed at
|
||||||
|
`average` | float | Average price the order filled at
|
||||||
|
`amount` | float | Amount in base currency
|
||||||
|
`filled` | float | Filled amount (in base currency)
|
||||||
|
`remaining` | float | Remaining amount
|
||||||
|
`cost` | float | Cost of the order - usually average * filled
|
||||||
|
`order_date` | datetime | Order creation date **use `order_date_utc` instead**
|
||||||
|
`order_date_utc` | datetime | Order creation date (in UTC)
|
||||||
|
`order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead**
|
||||||
|
`order_fill_date_utc` | datetime | Order fill date
|
@ -955,3 +955,47 @@ Print trades with id 2 and 3 as json
|
|||||||
``` bash
|
``` bash
|
||||||
freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json
|
freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Strategy-Updater
|
||||||
|
|
||||||
|
Updates listed strategies or all strategies within the strategies folder to be v3 compliant.
|
||||||
|
If the command runs without --strategy-list then all strategies inside the strategies folder will be converted.
|
||||||
|
Your original strategy will remain available in the `user_data/strategies_orig_updater/` directory.
|
||||||
|
|
||||||
|
!!! Warning "Conversion results"
|
||||||
|
Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion.
|
||||||
|
We also recommend to run a python formatter (e.g. `black`) to format results in a sane manner.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade strategy-updater [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
|
Provide a space-separated list of strategies to
|
||||||
|
backtest. Please note that timeframe needs to be set
|
||||||
|
either in config or via command line. When using this
|
||||||
|
together with `--export trades`, the strategy-name is
|
||||||
|
injected into the filename (so `backtest-data.json`
|
||||||
|
becomes `backtest-data-SampleStrategy.json`
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE, --log-file FILE
|
||||||
|
Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
Specify configuration file (default:
|
||||||
|
`userdir/config.json` or `config.json` whichever
|
||||||
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH, --data-dir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
@ -26,7 +26,7 @@ Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7
|
|||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.25-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.25-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
||||||
|
|
||||||
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows.
|
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows.
|
||||||
Other versions must be downloaded from the above link.
|
Other versions must be downloaded from the above link.
|
||||||
|
|
||||||
``` powershell
|
``` powershell
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
name: freqtrade
|
|
||||||
channels:
|
|
||||||
- conda-forge
|
|
||||||
# - defaults
|
|
||||||
dependencies:
|
|
||||||
# 1/4 req main
|
|
||||||
- python>=3.8,<=3.10
|
|
||||||
- numpy
|
|
||||||
- pandas
|
|
||||||
- pip
|
|
||||||
|
|
||||||
- py-find-1st
|
|
||||||
- aiohttp
|
|
||||||
- SQLAlchemy
|
|
||||||
- python-telegram-bot
|
|
||||||
- arrow
|
|
||||||
- cachetools
|
|
||||||
- requests
|
|
||||||
- urllib3
|
|
||||||
- jsonschema
|
|
||||||
- TA-Lib
|
|
||||||
- tabulate
|
|
||||||
- jinja2
|
|
||||||
- blosc
|
|
||||||
- sdnotify
|
|
||||||
- fastapi
|
|
||||||
- uvicorn
|
|
||||||
- pyjwt
|
|
||||||
- aiofiles
|
|
||||||
- psutil
|
|
||||||
- colorama
|
|
||||||
- questionary
|
|
||||||
- prompt-toolkit
|
|
||||||
- schedule
|
|
||||||
- python-dateutil
|
|
||||||
- joblib
|
|
||||||
- pyarrow
|
|
||||||
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# 2/4 req dev
|
|
||||||
|
|
||||||
- coveralls
|
|
||||||
- flake8
|
|
||||||
- mypy
|
|
||||||
- pytest
|
|
||||||
- pytest-asyncio
|
|
||||||
- pytest-cov
|
|
||||||
- pytest-mock
|
|
||||||
- isort
|
|
||||||
- nbconvert
|
|
||||||
|
|
||||||
# ============================
|
|
||||||
# 3/4 req hyperopt
|
|
||||||
|
|
||||||
- scipy
|
|
||||||
- scikit-learn
|
|
||||||
- filelock
|
|
||||||
- scikit-optimize
|
|
||||||
- progressbar2
|
|
||||||
# ============================
|
|
||||||
# 4/4 req plot
|
|
||||||
|
|
||||||
- plotly
|
|
||||||
- jupyter
|
|
||||||
|
|
||||||
- pip:
|
|
||||||
- pycoingecko
|
|
||||||
# - py_find_1st
|
|
||||||
- tables
|
|
||||||
- pytest-random-order
|
|
||||||
- ccxt
|
|
||||||
- flake8-tidy-imports
|
|
||||||
- -e .
|
|
||||||
# - python-rapidjso
|
|
@ -1,19 +1,20 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2022.12.dev'
|
__version__ = '2023.4.dev'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
|
from pathlib import Path
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
freqtrade_basedir = Path(__file__).parent
|
||||||
|
|
||||||
__version__ = __version__ + '-' + subprocess.check_output(
|
__version__ = __version__ + '-' + subprocess.check_output(
|
||||||
['git', 'log', '--format="%h"', '-n 1'],
|
['git', 'log', '--format="%h"', '-n 1'],
|
||||||
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"')
|
||||||
|
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
# git not available, ignore
|
# git not available, ignore
|
||||||
try:
|
try:
|
||||||
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
||||||
from pathlib import Path
|
|
||||||
versionfile = Path('./freqtrade_commit')
|
versionfile = Path('./freqtrade_commit')
|
||||||
if versionfile.is_file():
|
if versionfile.is_file():
|
||||||
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
||||||
|
0
freqtrade/__main__.py
Normal file → Executable file
0
freqtrade/__main__.py
Normal file → Executable file
@ -22,5 +22,6 @@ from freqtrade.commands.optimize_commands import (start_backtesting, start_backt
|
|||||||
start_edge, start_hyperopt)
|
start_edge, start_hyperopt)
|
||||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||||
|
from freqtrade.commands.strategy_utils_commands import start_strategy_update
|
||||||
from freqtrade.commands.trade_commands import start_trading
|
from freqtrade.commands.trade_commands import start_trading
|
||||||
from freqtrade.commands.webserver_commands import start_webserver
|
from freqtrade.commands.webserver_commands import start_webserver
|
||||||
|
4
freqtrade/commands/analyze_commands.py
Executable file → Normal file
4
freqtrade/commands/analyze_commands.py
Executable file → Normal file
@ -40,8 +40,8 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
|
|||||||
|
|
||||||
if (not Path(signals_file).exists()):
|
if (not Path(signals_file).exists()):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
(f"Cannot find latest backtest signals file: {signals_file}."
|
f"Cannot find latest backtest signals file: {signals_file}."
|
||||||
"Run backtesting with `--export signals`.")
|
"Run backtesting with `--export signals`."
|
||||||
)
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
@ -111,10 +111,13 @@ ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason
|
|||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
|
||||||
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter",
|
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv",
|
||||||
|
"strategy-updater"]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
|
|
||||||
|
ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||||
|
|
||||||
|
|
||||||
class Arguments:
|
class Arguments:
|
||||||
"""
|
"""
|
||||||
@ -198,8 +201,8 @@ class Arguments:
|
|||||||
start_list_freqAI_models, start_list_markets,
|
start_list_freqAI_models, start_list_markets,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
start_new_config, start_new_strategy, start_plot_dataframe,
|
||||||
start_plot_profit, start_show_trades, start_test_pairlist,
|
start_plot_profit, start_show_trades, start_strategy_update,
|
||||||
start_trading, start_webserver)
|
start_test_pairlist, start_trading, start_webserver)
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='command',
|
subparsers = self.parser.add_subparsers(dest='command',
|
||||||
# Use custom message when no subhandler is added
|
# Use custom message when no subhandler is added
|
||||||
@ -440,3 +443,11 @@ class Arguments:
|
|||||||
parents=[_common_parser])
|
parents=[_common_parser])
|
||||||
webserver_cmd.set_defaults(func=start_webserver)
|
webserver_cmd.set_defaults(func=start_webserver)
|
||||||
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
|
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
|
||||||
|
|
||||||
|
# Add strategy_updater subcommand
|
||||||
|
strategy_updater_cmd = subparsers.add_parser('strategy-updater',
|
||||||
|
help='updates outdated strategy'
|
||||||
|
'files to the current version',
|
||||||
|
parents=[_common_parser])
|
||||||
|
strategy_updater_cmd.set_defaults(func=start_strategy_update)
|
||||||
|
self._build_args(optionlist=ARGS_STRATEGY_UTILS, parser=strategy_updater_cmd)
|
||||||
|
@ -108,7 +108,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
"bittrex",
|
||||||
"gateio",
|
"gate",
|
||||||
"huobi",
|
"huobi",
|
||||||
"kraken",
|
"kraken",
|
||||||
"kucoin",
|
"kucoin",
|
||||||
@ -123,7 +123,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
||||||
"default": False,
|
"default": False,
|
||||||
"filter": lambda val: 'futures' if val else 'spot',
|
"filter": lambda val: 'futures' if val else 'spot',
|
||||||
"when": lambda x: x["exchange_name"] in ['binance', 'gateio', 'okx'],
|
"when": lambda x: x["exchange_name"] in ['binance', 'gate', 'okx'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "autocomplete",
|
"type": "autocomplete",
|
||||||
|
@ -251,7 +251,8 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"spaces": Arg(
|
"spaces": Arg(
|
||||||
'--spaces',
|
'--spaces',
|
||||||
help='Specify which parameters to hyperopt. Space-separated list.',
|
help='Specify which parameters to hyperopt. Space-separated list.',
|
||||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
|
choices=['all', 'buy', 'sell', 'roi', 'stoploss',
|
||||||
|
'trailing', 'protection', 'trades', 'default'],
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default='default',
|
default='default',
|
||||||
),
|
),
|
||||||
@ -632,10 +633,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"1: by enter_tag, "
|
"1: by enter_tag, "
|
||||||
"2: by enter_tag and exit_tag, "
|
"2: by enter_tag and exit_tag, "
|
||||||
"3: by pair and enter_tag, "
|
"3: by pair and enter_tag, "
|
||||||
"4: by pair, enter_ and exit_tag (this can get quite large)"),
|
"4: by pair, enter_ and exit_tag (this can get quite large), "
|
||||||
|
"5: by exit_tag"),
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default=['0', '1', '2'],
|
default=['0', '1', '2'],
|
||||||
choices=['0', '1', '2', '3', '4'],
|
choices=['0', '1', '2', '3', '4', '5'],
|
||||||
),
|
),
|
||||||
"enter_reason_list": Arg(
|
"enter_reason_list": Arg(
|
||||||
"--enter-reason-list",
|
"--enter-reason-list",
|
||||||
|
@ -5,7 +5,7 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
|
||||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
||||||
refresh_backtest_trades_data)
|
refresh_backtest_trades_data)
|
||||||
@ -14,20 +14,30 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.exchange import market_is_active, timeframe_to_minutes
|
from freqtrade.exchange import market_is_active, timeframe_to_minutes
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
|
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _data_download_sanity(config: Config) -> None:
|
||||||
|
if 'days' in config and 'timerange' in config:
|
||||||
|
raise OperationalException("--days and --timerange are mutually exclusive. "
|
||||||
|
"You can only specify one or the other.")
|
||||||
|
|
||||||
|
if 'pairs' not in config:
|
||||||
|
raise OperationalException(
|
||||||
|
"Downloading data requires a list of pairs. "
|
||||||
|
"Please check the documentation on how to configure this.")
|
||||||
|
|
||||||
|
|
||||||
def start_download_data(args: Dict[str, Any]) -> None:
|
def start_download_data(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Download data (former download_backtest_data.py script)
|
Download data (former download_backtest_data.py script)
|
||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
if 'days' in config and 'timerange' in config:
|
_data_download_sanity(config)
|
||||||
raise OperationalException("--days and --timerange are mutually exclusive. "
|
|
||||||
"You can only specify one or the other.")
|
|
||||||
timerange = TimeRange()
|
timerange = TimeRange()
|
||||||
if 'days' in config:
|
if 'days' in config:
|
||||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||||
@ -39,11 +49,6 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||||
config['stake_currency'] = ''
|
config['stake_currency'] = ''
|
||||||
|
|
||||||
if 'pairs' not in config:
|
|
||||||
raise OperationalException(
|
|
||||||
"Downloading data requires a list of pairs. "
|
|
||||||
"Please check the documentation on how to configure this.")
|
|
||||||
|
|
||||||
pairs_not_available: List[str] = []
|
pairs_not_available: List[str] = []
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
@ -86,6 +91,7 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
"Please use `--dl-trades` instead for this exchange "
|
"Please use `--dl-trades` instead for this exchange "
|
||||||
"(will unfortunately take a long time)."
|
"(will unfortunately take a long time)."
|
||||||
)
|
)
|
||||||
|
migrate_binance_futures_data(config)
|
||||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
datadir=config['datadir'], timerange=timerange,
|
datadir=config['datadir'], timerange=timerange,
|
||||||
@ -145,6 +151,7 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
|||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
if ohlcv:
|
if ohlcv:
|
||||||
|
migrate_binance_futures_data(config)
|
||||||
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
|
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
|
||||||
for candle_type in candle_types:
|
for candle_type in candle_types:
|
||||||
convert_ohlcv_format(config,
|
convert_ohlcv_format(config,
|
||||||
@ -197,11 +204,14 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
|||||||
pair, timeframe, candle_type,
|
pair, timeframe, candle_type,
|
||||||
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
|
||||||
) for pair, timeframe, candle_type in paircombs]
|
) for pair, timeframe, candle_type in paircombs]
|
||||||
|
|
||||||
print(tabulate([
|
print(tabulate([
|
||||||
(pair, timeframe, candle_type,
|
(pair, timeframe, candle_type,
|
||||||
start.strftime(DATETIME_PRINT_FORMAT),
|
start.strftime(DATETIME_PRINT_FORMAT),
|
||||||
end.strftime(DATETIME_PRINT_FORMAT))
|
end.strftime(DATETIME_PRINT_FORMAT))
|
||||||
for pair, timeframe, candle_type, start, end in paircombs1
|
for pair, timeframe, candle_type, start, end in sorted(
|
||||||
|
paircombs1,
|
||||||
|
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
|
||||||
],
|
],
|
||||||
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
|
||||||
tablefmt='psql', stralign='right'))
|
tablefmt='psql', stralign='right'))
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
@ -20,7 +20,7 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
init_db(config['db_url'])
|
init_db(config['db_url'])
|
||||||
session_target = Trade._session
|
session_target = Trade.session
|
||||||
init_db(config['db_url_from'])
|
init_db(config['db_url_from'])
|
||||||
logger.info("Starting db migration.")
|
logger.info("Starting db migration.")
|
||||||
|
|
||||||
@ -36,16 +36,16 @@ def start_convert_db(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
session_target.commit()
|
session_target.commit()
|
||||||
|
|
||||||
for pairlock in PairLock.query:
|
for pairlock in PairLock.get_all_locks():
|
||||||
pairlock_count += 1
|
pairlock_count += 1
|
||||||
make_transient(pairlock)
|
make_transient(pairlock)
|
||||||
session_target.add(pairlock)
|
session_target.add(pairlock)
|
||||||
session_target.commit()
|
session_target.commit()
|
||||||
|
|
||||||
# Update sequences
|
# Update sequences
|
||||||
max_trade_id = session_target.query(func.max(Trade.id)).scalar()
|
max_trade_id = session_target.scalar(select(func.max(Trade.id)))
|
||||||
max_order_id = session_target.query(func.max(Order.id)).scalar()
|
max_order_id = session_target.scalar(select(func.max(Order.id)))
|
||||||
max_pairlock_id = session_target.query(func.max(PairLock.id)).scalar()
|
max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
|
||||||
|
|
||||||
set_sequence_ids(session_target.get_bind(),
|
set_sequence_ids(session_target.get_bind(),
|
||||||
trade_id=max_trade_id,
|
trade_id=max_trade_id,
|
||||||
|
0
freqtrade/commands/hyperopt_commands.py
Executable file → Normal file
0
freqtrade/commands/hyperopt_commands.py
Executable file → Normal file
55
freqtrade/commands/strategy_utils_commands.py
Normal file
55
freqtrade/commands/strategy_utils_commands.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
|
from freqtrade.enums import RunMode
|
||||||
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.strategy.strategyupdater import StrategyUpdater
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def start_strategy_update(args: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Start the strategy updating script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if sys.version_info == (3, 8): # pragma: no cover
|
||||||
|
sys.exit("Freqtrade strategy updater requires Python version >= 3.9")
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
|
config, enum_failed=False, recursive=config.get('recursive_strategy_search', False))
|
||||||
|
|
||||||
|
filtered_strategy_objs = []
|
||||||
|
if args['strategy_list']:
|
||||||
|
filtered_strategy_objs = [
|
||||||
|
strategy_obj for strategy_obj in strategy_objs
|
||||||
|
if strategy_obj['name'] in args['strategy_list']
|
||||||
|
]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Use all available entries.
|
||||||
|
filtered_strategy_objs = strategy_objs
|
||||||
|
|
||||||
|
processed_locations = set()
|
||||||
|
for strategy_obj in filtered_strategy_objs:
|
||||||
|
if strategy_obj['location'] not in processed_locations:
|
||||||
|
processed_locations.add(strategy_obj['location'])
|
||||||
|
start_conversion(strategy_obj, config)
|
||||||
|
|
||||||
|
|
||||||
|
def start_conversion(strategy_obj, config):
|
||||||
|
print(f"Conversion of {Path(strategy_obj['location']).name} started.")
|
||||||
|
instance_strategy_updater = StrategyUpdater()
|
||||||
|
start = time.perf_counter()
|
||||||
|
instance_strategy_updater.start(config, strategy_obj)
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
print(f"Conversion of {Path(strategy_obj['location']).name} took {elapsed:.1f} seconds.")
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import signal
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
@ -12,15 +13,20 @@ def start_trading(args: Dict[str, Any]) -> int:
|
|||||||
# Import here to avoid loading worker module when it's not used
|
# Import here to avoid loading worker module when it's not used
|
||||||
from freqtrade.worker import Worker
|
from freqtrade.worker import Worker
|
||||||
|
|
||||||
|
def term_handler(signum, frame):
|
||||||
|
# Raise KeyboardInterrupt - so we can handle it in the same way as Ctrl-C
|
||||||
|
raise KeyboardInterrupt()
|
||||||
|
|
||||||
# Create and run worker
|
# Create and run worker
|
||||||
worker = None
|
worker = None
|
||||||
try:
|
try:
|
||||||
|
signal.signal(signal.SIGTERM, term_handler)
|
||||||
worker = Worker(args)
|
worker = Worker(args)
|
||||||
worker.run()
|
worker.run()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
logger.exception("Fatal exception!")
|
logger.exception("Fatal exception!")
|
||||||
except KeyboardInterrupt:
|
except (KeyboardInterrupt):
|
||||||
logger.info('SIGINT received, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
finally:
|
finally:
|
||||||
if worker:
|
if worker:
|
||||||
|
@ -27,10 +27,7 @@ def _extend_validator(validator_class):
|
|||||||
if 'default' in subschema:
|
if 'default' in subschema:
|
||||||
instance.setdefault(prop, subschema['default'])
|
instance.setdefault(prop, subschema['default'])
|
||||||
|
|
||||||
for error in validate_properties(
|
yield from validate_properties(validator, properties, instance, schema)
|
||||||
validator, properties, instance, schema,
|
|
||||||
):
|
|
||||||
yield error
|
|
||||||
|
|
||||||
return validators.extend(
|
return validators.extend(
|
||||||
validator_class, {'properties': set_defaults}
|
validator_class, {'properties': set_defaults}
|
||||||
|
@ -28,7 +28,7 @@ class Configuration:
|
|||||||
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None:
|
def __init__(self, args: Dict[str, Any], runmode: Optional[RunMode] = None) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.config: Optional[Config] = None
|
self.config: Optional[Config] = None
|
||||||
self.runmode = runmode
|
self.runmode = runmode
|
||||||
|
@ -32,7 +32,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
|||||||
:param prefix: Prefix to consider (usually FREQTRADE__)
|
:param prefix: Prefix to consider (usually FREQTRADE__)
|
||||||
:return: Nested dict based on available and relevant variables.
|
:return: Nested dict based on available and relevant variables.
|
||||||
"""
|
"""
|
||||||
no_convert = ['CHAT_ID']
|
no_convert = ['CHAT_ID', 'PASSWORD']
|
||||||
relevant_vars: Dict[str, Any] = {}
|
relevant_vars: Dict[str, Any] = {}
|
||||||
|
|
||||||
for env_var, val in sorted(env_dict.items()):
|
for env_var, val in sorted(env_dict.items()):
|
||||||
|
@ -6,7 +6,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Read config from stdin if requested in the options
|
# Read config from stdin if requested in the options
|
||||||
with open(path) if path != '-' else sys.stdin as file:
|
with Path(path).open() if path != '-' else sys.stdin as file:
|
||||||
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
@ -75,7 +75,8 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]:
|
def load_from_files(
|
||||||
|
files: List[str], base_path: Optional[Path] = None, level: int = 0) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Recursively load configuration files if specified.
|
Recursively load configuration files if specified.
|
||||||
Sub-files are assumed to be relative to the initial config.
|
Sub-files are assumed to be relative to the initial config.
|
||||||
|
@ -116,7 +116,7 @@ class TimeRange:
|
|||||||
:param text: value from --timerange
|
:param text: value from --timerange
|
||||||
:return: Start and End range period
|
:return: Start and End range period
|
||||||
"""
|
"""
|
||||||
if text is None:
|
if not text:
|
||||||
return TimeRange(None, None, 0, 0)
|
return TimeRange(None, None, 0, 0)
|
||||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||||
(r'^(\d{8})-$', ('date', None)),
|
(r'^(\d{8})-$', ('date', None)),
|
||||||
|
@ -5,7 +5,7 @@ bot constants
|
|||||||
"""
|
"""
|
||||||
from typing import Any, Dict, List, Literal, Tuple
|
from typing import Any, Dict, List, Literal, Tuple
|
||||||
|
|
||||||
from freqtrade.enums import CandleType, RPCMessageType
|
from freqtrade.enums import CandleType, PriceType, RPCMessageType
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = 'config.json'
|
DEFAULT_CONFIG = 'config.json'
|
||||||
@ -25,19 +25,21 @@ PRICING_SIDES = ['ask', 'bid', 'same', 'other']
|
|||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
_ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO']
|
_ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO']
|
||||||
ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES]
|
ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES]
|
||||||
|
STOPLOSS_PRICE_TYPES = [p for p in PriceType]
|
||||||
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||||
'CalmarHyperOptLoss',
|
'CalmarHyperOptLoss',
|
||||||
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
|
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
|
||||||
'ProfitDrawDownHyperOptLoss']
|
'ProfitDrawDownHyperOptLoss']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList',
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'RemotePairList',
|
||||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
||||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5']
|
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet']
|
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
|
||||||
|
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
|
||||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||||
BACKTEST_CACHE_DEFAULT = 'day'
|
BACKTEST_CACHE_DEFAULT = 'day'
|
||||||
@ -62,6 +64,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels'
|
|||||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||||
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||||
FULL_DATAFRAME_THRESHOLD = 100
|
FULL_DATAFRAME_THRESHOLD = 100
|
||||||
|
CUSTOM_TAG_MAX_LENGTH = 255
|
||||||
|
|
||||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||||
|
|
||||||
@ -229,6 +232,7 @@ CONF_SCHEMA = {
|
|||||||
'default': 'market'},
|
'default': 'market'},
|
||||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss_on_exchange': {'type': 'boolean'},
|
'stoploss_on_exchange': {'type': 'boolean'},
|
||||||
|
'stoploss_price_type': {'type': 'string', 'enum': STOPLOSS_PRICE_TYPES},
|
||||||
'stoploss_on_exchange_interval': {'type': 'number'},
|
'stoploss_on_exchange_interval': {'type': 'number'},
|
||||||
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
|
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
|
||||||
'maximum': 1.0}
|
'maximum': 1.0}
|
||||||
@ -544,7 +548,7 @@ CONF_SCHEMA = {
|
|||||||
"enabled": {"type": "boolean", "default": False},
|
"enabled": {"type": "boolean", "default": False},
|
||||||
"keras": {"type": "boolean", "default": False},
|
"keras": {"type": "boolean", "default": False},
|
||||||
"write_metrics_to_disk": {"type": "boolean", "default": False},
|
"write_metrics_to_disk": {"type": "boolean", "default": False},
|
||||||
"purge_old_models": {"type": "boolean", "default": True},
|
"purge_old_models": {"type": ["boolean", "number"], "default": 2},
|
||||||
"conv_width": {"type": "integer", "default": 1},
|
"conv_width": {"type": "integer", "default": 1},
|
||||||
"train_period_days": {"type": "integer", "default": 0},
|
"train_period_days": {"type": "integer", "default": 0},
|
||||||
"backtest_period_days": {"type": "number", "default": 7},
|
"backtest_period_days": {"type": "number", "default": 7},
|
||||||
@ -566,7 +570,9 @@ CONF_SCHEMA = {
|
|||||||
"shuffle": {"type": "boolean", "default": False},
|
"shuffle": {"type": "boolean", "default": False},
|
||||||
"nu": {"type": "number", "default": 0.1}
|
"nu": {"type": "number", "default": 0.1}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
"shuffle_after_split": {"type": "boolean", "default": False},
|
||||||
|
"buffer_train_data_candles": {"type": "integer", "default": 0}
|
||||||
},
|
},
|
||||||
"required": ["include_timeframes", "include_corr_pairlist", ]
|
"required": ["include_timeframes", "include_corr_pairlist", ]
|
||||||
},
|
},
|
||||||
@ -584,6 +590,7 @@ CONF_SCHEMA = {
|
|||||||
"rl_config": {
|
"rl_config": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"drop_ohlc_from_features": {"type": "boolean", "default": False},
|
||||||
"train_cycles": {"type": "integer"},
|
"train_cycles": {"type": "integer"},
|
||||||
"max_trade_duration_candles": {"type": "integer"},
|
"max_trade_duration_candles": {"type": "integer"},
|
||||||
"add_state_info": {"type": "boolean", "default": False},
|
"add_state_info": {"type": "boolean", "default": False},
|
||||||
@ -592,7 +599,7 @@ CONF_SCHEMA = {
|
|||||||
"model_type": {"type": "string", "default": "PPO"},
|
"model_type": {"type": "string", "default": "PPO"},
|
||||||
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
"policy_type": {"type": "string", "default": "MlpPolicy"},
|
||||||
"net_arch": {"type": "array", "default": [128, 128]},
|
"net_arch": {"type": "array", "default": [128, 128]},
|
||||||
"randomize_startinng_position": {"type": "boolean", "default": False},
|
"randomize_starting_position": {"type": "boolean", "default": False},
|
||||||
"model_reward_parameters": {
|
"model_reward_parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -636,7 +643,6 @@ SCHEMA_TRADE_REQUIRED = [
|
|||||||
|
|
||||||
SCHEMA_BACKTEST_REQUIRED = [
|
SCHEMA_BACKTEST_REQUIRED = [
|
||||||
'exchange',
|
'exchange',
|
||||||
'max_open_trades',
|
|
||||||
'stake_currency',
|
'stake_currency',
|
||||||
'stake_amount',
|
'stake_amount',
|
||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
@ -646,6 +652,7 @@ SCHEMA_BACKTEST_REQUIRED = [
|
|||||||
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
|
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
|
||||||
'stoploss',
|
'stoploss',
|
||||||
'minimal_roi',
|
'minimal_roi',
|
||||||
|
'max_open_trades'
|
||||||
]
|
]
|
||||||
|
|
||||||
SCHEMA_MINIMAL_REQUIRED = [
|
SCHEMA_MINIMAL_REQUIRED = [
|
||||||
@ -679,5 +686,7 @@ EntryExit = Literal['entry', 'exit']
|
|||||||
BuySell = Literal['buy', 'sell']
|
BuySell = Literal['buy', 'sell']
|
||||||
MakerTaker = Literal['maker', 'taker']
|
MakerTaker = Literal['maker', 'taker']
|
||||||
BidAsk = Literal['bid', 'ask']
|
BidAsk = Literal['bid', 'ask']
|
||||||
|
OBLiteral = Literal['asks', 'bids']
|
||||||
|
|
||||||
Config = Dict[str, Any]
|
Config = Dict[str, Any]
|
||||||
|
IntOrInf = float
|
||||||
|
@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import json_load
|
from freqtrade.misc import json_load
|
||||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
@ -20,8 +20,8 @@ from freqtrade.persistence import LocalTrade, Trade, init_db
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Newest format
|
# Newest format
|
||||||
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'max_stake_amount', 'amount',
|
||||||
'open_rate', 'close_rate',
|
'open_date', 'close_date', 'open_rate', 'close_rate',
|
||||||
'fee_open', 'fee_close', 'trade_duration',
|
'fee_open', 'fee_close', 'trade_duration',
|
||||||
'profit_ratio', 'profit_abs', 'exit_reason',
|
'profit_ratio', 'profit_abs', 'exit_reason',
|
||||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||||
@ -90,7 +90,8 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
|
|||||||
return 'hyperopt_results.pickle'
|
return 'hyperopt_results.pickle'
|
||||||
|
|
||||||
|
|
||||||
def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = None) -> Path:
|
def get_latest_hyperopt_file(
|
||||||
|
directory: Union[Path, str], predef_filename: Optional[str] = None) -> Path:
|
||||||
"""
|
"""
|
||||||
Get latest hyperopt export based on '.last_result.json'.
|
Get latest hyperopt export based on '.last_result.json'.
|
||||||
:param directory: Directory to search for last result
|
:param directory: Directory to search for last result
|
||||||
@ -193,7 +194,7 @@ def get_backtest_resultlist(dirname: Path):
|
|||||||
|
|
||||||
|
|
||||||
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
||||||
min_backtest_date: datetime = None) -> Dict[str, Any]:
|
min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Find existing backtest stats that match specified run IDs and load them.
|
Find existing backtest stats that match specified run IDs and load them.
|
||||||
:param dirname: pathlib.Path object, or string pointing to the file.
|
:param dirname: pathlib.Path object, or string pointing to the file.
|
||||||
@ -241,6 +242,27 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Compatibility support for older backtest data.
|
||||||
|
"""
|
||||||
|
df['open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
||||||
|
df['close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
||||||
|
# Compatibility support for pre short Columns
|
||||||
|
if 'is_short' not in df.columns:
|
||||||
|
df['is_short'] = False
|
||||||
|
if 'leverage' not in df.columns:
|
||||||
|
df['leverage'] = 1.0
|
||||||
|
if 'enter_tag' not in df.columns:
|
||||||
|
df['enter_tag'] = df['buy_tag']
|
||||||
|
df = df.drop(['buy_tag'], axis=1)
|
||||||
|
if 'max_stake_amount' not in df.columns:
|
||||||
|
df['max_stake_amount'] = df['stake_amount']
|
||||||
|
if 'orders' not in df.columns:
|
||||||
|
df['orders'] = None
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Load backtest data file.
|
Load backtest data file.
|
||||||
@ -269,24 +291,7 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
|||||||
data = data['strategy'][strategy]['trades']
|
data = data['strategy'][strategy]['trades']
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df['open_date'] = pd.to_datetime(df['open_date'],
|
df = _load_backtest_data_df_compatibility(df)
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
df['close_date'] = pd.to_datetime(df['close_date'],
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
# Compatibility support for pre short Columns
|
|
||||||
if 'is_short' not in df.columns:
|
|
||||||
df['is_short'] = 0
|
|
||||||
if 'leverage' not in df.columns:
|
|
||||||
df['leverage'] = 1.0
|
|
||||||
if 'enter_tag' not in df.columns:
|
|
||||||
df['enter_tag'] = df['buy_tag']
|
|
||||||
df = df.drop(['buy_tag'], axis=1)
|
|
||||||
if 'orders' not in df.columns:
|
|
||||||
df['orders'] = None
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# old format - only with lists.
|
# old format - only with lists.
|
||||||
@ -322,7 +327,7 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
|
|||||||
|
|
||||||
|
|
||||||
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
||||||
max_open_trades: int) -> pd.DataFrame:
|
max_open_trades: IntOrInf) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Find overlapping trades by expanding each trade once per period it was open
|
Find overlapping trades by expanding each trade once per period it was open
|
||||||
and then counting overlaps
|
and then counting overlaps
|
||||||
@ -335,7 +340,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
|||||||
return df_final[df_final['open_trades'] > max_open_trades]
|
return df_final[df_final['open_trades'] > max_open_trades]
|
||||||
|
|
||||||
|
|
||||||
def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
|
def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Convert list of Trade objects to pandas Dataframe
|
Convert list of Trade objects to pandas Dataframe
|
||||||
:param trades: List of trade objects
|
:param trades: List of trade objects
|
||||||
@ -362,7 +367,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
|
|||||||
filters = []
|
filters = []
|
||||||
if strategy:
|
if strategy:
|
||||||
filters.append(Trade.strategy == strategy)
|
filters.append(Trade.strategy == strategy)
|
||||||
trades = trade_list_to_dataframe(Trade.get_trades(filters).all())
|
trades = trade_list_to_dataframe(list(Trade.get_trades(filters).all()))
|
||||||
|
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *,
|
|||||||
cols = DEFAULT_DATAFRAME_COLUMNS
|
cols = DEFAULT_DATAFRAME_COLUMNS
|
||||||
df = DataFrame(ohlcv, columns=cols)
|
df = DataFrame(ohlcv, columns=cols)
|
||||||
|
|
||||||
df['date'] = to_datetime(df['date'], unit='ms', utc=True, infer_datetime_format=True)
|
df['date'] = to_datetime(df['date'], unit='ms', utc=True)
|
||||||
|
|
||||||
# Some exchanges return int values for Volume and even for OHLC.
|
# Some exchanges return int values for Volume and even for OHLC.
|
||||||
# Convert them since TA-LIB indicators used in the strategy assume floats
|
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||||
|
@ -9,7 +9,7 @@ from collections import deque
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from pandas import DataFrame, to_timedelta
|
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes,
|
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes,
|
||||||
@ -18,8 +18,10 @@ from freqtrade.data.history import load_pair_history
|
|||||||
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
||||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||||
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
||||||
|
from freqtrade.exchange.types import OrderBook
|
||||||
from freqtrade.misc import append_candles_to_dataframe
|
from freqtrade.misc import append_candles_to_dataframe
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
|
from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg
|
||||||
from freqtrade.util import PeriodicCache
|
from freqtrade.util import PeriodicCache
|
||||||
|
|
||||||
|
|
||||||
@ -117,8 +119,7 @@ class DataProvider:
|
|||||||
:param new_candle: This is a new candle
|
:param new_candle: This is a new candle
|
||||||
"""
|
"""
|
||||||
if self.__rpc:
|
if self.__rpc:
|
||||||
self.__rpc.send_msg(
|
msg: RPCAnalyzedDFMsg = {
|
||||||
{
|
|
||||||
'type': RPCMessageType.ANALYZED_DF,
|
'type': RPCMessageType.ANALYZED_DF,
|
||||||
'data': {
|
'data': {
|
||||||
'key': pair_key,
|
'key': pair_key,
|
||||||
@ -126,7 +127,7 @@ class DataProvider:
|
|||||||
'la': datetime.now(timezone.utc)
|
'la': datetime.now(timezone.utc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
self.__rpc.send_msg(msg)
|
||||||
if new_candle:
|
if new_candle:
|
||||||
self.__rpc.send_msg({
|
self.__rpc.send_msg({
|
||||||
'type': RPCMessageType.NEW_CANDLE,
|
'type': RPCMessageType.NEW_CANDLE,
|
||||||
@ -206,9 +207,11 @@ class DataProvider:
|
|||||||
existing_df, _ = self.__producer_pairs_df[producer_name][pair_key]
|
existing_df, _ = self.__producer_pairs_df[producer_name][pair_key]
|
||||||
|
|
||||||
# CHECK FOR MISSING CANDLES
|
# CHECK FOR MISSING CANDLES
|
||||||
timeframe_delta = to_timedelta(timeframe) # Convert the timeframe to a timedelta for pandas
|
# Convert the timeframe to a timedelta for pandas
|
||||||
local_last = existing_df.iloc[-1]['date'] # We want the last date from our copy
|
timeframe_delta: Timedelta = to_timedelta(timeframe)
|
||||||
incoming_first = dataframe.iloc[0]['date'] # We want the first date from the incoming
|
local_last: Timestamp = existing_df.iloc[-1]['date'] # We want the last date from our copy
|
||||||
|
# We want the first date from the incoming
|
||||||
|
incoming_first: Timestamp = dataframe.iloc[0]['date']
|
||||||
|
|
||||||
# Remove existing candles that are newer than the incoming first candle
|
# Remove existing candles that are newer than the incoming first candle
|
||||||
existing_df1 = existing_df[existing_df['date'] < incoming_first]
|
existing_df1 = existing_df[existing_df['date'] < incoming_first]
|
||||||
@ -221,7 +224,7 @@ class DataProvider:
|
|||||||
# we missed some candles between our data and the incoming
|
# we missed some candles between our data and the incoming
|
||||||
# so return False and candle_difference.
|
# so return False and candle_difference.
|
||||||
if candle_difference > 1:
|
if candle_difference > 1:
|
||||||
return (False, candle_difference)
|
return (False, int(candle_difference))
|
||||||
if existing_df1.empty:
|
if existing_df1.empty:
|
||||||
appended_df = dataframe
|
appended_df = dataframe
|
||||||
else:
|
else:
|
||||||
@ -281,7 +284,7 @@ class DataProvider:
|
|||||||
def historic_ohlcv(
|
def historic_ohlcv(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str = None,
|
timeframe: Optional[str] = None,
|
||||||
candle_type: str = ''
|
candle_type: str = ''
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -333,7 +336,7 @@ class DataProvider:
|
|||||||
def get_pair_dataframe(
|
def get_pair_dataframe(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str = None,
|
timeframe: Optional[str] = None,
|
||||||
candle_type: str = ''
|
candle_type: str = ''
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -415,16 +418,14 @@ class DataProvider:
|
|||||||
|
|
||||||
def refresh(self,
|
def refresh(self,
|
||||||
pairlist: ListPairsWithTimeframes,
|
pairlist: ListPairsWithTimeframes,
|
||||||
helping_pairs: ListPairsWithTimeframes = None) -> None:
|
helping_pairs: Optional[ListPairsWithTimeframes] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Refresh data, called with each cycle
|
Refresh data, called with each cycle
|
||||||
"""
|
"""
|
||||||
if self._exchange is None:
|
if self._exchange is None:
|
||||||
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||||
if helping_pairs:
|
final_pairs = (pairlist + helping_pairs) if helping_pairs else pairlist
|
||||||
self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs)
|
self._exchange.refresh_latest_ohlcv(final_pairs)
|
||||||
else:
|
|
||||||
self._exchange.refresh_latest_ohlcv(pairlist)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available_pairs(self) -> ListPairsWithTimeframes:
|
def available_pairs(self) -> ListPairsWithTimeframes:
|
||||||
@ -439,7 +440,7 @@ class DataProvider:
|
|||||||
def ohlcv(
|
def ohlcv(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
timeframe: str = None,
|
timeframe: Optional[str] = None,
|
||||||
copy: bool = True,
|
copy: bool = True,
|
||||||
candle_type: str = ''
|
candle_type: str = ''
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
@ -487,7 +488,7 @@ class DataProvider:
|
|||||||
except ExchangeError:
|
except ExchangeError:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
|
def orderbook(self, pair: str, maximum: int) -> OrderBook:
|
||||||
"""
|
"""
|
||||||
Fetch latest l2 orderbook data
|
Fetch latest l2 orderbook data
|
||||||
Warning: Does a network request - so use with common sense.
|
Warning: Does a network request - so use with common sense.
|
||||||
|
12
freqtrade/data/entryexitanalysis.py
Executable file → Normal file
12
freqtrade/data/entryexitanalysis.py
Executable file → Normal file
@ -24,7 +24,7 @@ def _load_signal_candles(backtest_dir: Path):
|
|||||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
|
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scp = open(scpf, "rb")
|
with scpf.open("rb") as scp:
|
||||||
signal_candles = joblib.load(scp)
|
signal_candles = joblib.load(scp)
|
||||||
logger.info(f"Loaded signal candles: {str(scpf)}")
|
logger.info(f"Loaded signal candles: {str(scpf)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -52,7 +52,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
|||||||
return analysed_trades_dict
|
return analysed_trades_dict
|
||||||
|
|
||||||
|
|
||||||
def _analyze_candles_and_indicators(pair, trades, signal_candles):
|
def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: pd.DataFrame):
|
||||||
buyf = signal_candles
|
buyf = signal_candles
|
||||||
|
|
||||||
if len(buyf) > 0:
|
if len(buyf) > 0:
|
||||||
@ -120,7 +120,7 @@ def _do_group_table_output(bigdf, glist):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
||||||
'profit_ratio': ['sum', 'median', 'mean']}
|
'profit_ratio': ['median', 'mean', 'sum']}
|
||||||
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
|
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
|
||||||
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
|
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
|
||||||
'total_profit_pct']
|
'total_profit_pct']
|
||||||
@ -141,6 +141,12 @@ def _do_group_table_output(bigdf, glist):
|
|||||||
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||||
if g == "4":
|
if g == "4":
|
||||||
group_mask = ['pair', 'enter_reason', 'exit_reason']
|
group_mask = ['pair', 'enter_reason', 'exit_reason']
|
||||||
|
|
||||||
|
# 5: profit summaries grouped by exit_tag
|
||||||
|
if g == "5":
|
||||||
|
group_mask = ['exit_reason']
|
||||||
|
sortcols = ['exit_reason']
|
||||||
|
|
||||||
if group_mask:
|
if group_mask:
|
||||||
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
|
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
|
||||||
new.columns = group_mask + agg_cols
|
new.columns = group_mask + agg_cols
|
||||||
|
@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from pandas import DataFrame, read_feather, to_datetime
|
from pandas import DataFrame, read_feather, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@ -63,10 +63,7 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
pairdata.columns = self._columns
|
pairdata.columns = self._columns
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
pairdata['date'] = to_datetime(pairdata['date'],
|
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
@ -92,12 +89,11 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
:param data: List of Lists containing trade data,
|
:param data: List of Lists containing trade data,
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
self.create_dir_if_needed(filename)
|
||||||
|
|
||||||
raise NotImplementedError()
|
tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
|
||||||
# array = pa.array(data)
|
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
|
||||||
# array
|
|
||||||
# feather.write_feather(data, filename)
|
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: TradeList):
|
||||||
"""
|
"""
|
||||||
@ -116,14 +112,13 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
if not filename.exists():
|
||||||
# tradesdata = misc.file_load_json(filename)
|
return []
|
||||||
|
|
||||||
# if not tradesdata:
|
tradesdata = read_feather(filename)
|
||||||
# return []
|
|
||||||
|
|
||||||
# return tradesdata
|
return tradesdata.values.tolist()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
@ -28,8 +28,8 @@ def load_pair_history(pair: str,
|
|||||||
fill_up_missing: bool = True,
|
fill_up_missing: bool = True,
|
||||||
drop_incomplete: bool = False,
|
drop_incomplete: bool = False,
|
||||||
startup_candles: int = 0,
|
startup_candles: int = 0,
|
||||||
data_format: str = None,
|
data_format: Optional[str] = None,
|
||||||
data_handler: IDataHandler = None,
|
data_handler: Optional[IDataHandler] = None,
|
||||||
candle_type: CandleType = CandleType.SPOT
|
candle_type: CandleType = CandleType.SPOT
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -69,7 +69,7 @@ def load_data(datadir: Path,
|
|||||||
fail_without_data: bool = False,
|
fail_without_data: bool = False,
|
||||||
data_format: str = 'json',
|
data_format: str = 'json',
|
||||||
candle_type: CandleType = CandleType.SPOT,
|
candle_type: CandleType = CandleType.SPOT,
|
||||||
user_futures_funding_rate: int = None,
|
user_futures_funding_rate: Optional[int] = None,
|
||||||
) -> Dict[str, DataFrame]:
|
) -> Dict[str, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Load ohlcv history data for a list of pairs.
|
Load ohlcv history data for a list of pairs.
|
||||||
@ -116,7 +116,7 @@ def refresh_data(*, datadir: Path,
|
|||||||
timeframe: str,
|
timeframe: str,
|
||||||
pairs: List[str],
|
pairs: List[str],
|
||||||
exchange: Exchange,
|
exchange: Exchange,
|
||||||
data_format: str = None,
|
data_format: Optional[str] = None,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
candle_type: CandleType,
|
candle_type: CandleType,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -189,7 +189,7 @@ def _download_pair_history(pair: str, *,
|
|||||||
timeframe: str = '5m',
|
timeframe: str = '5m',
|
||||||
process: str = '',
|
process: str = '',
|
||||||
new_pairs_days: int = 30,
|
new_pairs_days: int = 30,
|
||||||
data_handler: IDataHandler = None,
|
data_handler: Optional[IDataHandler] = None,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
candle_type: CandleType,
|
candle_type: CandleType,
|
||||||
erase: bool = False,
|
erase: bool = False,
|
||||||
@ -272,7 +272,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
datadir: Path, trading_mode: str,
|
datadir: Path, trading_mode: str,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
new_pairs_days: int = 30, erase: bool = False,
|
new_pairs_days: int = 30, erase: bool = False,
|
||||||
data_format: str = None,
|
data_format: Optional[str] = None,
|
||||||
prepend: bool = False,
|
prepend: bool = False,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
|
@ -308,7 +308,7 @@ class IDataHandler(ABC):
|
|||||||
timerange=timerange_startup,
|
timerange=timerange_startup,
|
||||||
candle_type=candle_type
|
candle_type=candle_type
|
||||||
)
|
)
|
||||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True):
|
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
||||||
return pairdf
|
return pairdf
|
||||||
else:
|
else:
|
||||||
enddate = pairdf.iloc[-1]['date']
|
enddate = pairdf.iloc[-1]['date']
|
||||||
@ -316,7 +316,7 @@ class IDataHandler(ABC):
|
|||||||
if timerange_startup:
|
if timerange_startup:
|
||||||
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
|
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
|
||||||
pairdf = trim_dataframe(pairdf, timerange_startup)
|
pairdf = trim_dataframe(pairdf, timerange_startup)
|
||||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True):
|
||||||
return pairdf
|
return pairdf
|
||||||
|
|
||||||
# incomplete candles should only be dropped if we didn't trim the end beforehand.
|
# incomplete candles should only be dropped if we didn't trim the end beforehand.
|
||||||
@ -374,6 +374,21 @@ class IDataHandler(ABC):
|
|||||||
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
|
||||||
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
||||||
|
|
||||||
|
def rename_futures_data(
|
||||||
|
self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType):
|
||||||
|
"""
|
||||||
|
Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT)
|
||||||
|
Only used for binance to support the binance futures naming unification.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file_old = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
|
||||||
|
file_new = self._pair_data_filename(self._datadir, new_pair, timeframe, candle_type)
|
||||||
|
# print(file_old, file_new)
|
||||||
|
if file_new.exists():
|
||||||
|
logger.warning(f"{file_new} exists already, can't migrate {pair}.")
|
||||||
|
return
|
||||||
|
file_old.rename(file_new)
|
||||||
|
|
||||||
|
|
||||||
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
||||||
"""
|
"""
|
||||||
@ -403,8 +418,8 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
|||||||
raise ValueError(f"No datahandler for datatype {datatype} available.")
|
raise ValueError(f"No datahandler for datatype {datatype} available.")
|
||||||
|
|
||||||
|
|
||||||
def get_datahandler(datadir: Path, data_format: str = None,
|
def get_datahandler(datadir: Path, data_format: Optional[str] = None,
|
||||||
data_handler: IDataHandler = None) -> IDataHandler:
|
data_handler: Optional[IDataHandler] = None) -> IDataHandler:
|
||||||
"""
|
"""
|
||||||
:param datadir: Folder to save data
|
:param datadir: Folder to save data
|
||||||
:param data_format: dataformat to use
|
:param data_format: dataformat to use
|
||||||
|
@ -75,10 +75,7 @@ class JsonDataHandler(IDataHandler):
|
|||||||
return DataFrame(columns=self._columns)
|
return DataFrame(columns=self._columns)
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
pairdata['date'] = to_datetime(pairdata['date'],
|
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
|
@ -62,10 +62,7 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
pairdata.columns = self._columns
|
pairdata.columns = self._columns
|
||||||
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
|
||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
pairdata['date'] = to_datetime(pairdata['date'],
|
pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True)
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_append(
|
def ohlcv_append(
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -190,3 +192,119 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
|||||||
:return: CAGR
|
:return: CAGR
|
||||||
"""
|
"""
|
||||||
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_expectancy(trades: pd.DataFrame) -> float:
|
||||||
|
"""
|
||||||
|
Calculate expectancy
|
||||||
|
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||||
|
:return: expectancy
|
||||||
|
"""
|
||||||
|
if len(trades) == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
expectancy = 1
|
||||||
|
|
||||||
|
profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum()
|
||||||
|
loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum())
|
||||||
|
nb_win_trades = len(trades.loc[trades['profit_abs'] > 0])
|
||||||
|
nb_loss_trades = len(trades.loc[trades['profit_abs'] < 0])
|
||||||
|
|
||||||
|
if (nb_win_trades > 0) and (nb_loss_trades > 0):
|
||||||
|
average_win = profit_sum / nb_win_trades
|
||||||
|
average_loss = loss_sum / nb_loss_trades
|
||||||
|
risk_reward_ratio = average_win / average_loss
|
||||||
|
winrate = nb_win_trades / len(trades)
|
||||||
|
expectancy = ((1 + risk_reward_ratio) * winrate) - 1
|
||||||
|
elif nb_win_trades == 0:
|
||||||
|
expectancy = 0
|
||||||
|
|
||||||
|
return expectancy
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate sortino
|
||||||
|
:param trades: DataFrame containing trades (requires columns profit_abs)
|
||||||
|
:return: sortino
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'] / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
|
|
||||||
|
down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance)
|
||||||
|
|
||||||
|
if down_stdev != 0 and not np.isnan(down_stdev):
|
||||||
|
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
|
||||||
|
sortino_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, down_stdev, sortino_ratio)
|
||||||
|
return sortino_ratio
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate sharpe
|
||||||
|
:param trades: DataFrame containing trades (requires column profit_abs)
|
||||||
|
:return: sharpe
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'] / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
|
up_stdev = np.std(total_profit)
|
||||||
|
|
||||||
|
if up_stdev != 0:
|
||||||
|
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||||
|
sharp_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||||
|
return sharp_ratio
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
|
||||||
|
starting_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate calmar
|
||||||
|
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||||
|
:return: calmar
|
||||||
|
"""
|
||||||
|
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_profit = trades['profit_abs'].sum() / starting_balance
|
||||||
|
days_period = max(1, (max_date - min_date).days)
|
||||||
|
|
||||||
|
# adding slippage of 0.1% per trade
|
||||||
|
# total_profit = total_profit - 0.0005
|
||||||
|
expected_returns_mean = total_profit / days_period * 100
|
||||||
|
|
||||||
|
# calculate max drawdown
|
||||||
|
try:
|
||||||
|
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
|
||||||
|
trades, value_col="profit_abs", starting_balance=starting_balance
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
max_drawdown = 0
|
||||||
|
|
||||||
|
if max_drawdown != 0:
|
||||||
|
calmar_ratio = expected_returns_mean / max_drawdown * math.sqrt(365)
|
||||||
|
else:
|
||||||
|
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
|
||||||
|
calmar_ratio = -100
|
||||||
|
|
||||||
|
# print(expected_returns_mean, max_drawdown, calmar_ratio)
|
||||||
|
return calmar_ratio
|
||||||
|
@ -195,7 +195,7 @@ class Edge:
|
|||||||
|
|
||||||
def stake_amount(self, pair: str, free_capital: float,
|
def stake_amount(self, pair: str, free_capital: float,
|
||||||
total_capital: float, capital_in_trade: float) -> float:
|
total_capital: float, capital_in_trade: float) -> float:
|
||||||
stoploss = self.stoploss(pair)
|
stoploss = self.get_stoploss(pair)
|
||||||
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
|
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
|
||||||
allowed_capital_at_risk = available_capital * self._allowed_risk
|
allowed_capital_at_risk = available_capital * self._allowed_risk
|
||||||
max_position_size = abs(allowed_capital_at_risk / stoploss)
|
max_position_size = abs(allowed_capital_at_risk / stoploss)
|
||||||
@ -214,7 +214,7 @@ class Edge:
|
|||||||
)
|
)
|
||||||
return round(position_size, 15)
|
return round(position_size, 15)
|
||||||
|
|
||||||
def stoploss(self, pair: str) -> float:
|
def get_stoploss(self, pair: str) -> float:
|
||||||
if pair in self._cached_pairs:
|
if pair in self._cached_pairs:
|
||||||
return self._cached_pairs[pair].stoploss
|
return self._cached_pairs[pair].stoploss
|
||||||
else:
|
else:
|
||||||
|
@ -5,7 +5,9 @@ from freqtrade.enums.exitchecktuple import ExitCheckTuple
|
|||||||
from freqtrade.enums.exittype import ExitType
|
from freqtrade.enums.exittype import ExitType
|
||||||
from freqtrade.enums.hyperoptstate import HyperoptState
|
from freqtrade.enums.hyperoptstate import HyperoptState
|
||||||
from freqtrade.enums.marginmode import MarginMode
|
from freqtrade.enums.marginmode import MarginMode
|
||||||
|
from freqtrade.enums.marketstatetype import MarketDirection
|
||||||
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||||
|
from freqtrade.enums.pricetype import PriceType
|
||||||
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
|
||||||
|
@ -13,6 +13,9 @@ class CandleType(str, Enum):
|
|||||||
FUNDING_RATE = "funding_rate"
|
FUNDING_RATE = "funding_rate"
|
||||||
# BORROW_RATE = "borrow_rate" # * unimplemented
|
# BORROW_RATE = "borrow_rate" # * unimplemented
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name.lower()}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(value: str) -> 'CandleType':
|
def from_string(value: str) -> 'CandleType':
|
||||||
if not value:
|
if not value:
|
||||||
|
15
freqtrade/enums/marketstatetype.py
Normal file
15
freqtrade/enums/marketstatetype.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class MarketDirection(Enum):
|
||||||
|
"""
|
||||||
|
Enum for various market directions.
|
||||||
|
"""
|
||||||
|
LONG = "long"
|
||||||
|
SHORT = "short"
|
||||||
|
EVEN = "even"
|
||||||
|
NONE = "none"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# convert to string
|
||||||
|
return self.value
|
8
freqtrade/enums/pricetype.py
Normal file
8
freqtrade/enums/pricetype.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class PriceType(str, Enum):
|
||||||
|
"""Enum to distinguish possible trigger prices for stoplosses"""
|
||||||
|
LAST = "last"
|
||||||
|
MARK = "mark"
|
||||||
|
INDEX = "index"
|
@ -4,6 +4,7 @@ from enum import Enum
|
|||||||
class RPCMessageType(str, Enum):
|
class RPCMessageType(str, Enum):
|
||||||
STATUS = 'status'
|
STATUS = 'status'
|
||||||
WARNING = 'warning'
|
WARNING = 'warning'
|
||||||
|
EXCEPTION = 'exception'
|
||||||
STARTUP = 'startup'
|
STARTUP = 'startup'
|
||||||
|
|
||||||
ENTRY = 'entry'
|
ENTRY = 'entry'
|
||||||
@ -37,5 +38,8 @@ class RPCRequestType(str, Enum):
|
|||||||
WHITELIST = 'whitelist'
|
WHITELIST = 'whitelist'
|
||||||
ANALYZED_DF = 'analyzed_df'
|
ANALYZED_DF = 'analyzed_df'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
||||||
|
@ -10,6 +10,9 @@ class SignalType(Enum):
|
|||||||
ENTER_SHORT = "enter_short"
|
ENTER_SHORT = "enter_short"
|
||||||
EXIT_SHORT = "exit_short"
|
EXIT_SHORT = "exit_short"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name.lower()}"
|
||||||
|
|
||||||
|
|
||||||
class SignalTagType(Enum):
|
class SignalTagType(Enum):
|
||||||
"""
|
"""
|
||||||
@ -18,7 +21,13 @@ class SignalTagType(Enum):
|
|||||||
ENTER_TAG = "enter_tag"
|
ENTER_TAG = "enter_tag"
|
||||||
EXIT_TAG = "exit_tag"
|
EXIT_TAG = "exit_tag"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name.lower()}"
|
||||||
|
|
||||||
|
|
||||||
class SignalDirection(str, Enum):
|
class SignalDirection(str, Enum):
|
||||||
LONG = 'long'
|
LONG = 'long'
|
||||||
SHORT = 'short'
|
SHORT = 'short'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name.lower()}"
|
||||||
|
@ -8,16 +8,16 @@ from freqtrade.exchange.bitpanda import Bitpanda
|
|||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
from freqtrade.exchange.bybit import Bybit
|
from freqtrade.exchange.bybit import Bybit
|
||||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||||
from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts,
|
from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision,
|
||||||
amount_to_precision, available_exchanges,
|
amount_to_contracts, amount_to_precision,
|
||||||
ccxt_exchanges, contracts_to_amount,
|
available_exchanges, ccxt_exchanges,
|
||||||
date_minus_candles, is_exchange_known_ccxt,
|
contracts_to_amount, date_minus_candles,
|
||||||
market_is_active, price_to_precision,
|
is_exchange_known_ccxt, market_is_active,
|
||||||
timeframe_to_minutes, timeframe_to_msecs,
|
price_to_precision, timeframe_to_minutes,
|
||||||
timeframe_to_next_date, timeframe_to_prev_date,
|
timeframe_to_msecs, timeframe_to_next_date,
|
||||||
timeframe_to_seconds, validate_exchange,
|
timeframe_to_prev_date, timeframe_to_seconds,
|
||||||
validate_exchanges)
|
validate_exchange, validate_exchanges)
|
||||||
from freqtrade.exchange.gateio import Gateio
|
from freqtrade.exchange.gate import Gate
|
||||||
from freqtrade.exchange.hitbtc import Hitbtc
|
from freqtrade.exchange.hitbtc import Hitbtc
|
||||||
from freqtrade.exchange.huobi import Huobi
|
from freqtrade.exchange.huobi import Huobi
|
||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
|
@ -7,11 +7,11 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import OHLCVResponse, Tickers
|
||||||
from freqtrade.misc import deep_merge_dicts, json_load
|
from freqtrade.misc import deep_merge_dicts, json_load
|
||||||
|
|
||||||
|
|
||||||
@ -23,16 +23,22 @@ class Binance(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||||
"order_time_in_force": ['GTC', 'FOK', 'IOC'],
|
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "fromId",
|
"trades_pagination_arg": "fromId",
|
||||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||||
"ccxt_futures_name": "future"
|
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||||
|
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
||||||
"tickers_have_price": False,
|
"tickers_have_price": False,
|
||||||
|
"floor_leverage": True,
|
||||||
|
"stop_price_type_field": "workingType",
|
||||||
|
"stop_price_type_value_mapping": {
|
||||||
|
PriceType.LAST: "CONTRACT_PRICE",
|
||||||
|
PriceType.MARK: "MARK_PRICE",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
@ -78,33 +84,9 @@ class Binance(Exchange):
|
|||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
|
||||||
except ccxt.BaseError as e:
|
) from e
|
||||||
raise OperationalException(e) from e
|
|
||||||
|
|
||||||
@retrier
|
|
||||||
def _set_leverage(
|
|
||||||
self,
|
|
||||||
leverage: float,
|
|
||||||
pair: Optional[str] = None,
|
|
||||||
trading_mode: Optional[TradingMode] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Set's the leverage before making a trade, in order to not
|
|
||||||
have the same leverage on every trade
|
|
||||||
"""
|
|
||||||
trading_mode = trading_mode or self.trading_mode
|
|
||||||
|
|
||||||
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._api.set_leverage(symbol=pair, leverage=round(leverage))
|
|
||||||
except ccxt.DDoSProtection as e:
|
|
||||||
raise DDosProtection(e) from e
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
|
||||||
raise TemporaryError(
|
|
||||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@ -112,7 +94,7 @@ class Binance(Exchange):
|
|||||||
since_ms: int, candle_type: CandleType,
|
since_ms: int, candle_type: CandleType,
|
||||||
is_new_pair: bool = False, raise_: bool = False,
|
is_new_pair: bool = False, raise_: bool = False,
|
||||||
until_ms: Optional[int] = None
|
until_ms: Optional[int] = None
|
||||||
) -> Tuple[str, str, str, List]:
|
) -> OHLCVResponse:
|
||||||
"""
|
"""
|
||||||
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||||
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||||
@ -150,6 +132,7 @@ class Binance(Exchange):
|
|||||||
is_short: bool,
|
is_short: bool,
|
||||||
amount: float,
|
amount: float,
|
||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
|
leverage: float,
|
||||||
wallet_balance: float, # Or margin balance
|
wallet_balance: float, # Or margin balance
|
||||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
@ -159,11 +142,12 @@ class Binance(Exchange):
|
|||||||
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
|
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
|
||||||
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
|
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
|
||||||
|
|
||||||
:param exchange_name:
|
:param pair: Pair to calculate liquidation price for
|
||||||
:param open_rate: Entry price of position
|
:param open_rate: Entry price of position
|
||||||
:param is_short: True if the trade is a short, false otherwise
|
:param is_short: True if the trade is a short, false otherwise
|
||||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||||
|
:param leverage: Leverage used for this position.
|
||||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||||
:param margin_mode: Either ISOLATED or CROSS
|
:param margin_mode: Either ISOLATED or CROSS
|
||||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||||
@ -212,7 +196,7 @@ class Binance(Exchange):
|
|||||||
leverage_tiers_path = (
|
leverage_tiers_path = (
|
||||||
Path(__file__).parent / 'binance_leverage_tiers.json'
|
Path(__file__).parent / 'binance_leverage_tiers.json'
|
||||||
)
|
)
|
||||||
with open(leverage_tiers_path) as json_file:
|
with leverage_tiers_path.open() as json_file:
|
||||||
return json_load(json_file)
|
return json_load(json_file)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,16 @@
|
|||||||
""" Bybit exchange subclass """
|
""" Bybit exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Tuple
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.constants import BuySell
|
||||||
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.exchange.common import retrier
|
||||||
|
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -20,18 +27,27 @@ class Bybit(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 200,
|
||||||
"ccxt_futures_name": "linear",
|
|
||||||
"ohlcv_has_history": False,
|
"ohlcv_has_history": False,
|
||||||
}
|
}
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"ohlcv_has_history": True,
|
"ohlcv_has_history": True,
|
||||||
|
"mark_ohlcv_timeframe": "4h",
|
||||||
|
"funding_fee_timeframe": "8h",
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
|
"stop_price_type_field": "triggerBy",
|
||||||
|
"stop_price_type_value_mapping": {
|
||||||
|
PriceType.LAST: "LastPrice",
|
||||||
|
PriceType.MARK: "MarkPrice",
|
||||||
|
PriceType.INDEX: "IndexPrice",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
# (TradingMode.FUTURES, MarginMode.CROSS),
|
# (TradingMode.FUTURES, MarginMode.CROSS),
|
||||||
# (TradingMode.FUTURES, MarginMode.ISOLATED)
|
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -47,3 +63,158 @@ class Bybit(Exchange):
|
|||||||
})
|
})
|
||||||
config.update(super()._ccxt_config)
|
config.update(super()._ccxt_config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
def market_is_future(self, market: Dict[str, Any]) -> bool:
|
||||||
|
main = super().market_is_future(market)
|
||||||
|
# For ByBit, we'll only support USDT markets for now.
|
||||||
|
return (
|
||||||
|
main and market['settle'] == 'USDT'
|
||||||
|
)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def additional_exchange_init(self) -> None:
|
||||||
|
"""
|
||||||
|
Additional exchange initialization logic.
|
||||||
|
.api will be available at this point.
|
||||||
|
Must be overridden in child methods if required.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
|
||||||
|
position_mode = self._api.set_position_mode(False)
|
||||||
|
self._log_exchange_response('set_position_mode', position_mode)
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
|
||||||
|
) from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
async def _fetch_funding_rate_history(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str,
|
||||||
|
limit: int,
|
||||||
|
since_ms: Optional[int] = None,
|
||||||
|
) -> List[List]:
|
||||||
|
"""
|
||||||
|
Fetch funding rate history
|
||||||
|
Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed.
|
||||||
|
"""
|
||||||
|
params = {}
|
||||||
|
if since_ms:
|
||||||
|
until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit'])
|
||||||
|
params.update({'until': until})
|
||||||
|
# Funding rate
|
||||||
|
data = await self._api_async.fetch_funding_rate_history(
|
||||||
|
pair, since=since_ms,
|
||||||
|
params=params)
|
||||||
|
# Convert funding rate to candle pattern
|
||||||
|
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||||
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
|
params = {'leverage': leverage}
|
||||||
|
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
|
||||||
|
self._set_leverage(leverage, pair, accept_fail=True)
|
||||||
|
|
||||||
|
def _get_params(
|
||||||
|
self,
|
||||||
|
side: BuySell,
|
||||||
|
ordertype: str,
|
||||||
|
leverage: float,
|
||||||
|
reduceOnly: bool,
|
||||||
|
time_in_force: str = 'GTC',
|
||||||
|
) -> Dict:
|
||||||
|
params = super()._get_params(
|
||||||
|
side=side,
|
||||||
|
ordertype=ordertype,
|
||||||
|
leverage=leverage,
|
||||||
|
reduceOnly=reduceOnly,
|
||||||
|
time_in_force=time_in_force,
|
||||||
|
)
|
||||||
|
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
|
||||||
|
params['position_idx'] = 0
|
||||||
|
return params
|
||||||
|
|
||||||
|
def dry_run_liquidation_price(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
open_rate: float, # Entry price of position
|
||||||
|
is_short: bool,
|
||||||
|
amount: float,
|
||||||
|
stake_amount: float,
|
||||||
|
leverage: float,
|
||||||
|
wallet_balance: float, # Or margin balance
|
||||||
|
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
|
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
|
) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||||
|
PERPETUAL:
|
||||||
|
bybit:
|
||||||
|
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
|
||||||
|
|
||||||
|
Long:
|
||||||
|
Liquidation Price = (
|
||||||
|
Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate)
|
||||||
|
- Extra Margin Added/ Contract)
|
||||||
|
Short:
|
||||||
|
Liquidation Price = (
|
||||||
|
Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate)
|
||||||
|
+ Extra Margin Added/ Contract)
|
||||||
|
|
||||||
|
Implementation Note: Extra margin is currently not used.
|
||||||
|
|
||||||
|
:param pair: Pair to calculate liquidation price for
|
||||||
|
:param open_rate: Entry price of position
|
||||||
|
:param is_short: True if the trade is a short, false otherwise
|
||||||
|
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||||
|
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||||
|
:param leverage: Leverage used for this position.
|
||||||
|
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||||
|
:param margin_mode: Either ISOLATED or CROSS
|
||||||
|
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||||
|
Cross-Margin Mode: crossWalletBalance
|
||||||
|
Isolated-Margin Mode: isolatedWalletBalance
|
||||||
|
"""
|
||||||
|
|
||||||
|
market = self.markets[pair]
|
||||||
|
mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
|
||||||
|
|
||||||
|
if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
|
||||||
|
|
||||||
|
if market['inverse']:
|
||||||
|
raise OperationalException(
|
||||||
|
"Freqtrade does not yet support inverse contracts")
|
||||||
|
initial_margin_rate = 1 / leverage
|
||||||
|
|
||||||
|
# See docstring - ignores extra margin!
|
||||||
|
if is_short:
|
||||||
|
return open_rate * (1 + initial_margin_rate - mm_ratio)
|
||||||
|
else:
|
||||||
|
return open_rate * (1 - initial_margin_rate + mm_ratio)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise OperationalException(
|
||||||
|
"Freqtrade only supports isolated futures for leverage trading")
|
||||||
|
|
||||||
|
def get_funding_fees(
|
||||||
|
self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float:
|
||||||
|
"""
|
||||||
|
Fetch funding fees, either from the exchange (live) or calculates them
|
||||||
|
based on funding rate/mark price history
|
||||||
|
:param pair: The quote/base pair of the trade
|
||||||
|
:param is_short: trade direction
|
||||||
|
:param amount: Trade amount
|
||||||
|
:param open_date: Open date of the trade
|
||||||
|
:return: funding fee since open_date
|
||||||
|
:raises: ExchangeError if something goes wrong.
|
||||||
|
"""
|
||||||
|
# Bybit does not provide "applied" funding fees per position.
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
return self._fetch_and_calculate_funding_fees(
|
||||||
|
pair, amount, is_short, open_date)
|
||||||
|
return 0.0
|
||||||
|
@ -46,13 +46,13 @@ MAP_EXCHANGE_CHILDCLASS = {
|
|||||||
'binanceje': 'binance',
|
'binanceje': 'binance',
|
||||||
'binanceusdm': 'binance',
|
'binanceusdm': 'binance',
|
||||||
'okex': 'okx',
|
'okex': 'okx',
|
||||||
'gate': 'gateio',
|
'gateio': 'gate',
|
||||||
}
|
}
|
||||||
|
|
||||||
SUPPORTED_EXCHANGES = [
|
SUPPORTED_EXCHANGES = [
|
||||||
'binance',
|
'binance',
|
||||||
'bittrex',
|
'bittrex',
|
||||||
'gateio',
|
'gate',
|
||||||
'huobi',
|
'huobi',
|
||||||
'kraken',
|
'kraken',
|
||||||
'okx',
|
'okx',
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
Cryptocurrency Exchanges support
|
Cryptocurrency Exchanges support
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import http
|
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from math import floor
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
|
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
@ -21,22 +21,24 @@ from pandas import DataFrame, concat
|
|||||||
|
|
||||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
|
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
|
||||||
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
|
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
|
||||||
PairWithTimeframe)
|
OBLiteral, PairWithTimeframe)
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
||||||
|
from freqtrade.enums.pricetype import PriceType
|
||||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, OperationalException, PricingError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
|
||||||
retrier_async)
|
retrier_async)
|
||||||
from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision,
|
from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType,
|
||||||
amount_to_contracts, amount_to_precision,
|
amount_to_contract_precision, amount_to_contracts,
|
||||||
contracts_to_amount, date_minus_candles,
|
amount_to_precision, contracts_to_amount,
|
||||||
is_exchange_known_ccxt, market_is_active,
|
date_minus_candles, is_exchange_known_ccxt,
|
||||||
price_to_precision, timeframe_to_minutes,
|
market_is_active, price_to_precision,
|
||||||
timeframe_to_msecs, timeframe_to_next_date,
|
timeframe_to_minutes, timeframe_to_msecs,
|
||||||
timeframe_to_prev_date, timeframe_to_seconds)
|
timeframe_to_next_date, timeframe_to_prev_date,
|
||||||
from freqtrade.exchange.types import Ticker, Tickers
|
timeframe_to_seconds)
|
||||||
|
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
|
||||||
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
|
||||||
safe_value_fallback2)
|
safe_value_fallback2)
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
@ -45,12 +47,6 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Workaround for adding samesite support to pre 3.8 python
|
|
||||||
# Only applies to python3.7, and only on certain exchanges (kraken)
|
|
||||||
# Replicates the fix from starlette (which is actually causing this problem)
|
|
||||||
http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Exchange:
|
class Exchange:
|
||||||
|
|
||||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||||
@ -64,8 +60,8 @@ class Exchange:
|
|||||||
# or by specifying them in the configuration.
|
# or by specifying them in the configuration.
|
||||||
_ft_has_default: Dict = {
|
_ft_has_default: Dict = {
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
|
"stop_price_param": "stopPrice",
|
||||||
"order_time_in_force": ["GTC"],
|
"order_time_in_force": ["GTC"],
|
||||||
"time_in_force_parameter": "timeInForce",
|
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_candle_limit": 500,
|
"ohlcv_candle_limit": 500,
|
||||||
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||||
@ -74,6 +70,7 @@ class Exchange:
|
|||||||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||||
"tickers_have_quoteVolume": True,
|
"tickers_have_quoteVolume": True,
|
||||||
|
"tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers
|
||||||
"tickers_have_price": True,
|
"tickers_have_price": True,
|
||||||
"trades_pagination": "time", # Possible are "time" or "id"
|
"trades_pagination": "time", # Possible are "time" or "id"
|
||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
@ -85,6 +82,8 @@ class Exchange:
|
|||||||
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
||||||
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
|
||||||
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||||
|
# Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
|
||||||
|
"marketOrderRequiresPrice": False,
|
||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
_ft_has_futures: Dict = {}
|
_ft_has_futures: Dict = {}
|
||||||
@ -210,6 +209,8 @@ class Exchange:
|
|||||||
and self._api_async.session):
|
and self._api_async.session):
|
||||||
logger.debug("Closing async ccxt session.")
|
logger.debug("Closing async ccxt session.")
|
||||||
self.loop.run_until_complete(self._api_async.close())
|
self.loop.run_until_complete(self._api_async.close())
|
||||||
|
if self.loop and not self.loop.is_closed():
|
||||||
|
self.loop.close()
|
||||||
|
|
||||||
def validate_config(self, config):
|
def validate_config(self, config):
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
@ -474,7 +475,7 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
if self._api_async:
|
if self._api_async:
|
||||||
self.loop.run_until_complete(
|
self.loop.run_until_complete(
|
||||||
self._api_async.load_markets(reload=reload))
|
self._api_async.load_markets(reload=reload, params={}))
|
||||||
|
|
||||||
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
||||||
logger.warning('Could not load async markets. Reason: %s', e)
|
logger.warning('Could not load async markets. Reason: %s', e)
|
||||||
@ -483,7 +484,7 @@ class Exchange:
|
|||||||
def _load_markets(self) -> None:
|
def _load_markets(self) -> None:
|
||||||
""" Initialize markets both sync and async """
|
""" Initialize markets both sync and async """
|
||||||
try:
|
try:
|
||||||
self._markets = self._api.load_markets()
|
self._markets = self._api.load_markets(params={})
|
||||||
self._load_async_markets()
|
self._load_async_markets()
|
||||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||||
if self._ft_has['needs_trading_fees']:
|
if self._ft_has['needs_trading_fees']:
|
||||||
@ -501,7 +502,7 @@ class Exchange:
|
|||||||
return None
|
return None
|
||||||
logger.debug("Performing scheduled market reload..")
|
logger.debug("Performing scheduled market reload..")
|
||||||
try:
|
try:
|
||||||
self._markets = self._api.load_markets(reload=True)
|
self._markets = self._api.load_markets(reload=True, params={})
|
||||||
# Also reload async markets to avoid issues with newly listed pairs
|
# Also reload async markets to avoid issues with newly listed pairs
|
||||||
self._load_async_markets(reload=True)
|
self._load_async_markets(reload=True)
|
||||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||||
@ -606,12 +607,27 @@ class Exchange:
|
|||||||
if not self.exchange_has('createMarketOrder'):
|
if not self.exchange_has('createMarketOrder'):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self.name} does not support market orders.')
|
f'Exchange {self.name} does not support market orders.')
|
||||||
|
self.validate_stop_ordertypes(order_types)
|
||||||
|
|
||||||
|
def validate_stop_ordertypes(self, order_types: Dict) -> None:
|
||||||
|
"""
|
||||||
|
Validate stoploss order types
|
||||||
|
"""
|
||||||
if (order_types.get("stoploss_on_exchange")
|
if (order_types.get("stoploss_on_exchange")
|
||||||
and not self._ft_has.get("stoploss_on_exchange", False)):
|
and not self._ft_has.get("stoploss_on_exchange", False)):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'On exchange stoploss is not supported for {self.name}.'
|
f'On exchange stoploss is not supported for {self.name}.'
|
||||||
)
|
)
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
price_mapping = self._ft_has.get('stop_price_type_value_mapping', {}).keys()
|
||||||
|
if (
|
||||||
|
order_types.get("stoploss_on_exchange", False) is True
|
||||||
|
and 'stoploss_price_type' in order_types
|
||||||
|
and order_types['stoploss_price_type'] not in price_mapping
|
||||||
|
):
|
||||||
|
raise OperationalException(
|
||||||
|
f'On exchange stoploss price type is not supported for {self.name}.'
|
||||||
|
)
|
||||||
|
|
||||||
def validate_pricing(self, pricing: Dict) -> None:
|
def validate_pricing(self, pricing: Dict) -> None:
|
||||||
if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
|
if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
|
||||||
@ -682,7 +698,7 @@ class Exchange:
|
|||||||
f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
|
f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_option(self, param: str, default: Any = None) -> Any:
|
def get_option(self, param: str, default: Optional[Any] = None) -> Any:
|
||||||
"""
|
"""
|
||||||
Get parameter value from _ft_has
|
Get parameter value from _ft_has
|
||||||
"""
|
"""
|
||||||
@ -720,12 +736,14 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
|
return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
|
||||||
|
|
||||||
def price_to_precision(self, pair: str, price: float) -> float:
|
def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
|
||||||
"""
|
"""
|
||||||
Returns the price rounded up to the precision the Exchange accepts.
|
Returns the price rounded to the precision the Exchange accepts.
|
||||||
Rounds up
|
The default price_rounding_mode in conf is ROUND.
|
||||||
|
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||||
"""
|
"""
|
||||||
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
|
return price_to_precision(price, self.get_precision_price(pair),
|
||||||
|
self.precisionMode, rounding_mode=rounding_mode)
|
||||||
|
|
||||||
def price_get_one_pip(self, pair: str, price: float) -> float:
|
def price_get_one_pip(self, pair: str, price: float) -> float:
|
||||||
"""
|
"""
|
||||||
@ -748,12 +766,12 @@ class Exchange:
|
|||||||
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
|
return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
|
||||||
|
|
||||||
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
|
def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
|
||||||
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
|
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage)
|
||||||
if max_stake_amount is None:
|
if max_stake_amount is None:
|
||||||
# * Should never be executed
|
# * Should never be executed
|
||||||
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
|
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
|
||||||
'never set max_stake_amount to None')
|
'never set max_stake_amount to None')
|
||||||
return max_stake_amount / leverage
|
return max_stake_amount
|
||||||
|
|
||||||
def _get_stake_amount_limit(
|
def _get_stake_amount_limit(
|
||||||
self,
|
self,
|
||||||
@ -771,43 +789,41 @@ class Exchange:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(f"Can't get market information for symbol {pair}")
|
raise ValueError(f"Can't get market information for symbol {pair}")
|
||||||
|
|
||||||
|
if isMin:
|
||||||
|
# reserve some percent defined in config (5% default) + stoploss
|
||||||
|
margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent',
|
||||||
|
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||||
|
stoploss_reserve = (
|
||||||
|
margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||||
|
)
|
||||||
|
# it should not be more than 50%
|
||||||
|
stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
|
||||||
|
else:
|
||||||
|
margin_reserve = 1.0
|
||||||
|
stoploss_reserve = 1.0
|
||||||
|
|
||||||
stake_limits = []
|
stake_limits = []
|
||||||
limits = market['limits']
|
limits = market['limits']
|
||||||
if (limits['cost'][limit] is not None):
|
if (limits['cost'][limit] is not None):
|
||||||
stake_limits.append(
|
stake_limits.append(
|
||||||
self._contracts_to_amount(
|
self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve
|
||||||
pair,
|
|
||||||
limits['cost'][limit]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (limits['amount'][limit] is not None):
|
if (limits['amount'][limit] is not None):
|
||||||
stake_limits.append(
|
stake_limits.append(
|
||||||
self._contracts_to_amount(
|
self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve
|
||||||
pair,
|
|
||||||
limits['amount'][limit] * price
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not stake_limits:
|
if not stake_limits:
|
||||||
return None if isMin else float('inf')
|
return None if isMin else float('inf')
|
||||||
|
|
||||||
# reserve some percent defined in config (5% default) + stoploss
|
|
||||||
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
|
|
||||||
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
|
||||||
amount_reserve_percent = (
|
|
||||||
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
|
||||||
)
|
|
||||||
# it should not be more than 50%
|
|
||||||
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
|
|
||||||
|
|
||||||
# The value returned should satisfy both limits: for amount (base currency) and
|
# The value returned should satisfy both limits: for amount (base currency) and
|
||||||
# for cost (quote, stake currency), so max() is used here.
|
# for cost (quote, stake currency), so max() is used here.
|
||||||
# See also #2575 at github.
|
# See also #2575 at github.
|
||||||
return self._get_stake_amount_considering_leverage(
|
return self._get_stake_amount_considering_leverage(
|
||||||
max(stake_limits) * amount_reserve_percent,
|
max(stake_limits) if isMin else min(stake_limits),
|
||||||
leverage or 1.0
|
leverage or 1.0
|
||||||
) if isMin else min(stake_limits)
|
)
|
||||||
|
|
||||||
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
|
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
|
||||||
"""
|
"""
|
||||||
@ -840,7 +856,7 @@ class Exchange:
|
|||||||
'remaining': _amount,
|
'remaining': _amount,
|
||||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'status': "closed" if ordertype == "market" and not stop_loss else "open",
|
'status': "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
'info': {},
|
'info': {},
|
||||||
'leverage': leverage
|
'leverage': leverage
|
||||||
@ -850,20 +866,33 @@ class Exchange:
|
|||||||
dry_order["stopPrice"] = dry_order["price"]
|
dry_order["stopPrice"] = dry_order["price"]
|
||||||
# Workaround to avoid filling stoploss orders immediately
|
# Workaround to avoid filling stoploss orders immediately
|
||||||
dry_order["ft_order_type"] = "stoploss"
|
dry_order["ft_order_type"] = "stoploss"
|
||||||
|
orderbook: Optional[OrderBook] = None
|
||||||
|
if self.exchange_has('fetchL2OrderBook'):
|
||||||
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
|
if ordertype == "limit" and orderbook:
|
||||||
|
# Allow a 3% price difference
|
||||||
|
allowed_diff = 0.03
|
||||||
|
if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
|
||||||
|
logger.info(
|
||||||
|
f"Converted order {pair} to market order due to price {rate} crossing spread "
|
||||||
|
f"by more than {allowed_diff:.2%}.")
|
||||||
|
dry_order["type"] = "market"
|
||||||
|
|
||||||
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||||
# Update market order pricing
|
# Update market order pricing
|
||||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
|
||||||
dry_order.update({
|
dry_order.update({
|
||||||
'average': average,
|
'average': average,
|
||||||
'filled': _amount,
|
'filled': _amount,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
|
'status': "closed",
|
||||||
'cost': (dry_order['amount'] * average) / leverage
|
'cost': (dry_order['amount'] * average) / leverage
|
||||||
})
|
})
|
||||||
# market orders will always incurr taker fees
|
# market orders will always incurr taker fees
|
||||||
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
|
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
|
||||||
|
|
||||||
dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True)
|
dry_order = self.check_dry_limit_order_filled(
|
||||||
|
dry_order, immediate=True, orderbook=orderbook)
|
||||||
|
|
||||||
self._dry_run_open_orders[dry_order["id"]] = dry_order
|
self._dry_run_open_orders[dry_order["id"]] = dry_order
|
||||||
# Copy order and close it - so the returned order is open unless it's a market order
|
# Copy order and close it - so the returned order is open unless it's a market order
|
||||||
@ -885,20 +914,22 @@ class Exchange:
|
|||||||
})
|
})
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float:
|
def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float,
|
||||||
|
orderbook: Optional[OrderBook]) -> float:
|
||||||
"""
|
"""
|
||||||
Get the market order fill price based on orderbook interpolation
|
Get the market order fill price based on orderbook interpolation
|
||||||
"""
|
"""
|
||||||
if self.exchange_has('fetchL2OrderBook'):
|
if self.exchange_has('fetchL2OrderBook'):
|
||||||
ob = self.fetch_l2_order_book(pair, 20)
|
if not orderbook:
|
||||||
ob_type = 'asks' if side == 'buy' else 'bids'
|
orderbook = self.fetch_l2_order_book(pair, 20)
|
||||||
|
ob_type: OBLiteral = 'asks' if side == 'buy' else 'bids'
|
||||||
slippage = 0.05
|
slippage = 0.05
|
||||||
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
|
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
|
||||||
|
|
||||||
remaining_amount = amount
|
remaining_amount = amount
|
||||||
filled_amount = 0.0
|
filled_amount = 0.0
|
||||||
book_entry_price = 0.0
|
book_entry_price = 0.0
|
||||||
for book_entry in ob[ob_type]:
|
for book_entry in orderbook[ob_type]:
|
||||||
book_entry_price = book_entry[0]
|
book_entry_price = book_entry[0]
|
||||||
book_entry_coin_volume = book_entry[1]
|
book_entry_coin_volume = book_entry[1]
|
||||||
if remaining_amount > 0:
|
if remaining_amount > 0:
|
||||||
@ -926,20 +957,20 @@ class Exchange:
|
|||||||
|
|
||||||
return rate
|
return rate
|
||||||
|
|
||||||
def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool:
|
def _dry_is_price_crossed(self, pair: str, side: str, limit: float,
|
||||||
|
orderbook: Optional[OrderBook] = None, offset: float = 0.0) -> bool:
|
||||||
if not self.exchange_has('fetchL2OrderBook'):
|
if not self.exchange_has('fetchL2OrderBook'):
|
||||||
return True
|
return True
|
||||||
ob = self.fetch_l2_order_book(pair, 1)
|
if not orderbook:
|
||||||
|
orderbook = self.fetch_l2_order_book(pair, 1)
|
||||||
try:
|
try:
|
||||||
if side == 'buy':
|
if side == 'buy':
|
||||||
price = ob['asks'][0][0]
|
price = orderbook['asks'][0][0]
|
||||||
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
if limit * (1 - offset) >= price:
|
||||||
if limit >= price:
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
price = ob['bids'][0][0]
|
price = orderbook['bids'][0][0]
|
||||||
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
if limit * (1 + offset) <= price:
|
||||||
if limit <= price:
|
|
||||||
return True
|
return True
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# Ignore empty orderbooks when filling - can be filled with the next iteration.
|
# Ignore empty orderbooks when filling - can be filled with the next iteration.
|
||||||
@ -947,7 +978,8 @@ class Exchange:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def check_dry_limit_order_filled(
|
def check_dry_limit_order_filled(
|
||||||
self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]:
|
self, order: Dict[str, Any], immediate: bool = False,
|
||||||
|
orderbook: Optional[OrderBook] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Check dry-run limit order fill and update fee (if it filled).
|
Check dry-run limit order fill and update fee (if it filled).
|
||||||
"""
|
"""
|
||||||
@ -955,7 +987,7 @@ class Exchange:
|
|||||||
and order['type'] in ["limit"]
|
and order['type'] in ["limit"]
|
||||||
and not order.get('ft_order_type')):
|
and not order.get('ft_order_type')):
|
||||||
pair = order['symbol']
|
pair = order['symbol']
|
||||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
if self._dry_is_price_crossed(pair, order['side'], order['price'], orderbook):
|
||||||
order.update({
|
order.update({
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'filled': order['amount'],
|
'filled': order['amount'],
|
||||||
@ -992,10 +1024,10 @@ class Exchange:
|
|||||||
|
|
||||||
# Order handling
|
# Order handling
|
||||||
|
|
||||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||||
if self.trading_mode != TradingMode.SPOT:
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
self.set_margin_mode(pair, self.margin_mode)
|
self.set_margin_mode(pair, self.margin_mode, accept_fail)
|
||||||
self._set_leverage(leverage, pair)
|
self._set_leverage(leverage, pair, accept_fail)
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
@ -1007,12 +1039,18 @@ class Exchange:
|
|||||||
) -> Dict:
|
) -> Dict:
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
if time_in_force != 'GTC' and ordertype != 'market':
|
if time_in_force != 'GTC' and ordertype != 'market':
|
||||||
param = self._ft_has.get('time_in_force_parameter', '')
|
params.update({'timeInForce': time_in_force.upper()})
|
||||||
params.update({param: time_in_force.upper()})
|
|
||||||
if reduceOnly:
|
if reduceOnly:
|
||||||
params.update({'reduceOnly': True})
|
params.update({'reduceOnly': True})
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def _order_needs_price(self, ordertype: str) -> bool:
|
||||||
|
return (
|
||||||
|
ordertype != 'market'
|
||||||
|
or self._api.options.get("createMarketBuyOrderRequiresPrice", False)
|
||||||
|
or self._ft_has.get('marketOrderRequiresPrice', False)
|
||||||
|
)
|
||||||
|
|
||||||
def create_order(
|
def create_order(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@ -1035,8 +1073,7 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||||
needs_price = (ordertype != 'market'
|
needs_price = self._order_needs_price(ordertype)
|
||||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
|
||||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||||
|
|
||||||
if not reduceOnly:
|
if not reduceOnly:
|
||||||
@ -1060,7 +1097,7 @@ class Exchange:
|
|||||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise ExchangeError(
|
raise InvalidOrderException(
|
||||||
f'Could not create {ordertype} {side} order on market {pair}. '
|
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to {side} amount {amount} at rate {rate}. '
|
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
@ -1079,11 +1116,11 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
if not self._ft_has.get('stoploss_on_exchange'):
|
if not self._ft_has.get('stoploss_on_exchange'):
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
price_param = self._ft_has['stop_price_param']
|
||||||
return (
|
return (
|
||||||
order.get('stopPrice', None) is None
|
order.get(price_param, None) is None
|
||||||
or ((side == "sell" and stop_loss > float(order['stopPrice'])) or
|
or ((side == "sell" and stop_loss > float(order[price_param])) or
|
||||||
(side == "buy" and stop_loss < float(order['stopPrice'])))
|
(side == "buy" and stop_loss < float(order[price_param])))
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
||||||
@ -1110,18 +1147,25 @@ class Exchange:
|
|||||||
"sell" else (stop_price >= limit_rate))
|
"sell" else (stop_price >= limit_rate))
|
||||||
# Ensure rate is less than stop price
|
# Ensure rate is less than stop price
|
||||||
if bad_stop_price:
|
if bad_stop_price:
|
||||||
raise OperationalException(
|
# This can for example happen if the stop / liquidation price is set to 0
|
||||||
'In stoploss limit order, stop price should be more than limit price')
|
# Which is possible if a market-order closes right away.
|
||||||
|
# The InvalidOrderException will bubble up to exit_positions, where it will be
|
||||||
|
# handled gracefully.
|
||||||
|
raise InvalidOrderException(
|
||||||
|
"In stoploss limit order, stop price should be more than limit price. "
|
||||||
|
f"Stop price: {stop_price}, Limit price: {limit_rate}, "
|
||||||
|
f"Limit Price pct: {limit_price_pct}"
|
||||||
|
)
|
||||||
return limit_rate
|
return limit_rate
|
||||||
|
|
||||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
# Verify if stopPrice works for your exchange!
|
# Verify if stopPrice works for your exchange, else configure stop_price_param
|
||||||
params.update({'stopPrice': stop_price})
|
params.update({self._ft_has['stop_price_param']: stop_price})
|
||||||
return params
|
return params
|
||||||
|
|
||||||
@retrier(retries=0)
|
@retrier(retries=0)
|
||||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
|
def create_stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
|
||||||
side: BuySell, leverage: float) -> Dict:
|
side: BuySell, leverage: float) -> Dict:
|
||||||
"""
|
"""
|
||||||
creates a stoploss order.
|
creates a stoploss order.
|
||||||
@ -1143,12 +1187,12 @@ class Exchange:
|
|||||||
|
|
||||||
user_order_type = order_types.get('stoploss', 'market')
|
user_order_type = order_types.get('stoploss', 'market')
|
||||||
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
||||||
|
round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
|
||||||
stop_price_norm = self.price_to_precision(pair, stop_price)
|
stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
|
||||||
limit_rate = None
|
limit_rate = None
|
||||||
if user_order_type == 'limit':
|
if user_order_type == 'limit':
|
||||||
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
||||||
limit_rate = self.price_to_precision(pair, limit_rate)
|
limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
|
||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
@ -1167,10 +1211,14 @@ class Exchange:
|
|||||||
stop_price=stop_price_norm)
|
stop_price=stop_price_norm)
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
params['reduceOnly'] = True
|
params['reduceOnly'] = True
|
||||||
|
if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has:
|
||||||
|
price_type = self._ft_has['stop_price_type_value_mapping'][
|
||||||
|
order_types.get('stoploss_price_type', PriceType.LAST)]
|
||||||
|
params[self._ft_has['stop_price_type_field']] = price_type
|
||||||
|
|
||||||
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||||
|
|
||||||
self._lev_prep(pair, leverage, side)
|
self._lev_prep(pair, leverage, side, accept_fail=True)
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||||
amount=amount, price=limit_rate, params=params)
|
amount=amount, price=limit_rate, params=params)
|
||||||
self._log_exchange_response('create_stoploss_order', order)
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
@ -1357,7 +1405,7 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_positions(self, pair: str = None) -> List[Dict]:
|
def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Fetch positions from the exchange.
|
Fetch positions from the exchange.
|
||||||
If no pair is given, all positions are returned.
|
If no pair is given, all positions are returned.
|
||||||
@ -1497,7 +1545,7 @@ class Exchange:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> OrderBook:
|
||||||
"""
|
"""
|
||||||
Get L2 order book from exchange.
|
Get L2 order book from exchange.
|
||||||
Can be limited to a certain amount (if supported).
|
Can be limited to a certain amount (if supported).
|
||||||
@ -1540,7 +1588,7 @@ class Exchange:
|
|||||||
|
|
||||||
def get_rate(self, pair: str, refresh: bool,
|
def get_rate(self, pair: str, refresh: bool,
|
||||||
side: EntryExit, is_short: bool,
|
side: EntryExit, is_short: bool,
|
||||||
order_book: Optional[dict] = None, ticker: Optional[Ticker] = None) -> float:
|
order_book: Optional[OrderBook] = None, ticker: Optional[Ticker] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates bid/ask target
|
Calculates bid/ask target
|
||||||
bid rate - between current ask price and last price
|
bid rate - between current ask price and last price
|
||||||
@ -1578,7 +1626,8 @@ class Exchange:
|
|||||||
logger.debug('order_book %s', order_book)
|
logger.debug('order_book %s', order_book)
|
||||||
# top 1 = index 0
|
# top 1 = index 0
|
||||||
try:
|
try:
|
||||||
rate = order_book[f"{price_side}s"][order_book_top - 1][0]
|
obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
|
||||||
|
rate = order_book[obside][order_book_top - 1][0]
|
||||||
except (IndexError, KeyError) as e:
|
except (IndexError, KeyError) as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{pair} - {name} Price at location {order_book_top} from orderbook "
|
f"{pair} - {name} Price at location {order_book_top} from orderbook "
|
||||||
@ -1705,7 +1754,7 @@ class Exchange:
|
|||||||
return self._config['fee']
|
return self._config['fee']
|
||||||
# validate that markets are loaded before trying to get fee
|
# validate that markets are loaded before trying to get fee
|
||||||
if self._api.markets is None or len(self._api.markets) == 0:
|
if self._api.markets is None or len(self._api.markets) == 0:
|
||||||
self._api.load_markets()
|
self._api.load_markets(params={})
|
||||||
|
|
||||||
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
|
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
|
||||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||||
@ -1801,7 +1850,7 @@ class Exchange:
|
|||||||
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, candle_type: CandleType,
|
since_ms: int, candle_type: CandleType,
|
||||||
is_new_pair: bool = False,
|
is_new_pair: bool = False,
|
||||||
until_ms: int = None) -> List:
|
until_ms: Optional[int] = None) -> List:
|
||||||
"""
|
"""
|
||||||
Get candle history using asyncio and returns the list of candles.
|
Get candle history using asyncio and returns the list of candles.
|
||||||
Handles all async work for this.
|
Handles all async work for this.
|
||||||
@ -1813,32 +1862,18 @@ class Exchange:
|
|||||||
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
:return: List with candle (OHLCV) data
|
:return: List with candle (OHLCV) data
|
||||||
"""
|
"""
|
||||||
pair, _, _, data = self.loop.run_until_complete(
|
pair, _, _, data, _ = self.loop.run_until_complete(
|
||||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||||
since_ms=since_ms, until_ms=until_ms,
|
since_ms=since_ms, until_ms=until_ms,
|
||||||
is_new_pair=is_new_pair, candle_type=candle_type))
|
is_new_pair=is_new_pair, candle_type=candle_type))
|
||||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
|
||||||
since_ms: int, candle_type: CandleType) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe
|
|
||||||
:param pair: Pair to download
|
|
||||||
:param timeframe: Timeframe to get data for
|
|
||||||
:param since_ms: Timestamp in milliseconds to get history from
|
|
||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
|
||||||
:return: OHLCV DataFrame
|
|
||||||
"""
|
|
||||||
ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms, candle_type=candle_type)
|
|
||||||
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
|
||||||
drop_incomplete=self._ohlcv_partial_candle)
|
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, candle_type: CandleType,
|
since_ms: int, candle_type: CandleType,
|
||||||
is_new_pair: bool = False, raise_: bool = False,
|
is_new_pair: bool = False, raise_: bool = False,
|
||||||
until_ms: Optional[int] = None
|
until_ms: Optional[int] = None
|
||||||
) -> Tuple[str, str, str, List]:
|
) -> OHLCVResponse:
|
||||||
"""
|
"""
|
||||||
Download historic ohlcv
|
Download historic ohlcv
|
||||||
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||||
@ -1869,15 +1904,16 @@ class Exchange:
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
# Deconstruct tuple if it's not an exception
|
# Deconstruct tuple if it's not an exception
|
||||||
p, _, c, new_data = res
|
p, _, c, new_data, _ = res
|
||||||
if p == pair and c == candle_type:
|
if p == pair and c == candle_type:
|
||||||
data.extend(new_data)
|
data.extend(new_data)
|
||||||
# Sort data again after extending the result - above calls return in "async order"
|
# Sort data again after extending the result - above calls return in "async order"
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
return pair, timeframe, candle_type, data
|
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
|
||||||
|
|
||||||
def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType,
|
def _build_coroutine(
|
||||||
since_ms: Optional[int], cache: bool) -> Coroutine:
|
self, pair: str, timeframe: str, candle_type: CandleType,
|
||||||
|
since_ms: Optional[int], cache: bool) -> Coroutine[Any, Any, OHLCVResponse]:
|
||||||
not_all_data = cache and self.required_candle_call_count > 1
|
not_all_data = cache and self.required_candle_call_count > 1
|
||||||
if cache and (pair, timeframe, candle_type) in self._klines:
|
if cache and (pair, timeframe, candle_type) in self._klines:
|
||||||
candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
|
candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
|
||||||
@ -1914,7 +1950,7 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Build Coroutines to execute as part of refresh_latest_ohlcv
|
Build Coroutines to execute as part of refresh_latest_ohlcv
|
||||||
"""
|
"""
|
||||||
input_coroutines = []
|
input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = []
|
||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
for pair, timeframe, candle_type in set(pair_list):
|
for pair, timeframe, candle_type in set(pair_list):
|
||||||
if (timeframe not in self.timeframes
|
if (timeframe not in self.timeframes
|
||||||
@ -1943,7 +1979,8 @@ class Exchange:
|
|||||||
cache: bool, drop_incomplete: bool) -> DataFrame:
|
cache: bool, drop_incomplete: bool) -> DataFrame:
|
||||||
# keeping last candle time as last refreshed time of the pair
|
# keeping last candle time as last refreshed time of the pair
|
||||||
if ticks and cache:
|
if ticks and cache:
|
||||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000
|
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
|
||||||
|
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] // 1000
|
||||||
# keeping parsed dataframe in cache
|
# keeping parsed dataframe in cache
|
||||||
ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||||
drop_incomplete=drop_incomplete)
|
drop_incomplete=drop_incomplete)
|
||||||
@ -1978,7 +2015,6 @@ class Exchange:
|
|||||||
:return: Dict of [{(pair, timeframe): Dataframe}]
|
:return: Dict of [{(pair, timeframe): Dataframe}]
|
||||||
"""
|
"""
|
||||||
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
||||||
drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete
|
|
||||||
|
|
||||||
# Gather coroutines to run
|
# Gather coroutines to run
|
||||||
input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
|
input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
|
||||||
@ -1996,10 +2032,11 @@ class Exchange:
|
|||||||
if isinstance(res, Exception):
|
if isinstance(res, Exception):
|
||||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||||
continue
|
continue
|
||||||
# Deconstruct tuple (has 4 elements)
|
# Deconstruct tuple (has 5 elements)
|
||||||
pair, timeframe, c_type, ticks = res
|
pair, timeframe, c_type, ticks, drop_hint = res
|
||||||
|
drop_incomplete_ = drop_hint if drop_incomplete is None else drop_incomplete
|
||||||
ohlcv_df = self._process_ohlcv_df(
|
ohlcv_df = self._process_ohlcv_df(
|
||||||
pair, timeframe, c_type, ticks, cache, drop_incomplete)
|
pair, timeframe, c_type, ticks, cache, drop_incomplete_)
|
||||||
|
|
||||||
results_df[(pair, timeframe, c_type)] = ohlcv_df
|
results_df[(pair, timeframe, c_type)] = ohlcv_df
|
||||||
|
|
||||||
@ -2016,7 +2053,9 @@ class Exchange:
|
|||||||
# Timeframe in seconds
|
# Timeframe in seconds
|
||||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||||
plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
|
plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
|
||||||
return plr < arrow.utcnow().int_timestamp
|
# current,active candle open date
|
||||||
|
now = int(timeframe_to_prev_date(timeframe).timestamp())
|
||||||
|
return plr < now
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
async def _async_get_candle_history(
|
async def _async_get_candle_history(
|
||||||
@ -2025,7 +2064,7 @@ class Exchange:
|
|||||||
timeframe: str,
|
timeframe: str,
|
||||||
candle_type: CandleType,
|
candle_type: CandleType,
|
||||||
since_ms: Optional[int] = None,
|
since_ms: Optional[int] = None,
|
||||||
) -> Tuple[str, str, str, List]:
|
) -> OHLCVResponse:
|
||||||
"""
|
"""
|
||||||
Asynchronously get candle history data using fetch_ohlcv
|
Asynchronously get candle history data using fetch_ohlcv
|
||||||
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
@ -2035,8 +2074,8 @@ class Exchange:
|
|||||||
# Fetch OHLCV asynchronously
|
# Fetch OHLCV asynchronously
|
||||||
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Fetching pair %s, interval %s, since %s %s...",
|
"Fetching pair %s, %s, interval %s, since %s %s...",
|
||||||
pair, timeframe, since_ms, s
|
pair, candle_type, timeframe, since_ms, s
|
||||||
)
|
)
|
||||||
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
||||||
candle_limit = self.ohlcv_candle_limit(
|
candle_limit = self.ohlcv_candle_limit(
|
||||||
@ -2050,11 +2089,12 @@ class Exchange:
|
|||||||
limit=candle_limit, params=params)
|
limit=candle_limit, params=params)
|
||||||
else:
|
else:
|
||||||
# Funding rate
|
# Funding rate
|
||||||
data = await self._api_async.fetch_funding_rate_history(
|
data = await self._fetch_funding_rate_history(
|
||||||
pair, since=since_ms,
|
pair=pair,
|
||||||
limit=candle_limit)
|
timeframe=timeframe,
|
||||||
# Convert funding rate to candle pattern
|
limit=candle_limit,
|
||||||
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
since_ms=since_ms,
|
||||||
|
)
|
||||||
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
||||||
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
|
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
|
||||||
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
|
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
|
||||||
@ -2064,9 +2104,9 @@ class Exchange:
|
|||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.exception("Error loading %s. Result was %s.", pair, data)
|
logger.exception("Error loading %s. Result was %s.", pair, data)
|
||||||
return pair, timeframe, candle_type, []
|
return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
|
||||||
logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
|
logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
|
||||||
return pair, timeframe, candle_type, data
|
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
|
||||||
|
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
@ -2082,6 +2122,24 @@ class Exchange:
|
|||||||
raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
|
raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
|
||||||
f'for pair {pair}. Message: {e}') from e
|
f'for pair {pair}. Message: {e}') from e
|
||||||
|
|
||||||
|
async def _fetch_funding_rate_history(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
timeframe: str,
|
||||||
|
limit: int,
|
||||||
|
since_ms: Optional[int] = None,
|
||||||
|
) -> List[List]:
|
||||||
|
"""
|
||||||
|
Fetch funding rate history - used to selectively override this by subclasses.
|
||||||
|
"""
|
||||||
|
# Funding rate
|
||||||
|
data = await self._api_async.fetch_funding_rate_history(
|
||||||
|
pair, since=since_ms,
|
||||||
|
limit=limit)
|
||||||
|
# Convert funding rate to candle pattern
|
||||||
|
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
||||||
|
return data
|
||||||
|
|
||||||
# Fetch historic trades
|
# Fetch historic trades
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
@ -2485,7 +2543,7 @@ class Exchange:
|
|||||||
self,
|
self,
|
||||||
leverage: float,
|
leverage: float,
|
||||||
pair: Optional[str] = None,
|
pair: Optional[str] = None,
|
||||||
trading_mode: Optional[TradingMode] = None
|
accept_fail: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Set's the leverage before making a trade, in order to not
|
Set's the leverage before making a trade, in order to not
|
||||||
@ -2494,12 +2552,18 @@ class Exchange:
|
|||||||
if self._config['dry_run'] or not self.exchange_has("setLeverage"):
|
if self._config['dry_run'] or not self.exchange_has("setLeverage"):
|
||||||
# Some exchanges only support one margin_mode type
|
# Some exchanges only support one margin_mode type
|
||||||
return
|
return
|
||||||
|
if self._ft_has.get('floor_leverage', False) is True:
|
||||||
|
# Rounding for binance ...
|
||||||
|
leverage = floor(leverage)
|
||||||
try:
|
try:
|
||||||
res = self._api.set_leverage(symbol=pair, leverage=leverage)
|
res = self._api.set_leverage(symbol=pair, leverage=leverage)
|
||||||
self._log_exchange_response('set_leverage', res)
|
self._log_exchange_response('set_leverage', res)
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
|
||||||
|
if not accept_fail:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
@ -2521,7 +2585,8 @@ class Exchange:
|
|||||||
return open_date.minute > 0 or open_date.second > 0
|
return open_date.minute > 0 or open_date.second > 0
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def set_margin_mode(self, pair: str, margin_mode: MarginMode, params: dict = {}):
|
def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
|
||||||
|
params: dict = {}):
|
||||||
"""
|
"""
|
||||||
Set's the margin mode on the exchange to cross or isolated for a specific pair
|
Set's the margin mode on the exchange to cross or isolated for a specific pair
|
||||||
:param pair: base/quote currency pair (e.g. "ADA/USDT")
|
:param pair: base/quote currency pair (e.g. "ADA/USDT")
|
||||||
@ -2535,6 +2600,10 @@ class Exchange:
|
|||||||
self._log_exchange_response('set_margin_mode', res)
|
self._log_exchange_response('set_margin_mode', res)
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
|
except ccxt.BadRequest as e:
|
||||||
|
if not accept_fail:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
@ -2668,7 +2737,7 @@ class Exchange:
|
|||||||
:param amount: Trade amount
|
:param amount: Trade amount
|
||||||
:param open_date: Open date of the trade
|
:param open_date: Open date of the trade
|
||||||
:return: funding fee since open_date
|
:return: funding fee since open_date
|
||||||
:raies: ExchangeError if something goes wrong.
|
:raises: ExchangeError if something goes wrong.
|
||||||
"""
|
"""
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
@ -2688,6 +2757,7 @@ class Exchange:
|
|||||||
is_short: bool,
|
is_short: bool,
|
||||||
amount: float, # Absolute value of position size
|
amount: float, # Absolute value of position size
|
||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
|
leverage: float,
|
||||||
wallet_balance: float,
|
wallet_balance: float,
|
||||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
@ -2701,14 +2771,15 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
|
f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
|
||||||
|
|
||||||
isolated_liq = None
|
liquidation_price = None
|
||||||
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
|
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
|
||||||
|
|
||||||
isolated_liq = self.dry_run_liquidation_price(
|
liquidation_price = self.dry_run_liquidation_price(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
open_rate=open_rate,
|
open_rate=open_rate,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
|
leverage=leverage,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
wallet_balance=wallet_balance,
|
wallet_balance=wallet_balance,
|
||||||
mm_ex_1=mm_ex_1,
|
mm_ex_1=mm_ex_1,
|
||||||
@ -2718,16 +2789,16 @@ class Exchange:
|
|||||||
positions = self.fetch_positions(pair)
|
positions = self.fetch_positions(pair)
|
||||||
if len(positions) > 0:
|
if len(positions) > 0:
|
||||||
pos = positions[0]
|
pos = positions[0]
|
||||||
isolated_liq = pos['liquidationPrice']
|
liquidation_price = pos['liquidationPrice']
|
||||||
|
|
||||||
if isolated_liq:
|
if liquidation_price is not None:
|
||||||
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
|
buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
|
||||||
isolated_liq = (
|
liquidation_price_buffer = (
|
||||||
isolated_liq - buffer_amount
|
liquidation_price - buffer_amount
|
||||||
if is_short else
|
if is_short else
|
||||||
isolated_liq + buffer_amount
|
liquidation_price + buffer_amount
|
||||||
)
|
)
|
||||||
return isolated_liq
|
return max(liquidation_price_buffer, 0.0)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -2738,6 +2809,7 @@ class Exchange:
|
|||||||
is_short: bool,
|
is_short: bool,
|
||||||
amount: float,
|
amount: float,
|
||||||
stake_amount: float,
|
stake_amount: float,
|
||||||
|
leverage: float,
|
||||||
wallet_balance: float, # Or margin balance
|
wallet_balance: float, # Or margin balance
|
||||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||||
@ -2745,22 +2817,28 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||||
PERPETUAL:
|
PERPETUAL:
|
||||||
gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price
|
gate: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price
|
||||||
|
> Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
|
||||||
|
[ 1 ± (Maintenance Margin Ratio + Taker Rate)]
|
||||||
|
Wherein, "+" or "-" depends on whether the contract goes long or short:
|
||||||
|
"-" for long, and "+" for short.
|
||||||
|
|
||||||
okex: https://www.okex.com/support/hc/en-us/articles/
|
okex: https://www.okex.com/support/hc/en-us/articles/
|
||||||
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
|
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
|
||||||
|
|
||||||
:param exchange_name:
|
:param pair: Pair to calculate liquidation price for
|
||||||
:param open_rate: Entry price of position
|
:param open_rate: Entry price of position
|
||||||
:param is_short: True if the trade is a short, false otherwise
|
:param is_short: True if the trade is a short, false otherwise
|
||||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||||
|
:param leverage: Leverage used for this position.
|
||||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||||
:param margin_mode: Either ISOLATED or CROSS
|
:param margin_mode: Either ISOLATED or CROSS
|
||||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||||
Cross-Margin Mode: crossWalletBalance
|
Cross-Margin Mode: crossWalletBalance
|
||||||
Isolated-Margin Mode: isolatedWalletBalance
|
Isolated-Margin Mode: isolatedWalletBalance
|
||||||
|
|
||||||
# * Not required by Gateio or OKX
|
# * Not required by Gate or OKX
|
||||||
:param mm_ex_1:
|
:param mm_ex_1:
|
||||||
:param upnl_ex_1:
|
:param upnl_ex_1:
|
||||||
"""
|
"""
|
||||||
@ -2789,7 +2867,7 @@ class Exchange:
|
|||||||
def get_maintenance_ratio_and_amt(
|
def get_maintenance_ratio_and_amt(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
nominal_value: float = 0.0,
|
nominal_value: float,
|
||||||
) -> Tuple[float, Optional[float]]:
|
) -> Tuple[float, Optional[float]]:
|
||||||
"""
|
"""
|
||||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
Exchange support utils
|
Exchange support utils
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import ceil
|
from math import ceil, floor
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
|
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
|
||||||
|
TRUNCATE, decimal_to_precision)
|
||||||
|
|
||||||
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
||||||
from freqtrade.util import FtPrecise
|
from freqtrade.util import FtPrecise
|
||||||
@ -15,18 +16,19 @@ from freqtrade.util import FtPrecise
|
|||||||
CcxtModuleType = Any
|
CcxtModuleType = Any
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
def is_exchange_known_ccxt(
|
||||||
|
exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None) -> bool:
|
||||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return the list of all exchanges known to ccxt
|
Return the list of all exchanges known to ccxt
|
||||||
"""
|
"""
|
||||||
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||||
|
|
||||||
|
|
||||||
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
||||||
"""
|
"""
|
||||||
@ -86,7 +88,7 @@ def timeframe_to_msecs(timeframe: str) -> int:
|
|||||||
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
||||||
"""
|
"""
|
||||||
Use Timeframe and determine the candle start date for this date.
|
Use Timeframe and determine the candle start date for this date.
|
||||||
Does not round when given a candle start date.
|
Does not round when given a candle start date.
|
||||||
@ -102,7 +104,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
|||||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
|
||||||
"""
|
"""
|
||||||
Use Timeframe and determine next candle.
|
Use Timeframe and determine next candle.
|
||||||
:param timeframe: timeframe in string format (e.g. "5m")
|
:param timeframe: timeframe in string format (e.g. "5m")
|
||||||
@ -218,35 +220,51 @@ def amount_to_contract_precision(
|
|||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
def price_to_precision(price: float, price_precision: Optional[float],
|
def price_to_precision(
|
||||||
precisionMode: Optional[int]) -> float:
|
price: float,
|
||||||
|
price_precision: Optional[float],
|
||||||
|
precisionMode: Optional[int],
|
||||||
|
*,
|
||||||
|
rounding_mode: int = ROUND,
|
||||||
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Returns the price rounded up to the precision the Exchange accepts.
|
Returns the price rounded to the precision the Exchange accepts.
|
||||||
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
||||||
which does not support rounding up
|
which does not support rounding up.
|
||||||
|
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||||
|
|
||||||
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||||
align with amount_to_precision().
|
align with amount_to_precision().
|
||||||
!!! Rounds up
|
|
||||||
:param price: price to convert
|
:param price: price to convert
|
||||||
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
||||||
:param precisionMode: precision mode to use. Should be used from precisionMode
|
:param precisionMode: precision mode to use. Should be used from precisionMode
|
||||||
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
||||||
|
:param rounding_mode: rounding mode to use. Defaults to ROUND
|
||||||
:return: price rounded up to the precision the Exchange accepts
|
:return: price rounded up to the precision the Exchange accepts
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if price_precision is not None and precisionMode is not None:
|
if price_precision is not None and precisionMode is not None:
|
||||||
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
|
||||||
# precision=price_precision,
|
|
||||||
# counting_mode=self.precisionMode,
|
|
||||||
# ))
|
|
||||||
if precisionMode == TICK_SIZE:
|
if precisionMode == TICK_SIZE:
|
||||||
|
if rounding_mode == ROUND:
|
||||||
|
ticks = price / price_precision
|
||||||
|
rounded_ticks = round(ticks)
|
||||||
|
return rounded_ticks * price_precision
|
||||||
precision = FtPrecise(price_precision)
|
precision = FtPrecise(price_precision)
|
||||||
price_str = FtPrecise(price)
|
price_str = FtPrecise(price)
|
||||||
missing = price_str % precision
|
missing = price_str % precision
|
||||||
if not missing == FtPrecise("0"):
|
if not missing == FtPrecise("0"):
|
||||||
price = round(float(str(price_str - missing + precision)), 14)
|
return round(float(str(price_str - missing + precision)), 14)
|
||||||
else:
|
return price
|
||||||
symbol_prec = price_precision
|
elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
|
||||||
big_price = price * pow(10, symbol_prec)
|
ndigits = round(price_precision)
|
||||||
price = ceil(big_price) / pow(10, symbol_prec)
|
if rounding_mode == ROUND:
|
||||||
|
return round(price, ndigits)
|
||||||
|
ticks = price * (10**ndigits)
|
||||||
|
if rounding_mode == ROUND_UP:
|
||||||
|
return ceil(ticks) / (10**ndigits)
|
||||||
|
if rounding_mode == TRUNCATE:
|
||||||
|
return int(ticks) / (10**ndigits)
|
||||||
|
if rounding_mode == ROUND_DOWN:
|
||||||
|
return floor(ticks) / (10**ndigits)
|
||||||
|
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
|
||||||
|
raise ValueError(f"Unknown precisionMode {precisionMode}")
|
||||||
return price
|
return price
|
||||||
|
@ -4,8 +4,7 @@ from datetime import datetime
|
|||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.misc import safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback2
|
||||||
|
|
||||||
@ -13,7 +12,7 @@ from freqtrade.misc import safe_value_fallback2
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Gateio(Exchange):
|
class Gate(Exchange):
|
||||||
"""
|
"""
|
||||||
Gate.io exchange class. Contains adjustments needed for Freqtrade to work
|
Gate.io exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
with this exchange.
|
with this exchange.
|
||||||
@ -28,12 +27,21 @@ class Gateio(Exchange):
|
|||||||
"order_time_in_force": ['GTC', 'IOC'],
|
"order_time_in_force": ['GTC', 'IOC'],
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"marketOrderRequiresPrice": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
_ft_has_futures: Dict = {
|
_ft_has_futures: Dict = {
|
||||||
"needs_trading_fees": True,
|
"needs_trading_fees": True,
|
||||||
|
"marketOrderRequiresPrice": False,
|
||||||
|
"tickers_have_bid_ask": False,
|
||||||
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
||||||
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||||
|
"stop_price_type_field": "price_type",
|
||||||
|
"stop_price_type_value_mapping": {
|
||||||
|
PriceType.LAST: 0,
|
||||||
|
PriceType.MARK: 1,
|
||||||
|
PriceType.INDEX: 2,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
@ -43,13 +51,6 @@ class Gateio(Exchange):
|
|||||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
|
||||||
|
|
||||||
if self.trading_mode != TradingMode.FUTURES:
|
|
||||||
if any(v == 'market' for k, v in order_types.items()):
|
|
||||||
raise OperationalException(
|
|
||||||
f'Exchange {self.name} does not support market orders.')
|
|
||||||
|
|
||||||
def _get_params(
|
def _get_params(
|
||||||
self,
|
self,
|
||||||
side: BuySell,
|
side: BuySell,
|
||||||
@ -67,8 +68,7 @@ class Gateio(Exchange):
|
|||||||
)
|
)
|
||||||
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
|
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
|
||||||
params['type'] = 'market'
|
params['type'] = 'market'
|
||||||
param = self._ft_has.get('time_in_force_parameter', '')
|
params.update({'timeInForce': 'IOC'})
|
||||||
params.update({param: 'IOC'})
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||||
@ -77,7 +77,7 @@ class Gateio(Exchange):
|
|||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
# Futures usually don't contain fees in the response.
|
# Futures usually don't contain fees in the response.
|
||||||
# As such, futures orders on gateio will not contain a fee, which causes
|
# As such, futures orders on gate will not contain a fee, which causes
|
||||||
# a repeated "update fee" cycle and wrong calculations.
|
# a repeated "update fee" cycle and wrong calculations.
|
||||||
# Therefore we patch the response with fees if it's not available.
|
# Therefore we patch the response with fees if it's not available.
|
||||||
# An alternative also contianing fees would be
|
# An alternative also contianing fees would be
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user