mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Compare commits
782 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63e808521 | ||
|
|
d549fe351c | ||
|
|
9137338771 | ||
|
|
d0c7b7c582 | ||
|
|
b130a923f7 | ||
|
|
3af3094a56 | ||
|
|
9d70d25064 | ||
|
|
05adebb536 | ||
|
|
e1ddddad4f | ||
|
|
84622dc84b | ||
|
|
36e9abc841 | ||
|
|
1b290ffb5d | ||
|
|
334e7553e1 | ||
|
|
f4585a2745 | ||
|
|
448f3a7197 | ||
|
|
6e66763e5f | ||
|
|
89b515be60 | ||
|
|
d481895763 | ||
|
|
4ad3e96a2f | ||
|
|
3893b638fe | ||
|
|
5dac3b5664 | ||
|
|
bcb13d041e | ||
|
|
f790f95319 | ||
|
|
766d32897d | ||
|
|
e09674b77f | ||
|
|
88ccdc0366 | ||
|
|
d04247cd9e | ||
|
|
d13e87d7a4 | ||
|
|
bbcbf6adc8 | ||
|
|
6116c27aa9 | ||
|
|
12e6287875 | ||
|
|
0e168159c1 | ||
|
|
e1c9b77c44 | ||
|
|
54b714ba3f | ||
|
|
f302882f67 | ||
|
|
8e659af580 | ||
|
|
567211e9f9 | ||
|
|
95f884f4f3 | ||
|
|
53c0f01bef | ||
|
|
0aa8557c03 | ||
|
|
4d5e368c2e | ||
|
|
2d4d1d7306 | ||
|
|
2c5b6aca91 | ||
|
|
eaa657aa3b | ||
|
|
a5d4de8037 | ||
|
|
52b75c5997 | ||
|
|
f04e4f2123 | ||
|
|
176bae2d59 | ||
|
|
14e21765f2 | ||
|
|
eebaede80d | ||
|
|
9b83a09224 | ||
|
|
0a4b2f19e3 | ||
|
|
3abc294e5f | ||
|
|
6aa18bddc9 | ||
|
|
16279bc171 | ||
|
|
14961e2e38 | ||
|
|
30ae5829f5 | ||
|
|
200dfa7575 | ||
|
|
51b3eb78d7 | ||
|
|
9685c09c1a | ||
|
|
4303e86e09 | ||
|
|
f4d26961c8 | ||
|
|
029a6798a4 | ||
|
|
f5ba34addf | ||
|
|
bcf47b29ed | ||
|
|
91c0e3640f | ||
|
|
fadf82dd32 | ||
|
|
241b23e5d8 | ||
|
|
c429eae6e4 | ||
|
|
674bad2a4f | ||
|
|
14b7fc42fa | ||
|
|
14717b1701 | ||
|
|
51ef137981 | ||
|
|
f954efbd64 | ||
|
|
0a29096794 | ||
|
|
687dc78dbd | ||
|
|
8aaf174578 | ||
|
|
2660be9b13 | ||
|
|
65ad9cf741 | ||
|
|
179bcf3907 | ||
|
|
062eca19b8 | ||
|
|
4692174677 | ||
|
|
65699f702e | ||
|
|
e57be10772 | ||
|
|
5ba6cfe406 | ||
|
|
f0c7394bc8 | ||
|
|
fb4f83b32c | ||
|
|
a49a60b4fa | ||
|
|
13ffd88053 | ||
|
|
4e847f26bc | ||
|
|
0004b32411 | ||
|
|
4f583d61c8 | ||
|
|
3eb2e92d53 | ||
|
|
a748c0794e | ||
|
|
1682d6b365 | ||
|
|
27ffce4c3f | ||
|
|
d62f97dc3b | ||
|
|
9c1cd4bee2 | ||
|
|
754027efed | ||
|
|
e9deb928f6 | ||
|
|
6b74fb0893 | ||
|
|
feb14990c2 | ||
|
|
3831f198e9 | ||
|
|
adfd8c7f5c | ||
|
|
3fd00c9a9c | ||
|
|
2ec5a536aa | ||
|
|
d35d3bb38c | ||
|
|
cb46aeb73c | ||
|
|
b8624e5909 | ||
|
|
fa5c8e4bb1 | ||
|
|
9945b97595 | ||
|
|
17d6d92302 | ||
|
|
9560cb8056 | ||
|
|
3ed97fe5e8 | ||
|
|
35c5d4f580 | ||
|
|
a1bd30aa60 | ||
|
|
ffd4469c1d | ||
|
|
54ddd908e6 | ||
|
|
d41f0667b8 | ||
|
|
9f8e68ce02 | ||
|
|
f7b67cec5b | ||
|
|
e14e7d9b8a | ||
|
|
b659ec00ee | ||
|
|
b6b89a464f | ||
|
|
c9ee528050 | ||
|
|
9bce6c5f48 | ||
|
|
cdfff57403 | ||
|
|
19628d317a | ||
|
|
32ae344e59 | ||
|
|
c99ff78f2f | ||
|
|
188cfc435d | ||
|
|
1a9c085f10 | ||
|
|
eefc5349c8 | ||
|
|
fe169483ed | ||
|
|
4dfaf1d284 | ||
|
|
c5efcace47 | ||
|
|
c770eae70b | ||
|
|
2ee1a2d851 | ||
|
|
42587741dd | ||
|
|
a489a044ad | ||
|
|
1d0802192d | ||
|
|
ab628c1381 | ||
|
|
a37802e21c | ||
|
|
8c0e33753e | ||
|
|
cac7e2c745 | ||
|
|
ebc072396b | ||
|
|
4508349d07 | ||
|
|
7376a0d538 | ||
|
|
36e0e652f0 | ||
|
|
5e4ae46b3c | ||
|
|
66d52c1236 | ||
|
|
6e90d482ef | ||
|
|
37bb6ac57b | ||
|
|
8a844488d4 | ||
|
|
e5707b8a2c | ||
|
|
8f41e0e190 | ||
|
|
4bf0542204 | ||
|
|
43f73c5aec | ||
|
|
a077955efa | ||
|
|
0674c3e8f0 | ||
|
|
6d1c82a5fa | ||
|
|
de0f3e43bf | ||
|
|
694b8be32f | ||
|
|
9403248e4d | ||
|
|
c955c7c494 | ||
|
|
5a0876704a | ||
|
|
97e9a44fd2 | ||
|
|
088c54b88c | ||
|
|
d722c12109 | ||
|
|
d556f669b0 | ||
|
|
66255b8c61 | ||
|
|
bc22320f77 | ||
|
|
64781643d3 | ||
|
|
56188f2f67 | ||
|
|
eb4bc66443 | ||
|
|
d1c5eebff2 | ||
|
|
98240e0e48 | ||
|
|
0750d356a1 | ||
|
|
f57bf8f269 | ||
|
|
dc41a19f99 | ||
|
|
16fa877b67 | ||
|
|
ff8ed564f1 | ||
|
|
e6e2799f03 | ||
|
|
4a8c120926 | ||
|
|
aa10c6e6fe | ||
|
|
a2d9126917 | ||
|
|
e02f964e3a | ||
|
|
be373e7563 | ||
|
|
baeffee80d | ||
|
|
76914c2c07 | ||
|
|
ca6594cd24 | ||
|
|
d007ac4b96 | ||
|
|
6e2a2abe80 | ||
|
|
dd7f540e5a | ||
|
|
78d1a677d7 | ||
|
|
2999588ea7 | ||
|
|
1edbc494ee | ||
|
|
b34aa46181 | ||
|
|
48e218d6c0 | ||
|
|
2bc7a668a3 | ||
|
|
8b9f1cadaa | ||
|
|
3aa210cf93 | ||
|
|
e37cb49dc2 | ||
|
|
67cbbc86f2 | ||
|
|
37e504610a | ||
|
|
8528143ffa | ||
|
|
69cc6aa958 | ||
|
|
a6b69da391 | ||
|
|
05cfbde8fc | ||
|
|
04878da66b | ||
|
|
0b44dda7b7 | ||
|
|
78610bb47f | ||
|
|
50494858f1 | ||
|
|
eca8682528 | ||
|
|
a488734efa | ||
|
|
2e7837976d | ||
|
|
a0bc17d1ef | ||
|
|
2b37c1ff0e | ||
|
|
7d72e364aa | ||
|
|
bd61478367 | ||
|
|
f7afd9a5ff | ||
|
|
7f6f5791ea | ||
|
|
e3e79a55fa | ||
|
|
e73331b9b6 | ||
|
|
ffa47151ee | ||
|
|
5f8ec82319 | ||
|
|
3ad6ee6b2c | ||
|
|
5bec389e85 | ||
|
|
88e85e8d33 | ||
|
|
fce071843d | ||
|
|
a852d2ff32 | ||
|
|
a107c4c7b4 | ||
|
|
74d6816a1a | ||
|
|
e34f2abc3a | ||
|
|
8a0fc888d6 | ||
|
|
36f05af79a | ||
|
|
e654b76bc8 | ||
|
|
56768f1a61 | ||
|
|
b008649d79 | ||
|
|
3b2f161573 | ||
|
|
df960241bd | ||
|
|
4ece5d6d7a | ||
|
|
e36067afd3 | ||
|
|
c4e43039f2 | ||
|
|
853374d156 | ||
|
|
1bcd4333fc | ||
|
|
029d61b8c5 | ||
|
|
280ead7bdb | ||
|
|
98730939d4 | ||
|
|
d1306a2177 | ||
|
|
cb26085229 | ||
|
|
ed4771bf6e | ||
|
|
cef09f49a6 | ||
|
|
e1921c8849 | ||
|
|
3c451e0677 | ||
|
|
636ae1dcd8 | ||
|
|
4d03fc213f | ||
|
|
863110422a | ||
|
|
3d94720be9 | ||
|
|
c9c0e108ab | ||
|
|
c9580b31d0 | ||
|
|
255f303850 | ||
|
|
131d268721 | ||
|
|
eca5c6f389 | ||
|
|
bc62f626c5 | ||
|
|
199bd7bc50 | ||
|
|
8fc0f6ecec | ||
|
|
65f7b75c34 | ||
|
|
848ecb91bb | ||
|
|
a5554604e0 | ||
|
|
0b825e96aa | ||
|
|
a2730cd86e | ||
|
|
1309c2b14f | ||
|
|
7143b64fb7 | ||
|
|
26d591ea43 | ||
|
|
ba4de4137e | ||
|
|
be9436b2a6 | ||
|
|
4a9bf78770 | ||
|
|
d73d0a5253 | ||
|
|
ea506b05c6 | ||
|
|
6ef14677de | ||
|
|
721341e412 | ||
|
|
3ce4d20ab9 | ||
|
|
af93b18475 | ||
|
|
a586a7526e | ||
|
|
3987a8aeb8 | ||
|
|
59b9a6d94d | ||
|
|
b963b95ee9 | ||
|
|
3037d85529 | ||
|
|
10ab6c7ffa | ||
|
|
71b0e15182 | ||
|
|
1fef384bba | ||
|
|
d2a728cebd | ||
|
|
6b3e8dcc33 | ||
|
|
c38d94df2d | ||
|
|
2cfa3b7607 | ||
|
|
85c73ea850 | ||
|
|
337d9174d9 | ||
|
|
80a1c6ea64 | ||
|
|
05ca78d2a3 | ||
|
|
2ec2f1abce | ||
|
|
7dc440b874 | ||
|
|
ea72af7ce4 | ||
|
|
145008421f | ||
|
|
398c61786a | ||
|
|
00b81e3f0d | ||
|
|
0fc4a7910d | ||
|
|
7f4472ad77 | ||
|
|
e282d57a91 | ||
|
|
3a5b435dfa | ||
|
|
17d78b7807 | ||
|
|
1f97d0d78b | ||
|
|
a741f1144a | ||
|
|
f619cd1d2a | ||
|
|
9c08cdc81d | ||
|
|
915160f21f | ||
|
|
c466a028e0 | ||
|
|
29dcd2ea43 | ||
|
|
f7f75b4b04 | ||
|
|
7458aa438c | ||
|
|
36f91fcdf5 | ||
|
|
5b8ee214f9 | ||
|
|
038e97667f | ||
|
|
b47c5f1d9a | ||
|
|
40ee86b357 | ||
|
|
76fbb89a03 | ||
|
|
c648e2acfc | ||
|
|
765d1c769c | ||
|
|
028589abd2 | ||
|
|
5125076f5d | ||
|
|
4ea6780153 | ||
|
|
a8b55b8989 | ||
|
|
a57a2f4a75 | ||
|
|
bd3563df67 | ||
|
|
644f729aea | ||
|
|
5f2e92ec5c | ||
|
|
65aaa3dffd | ||
|
|
9a42aac0f2 | ||
|
|
56046b3cb3 | ||
|
|
e7d0439741 | ||
|
|
136442245c | ||
|
|
12417cc303 | ||
|
|
52065178e1 | ||
|
|
b45d465ed8 | ||
|
|
31870abd25 | ||
|
|
a486b1d01c | ||
|
|
e38e0e60e1 | ||
|
|
74fa4ddca4 | ||
|
|
66a0986496 | ||
|
|
72480188b7 | ||
|
|
ab4343b7c0 | ||
|
|
be1298dbd2 | ||
|
|
154e4569d7 | ||
|
|
c8f125dbb9 | ||
|
|
1044d15b17 | ||
|
|
2d7ef30185 | ||
|
|
b83487cc36 | ||
|
|
d048f3ce6d | ||
|
|
5a55cd25ff | ||
|
|
f85cc422a3 | ||
|
|
155e134f50 | ||
|
|
81cf7229be | ||
|
|
fe27ca63b4 | ||
|
|
012fe94333 | ||
|
|
075a42d615 | ||
|
|
8b8d3f3b75 | ||
|
|
3ecc502d86 | ||
|
|
67d1693901 | ||
|
|
3083e5d2be | ||
|
|
affdeb8fd8 | ||
|
|
fb80964b69 | ||
|
|
1c20ef873d | ||
|
|
df53e912f0 | ||
|
|
e242842805 | ||
|
|
2401fa15d2 | ||
|
|
787d6042de | ||
|
|
941879dc19 | ||
|
|
82680ac6aa | ||
|
|
5fbce13830 | ||
|
|
39cf0decce | ||
|
|
f286ba6b87 | ||
|
|
98665dcef4 | ||
|
|
cf83416d69 | ||
|
|
791c5ff071 | ||
|
|
8a9c54ed61 | ||
|
|
18b8f20f1c | ||
|
|
f12167f0dc | ||
|
|
df8700ead0 | ||
|
|
0eff6719c2 | ||
|
|
aa772c28ad | ||
|
|
4ebd706cb8 | ||
|
|
fa48b8a535 | ||
|
|
c9a97bccb7 | ||
|
|
2f905cb696 | ||
|
|
7300c0a0fe | ||
|
|
921f645623 | ||
|
|
0dcaa82c3b | ||
|
|
3dd7d209e9 | ||
|
|
abc55a6e6b | ||
|
|
5871488858 | ||
|
|
2e6e5029ba | ||
|
|
19b9966417 | ||
|
|
57f683697d | ||
|
|
296d3d8bbe | ||
|
|
336cd524a3 | ||
|
|
f832edf5bc | ||
|
|
1bbb86c621 | ||
|
|
2ef35400c9 | ||
|
|
9c7f53d90d | ||
|
|
ebfcc0fc13 | ||
|
|
42024134ec | ||
|
|
7f27beff4b | ||
|
|
dd71071740 | ||
|
|
c85c7a3a77 | ||
|
|
1e804c0df5 | ||
|
|
fc06d028b8 | ||
|
|
618784d060 | ||
|
|
cfcc2e61e5 | ||
|
|
187e039a58 | ||
|
|
b3df1b1ba7 | ||
|
|
0a059662b3 | ||
|
|
cb2fff8909 | ||
|
|
cdd8cc551c | ||
|
|
8648ac9da2 | ||
|
|
083befaafc | ||
|
|
099e7020c8 | ||
|
|
6ab8fa8c71 | ||
|
|
b2b81c8b2d | ||
|
|
243b63e39c | ||
|
|
a3d870ad3e | ||
|
|
1ceaa2200a | ||
|
|
c8ac98501c | ||
|
|
ca0d658f15 | ||
|
|
4547ae930a | ||
|
|
40ae250193 | ||
|
|
c47253133a | ||
|
|
7efa81073a | ||
|
|
d23b3ccc5e | ||
|
|
48cd468b6c | ||
|
|
df3e76a65d | ||
|
|
f2a9be3684 | ||
|
|
3324cdfcbe | ||
|
|
484103b957 | ||
|
|
6e437a7290 | ||
|
|
0c7ceadb27 | ||
|
|
726b94b077 | ||
|
|
452a1cad9d | ||
|
|
7b49f746d1 | ||
|
|
78f8c6566e | ||
|
|
4b38c8b11d | ||
|
|
3fa1c5b19f | ||
|
|
4f4daf4071 | ||
|
|
dc1ad3cbf6 | ||
|
|
ff6435948e | ||
|
|
23c2a75fc4 | ||
|
|
7feea8c7a6 | ||
|
|
cf6e229729 | ||
|
|
4928686af9 | ||
|
|
30b72ad98a | ||
|
|
1a9ead45eb | ||
|
|
0b3190552e | ||
|
|
456e49fe35 | ||
|
|
ab67822af2 | ||
|
|
7f877aed6f | ||
|
|
4575919d78 | ||
|
|
10fc2c67c7 | ||
|
|
643de58c4d | ||
|
|
aba3c69765 | ||
|
|
0775a371fe | ||
|
|
23fe0db2df | ||
|
|
f54ac5a8de | ||
|
|
4c8411537f | ||
|
|
bd2771b8f9 | ||
|
|
4d864df59e | ||
|
|
fae4c3a4e3 | ||
|
|
2b297869a1 | ||
|
|
6cc0a72bca | ||
|
|
f53e03767c | ||
|
|
5ab1e66978 | ||
|
|
849ded7772 | ||
|
|
f297d22edb | ||
|
|
0681a806cc | ||
|
|
be3f04775a | ||
|
|
9467461160 | ||
|
|
66af41192a | ||
|
|
6f7898809a | ||
|
|
ab3478a742 | ||
|
|
00fa41d63f | ||
|
|
7f6c79eb76 | ||
|
|
b45128f53d | ||
|
|
dd1290e38e | ||
|
|
62701888c9 | ||
|
|
90915b6b2f | ||
|
|
1b2bfad348 | ||
|
|
060469fefc | ||
|
|
4fb9823cfb | ||
|
|
760c79c5e9 | ||
|
|
a452864b41 | ||
|
|
ad98c62329 | ||
|
|
506aa0e3d3 | ||
|
|
426c25f631 | ||
|
|
4059871c28 | ||
|
|
2a61629014 | ||
|
|
8c0b19f80c | ||
|
|
838b0e7b76 | ||
|
|
cbffd3650b | ||
|
|
0147b1631a | ||
|
|
49a7c7f08e | ||
|
|
1af24af391 | ||
|
|
0cc1b66ae7 | ||
|
|
6070d819b8 | ||
|
|
f2bfc9ccc2 | ||
|
|
f991109b0a | ||
|
|
6bb7167b56 | ||
|
|
365ba98131 | ||
|
|
6a3c8e3933 | ||
|
|
c0a7725c1f | ||
|
|
71100a67c9 | ||
|
|
8f254031c6 | ||
|
|
aa69177436 | ||
|
|
64f933477d | ||
|
|
aaa58a956d | ||
|
|
75c0a476f8 | ||
|
|
1ab7f5fb6d | ||
|
|
789b98015f | ||
|
|
7134c15e86 | ||
|
|
79b1030435 | ||
|
|
ac6955fd3b | ||
|
|
a374f95687 | ||
|
|
f9f6a3bd04 | ||
|
|
8e4d2abd4e | ||
|
|
08237abe20 | ||
|
|
5b3fa3c635 | ||
|
|
ee8e890f50 | ||
|
|
3df79b8542 | ||
|
|
a290286fef | ||
|
|
c82276ecbe | ||
|
|
b29eed32ca | ||
|
|
e17618407b | ||
|
|
85fd4dd3ff | ||
|
|
78205da4f0 | ||
|
|
e021d22c7f | ||
|
|
4a26eb34ea | ||
|
|
50b15b8052 | ||
|
|
e11ec28962 | ||
|
|
06d024cc46 | ||
|
|
084264669f | ||
|
|
dbc3874b4f | ||
|
|
78af4bc785 | ||
|
|
2795db3ea0 | ||
|
|
4f957728bf | ||
|
|
62f4d734b9 | ||
|
|
a3466f4b42 | ||
|
|
050afe2bc0 | ||
|
|
5c87c420c7 | ||
|
|
aeb4102bcb | ||
|
|
f6b8c2b40f | ||
|
|
85e6c9585a | ||
|
|
a74147c472 | ||
|
|
727f569e3a | ||
|
|
8f59759e97 | ||
|
|
158226012a | ||
|
|
b4ba641131 | ||
|
|
682f4c1ade | ||
|
|
e1de988f85 | ||
|
|
bc83c34118 | ||
|
|
278e7159bc | ||
|
|
1284627219 | ||
|
|
120fc29643 | ||
|
|
6336d8a0e2 | ||
|
|
ee2f6ccbe9 | ||
|
|
144d308e5e | ||
|
|
3ca161f196 | ||
|
|
f55df7ba63 | ||
|
|
71df41c4eb | ||
|
|
a4643066a8 | ||
|
|
25250f7c10 | ||
|
|
fa8512789f | ||
|
|
ae22af1ea3 | ||
|
|
6e16c1d80d | ||
|
|
266092a05d | ||
|
|
fa8b349200 | ||
|
|
04bed3e53e | ||
|
|
68ddd1b951 | ||
|
|
b6e1020f39 | ||
|
|
5b02b87735 | ||
|
|
c17e8d6abb | ||
|
|
cb8cd21e22 | ||
|
|
a559e22f16 | ||
|
|
7eaeb8d146 | ||
|
|
0920fb6120 | ||
|
|
4cb1aa1d97 | ||
|
|
96a405feb7 | ||
|
|
112998c205 | ||
|
|
f1a370b3b9 | ||
|
|
29670b9814 | ||
|
|
df8ba28ce5 | ||
|
|
5288e18f2f | ||
|
|
ddfc4722b9 | ||
|
|
bd46b4faf3 | ||
|
|
46708e7d29 | ||
|
|
06c9494a46 | ||
|
|
8f6252b312 | ||
|
|
1f16ff268f | ||
|
|
aa2366346a | ||
|
|
8b72560eba | ||
|
|
773fb5953b | ||
|
|
3540ba3712 | ||
|
|
d546a4b29f | ||
|
|
b4be3c2499 | ||
|
|
85c60519b0 | ||
|
|
6be6448334 | ||
|
|
f5bc65b877 | ||
|
|
a7a82635b4 | ||
|
|
b9916b60f9 | ||
|
|
b773e3472a | ||
|
|
4654792784 | ||
|
|
750d737b7d | ||
|
|
0bd9674b5c | ||
|
|
8b06000f0f | ||
|
|
efaa8f16e7 | ||
|
|
38487644f0 | ||
|
|
1a24afef77 | ||
|
|
8fb146ba6a | ||
|
|
05b078b8dd | ||
|
|
6926e468a4 | ||
|
|
34764108cc | ||
|
|
17c9c183f5 | ||
|
|
cc107bb3cc | ||
|
|
8dd6e29426 | ||
|
|
3e03a208f1 | ||
|
|
570d27a0c4 | ||
|
|
7c8c8e83d3 | ||
|
|
2b488d1da2 | ||
|
|
e98efe3a35 | ||
|
|
3f6e9cd28f | ||
|
|
af17cef002 | ||
|
|
742fefa786 | ||
|
|
08fe10e302 | ||
|
|
9906da46f6 | ||
|
|
54976fa103 | ||
|
|
e1d7c72bb8 | ||
|
|
af03c17209 | ||
|
|
1897a1cb6a | ||
|
|
58879ff012 | ||
|
|
e1f5745f59 | ||
|
|
1c48902e64 | ||
|
|
8bbee4038b | ||
|
|
c35d1b9c9d | ||
|
|
4f642b769c | ||
|
|
e808b3a2a1 | ||
|
|
df68b0990f | ||
|
|
adbffc69e1 | ||
|
|
21fc933678 | ||
|
|
a2063ede55 | ||
|
|
7dca3c6d03 | ||
|
|
03c112a601 | ||
|
|
c77686c7a7 | ||
|
|
239f8606e1 | ||
|
|
bfd1e90154 | ||
|
|
5ab644dea6 | ||
|
|
966668f48a | ||
|
|
d8d0579c5a | ||
|
|
64c68d93c3 | ||
|
|
700f02dde8 | ||
|
|
ac20bf31df | ||
|
|
bf4d0a9b70 | ||
|
|
96bb2efe69 | ||
|
|
c4a8435e00 | ||
|
|
9dbe0f50a3 | ||
|
|
3a7056ea1b | ||
|
|
2cde540645 | ||
|
|
ef59f9ad24 | ||
|
|
e91cfbfeeb | ||
|
|
2c0e950486 | ||
|
|
ee4754cfb9 | ||
|
|
4a26b88a17 | ||
|
|
2713fdb860 | ||
|
|
79aab4cce2 | ||
|
|
2b34d10973 | ||
|
|
76343ecb77 | ||
|
|
fa8fc3e4ce | ||
|
|
aec3f582e1 | ||
|
|
a58d51ded0 | ||
|
|
5e4a6ba7ba | ||
|
|
3c5be55eb9 | ||
|
|
782570e71e | ||
|
|
ed2a1becef | ||
|
|
937644a04b | ||
|
|
e39d88ef65 | ||
|
|
f91263c8ef | ||
|
|
e2127f5af1 | ||
|
|
2dc881558d | ||
|
|
c66f858b98 | ||
|
|
8023fdf923 | ||
|
|
2cee8e52c1 | ||
|
|
8f49d5eb10 | ||
|
|
9e3e900f78 | ||
|
|
14e12bd3c0 | ||
|
|
c29163a51c | ||
|
|
5a591e01c0 | ||
|
|
c447644fd1 | ||
|
|
98108a78f1 | ||
|
|
0ce08932ed | ||
|
|
6dd5f85fb6 | ||
|
|
d8f2a683c6 | ||
|
|
8a941f3aa8 | ||
|
|
cf6b1a637a | ||
|
|
dcdc18a338 | ||
|
|
15c7854e7f | ||
|
|
fe8a21681e | ||
|
|
ebbfc720b2 | ||
|
|
8ec9a09749 | ||
|
|
2d4ce593b5 | ||
|
|
c5a00b4d45 | ||
|
|
7cecae5279 | ||
|
|
d5ad066f8d | ||
|
|
860b270e30 | ||
|
|
35e07bf11e | ||
|
|
19beb0941f | ||
|
|
8ecdae67e1 | ||
|
|
e6e868a03c | ||
|
|
78e6c9fdf6 | ||
|
|
c997aa9864 | ||
|
|
a91d75b3b2 | ||
|
|
e9d5bceeb9 | ||
|
|
88b898cce4 | ||
|
|
8bec505bbe | ||
|
|
a3708bc56e | ||
|
|
03005bc0f1 | ||
|
|
da5be9fbd0 | ||
|
|
3e167e1170 | ||
|
|
5015bc9bb0 | ||
|
|
243c36b39b | ||
|
|
9ac3c559b6 | ||
|
|
257e1847b1 | ||
|
|
54f52fb366 | ||
|
|
e1d8a59b69 | ||
|
|
7c2a50cef9 | ||
|
|
4c7d1c90db | ||
|
|
4f1fa28658 | ||
|
|
2b6407e598 | ||
|
|
0bddc58ec4 | ||
|
|
17ee7f8be5 | ||
|
|
375ea940f4 | ||
|
|
43f1a1d264 | ||
|
|
e70cb963f7 | ||
|
|
a8cb0b0321 | ||
|
|
118a43cbb8 | ||
|
|
5e7e977ffa | ||
|
|
660ec6f443 | ||
|
|
e98f22ef2f | ||
|
|
1529ce8bdb | ||
|
|
d8cb63efdd | ||
|
|
5055563458 | ||
|
|
f506ebcd62 | ||
|
|
3cedace2f6 | ||
|
|
3384679bad | ||
|
|
642ad02316 | ||
|
|
ab9e2fcea0 | ||
|
|
136456afc0 | ||
|
|
09261b11af | ||
|
|
0440a19171 | ||
|
|
e8f2e6956d | ||
|
|
dde7df7fd3 | ||
|
|
a525cba8e9 | ||
|
|
8272120c3a | ||
|
|
8fee2e2409 | ||
|
|
c415014153 | ||
|
|
964cbdc262 | ||
|
|
a46badd5c0 | ||
|
|
0cb1aedf5b | ||
|
|
b485e6e0ba | ||
|
|
810d7de869 | ||
|
|
398b21a11d | ||
|
|
78f50a1471 | ||
|
|
5aae215c94 | ||
|
|
2738d3aed8 | ||
|
|
01d45bee76 | ||
|
|
c1691f21f3 | ||
|
|
a68c90c512 | ||
|
|
e759a90b2d |
@@ -13,12 +13,12 @@ addons:
|
||||
install:
|
||||
- ./install_ta-lib.sh
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||
- pip install --upgrade flake8 coveralls pytest-random-order mypy
|
||||
- pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy
|
||||
- pip install -r requirements.txt
|
||||
- pip install -e .
|
||||
jobs:
|
||||
include:
|
||||
- script:
|
||||
- script:
|
||||
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||
- coveralls
|
||||
- script:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
FROM python:3.6.5-slim-stretch
|
||||
FROM python:3.7.0-slim-stretch
|
||||
|
||||
# Install TA-lib
|
||||
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
|
||||
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
|
||||
tar xzvf - && \
|
||||
cd ta-lib && \
|
||||
sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && \
|
||||
./configure && make && make install && \
|
||||
cd .. && rm -rf ta-lib
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
@@ -15,9 +16,10 @@ WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt /freqtrade/
|
||||
RUN pip install -r requirements.txt
|
||||
RUN pip install numpy --no-cache-dir \
|
||||
&& pip install -r requirements.txt --no-cache-dir
|
||||
|
||||
# Install and execute
|
||||
COPY . /freqtrade/
|
||||
RUN pip install -e .
|
||||
RUN pip install -e . --no-cache-dir
|
||||
ENTRYPOINT ["freqtrade"]
|
||||
|
||||
91
README.md
91
README.md
@@ -4,13 +4,12 @@
|
||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||
|
||||
|
||||
Simple High frequency trading bot for crypto currencies designed to
|
||||
support multi exchanges and be controlled via Telegram.
|
||||
Simple High frequency trading bot for crypto currencies designed to support multi exchanges and be controlled via Telegram.
|
||||
|
||||

|
||||
|
||||
## Disclaimer
|
||||
|
||||
This software is for educational purposes only. Do not risk money which
|
||||
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
|
||||
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
||||
@@ -23,18 +22,18 @@ We strongly recommend you to have coding and Python knowledge. Do not
|
||||
hesitate to read the source code and understand the mechanism of this bot.
|
||||
|
||||
## Exchange marketplaces supported
|
||||
|
||||
- [X] [Bittrex](https://bittrex.com/)
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance))
|
||||
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
## Features
|
||||
- [x] **Based on Python 3.6+**: For botting on any operating system -
|
||||
Windows, macOS and Linux
|
||||
|
||||
- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux
|
||||
- [x] **Persistence**: Persistence is achieved through sqlite
|
||||
- [x] **Dry-run**: Run the bot without playing money.
|
||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell
|
||||
strategy parameters with real exchange data.
|
||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
|
||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
||||
@@ -43,38 +42,45 @@ strategy parameters with real exchange data.
|
||||
- [x] **Performance status report**: Provide a performance status of your current trades.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick start](#quick-start)
|
||||
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [Bot commands](#bot-commands)
|
||||
- [Telegram RPC commands](#telegram-rpc-commands)
|
||||
- [Support](#support)
|
||||
- [Help](#help--slack)
|
||||
- [Bugs](#bugs--issues)
|
||||
- [Feature Requests](#feature-requests)
|
||||
- [Pull Requests](#pull-requests)
|
||||
- [Help](#help--slack)
|
||||
- [Bugs](#bugs--issues)
|
||||
- [Feature Requests](#feature-requests)
|
||||
- [Pull Requests](#pull-requests)
|
||||
- [Requirements](#requirements)
|
||||
- [Min hardware required](#min-hardware-required)
|
||||
- [Software requirements](#software-requirements)
|
||||
- [Min hardware required](#min-hardware-required)
|
||||
- [Software requirements](#software-requirements)
|
||||
|
||||
|
||||
## Quick start
|
||||
|
||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:freqtrade/freqtrade.git
|
||||
git checkout develop
|
||||
cd freqtrade
|
||||
git checkout develop
|
||||
./setup.sh --install
|
||||
```
|
||||
|
||||
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
||||
|
||||
- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||
@@ -86,7 +92,6 @@ We invite you to read the bot documentation to ensure you understand how the bot
|
||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Bot commands
|
||||
@@ -125,17 +130,15 @@ optional arguments:
|
||||
```
|
||||
|
||||
### Telegram RPC commands
|
||||
Telegram is not mandatory. However, this is a great way to control your
|
||||
bot. More details on our
|
||||
[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||
|
||||
Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||
|
||||
- `/start`: Starts the trader
|
||||
- `/stop`: Stops the trader
|
||||
- `/status [table]`: Lists all open trades
|
||||
- `/count`: Displays number of open trades
|
||||
- `/profit`: Lists cumulative profit from all finished trades
|
||||
- `/forcesell <trade_id>|all`: Instantly sells the given trade
|
||||
(Ignoring `minimum_roi`).
|
||||
- `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
- `/performance`: Show performance of each finished trade grouped by pair
|
||||
- `/balance`: Show account balance per currency
|
||||
- `/daily <n>`: Shows profit or loss per day, over the last n days
|
||||
@@ -144,20 +147,30 @@ bot. More details on our
|
||||
|
||||
|
||||
## Development branches
|
||||
The project is currently setup in two main branches:
|
||||
- `develop` - This branch has often new features, but might also cause
|
||||
breaking changes.
|
||||
- `master` - This branch contains the latest stable release. The bot
|
||||
'should' be stable on this branch, and is generally well tested.
|
||||
|
||||
The project is currently setup in two main branches:
|
||||
|
||||
- `develop` - This branch has often new features, but might also cause breaking changes.
|
||||
- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested.
|
||||
- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature.
|
||||
|
||||
|
||||
## A note on Binance
|
||||
|
||||
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
||||
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
|
||||
|
||||
## Support
|
||||
|
||||
### Help / Slack
|
||||
|
||||
For any questions not covered by the documentation or for further
|
||||
information about the bot, we encourage you to join our slack channel.
|
||||
|
||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
||||
|
||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||
|
||||
If you discover a bug in the bot, please
|
||||
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||
first. If it hasn't been reported, please
|
||||
@@ -166,6 +179,7 @@ ensure you follow the template guide so that our team can assist you as
|
||||
quickly as possible.
|
||||
|
||||
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
||||
|
||||
Have you a great idea to improve the bot you want to share? Please,
|
||||
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
|
||||
If it hasn't been requested, please
|
||||
@@ -174,6 +188,7 @@ and ensure you follow the template guide so that it does not get lost
|
||||
in the bug reports.
|
||||
|
||||
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
||||
|
||||
Feel like our bot is missing a feature? We welcome your pull requests!
|
||||
Please read our
|
||||
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
@@ -181,16 +196,22 @@ to understand the requirements before sending your pull-requests.
|
||||
|
||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||
|
||||
**Important:** Always create your PR against the `develop` branch, not
|
||||
`master`.
|
||||
**Important:** Always create your PR against the `develop` branch, not `master`.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Uptodate clock
|
||||
|
||||
The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges.
|
||||
|
||||
### Min hardware required
|
||||
|
||||
To run this bot we recommend you a cloud instance with a minimum of:
|
||||
* Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU
|
||||
|
||||
- Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU
|
||||
|
||||
### Software requirements
|
||||
|
||||
- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
|
||||
@@ -5,14 +5,30 @@
|
||||
"fiat_display_currency": "USD",
|
||||
"ticker_interval" : "5m",
|
||||
"dry_run": false,
|
||||
"unfilledtimeout": 600,
|
||||
"trailing_stop": false,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
"ask_last_balance": 0.0,
|
||||
"use_order_book": false,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 9
|
||||
},
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"ccxt_rate_limit": true,
|
||||
"pair_whitelist": [
|
||||
"ETH/BTC",
|
||||
"LTC/BTC",
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": false,
|
||||
"ticker_interval": "5m",
|
||||
"trailing_stop": false,
|
||||
"trailing_stop_positive": 0.005,
|
||||
"trailing_stop_positive_offset": 0.0051,
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
@@ -12,14 +15,29 @@
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.10,
|
||||
"unfilledtimeout": 600,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
"ask_last_balance": 0.0,
|
||||
"use_order_book": false,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 9
|
||||
},
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"ccxt_rate_limit": true,
|
||||
"pair_whitelist": [
|
||||
"ETH/BTC",
|
||||
"LTC/BTC",
|
||||
@@ -34,7 +52,8 @@
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"DOGE/BTC"
|
||||
]
|
||||
],
|
||||
"outdated_offset": 5
|
||||
},
|
||||
"experimental": {
|
||||
"use_sell_signal": false,
|
||||
|
||||
@@ -29,25 +29,25 @@ The backtesting is very easy with freqtrade.
|
||||
#### With 5 min tickers (Per default)
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py backtesting --realistic-simulation
|
||||
python3 ./freqtrade/main.py backtesting
|
||||
```
|
||||
|
||||
#### With 1 min tickers
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
|
||||
python3 ./freqtrade/main.py backtesting --ticker-interval 1m
|
||||
```
|
||||
|
||||
#### Update cached pairs with the latest data
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
|
||||
python3 ./freqtrade/main.py backtesting --refresh-pairs-cached
|
||||
```
|
||||
|
||||
#### With live data (do not alter your testdata files)
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
||||
python3 ./freqtrade/main.py backtesting --live
|
||||
```
|
||||
|
||||
#### Using a different on-disk ticker-data source
|
||||
@@ -70,6 +70,36 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_
|
||||
python3 ./freqtrade/main.py backtesting --export trades
|
||||
```
|
||||
|
||||
The exported trades can be read using the following code for manual analysis, or can be used by the plotting script `plot_dataframe.py` in the scripts folder.
|
||||
|
||||
``` python
|
||||
import json
|
||||
from pathlib import Path
|
||||
import pandas as pd
|
||||
|
||||
filename=Path('user_data/backtest_data/backtest-result.json')
|
||||
|
||||
with filename.open() as file:
|
||||
data = json.load(file)
|
||||
|
||||
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||
df = pd.DataFrame(data, columns=columns)
|
||||
|
||||
df['opents'] = pd.to_datetime(df['opents'],
|
||||
unit='s',
|
||||
utc=True,
|
||||
infer_datetime_format=True
|
||||
)
|
||||
df['closets'] = pd.to_datetime(df['closets'],
|
||||
unit='s',
|
||||
utc=True,
|
||||
infer_datetime_format=True
|
||||
)
|
||||
```
|
||||
|
||||
If you have some ideas for interesting / helpful backtest data analysis, feel free to submit a PR so the community can benefit from it.
|
||||
|
||||
#### Exporting trades to file specifying a custom filename
|
||||
|
||||
```bash
|
||||
@@ -121,7 +151,7 @@ cp freqtrade/tests/testdata/pairs.json user_data/data/binance
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
python scripts/download_backtest_data --exchange binance
|
||||
python scripts/download_backtest_data.py --exchange binance
|
||||
```
|
||||
|
||||
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
||||
@@ -208,6 +238,31 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
|
||||
profit. Hence, keep in mind that your performance is a mix of your
|
||||
strategies, your configuration, and the crypto-currency you have set up.
|
||||
|
||||
## Backtesting multiple strategies
|
||||
|
||||
To backtest multiple strategies, a list of Strategies can be provided.
|
||||
|
||||
This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple
|
||||
strategies you'd like to compare, this should give a nice runtime boost.
|
||||
|
||||
All listed Strategies need to be in the same folder.
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades
|
||||
```
|
||||
|
||||
This will save the results to `user_data/backtest_data/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
||||
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
|
||||
Detailed output for all strategies one after the other will be available, so make sure to scroll up.
|
||||
|
||||
```
|
||||
=================================================== Strategy Summary ====================================================
|
||||
| Strategy | buy count | avg profit % | cum profit % | total profit ETH | avg duration | profit | loss |
|
||||
|:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:|
|
||||
| Strategy1 | 19 | -0.76 | -14.39 | -0.01440287 | 15:48:00 | 15 | 4 |
|
||||
| Strategy2 | 6 | -2.73 | -16.40 | -0.01641299 | 1 day, 14:12:00 | 3 | 3 |
|
||||
```
|
||||
|
||||
## Next step
|
||||
|
||||
Great, your strategy is profitable. What if the bot can give your the
|
||||
|
||||
@@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy:
|
||||
- Sell strategy rules
|
||||
- Minimal ROI recommended
|
||||
- Stoploss recommended
|
||||
- Hyperopt parameter
|
||||
|
||||
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
|
||||
You can test it with the parameter: `--strategy TestStrategy`
|
||||
@@ -61,22 +60,22 @@ file as reference.**
|
||||
|
||||
### Buy strategy
|
||||
|
||||
Edit the method `populate_buy_trend()` into your strategy file to
|
||||
update your buy strategy.
|
||||
Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy.
|
||||
|
||||
Sample from `user_data/strategies/test_strategy.py`:
|
||||
|
||||
```python
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['tema'] <= dataframe['blower']) &
|
||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||
),
|
||||
'buy'] = 1
|
||||
@@ -87,38 +86,47 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
### Sell strategy
|
||||
|
||||
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
||||
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
|
||||
|
||||
Sample from `user_data/strategies/test_strategy.py`:
|
||||
|
||||
```python
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['tema'] > dataframe['blower']) &
|
||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
```
|
||||
|
||||
## Add more Indicator
|
||||
## Add more Indicators
|
||||
|
||||
As you have seen, buy and sell strategies need indicators. You can add
|
||||
more indicators by extending the list contained in
|
||||
the method `populate_indicators()` from your strategy file.
|
||||
As you have seen, buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||
|
||||
You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
|
||||
|
||||
Sample:
|
||||
|
||||
```python
|
||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
@@ -149,6 +157,11 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
return dataframe
|
||||
```
|
||||
|
||||
### Metadata dict
|
||||
|
||||
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
|
||||
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
||||
|
||||
### Want more indicator examples
|
||||
|
||||
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
# Bot usage
|
||||
This page explains the difference parameters of the bot and how to run
|
||||
it.
|
||||
|
||||
This page explains the difference parameters of the bot and how to run it.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Bot commands](#bot-commands)
|
||||
- [Backtesting commands](#backtesting-commands)
|
||||
- [Hyperopt commands](#hyperopt-commands)
|
||||
|
||||
## Bot commands
|
||||
|
||||
```
|
||||
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
||||
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
||||
@@ -41,6 +43,7 @@ optional arguments:
|
||||
```
|
||||
|
||||
### How to use a different config file?
|
||||
|
||||
The bot allows you to select which config file you want to use. Per
|
||||
default, the bot will load the file `./config.json`
|
||||
|
||||
@@ -49,6 +52,7 @@ python3 ./freqtrade/main.py -c path/far/far/away/config.json
|
||||
```
|
||||
|
||||
### How to use --strategy?
|
||||
|
||||
This parameter will allow you to load your custom strategy class.
|
||||
Per default without `--strategy` or `-s` the bot will load the
|
||||
`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
|
||||
@@ -60,6 +64,7 @@ To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this
|
||||
**Example:**
|
||||
In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
|
||||
a strategy class called `AwesomeStrategy` to load it:
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
||||
```
|
||||
@@ -70,6 +75,7 @@ message the reason (File not found, or errors in your code).
|
||||
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||
|
||||
### How to use --strategy-path?
|
||||
|
||||
This parameter allows you to add an additional strategy lookup path, which gets
|
||||
checked before the default locations (The passed path must be a folder!):
|
||||
```bash
|
||||
@@ -77,21 +83,25 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol
|
||||
```
|
||||
|
||||
#### How to install a strategy?
|
||||
|
||||
This is very simple. Copy paste your strategy file into the folder
|
||||
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
|
||||
|
||||
### How to use --dynamic-whitelist?
|
||||
|
||||
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
||||
on BaseVolume. This value can be changed when you run the script.
|
||||
|
||||
**By Default**
|
||||
Get the 20 currencies based on BaseVolume.
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py --dynamic-whitelist
|
||||
```
|
||||
|
||||
**Customize the number of currencies to retrieve**
|
||||
Get the 30 currencies based on BaseVolume.
|
||||
|
||||
```bash
|
||||
python3 ./freqtrade/main.py --dynamic-whitelist 30
|
||||
```
|
||||
@@ -102,6 +112,7 @@ negative value (e.g -2), `--dynamic-whitelist` will use the default
|
||||
value (20).
|
||||
|
||||
### How to use --db-url?
|
||||
|
||||
When you run the bot in Dry-run mode, per default no transactions are
|
||||
stored in a database. If you want to store your bot actions in a DB
|
||||
using `--db-url`. This can also be used to specify a custom database
|
||||
@@ -111,24 +122,27 @@ in production mode. Example command:
|
||||
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
|
||||
```
|
||||
|
||||
|
||||
## Backtesting commands
|
||||
|
||||
Backtesting also uses the config specified via `-c/--config`.
|
||||
|
||||
```
|
||||
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
||||
[--timerange TIMERANGE] [-l] [-r] [--export EXPORT]
|
||||
[--export-filename EXPORTFILENAME]
|
||||
|
||||
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp]
|
||||
[--timerange TIMERANGE] [-l] [-r]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export EXPORT] [--export-filename PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||
--realistic-simulation
|
||||
uses max_open_trades from config to simulate real
|
||||
world limitations
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking)
|
||||
--dmmp, --disable-max-market-positions
|
||||
Disable applying `max_open_trades` during backtest
|
||||
(same as setting `max_open_trades` to a very high
|
||||
number)
|
||||
--timerange TIMERANGE
|
||||
specify what timerange of data to use.
|
||||
-l, --live using live data
|
||||
@@ -136,16 +150,26 @@ optional arguments:
|
||||
refresh the pairs files in tests/testdata with the
|
||||
latest data from the exchange. Use it if you want to
|
||||
run your backtesting with up-to-date data.
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
Provide a commaseparated list of strategies to
|
||||
backtest Please note that ticker-interval needs to be
|
||||
set either in config or via command line. When using
|
||||
this together with --export trades, the strategy-name
|
||||
is injected into the filename (so backtest-data.json
|
||||
becomes backtest-data-DefaultStrategy.json
|
||||
--export EXPORT export backtest results, argument are: trades Example
|
||||
--export=trades
|
||||
--export-filename EXPORTFILENAME
|
||||
--export-filename PATH
|
||||
Save backtest results to this filename requires
|
||||
--export to be set as well Example --export-
|
||||
filename=backtest_today.json (default: backtest-
|
||||
result.json
|
||||
filename=user_data/backtest_data/backtest_today.json
|
||||
(default: user_data/backtest_data/backtest-
|
||||
result.json)
|
||||
|
||||
```
|
||||
|
||||
### How to use --refresh-pairs-cached parameter?
|
||||
|
||||
The first time your run Backtesting, it will take the pairs you have
|
||||
set in your config file and download data from Bittrex.
|
||||
|
||||
@@ -157,36 +181,42 @@ to come back to the previous version.**
|
||||
To test your strategy with latest data, we recommend continuing using
|
||||
the parameter `-l` or `--live`.
|
||||
|
||||
|
||||
## Hyperopt commands
|
||||
|
||||
To optimize your strategy, you can use hyperopt parameter hyperoptimization
|
||||
to find optimal parameter values for your stategy.
|
||||
|
||||
```
|
||||
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
||||
[--timerange TIMERANGE] [-e INT]
|
||||
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
|
||||
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp]
|
||||
[--timerange TIMERANGE] [-e INT]
|
||||
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||
--realistic-simulation
|
||||
uses max_open_trades from config to simulate real
|
||||
world limitations
|
||||
--timerange TIMERANGE specify what timerange of data to use.
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking)
|
||||
--dmmp, --disable-max-market-positions
|
||||
Disable applying `max_open_trades` during backtest
|
||||
(same as setting `max_open_trades` to a very high
|
||||
number)
|
||||
--timerange TIMERANGE
|
||||
specify what timerange of data to use.
|
||||
-e INT, --epochs INT specify number of epochs (default: 100)
|
||||
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
|
||||
Specify which parameters to hyperopt. Space separate
|
||||
list. Default: all
|
||||
|
||||
```
|
||||
|
||||
## A parameter missing in the configuration?
|
||||
|
||||
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
|
||||
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
|
||||
|
||||
## Next step
|
||||
The optimal strategy of the bot will change with time depending of the
|
||||
market trends. The next step is to
|
||||
|
||||
The optimal strategy of the bot will change with time depending of the market trends. The next step is to
|
||||
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# Configure the bot
|
||||
|
||||
This page explains how to configure your `config.json` file.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Bot commands](#bot-commands)
|
||||
- [Backtesting commands](#backtesting-commands)
|
||||
- [Hyperopt commands](#hyperopt-commands)
|
||||
|
||||
## Setup config.json
|
||||
|
||||
We recommend to copy and use the `config.json.example` as a template
|
||||
for your bot configuration.
|
||||
|
||||
@@ -19,32 +22,50 @@ The table below will list all configuration parameters.
|
||||
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance.
|
||||
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
|
||||
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
||||
| `process_only_new_candles` | false | No | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. Can be set either in Configuration or in the strategy.
|
||||
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
||||
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
||||
| `trailing_stop` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file).
|
||||
| `trailing_stop_positve` | 0 | No | Changes stop-loss once profit has been reached.
|
||||
| `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive.
|
||||
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
|
||||
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
|
||||
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
||||
| `bid_strategy.use_order_book` | false | No | Allows buying of pair using the rates in Order Book Bids.
|
||||
| `bid_strategy.order_book_top` | 0 | No | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
|
||||
| `bid_strategy.check_depth_of_market.enabled` | false | No | Does not buy if the % difference of buy orders and sell orders is met in Order Book.
|
||||
| `bid_strategy.check_depth_of_market.bids_to_ask_delta` | 0 | No | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher.
|
||||
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
|
||||
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
||||
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
||||
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||
| `exchange.ccxt_rate_limit` | True | No | Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
|
||||
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
||||
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
||||
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
|
||||
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
||||
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
||||
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||
| `webhook.enabled` | false | No | Enable useage of Webhook notifications
|
||||
| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
|
||||
| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||
| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||
| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
||||
| `initial_state` | running | No | Defines the initial application state. More information below.
|
||||
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
||||
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
||||
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
||||
|
||||
The definition of each config parameters is in
|
||||
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
||||
The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
||||
|
||||
### Understand stake_amount
|
||||
|
||||
`stake_amount` is an amount of crypto-currency your bot will use for each trade.
|
||||
The minimal value is 0.0005. If there is not enough crypto-currency in
|
||||
the account an exception is generated.
|
||||
@@ -52,9 +73,11 @@ To allow the bot to trade all the avaliable `stake_currency` in your account set
|
||||
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
|
||||
|
||||
### Understand minimal_roi
|
||||
|
||||
`minimal_roi` is a JSON object where the key is a duration
|
||||
in minutes and the value is the minimum ROI in percent.
|
||||
See the example below:
|
||||
|
||||
```
|
||||
"minimal_roi": {
|
||||
"40": 0.0, # Sell after 40 minutes if the profit is not negative
|
||||
@@ -69,6 +92,7 @@ value. This parameter is optional. If you use it, it will take over the
|
||||
`minimal_roi` value from the strategy file.
|
||||
|
||||
### Understand stoploss
|
||||
|
||||
`stoploss` is loss in percentage that should trigger a sale.
|
||||
For example value `-0.10` will cause immediate sell if the
|
||||
profit dips below -10% for a given trade. This parameter is optional.
|
||||
@@ -77,82 +101,100 @@ Most of the strategy files already include the optimal `stoploss`
|
||||
value. This parameter is optional. If you use it, it will take over the
|
||||
`stoploss` value from the strategy file.
|
||||
|
||||
### Understand trailing stoploss
|
||||
|
||||
Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss.
|
||||
|
||||
### Understand initial_state
|
||||
|
||||
`initial_state` is an optional field that defines the initial application state.
|
||||
Possible values are `running` or `stopped`. (default=`running`)
|
||||
If the value is `stopped` the bot has to be started with `/start` first.
|
||||
|
||||
### Understand process_throttle_secs
|
||||
|
||||
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
||||
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
|
||||
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
|
||||
the static list of pairs) if we should buy.
|
||||
|
||||
### Understand ask_last_balance
|
||||
|
||||
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
||||
use the `last` price and values between those interpolate between ask and last
|
||||
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
||||
end up paying more then would probably have been necessary.
|
||||
|
||||
### What values for exchange.name?
|
||||
|
||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
||||
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
||||
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
|
||||
with only Bittrex and Binance.
|
||||
|
||||
The bot was tested with the following exchanges:
|
||||
|
||||
- [Bittrex](https://bittrex.com/): "bittrex"
|
||||
- [Binance](https://www.binance.com/): "binance"
|
||||
|
||||
Feel free to test other exchanges and submit your PR to improve the bot.
|
||||
|
||||
### What values for fiat_display_currency?
|
||||
|
||||
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
||||
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
||||
In addition to central bank currencies, a range of cryto currencies are supported.
|
||||
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
||||
|
||||
## Switch to dry-run mode
|
||||
|
||||
We recommend starting the bot in dry-run mode to see how your bot will
|
||||
behave and how is the performance of your strategy. In Dry-run mode the
|
||||
bot does not engage your money. It only runs a live simulation without
|
||||
creating trades.
|
||||
|
||||
### To switch your bot in Dry-run mode:
|
||||
|
||||
1. Edit your `config.json` file
|
||||
2. Switch dry-run to true and specify db_url for a persistent db
|
||||
|
||||
```json
|
||||
"dry_run": true,
|
||||
"db_url": "sqlite///tradesv3.dryrun.sqlite",
|
||||
```
|
||||
|
||||
3. Remove your Exchange API key (change them by fake api credentials)
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once you will be happy with your bot performance, you can switch it to
|
||||
production mode.
|
||||
|
||||
## Switch to production mode
|
||||
|
||||
In production mode, the bot will engage your money. Be careful a wrong
|
||||
strategy can lose all your money. Be aware of what you are doing when
|
||||
you run it in production mode.
|
||||
|
||||
### To switch your bot in production mode:
|
||||
|
||||
1. Edit your `config.json` file
|
||||
|
||||
2. Switch dry-run to false and don't forget to adapt your database URL if set
|
||||
|
||||
```json
|
||||
"dry_run": false,
|
||||
```
|
||||
|
||||
3. Insert your Exchange API key (change them by fake api keys)
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
@@ -160,10 +202,37 @@ you run it in production mode.
|
||||
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
If you have not your Bittrex API key yet,
|
||||
[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
||||
If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
||||
|
||||
|
||||
### Embedding Strategies
|
||||
|
||||
FreqTrade provides you with with an easy way to embed the strategy into your configuration file.
|
||||
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
|
||||
in your chosen config file.
|
||||
|
||||
##### Encoding a string as BASE64
|
||||
|
||||
This is a quick example, how to generate the BASE64 string in python
|
||||
|
||||
```python
|
||||
from base64 import urlsafe_b64encode
|
||||
|
||||
with open(file, 'r') as f:
|
||||
content = f.read()
|
||||
content = urlsafe_b64encode(content.encode('utf-8'))
|
||||
```
|
||||
|
||||
The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following
|
||||
|
||||
```json
|
||||
"strategy": "NameOfStrategy:BASE64String"
|
||||
```
|
||||
|
||||
Please ensure that 'NameOfStrategy' is identical to the strategy name!
|
||||
|
||||
## Next step
|
||||
Now you have configured your config.json, the next step is to
|
||||
[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
||||
|
||||
Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
||||
|
||||
302
docs/hyperopt.md
302
docs/hyperopt.md
@@ -1,155 +1,116 @@
|
||||
# Hyperopt
|
||||
This page explains how to tune your strategy by finding the optimal
|
||||
parameters with Hyperopt.
|
||||
This page explains how to tune your strategy by finding the optimal
|
||||
parameters, a process called hyperparameter optimization. The bot uses several
|
||||
algorithms included in the `scikit-optimize` package to accomplish this. The
|
||||
search will burn all your CPU cores, make your laptop sound like a fighter jet
|
||||
and still take a long time.
|
||||
|
||||
*Note:* Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
||||
|
||||
## Table of Contents
|
||||
- [Prepare your Hyperopt](#prepare-hyperopt)
|
||||
- [1. Configure your Guards and Triggers](#1-configure-your-guards-and-triggers)
|
||||
- [2. Update the hyperopt config file](#2-update-the-hyperopt-config-file)
|
||||
- [Advanced Hyperopt notions](#advanced-notions)
|
||||
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
|
||||
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
|
||||
- [Solving a Mystery](#solving-a-mystery)
|
||||
- [Adding New Indicators](#adding-new-indicators)
|
||||
- [Execute Hyperopt](#execute-hyperopt)
|
||||
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
||||
|
||||
## Prepare Hyperopt
|
||||
Before we start digging in Hyperopt, we recommend you to take a look at
|
||||
your strategy file located into [user_data/strategies/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||
## Prepare Hyperopting
|
||||
We recommend you start by taking a look at `hyperopt.py` file located in [freqtrade/optimize](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
|
||||
|
||||
### 1. Configure your Guards and Triggers
|
||||
There are two places you need to change in your strategy file to add a
|
||||
new buy strategy for testing:
|
||||
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
||||
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
||||
### Configure your Guards and Triggers
|
||||
There are two places you need to change to add a new buy strategy for testing:
|
||||
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L278-L294).
|
||||
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L218-L229)
|
||||
and the associated methods `indicator_space`, `roi_space`, `stoploss_space`.
|
||||
|
||||
There you have two different type of indicators: 1. `guards` and 2.
|
||||
`triggers`.
|
||||
1. Guards are conditions like "never buy if ADX < 10", or never buy if
|
||||
current price is over EMA10.
|
||||
There you have two different type of indicators: 1. `guards` and 2. `triggers`.
|
||||
1. Guards are conditions like "never buy if ADX < 10", or "never buy if
|
||||
current price is over EMA10".
|
||||
2. Triggers are ones that actually trigger buy in specific moment, like
|
||||
"buy when EMA5 crosses over EMA10" or buy when close price touches lower
|
||||
bollinger band.
|
||||
"buy when EMA5 crosses over EMA10" or "buy when close price touches lower
|
||||
bollinger band".
|
||||
|
||||
HyperOpt will, for each eval round, pick just ONE trigger, and possibly
|
||||
multiple guards. So that the constructed strategy will be something like
|
||||
Hyperoptimization will, for each eval round, pick one trigger and possibly
|
||||
multiple guards. The constructed strategy will be something like
|
||||
"*buy exactly when close price touches lower bollinger band, BUT only if
|
||||
ADX > 10*".
|
||||
|
||||
|
||||
If you have updated the buy strategy, means change the content of
|
||||
If you have updated the buy strategy, ie. changed the contents of
|
||||
`populate_buy_trend()` method you have to update the `guards` and
|
||||
`triggers` hyperopts must used.
|
||||
`triggers` hyperopts must use.
|
||||
|
||||
As for an example if your `populate_buy_trend()` method is:
|
||||
```python
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(dataframe['rsi'] < 35) &
|
||||
(dataframe['adx'] > 65),
|
||||
'buy'] = 1
|
||||
## Solving a Mystery
|
||||
|
||||
return dataframe
|
||||
```
|
||||
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
||||
Bands to trigger your buys. And you also wonder should you use RSI or ADX to
|
||||
help with those buy decisions. If you decide to use RSI or ADX, which values
|
||||
should I use for them? So let's use hyperparameter optimization to solve this
|
||||
mystery.
|
||||
|
||||
Your hyperopt file must contain `guards` to find the right value for
|
||||
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
|
||||
means you will need to enable/disable triggers.
|
||||
|
||||
In our case the `SPACE` and `populate_buy_trend` in your strategy file
|
||||
will look like:
|
||||
```python
|
||||
space = {
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||
]),
|
||||
'adx': hp.choice('adx', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
|
||||
]),
|
||||
'trigger': hp.choice('trigger', [
|
||||
{'type': 'lower_bb'},
|
||||
{'type': 'faststoch10'},
|
||||
{'type': 'ao_cross_zero'},
|
||||
{'type': 'ema5_cross_ema10'},
|
||||
{'type': 'macd_cross_signal'},
|
||||
{'type': 'sar_reversal'},
|
||||
{'type': 'stochf_cross'},
|
||||
{'type': 'ht_sine'},
|
||||
]),
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if params['adx']['enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||
if params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
|
||||
# TRIGGERS
|
||||
triggers = {
|
||||
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
||||
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
|
||||
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
||||
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
||||
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
|
||||
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
|
||||
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
|
||||
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
### 2. Update the hyperopt config file
|
||||
Hyperopt is using a dedicated config file. Currently hyperopt
|
||||
cannot use your config file. It is also made on purpose to allow you
|
||||
testing your strategy with different configurations.
|
||||
|
||||
The Hyperopt configuration is located in
|
||||
[user_data/hyperopt_conf.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
||||
|
||||
|
||||
## Advanced notions
|
||||
### Understand the Guards and Triggers
|
||||
When you need to add the new guards and triggers to be hyperopt
|
||||
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
||||
|
||||
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
||||
|
||||
If it's a guard, you will add a line like this:
|
||||
```
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||
]),
|
||||
```
|
||||
This says, "*one of the guards is RSI, it can have two values, enabled or
|
||||
disabled. If it is enabled, try different values for it between 20 and 40*".
|
||||
|
||||
So, the part of the strategy builder using the above setting looks like
|
||||
this:
|
||||
We will start by defining a search space:
|
||||
|
||||
```
|
||||
if params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching strategy parameters
|
||||
"""
|
||||
return [
|
||||
Integer(20, 40, name='adx-value'),
|
||||
Integer(20, 40, name='rsi-value'),
|
||||
Categorical([True, False], name='adx-enabled'),
|
||||
Categorical([True, False], name='rsi-enabled'),
|
||||
Categorical(['bb_lower', 'macd_cross_signal'], name='trigger')
|
||||
]
|
||||
```
|
||||
|
||||
It checks if Hyperopt wants the RSI guard to be enabled for this
|
||||
round `params['rsi']['enabled']` and if it is, then it will add a
|
||||
condition that says RSI must be smaller than the value hyperopt picked
|
||||
for this evaluation, which is given in the `params['rsi']['value']`.
|
||||
Above definition says: I have five parameters I want you to randomly combine
|
||||
to find the best combination. Two of them are integer values (`adx-value`
|
||||
and `rsi-value`) and I want you test in the range of values 20 to 40.
|
||||
Then we have three category variables. First two are either `True` or `False`.
|
||||
We use these to either enable or disable the ADX and RSI guards. The last
|
||||
one we call `trigger` and use it to decide which buy trigger we want to use.
|
||||
|
||||
That's it. Now you can add new parts of strategies to Hyperopt and it
|
||||
will try all the combinations with all different values in the search
|
||||
for best working algo.
|
||||
So let's write the buy strategy using these values:
|
||||
|
||||
```
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if 'adx-enabled' in params and params['adx-enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||
|
||||
### Add a new Indicators
|
||||
If you want to test an indicator that isn't used by the bot currently,
|
||||
you need to add it to the `populate_indicators()` method in `hyperopt.py`.
|
||||
# TRIGGERS
|
||||
if params['trigger'] == 'bb_lower':
|
||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||
if params['trigger'] == 'macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macd'], dataframe['macdsignal']
|
||||
))
|
||||
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
return populate_buy_trend
|
||||
```
|
||||
|
||||
Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`)
|
||||
with different value combinations. It will then use the given historical data and make
|
||||
buys based on the buy signals generated with the above function and based on the results
|
||||
it will end with telling you which paramter combination produced the best profits.
|
||||
|
||||
The search for best parameters starts with a few random combinations and then uses a
|
||||
regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination
|
||||
that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`.
|
||||
|
||||
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
||||
When you want to test an indicator that isn't used by the bot currently, remember to
|
||||
add it to the `populate_indicators()` method in `hyperopt.py`.
|
||||
|
||||
## Execute Hyperopt
|
||||
Once you have updated your hyperopt configuration you can run it.
|
||||
@@ -164,12 +125,12 @@ python3 ./freqtrade/main.py -c config.json hyperopt -e 5000
|
||||
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
||||
running at least several thousand evaluations.
|
||||
|
||||
### Execute hyperopt with different ticker-data source
|
||||
### Execute Hyperopt with Different Ticker-Data Source
|
||||
If you would like to hyperopt parameters using an alternate ticker data that
|
||||
you have on-disk, use the `--datadir PATH` option. Default hyperopt will
|
||||
use data from directory `user_data/data`.
|
||||
|
||||
### Running hyperopt with smaller testset
|
||||
### Running Hyperopt with Smaller Testset
|
||||
Use the `--timeperiod` argument to change how much of the testset
|
||||
you want to use. The last N ticks/timeframes will be used.
|
||||
Example:
|
||||
@@ -178,7 +139,7 @@ Example:
|
||||
python3 ./freqtrade/main.py hyperopt --timeperiod -200
|
||||
```
|
||||
|
||||
### Running hyperopt with smaller search space
|
||||
### Running Hyperopt with Smaller Search Space
|
||||
Use the `--spaces` argument to limit the search space used by hyperopt.
|
||||
Letting Hyperopt optimize everything is a huuuuge search space. Often it
|
||||
might make more sense to start by just searching for initial buy algorithm.
|
||||
@@ -193,87 +154,44 @@ Legal values are:
|
||||
- `stoploss`: search for the best stoploss value
|
||||
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||
|
||||
## Understand the hyperopts result
|
||||
Once Hyperopt is completed you can use the result to adding new buy
|
||||
signal. Given following result from hyperopt:
|
||||
```
|
||||
Best parameters:
|
||||
{
|
||||
"adx": {
|
||||
"enabled": true,
|
||||
"value": 15.0
|
||||
},
|
||||
"fastd": {
|
||||
"enabled": true,
|
||||
"value": 40.0
|
||||
},
|
||||
"green_candle": {
|
||||
"enabled": true
|
||||
},
|
||||
"mfi": {
|
||||
"enabled": false
|
||||
},
|
||||
"over_sar": {
|
||||
"enabled": false
|
||||
},
|
||||
"rsi": {
|
||||
"enabled": true,
|
||||
"value": 37.0
|
||||
},
|
||||
"trigger": {
|
||||
"type": "lower_bb"
|
||||
},
|
||||
"uptrend_long_ema": {
|
||||
"enabled": true
|
||||
},
|
||||
"uptrend_short_ema": {
|
||||
"enabled": false
|
||||
},
|
||||
"uptrend_sma": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
## Understand the Hyperopts Result
|
||||
Once Hyperopt is completed you can use the result to create a new strategy.
|
||||
Given the following result from hyperopt:
|
||||
|
||||
Best Result:
|
||||
2197 trades. Avg profit 1.84%. Total profit 0.79367541 BTC. Avg duration 241.0 mins.
|
||||
```
|
||||
Best result:
|
||||
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
||||
with values:
|
||||
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower'}
|
||||
```
|
||||
|
||||
You should understand this result like:
|
||||
- You should **consider** the guard "adx" (`"adx"` is `"enabled": true`)
|
||||
and the best value is `15.0` (`"value": 15.0,`)
|
||||
- You should **consider** the guard "fastd" (`"fastd"` is `"enabled":
|
||||
true`) and the best value is `40.0` (`"value": 40.0,`)
|
||||
- You should **consider** to enable the guard "green_candle"
|
||||
(`"green_candle"` is `"enabled": true`) but this guards as no
|
||||
customizable value.
|
||||
- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
|
||||
- and so on...
|
||||
- The buy trigger that worked best was `bb_lower`.
|
||||
- You should not use ADX because `adx-enabled: False`)
|
||||
- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
|
||||
|
||||
You have to look inside your strategy file into `buy_strategy_generator()`
|
||||
method, what those values match to.
|
||||
|
||||
So for example you had `adx:` with the `value: 15.0` so we would look
|
||||
at `adx`-block, that translates to the following code block:
|
||||
So for example you had `rsi-value: 29.0` so we would look
|
||||
at `rsi`-block, that translates to the following code block:
|
||||
```
|
||||
(dataframe['adx'] > 15.0)
|
||||
(dataframe['rsi'] < 29.0)
|
||||
```
|
||||
|
||||
Translating your whole hyperopt result to as the new buy-signal
|
||||
would be the following:
|
||||
Translating your whole hyperopt result as the new buy-signal
|
||||
would then look like:
|
||||
```
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 15.0) & # adx-value
|
||||
(dataframe['fastd'] < 40.0) & # fastd-value
|
||||
(dataframe['close'] > dataframe['open']) & # green_candle
|
||||
(dataframe['rsi'] < 37.0) & # rsi-value
|
||||
(dataframe['ema50'] > dataframe['ema100']) # uptrend_long_ema
|
||||
(dataframe['rsi'] < 29.0) & # rsi-value
|
||||
dataframe['close'] < dataframe['bb_lowerband'] # trigger
|
||||
),
|
||||
'buy'] = 1
|
||||
return dataframe
|
||||
```
|
||||
|
||||
## Next step
|
||||
## Next Step
|
||||
Now you have a perfect bot and want to control it from Telegram. Your
|
||||
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# freqtrade documentation
|
||||
|
||||
Welcome to freqtrade documentation. Please feel free to contribute to
|
||||
this documentation if you see it became outdated by sending us a
|
||||
Pull-request. Do not hesitate to reach us on
|
||||
@@ -6,6 +7,7 @@ Pull-request. Do not hesitate to reach us on
|
||||
if you do not find the answer to your questions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
||||
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
||||
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
||||
@@ -25,8 +27,10 @@ Pull-request. Do not hesitate to reach us on
|
||||
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
|
||||
- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md)
|
||||
- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
- [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
|
||||
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
||||
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md))
|
||||
|
||||
@@ -8,7 +8,6 @@ To understand how to set up the bot please read the [Bot Configuration](https://
|
||||
|
||||
* [Table of Contents](#table-of-contents)
|
||||
* [Easy Installation - Linux Script](#easy-installation---linux-script)
|
||||
* [Manual installation](#manual-installation)
|
||||
* [Automatic Installation - Docker](#automatic-installation---docker)
|
||||
* [Custom Linux MacOS Installation](#custom-installation)
|
||||
- [Requirements](#requirements)
|
||||
@@ -56,28 +55,6 @@ Reset parameter will hard reset your branch (only if you are on `master` or `dev
|
||||
|
||||
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
|
||||
|
||||
|
||||
## Manual installation - Linux/MacOS
|
||||
The following steps are made for Linux/MacOS environment
|
||||
|
||||
**1. Clone the repo**
|
||||
```bash
|
||||
git clone git@github.com:freqtrade/freqtrade.git
|
||||
git checkout develop
|
||||
cd freqtrade
|
||||
```
|
||||
**2. Create the config file**
|
||||
Switch `"dry_run": true,`
|
||||
```bash
|
||||
cp config.json.example config.json
|
||||
vi config.json
|
||||
```
|
||||
**3. Build your docker image and run it**
|
||||
```bash
|
||||
docker build -t freqtrade .
|
||||
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
## Automatic Installation - Docker
|
||||
@@ -190,7 +167,7 @@ docker run -d \
|
||||
freqtrade --db-url sqlite:///tradesv3.sqlite
|
||||
```
|
||||
|
||||
NOTE: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
||||
*Note*: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
||||
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
||||
|
||||
### 6. Monitor your Docker instance
|
||||
@@ -205,14 +182,15 @@ docker stop freqtrade
|
||||
docker start freqtrade
|
||||
```
|
||||
|
||||
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
||||
For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
|
||||
|
||||
*Note*: You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
||||
|
||||
### 7. Backtest with docker
|
||||
|
||||
The following assumes that the above steps (1-4) have been completed successfully.
|
||||
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
||||
|
||||
|
||||
``` bash
|
||||
docker run -d \
|
||||
--name freqtrade \
|
||||
@@ -232,12 +210,13 @@ Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtr
|
||||
## Custom Installation
|
||||
|
||||
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
||||
OS Specific steps are listed first, the [common](#common) section below is necessary for all systems.
|
||||
|
||||
### Requirements
|
||||
|
||||
Click each one for install guide:
|
||||
|
||||
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x
|
||||
* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||
@@ -245,7 +224,7 @@ Click each one for install guide:
|
||||
|
||||
### Linux - Ubuntu 16.04
|
||||
|
||||
#### 1. Install Python 3.6, Git, and wget
|
||||
#### Install Python 3.6, Git, and wget
|
||||
|
||||
```bash
|
||||
sudo add-apt-repository ppa:jonathonf/python-3.6
|
||||
@@ -253,7 +232,34 @@ sudo apt-get update
|
||||
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
|
||||
```
|
||||
|
||||
#### 2. Install TA-Lib
|
||||
#### Raspberry Pi / Raspbian
|
||||
|
||||
Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/).
|
||||
|
||||
The following assumes that miniconda3 is installed and available in your environment, and is installed.
|
||||
It's recommended to use (mini)conda for this as installation/compilation of `scipy` and `pandas` takes a long time.
|
||||
|
||||
``` bash
|
||||
conda config --add channels rpi
|
||||
conda install python=3.6
|
||||
conda create -n freqtrade python=3.6
|
||||
conda install scipy pandas
|
||||
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### MacOS
|
||||
|
||||
#### Install Python 3.6, git, wget and ta-lib
|
||||
|
||||
```bash
|
||||
brew install python3 git wget
|
||||
```
|
||||
|
||||
### common
|
||||
|
||||
#### 1. Install TA-Lib
|
||||
|
||||
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
||||
|
||||
@@ -261,6 +267,7 @@ Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
||||
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||
tar xvzf ta-lib-0.4.0-src.tar.gz
|
||||
cd ta-lib
|
||||
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||
./configure --prefix=/usr
|
||||
make
|
||||
make install
|
||||
@@ -268,15 +275,60 @@ cd ..
|
||||
rm -rf ./ta-lib*
|
||||
```
|
||||
|
||||
*Note*: An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently.
|
||||
|
||||
#### 2. Setup your Python virtual environment (virtualenv)
|
||||
|
||||
*Note*: This step is optional but strongly recommended to keep your system organized
|
||||
|
||||
```bash
|
||||
python3 -m venv .env
|
||||
source .env/bin/activate
|
||||
```
|
||||
|
||||
#### 3. Install FreqTrade
|
||||
|
||||
Clone the git repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/freqtrade/freqtrade.git
|
||||
|
||||
```
|
||||
|
||||
#### 4. Configure `freqtrade` as a `systemd` service
|
||||
Optionally checkout the stable/master branch:
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
```
|
||||
|
||||
#### 4. Initialize the configuration
|
||||
|
||||
```bash
|
||||
cd freqtrade
|
||||
cp config.json.example config.json
|
||||
```
|
||||
|
||||
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
|
||||
|
||||
#### 5. Install python dependencies
|
||||
|
||||
``` bash
|
||||
pip3 install --upgrade pip
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install -e .
|
||||
```
|
||||
|
||||
#### 6. Run the Bot
|
||||
|
||||
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
|
||||
|
||||
```bash
|
||||
python3.6 ./freqtrade/main.py -c config.json
|
||||
```
|
||||
|
||||
*Note*: If you run the bot on a server, you should consider using [Docker](#automatic-installation---docker) a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
|
||||
|
||||
#### 7. [Optional] Configure `freqtrade` as a `systemd` service
|
||||
|
||||
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||
|
||||
@@ -292,57 +344,6 @@ For this to be persistent (run when user is logged out) you'll need to enable `l
|
||||
sudo loginctl enable-linger "$USER"
|
||||
```
|
||||
|
||||
### MacOS
|
||||
|
||||
#### 1. Install Python 3.6, git, wget and ta-lib
|
||||
|
||||
```bash
|
||||
brew install python3 git wget ta-lib
|
||||
```
|
||||
|
||||
#### 2. Install FreqTrade
|
||||
|
||||
Clone the git repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/freqtrade/freqtrade.git
|
||||
```
|
||||
|
||||
Optionally checkout the develop branch:
|
||||
|
||||
```bash
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
### Setup Config and virtual env
|
||||
|
||||
#### 1. Initialize the configuration
|
||||
|
||||
```bash
|
||||
cd freqtrade
|
||||
cp config.json.example config.json
|
||||
```
|
||||
|
||||
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
|
||||
|
||||
#### 2. Setup your Python virtual environment (virtualenv)
|
||||
|
||||
```bash
|
||||
python3.6 -m venv .env
|
||||
source .env/bin/activate
|
||||
pip3.6 install --upgrade pip
|
||||
pip3.6 install -r requirements.txt
|
||||
pip3.6 install -e .
|
||||
```
|
||||
|
||||
#### 3. Run the Bot
|
||||
|
||||
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
|
||||
|
||||
```bash
|
||||
python3.6 ./freqtrade/main.py -c config.json
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
## Windows
|
||||
@@ -362,7 +363,7 @@ git clone https://github.com/freqtrade/freqtrade.git
|
||||
|
||||
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
||||
|
||||
#### install ta-lib
|
||||
#### Install ta-lib
|
||||
|
||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||
|
||||
@@ -383,5 +384,17 @@ REM >pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl
|
||||
|
||||
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
||||
|
||||
#### Error during installation under Windows
|
||||
|
||||
``` bash
|
||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||
```
|
||||
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
|
||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or docker first.
|
||||
|
||||
---
|
||||
|
||||
Now you have an environment ready, the next step is
|
||||
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...
|
||||
|
||||
@@ -24,7 +24,7 @@ script/plot_dataframe.py [-h] [-p pair] [--live]
|
||||
|
||||
Example
|
||||
```
|
||||
python scripts/plot_dataframe.py -p BTC_ETH
|
||||
python scripts/plot_dataframe.py -p BTC/ETH
|
||||
```
|
||||
|
||||
The `-p` pair argument, can be used to specify what
|
||||
@@ -34,18 +34,18 @@ pair you would like to plot.
|
||||
|
||||
To plot the current live price use the `--live` flag:
|
||||
```
|
||||
python scripts/plot_dataframe.py -p BTC_ETH --live
|
||||
python scripts/plot_dataframe.py -p BTC/ETH --live
|
||||
```
|
||||
|
||||
To plot a timerange (to zoom in):
|
||||
```
|
||||
python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200
|
||||
python scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200
|
||||
```
|
||||
Timerange doesn't work with live data.
|
||||
|
||||
To plot trades stored in a database use `--db-url` argument:
|
||||
```
|
||||
python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH
|
||||
python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH
|
||||
```
|
||||
|
||||
To plot a test strategy the strategy should have first be backtested.
|
||||
|
||||
141
docs/sandbox-testing.md
Normal file
141
docs/sandbox-testing.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Sandbox API testing
|
||||
|
||||
Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these.
|
||||
|
||||
This document is a *light overview of configuring Freqtrade and GDAX sandbox.
|
||||
This can be useful to developers and trader alike as Freqtrade is quite customisable.
|
||||
|
||||
When testing your API connectivity, make sure to use the following URLs.
|
||||
***Website**
|
||||
https://public.sandbox.gdax.com
|
||||
***REST API**
|
||||
https://api-public.sandbox.gdax.com
|
||||
|
||||
---
|
||||
|
||||
# Configure a Sandbox account on Gdax
|
||||
|
||||
Aim of this document section
|
||||
|
||||
- An sanbox account
|
||||
- create 2FA (needed to create an API)
|
||||
- Add test 50BTC to account
|
||||
- Create :
|
||||
- - API-KEY
|
||||
- - API-Secret
|
||||
- - API Password
|
||||
|
||||
## Acccount
|
||||
|
||||
This link will redirect to the sandbox main page to login / create account dialogues:
|
||||
https://public.sandbox.pro.coinbase.com/orders/
|
||||
|
||||
After registration and Email confimation you wil be redirected into your sanbox account. It is easy to verify you're in sandbox by checking the URL bar.
|
||||
> https://public.sandbox.pro.coinbase.com/
|
||||
|
||||
## Enable 2Fa (a prerequisite to creating sandbox API Keys)
|
||||
|
||||
From within sand box site select your profile, top right.
|
||||
>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile
|
||||
|
||||
From the menu panel to the left of the screen select
|
||||
|
||||
> Security: "*View or Update*"
|
||||
|
||||
In the new site select "enable authenticator" as typical google Authenticator.
|
||||
|
||||
- open Google Authenticator on your phone
|
||||
- scan barcode
|
||||
- enter your generated 2fa
|
||||
|
||||
## Enable API Access
|
||||
|
||||
From within sandbox select profile>api>create api-keys
|
||||
>or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api
|
||||
|
||||
Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2FA
|
||||
|
||||
- **Copy and paste the Passphase** into a notepade this will be needed later
|
||||
- **Copy and paste the API Secret** popup into a notepad this will needed later
|
||||
- **Copy and paste the API Key** into a notepad this will needed later
|
||||
|
||||
## Add 50 BTC test funds
|
||||
|
||||
To add funds, use the web interface deposit and withdraw buttons.
|
||||
|
||||
To begin select 'Wallets' from the top menu.
|
||||
> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets
|
||||
|
||||
- Deposits (bottom left of screen)
|
||||
- - Deposit Funds Bitcoin
|
||||
- - - Coinbase BTC Wallet
|
||||
- - - - Max (50 BTC)
|
||||
- - - - - Deposit
|
||||
|
||||
*This process may be repeated for other currencies, ETH as example*
|
||||
|
||||
---
|
||||
|
||||
# Configure Freqtrade to use Gax Sandbox
|
||||
|
||||
The aim of this document section
|
||||
|
||||
- Enable sandbox URLs in Freqtrade
|
||||
- Configure API
|
||||
- - secret
|
||||
- - key
|
||||
- - passphrase
|
||||
|
||||
## Sandbox URLs
|
||||
|
||||
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
|
||||
These include `['test']` and `['api']`.
|
||||
|
||||
- `[Test]` if available will point to an Exchanges sandbox.
|
||||
- `[Api]` normally used, and resolves to live API target on the exchange
|
||||
|
||||
To make use of sandbox / test add "sandbox": true, to your config.json
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "gdax",
|
||||
"sandbox": true,
|
||||
"key": "5wowfxemogxeowo;heiohgmd",
|
||||
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
|
||||
"password": "1bkjfkhfhfu6sr",
|
||||
"outdated_offset": 5
|
||||
"pair_whitelist": [
|
||||
"BTC/USD"
|
||||
```
|
||||
|
||||
Also insert your
|
||||
|
||||
- api-key (noted earlier)
|
||||
- api-secret (noted earlier)
|
||||
- password (the passphrase - noted earlier)
|
||||
|
||||
---
|
||||
|
||||
## You should now be ready to test your sandbox
|
||||
|
||||
Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox.
|
||||
** Typically the BTC/USD has the most activity in sandbox to test against.
|
||||
|
||||
## GDAX - Old Candles problem
|
||||
|
||||
It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks.
|
||||
|
||||
To disable this check, add / change the `"outdated_offset"` parameter in the exchange section of your configuration to adjust for this delay.
|
||||
Example based on the above configuration:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "gdax",
|
||||
"sandbox": true,
|
||||
"key": "5wowfxemogxeowo;heiohgmd",
|
||||
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
|
||||
"password": "1bkjfkhfhfu6sr",
|
||||
"outdated_offset": 30
|
||||
"pair_whitelist": [
|
||||
"BTC/USD"
|
||||
```
|
||||
@@ -59,7 +59,7 @@ SELECT * FROM trades;
|
||||
|
||||
```sql
|
||||
UPDATE trades
|
||||
SET is_open=0, close_date=<close_date>, close_rate=<close_rate>, close_profit=close_rate/open_rate
|
||||
SET is_open=0, close_date=<close_date>, close_rate=<close_rate>, close_profit=close_rate/open_rate-1
|
||||
WHERE id=<trade_ID_to_update>;
|
||||
```
|
||||
|
||||
|
||||
51
docs/stoploss.md
Normal file
51
docs/stoploss.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Stop Loss support
|
||||
|
||||
At this stage the bot contains the following stoploss support modes:
|
||||
|
||||
1. static stop loss, defined in either the strategy or configuration
|
||||
2. trailing stop loss, defined in the configuration
|
||||
3. trailing stop loss, custom positive loss, defined in configuration
|
||||
|
||||
## Static Stop Loss
|
||||
|
||||
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
|
||||
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
|
||||
|
||||
## Trail Stop Loss
|
||||
|
||||
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
|
||||
To enable this Feauture all you have to do is to define the configuration element:
|
||||
|
||||
``` json
|
||||
"trailing_stop" : True
|
||||
```
|
||||
|
||||
This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases.
|
||||
|
||||
For example, simplified math,
|
||||
|
||||
* you buy an asset at a price of 100$
|
||||
* your stop loss is defined at 2%
|
||||
* which means your stop loss, gets triggered once your asset dropped below 98$
|
||||
* assuming your asset now increases to 102$
|
||||
* your stop loss, will now be 2% of 102$ or 99.96$
|
||||
* now your asset drops in value to 101$, your stop loss, will still be 99.96$
|
||||
|
||||
basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price
|
||||
|
||||
### Custom positive loss
|
||||
|
||||
Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage,
|
||||
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you have 1.1% profit,
|
||||
it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them.
|
||||
|
||||
Both values can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true.
|
||||
|
||||
``` json
|
||||
"trailing_stop_positive": 0.01,
|
||||
"trailing_stop_positive_offset": 0.011,
|
||||
```
|
||||
|
||||
The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit.
|
||||
|
||||
You should also make sure to have this value higher than your minimal ROI, otherwise minimal ROI will apply first and sell your trade.
|
||||
74
docs/webhook-config.md
Normal file
74
docs/webhook-config.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Webhook usage
|
||||
|
||||
This page explains how to configure your bot to talk to webhooks.
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable webhooks by adding a webhook-section to your configuration file, and setting `webhook.enabled` to `true`.
|
||||
|
||||
Sample configuration (tested using IFTTT).
|
||||
|
||||
```json
|
||||
"webhook": {
|
||||
"enabled": true,
|
||||
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
|
||||
"webhookbuy": {
|
||||
"value1": "Buying {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "{stake_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhooksell": {
|
||||
"value1": "Selling {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhookstatus": {
|
||||
"value1": "Status: {status}",
|
||||
"value2": "",
|
||||
"value3": ""
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url.
|
||||
|
||||
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
||||
|
||||
### Webhookbuy
|
||||
|
||||
The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* exchange
|
||||
* pair
|
||||
* market_url
|
||||
* limit
|
||||
* stake_amount
|
||||
* stake_amount_fiat
|
||||
* stake_currency
|
||||
* fiat_currency
|
||||
|
||||
### Webhooksell
|
||||
|
||||
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* exchange
|
||||
* pair
|
||||
* gain
|
||||
* market_url
|
||||
* limit
|
||||
* amount
|
||||
* open_rate
|
||||
* current_rate
|
||||
* profit_amount
|
||||
* profit_percent
|
||||
* profit_fiat
|
||||
* stake_currency
|
||||
* fiat_currency
|
||||
|
||||
### Webhookstatus
|
||||
|
||||
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
||||
|
||||
The only possible value here is `{status}`.
|
||||
@@ -1,5 +1,5 @@
|
||||
""" FreqTrade bot """
|
||||
__version__ = '0.17.0'
|
||||
__version__ = '0.17.1'
|
||||
|
||||
|
||||
class DependencyException(BaseException):
|
||||
|
||||
@@ -7,8 +7,8 @@ To launch Freqtrade as a module
|
||||
"""
|
||||
|
||||
import sys
|
||||
from freqtrade import main
|
||||
|
||||
from freqtrade import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main.set_loggers()
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
"""
|
||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SignalType(Enum):
|
||||
"""
|
||||
Enum to distinguish between buy and sell signals
|
||||
"""
|
||||
BUY = "buy"
|
||||
SELL = "sell"
|
||||
|
||||
|
||||
class Analyze(object):
|
||||
"""
|
||||
Analyze class contains everything the bot need to determine if the situation is good for
|
||||
buying or selling.
|
||||
"""
|
||||
def __init__(self, config: dict) -> None:
|
||||
"""
|
||||
Init Analyze
|
||||
:param config: Bot configuration (use the one from Configuration())
|
||||
"""
|
||||
self.config = config
|
||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||
|
||||
@staticmethod
|
||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||
"""
|
||||
Analyses the trend for the given ticker history
|
||||
:param ticker: See exchange.get_ticker_history
|
||||
:return: DataFrame
|
||||
"""
|
||||
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
frame = DataFrame(ticker, columns=cols)
|
||||
|
||||
frame['date'] = to_datetime(frame['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
|
||||
# group by index and aggregate results to eliminate duplicate ticks
|
||||
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
||||
'open': 'first',
|
||||
'high': 'max',
|
||||
'low': 'min',
|
||||
'close': 'last',
|
||||
'volume': 'max',
|
||||
})
|
||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||
return frame
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
"""
|
||||
return self.strategy.populate_indicators(dataframe=dataframe)
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
return self.strategy.populate_buy_trend(dataframe=dataframe)
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
return self.strategy.populate_sell_trend(dataframe=dataframe)
|
||||
|
||||
def get_ticker_interval(self) -> str:
|
||||
"""
|
||||
Return ticker interval to use
|
||||
:return: Ticker interval value to use
|
||||
"""
|
||||
return self.strategy.ticker_interval
|
||||
|
||||
def get_stoploss(self) -> float:
|
||||
"""
|
||||
Return stoploss to use
|
||||
:return: Strategy stoploss value to use
|
||||
"""
|
||||
return self.strategy.stoploss
|
||||
|
||||
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
dataframe = self.parse_ticker_dataframe(ticker_history)
|
||||
dataframe = self.populate_indicators(dataframe)
|
||||
dataframe = self.populate_buy_trend(dataframe)
|
||||
dataframe = self.populate_sell_trend(dataframe)
|
||||
return dataframe
|
||||
|
||||
def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]:
|
||||
"""
|
||||
Calculates current signal based several technical analysis indicators
|
||||
:param pair: pair in format ANT/BTC
|
||||
:param interval: Interval to use (in min)
|
||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||
"""
|
||||
ticker_hist = exchange.get_ticker_history(pair, interval)
|
||||
if not ticker_hist:
|
||||
logger.warning('Empty ticker history for pair %s', pair)
|
||||
return False, False
|
||||
|
||||
try:
|
||||
dataframe = self.analyze_ticker(ticker_hist)
|
||||
except ValueError as error:
|
||||
logger.warning(
|
||||
'Unable to analyze ticker for pair %s: %s',
|
||||
pair,
|
||||
str(error)
|
||||
)
|
||||
return False, False
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
'Unexpected error when analyzing ticker for pair %s: %s',
|
||||
pair,
|
||||
str(error)
|
||||
)
|
||||
return False, False
|
||||
|
||||
if dataframe.empty:
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return False, False
|
||||
|
||||
latest = dataframe.iloc[-1]
|
||||
|
||||
# Check if dataframe is out of date
|
||||
signal_date = arrow.get(latest['date'])
|
||||
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||
if signal_date < (arrow.utcnow() - timedelta(minutes=(interval_minutes + 5))):
|
||||
logger.warning(
|
||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||
pair,
|
||||
(arrow.utcnow() - signal_date).seconds // 60
|
||||
)
|
||||
return False, False
|
||||
|
||||
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
||||
logger.debug(
|
||||
'trigger: %s (pair=%s) buy=%s sell=%s',
|
||||
latest['date'],
|
||||
pair,
|
||||
str(buy),
|
||||
str(sell)
|
||||
)
|
||||
return buy, sell
|
||||
|
||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
|
||||
"""
|
||||
This function evaluate if on the condition required to trigger a sell has been reached
|
||||
if the threshold is reached and updates the trade record.
|
||||
:return: True if trade should be sold, False otherwise
|
||||
"""
|
||||
current_profit = trade.calc_profit_percent(rate)
|
||||
if self.stop_loss_reached(current_profit=current_profit):
|
||||
return True
|
||||
|
||||
experimental = self.config.get('experimental', {})
|
||||
|
||||
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
||||
logger.debug('Buy signal still active - not selling.')
|
||||
return False
|
||||
|
||||
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
||||
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
|
||||
logger.debug('Required profit reached. Selling..')
|
||||
return True
|
||||
|
||||
if experimental.get('sell_profit_only', False):
|
||||
logger.debug('Checking if trade is profitable..')
|
||||
if trade.calc_profit(rate=rate) <= 0:
|
||||
return False
|
||||
if sell and not buy and experimental.get('use_sell_signal', False):
|
||||
logger.debug('Sell signal received. Selling..')
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def stop_loss_reached(self, current_profit: float) -> bool:
|
||||
"""Based on current profit of the trade and configured stoploss, decides to sell or not"""
|
||||
|
||||
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
||||
logger.debug('Stop loss hit.')
|
||||
return True
|
||||
return False
|
||||
|
||||
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||
"""
|
||||
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
||||
sell
|
||||
:return True if bot should sell at current rate
|
||||
"""
|
||||
|
||||
# Check if time matches and current rate is above threshold
|
||||
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||
for duration, threshold in self.strategy.minimal_roi.items():
|
||||
if time_diff <= duration:
|
||||
return False
|
||||
if current_profit > threshold:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Creates a dataframe and populates indicators for given ticker data
|
||||
"""
|
||||
return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data))
|
||||
for pair, pair_data in tickerdata.items()}
|
||||
@@ -2,12 +2,12 @@
|
||||
This module contains the argument manager class
|
||||
"""
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
import arrow
|
||||
from typing import List, Optional, NamedTuple
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
|
||||
@@ -63,11 +63,10 @@ class Arguments(object):
|
||||
"""
|
||||
self.parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
help='be verbose',
|
||||
action='store_const',
|
||||
help='verbose mode (-vv for more, -vvv to get all messages)',
|
||||
action='count',
|
||||
dest='loglevel',
|
||||
const=logging.DEBUG,
|
||||
default=logging.INFO,
|
||||
default=0,
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'--version',
|
||||
@@ -120,7 +119,6 @@ class Arguments(object):
|
||||
help='Override trades database URL, this is useful if dry_run is enabled'
|
||||
' or in custom deployments (default: %(default)s)',
|
||||
dest='db_url',
|
||||
default=constants.DEFAULT_DB_PROD_URL,
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
@@ -143,6 +141,16 @@ class Arguments(object):
|
||||
action='store_true',
|
||||
dest='refresh_pairs',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--strategy-list',
|
||||
help='Provide a commaseparated list of strategies to backtest '
|
||||
'Please note that ticker-interval needs to be set either in config '
|
||||
'or via command line. When using this together with --export trades, '
|
||||
'the strategy-name is injected into the filename '
|
||||
'(so backtest-data.json becomes backtest-data-DefaultStrategy.json',
|
||||
nargs='+',
|
||||
dest='strategy_list',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--export',
|
||||
help='export backtest results, argument are: trades\
|
||||
@@ -177,11 +185,22 @@ class Arguments(object):
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--realistic-simulation',
|
||||
help='uses max_open_trades from config to simulate real world limitations',
|
||||
'--eps', '--enable-position-stacking',
|
||||
help='Allow buying the same pair multiple times (position stacking)',
|
||||
action='store_true',
|
||||
dest='realistic_simulation',
|
||||
dest='position_stacking',
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--dmmp', '--disable-max-market-positions',
|
||||
help='Disable applying `max_open_trades` during backtest '
|
||||
'(same as setting `max_open_trades` to a very high number)',
|
||||
action='store_false',
|
||||
dest='use_max_market_positions',
|
||||
default=True
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--timerange',
|
||||
help='specify what timerange of data to use.',
|
||||
@@ -334,3 +353,10 @@ class Arguments(object):
|
||||
nargs='+',
|
||||
dest='timeframes',
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
'--erase',
|
||||
help='Clean all existing data for the selected exchange/pairs/timeframes',
|
||||
dest='erase',
|
||||
action='store_true'
|
||||
)
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
"""
|
||||
This module contains the configuration class
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import ccxt
|
||||
from jsonschema import Draft4Validator, validate
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
import ccxt
|
||||
|
||||
from freqtrade import OperationalException, constants
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def set_loggers(log_level: int = 0) -> None:
|
||||
"""
|
||||
Set the logger level for Third party libs
|
||||
:return: None
|
||||
"""
|
||||
|
||||
logging.getLogger('requests').setLevel(logging.INFO if log_level <= 1 else logging.DEBUG)
|
||||
logging.getLogger("urllib3").setLevel(logging.INFO if log_level <= 1 else logging.DEBUG)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(
|
||||
logging.INFO if log_level <= 2 else logging.DEBUG)
|
||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||
|
||||
|
||||
class Configuration(object):
|
||||
"""
|
||||
Class to read and init the bot configuration
|
||||
@@ -62,8 +74,8 @@ class Configuration(object):
|
||||
conf = json.load(file)
|
||||
except FileNotFoundError:
|
||||
raise OperationalException(
|
||||
'Config file "{}" not found!'
|
||||
' Please create a config file or check whether it exists.'.format(path))
|
||||
f'Config file "{path}" not found!'
|
||||
' Please create a config file or check whether it exists.')
|
||||
|
||||
if 'internals' not in conf:
|
||||
conf['internals'] = {}
|
||||
@@ -79,12 +91,15 @@ class Configuration(object):
|
||||
|
||||
# Log level
|
||||
if 'loglevel' in self.args and self.args.loglevel:
|
||||
config.update({'loglevel': self.args.loglevel})
|
||||
logging.basicConfig(
|
||||
level=config['loglevel'],
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
)
|
||||
logger.info('Log level set to %s', logging.getLevelName(config['loglevel']))
|
||||
config.update({'verbosity': self.args.loglevel})
|
||||
else:
|
||||
config.update({'verbosity': 0})
|
||||
logging.basicConfig(
|
||||
level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
)
|
||||
set_loggers(config['verbosity'])
|
||||
logger.info('Verbosity set to %s', config['verbosity'])
|
||||
|
||||
# Add dynamic_whitelist if found
|
||||
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
||||
@@ -95,9 +110,12 @@ class Configuration(object):
|
||||
'(not applicable with Backtesting and Hyperopt)'
|
||||
)
|
||||
|
||||
if self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
||||
if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
||||
config.update({'db_url': self.args.db_url})
|
||||
logger.info('Parameter --db-url detected ...')
|
||||
else:
|
||||
# Set default here
|
||||
config.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||
|
||||
if config.get('dry_run', False):
|
||||
logger.info('Dry run is enabled')
|
||||
@@ -109,7 +127,7 @@ class Configuration(object):
|
||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||
logger.info('Dry run is disabled')
|
||||
|
||||
logger.info('Using DB: "{}"'.format(config['db_url']))
|
||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
self.check_exchange(config)
|
||||
@@ -142,11 +160,18 @@ class Configuration(object):
|
||||
config.update({'live': True})
|
||||
logger.info('Parameter -l/--live detected ...')
|
||||
|
||||
# If --realistic-simulation is used we add it to the configuration
|
||||
if 'realistic_simulation' in self.args and self.args.realistic_simulation:
|
||||
config.update({'realistic_simulation': True})
|
||||
logger.info('Parameter --realistic-simulation detected ...')
|
||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||
# If --enable-position-stacking is used we add it to the configuration
|
||||
if 'position_stacking' in self.args and self.args.position_stacking:
|
||||
config.update({'position_stacking': True})
|
||||
logger.info('Parameter --enable-position-stacking detected ...')
|
||||
|
||||
# If --disable-max-market-positions is used we add it to the configuration
|
||||
if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions:
|
||||
config.update({'use_max_market_positions': False})
|
||||
logger.info('Parameter --disable-max-market-positions detected ...')
|
||||
logger.info('max_open_trades set to unlimited ...')
|
||||
else:
|
||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||
|
||||
# If --timerange is used we add it to the configuration
|
||||
if 'timerange' in self.args and self.args.timerange:
|
||||
@@ -165,6 +190,14 @@ class Configuration(object):
|
||||
config.update({'refresh_pairs': True})
|
||||
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
||||
|
||||
if 'strategy_list' in self.args and self.args.strategy_list:
|
||||
config.update({'strategy_list': self.args.strategy_list})
|
||||
logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list))
|
||||
|
||||
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
||||
config.update({'ticker_interval': self.args.ticker_interval})
|
||||
logger.info('Overriding ticker interval with Command line argument')
|
||||
|
||||
# If --export is used we add it to the configuration
|
||||
if 'export' in self.args and self.args.export:
|
||||
config.update({'export': self.args.export})
|
||||
@@ -182,7 +215,7 @@ class Configuration(object):
|
||||
Extract information for sys.argv and load Hyperopt configuration
|
||||
:return: configuration as dictionary
|
||||
"""
|
||||
# If --realistic-simulation is used we add it to the configuration
|
||||
# If --epochs is used we add it to the configuration
|
||||
if 'epochs' in self.args and self.args.epochs:
|
||||
config.update({'epochs': self.args.epochs})
|
||||
logger.info('Parameter --epochs detected ...')
|
||||
|
||||
@@ -36,7 +36,7 @@ SUPPORTED_FIAT = [
|
||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
||||
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||
"BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||
]
|
||||
|
||||
# Required json-schema for user specified config
|
||||
@@ -45,7 +45,7 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
||||
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
|
||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
|
||||
'stake_amount': {
|
||||
"type": ["number", "string"],
|
||||
"minimum": 0.0005,
|
||||
@@ -53,6 +53,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||
'dry_run': {'type': 'boolean'},
|
||||
'process_only_new_candles': {'type': 'boolean'},
|
||||
'minimal_roi': {
|
||||
'type': 'object',
|
||||
'patternProperties': {
|
||||
@@ -61,7 +62,16 @@ CONF_SCHEMA = {
|
||||
'minProperties': 1
|
||||
},
|
||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
|
||||
'trailing_stop': {'type': 'boolean'},
|
||||
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
'unfilledtimeout': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'buy': {'type': 'number', 'minimum': 3},
|
||||
'sell': {'type': 'number', 'minimum': 10}
|
||||
}
|
||||
},
|
||||
'bid_strategy': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
@@ -69,18 +79,35 @@ CONF_SCHEMA = {
|
||||
'type': 'number',
|
||||
'minimum': 0,
|
||||
'maximum': 1,
|
||||
'exclusiveMaximum': False
|
||||
'exclusiveMaximum': False,
|
||||
'use_order_book': {'type': 'boolean'},
|
||||
'order_book_top': {'type': 'number', 'maximum': 20, 'minimum': 1},
|
||||
'check_depth_of_market': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'enabled': {'type': 'boolean'},
|
||||
'bids_to_ask_delta': {'type': 'number', 'minimum': 0},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
'required': ['ask_last_balance']
|
||||
},
|
||||
'ask_strategy': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'use_order_book': {'type': 'boolean'},
|
||||
'order_book_min': {'type': 'number', 'minimum': 1},
|
||||
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}
|
||||
}
|
||||
},
|
||||
'exchange': {'$ref': '#/definitions/exchange'},
|
||||
'experimental': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'use_sell_signal': {'type': 'boolean'},
|
||||
'sell_profit_only': {'type': 'boolean'},
|
||||
"ignore_roi_if_buy_signal_true": {'type': 'boolean'}
|
||||
'ignore_roi_if_buy_signal_true': {'type': 'boolean'}
|
||||
}
|
||||
},
|
||||
'telegram': {
|
||||
@@ -92,6 +119,15 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'required': ['enabled', 'token', 'chat_id']
|
||||
},
|
||||
'webhook': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'enabled': {'type': 'boolean'},
|
||||
'webhookbuy': {'type': 'object'},
|
||||
'webhooksell': {'type': 'object'},
|
||||
'webhookstatus': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
'db_url': {'type': 'string'},
|
||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||
'internals': {
|
||||
@@ -107,8 +143,11 @@ CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'sandbox': {'type': 'boolean'},
|
||||
'key': {'type': 'string'},
|
||||
'secret': {'type': 'string'},
|
||||
'password': {'type': 'string'},
|
||||
'uid': {'type': 'string'},
|
||||
'pair_whitelist': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
@@ -124,7 +163,8 @@ CONF_SCHEMA = {
|
||||
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
|
||||
},
|
||||
'uniqueItems': True
|
||||
}
|
||||
},
|
||||
'outdated_offset': {'type': 'integer', 'minimum': 1}
|
||||
},
|
||||
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||
}
|
||||
@@ -136,7 +176,6 @@ CONF_SCHEMA = {
|
||||
'max_open_trades',
|
||||
'stake_currency',
|
||||
'stake_amount',
|
||||
'fiat_display_currency',
|
||||
'dry_run',
|
||||
'bid_strategy',
|
||||
'telegram'
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# pragma pylint: disable=W0603
|
||||
""" Cryptocurrency Exchanges support """
|
||||
import logging
|
||||
import inspect
|
||||
from random import randint
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import List, Dict, Tuple, Any, Optional
|
||||
from datetime import datetime
|
||||
from math import floor, ceil
|
||||
|
||||
import asyncio
|
||||
import ccxt
|
||||
import ccxt.async_support as ccxt_async
|
||||
import arrow
|
||||
|
||||
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
|
||||
@@ -22,6 +26,24 @@ _EXCHANGE_URLS = {
|
||||
}
|
||||
|
||||
|
||||
def retrier_async(f):
|
||||
async def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||
try:
|
||||
return await f(*args, **kwargs)
|
||||
except (TemporaryError, DependencyException) as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
if count > 0:
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
return await wrapper(*args, **kwargs)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
raise ex
|
||||
return wrapper
|
||||
|
||||
|
||||
def retrier(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||
@@ -44,8 +66,8 @@ class Exchange(object):
|
||||
|
||||
# Current selected exchange
|
||||
_api: ccxt.Exchange = None
|
||||
_api_async: ccxt_async.Exchange = None
|
||||
_conf: Dict = {}
|
||||
_cached_ticker: Dict[str, Any] = {}
|
||||
|
||||
# Holds all open sell orders for dry_run
|
||||
_dry_run_open_orders: Dict[str, Any] = {}
|
||||
@@ -59,18 +81,40 @@ class Exchange(object):
|
||||
"""
|
||||
self._conf.update(config)
|
||||
|
||||
self._cached_ticker: Dict[str, Any] = {}
|
||||
|
||||
# Holds last candle refreshed time of each pair
|
||||
self._pairs_last_refresh_time: Dict[str, int] = {}
|
||||
|
||||
# Holds candles
|
||||
self.klines: Dict[str, Any] = {}
|
||||
|
||||
if config['dry_run']:
|
||||
logger.info('Instance is running with dry_run enabled')
|
||||
|
||||
exchange_config = config['exchange']
|
||||
self._api = self._init_ccxt(exchange_config)
|
||||
self._api_async = self._init_ccxt(exchange_config, ccxt_async)
|
||||
|
||||
logger.info('Using Exchange "%s"', self.name)
|
||||
|
||||
self.markets = self._load_markets()
|
||||
# Check if all pairs are available
|
||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||
|
||||
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
|
||||
if config.get('ticker_interval'):
|
||||
# Check if timeframe is available
|
||||
self.validate_timeframes(config['ticker_interval'])
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Destructor - clean up async stuff
|
||||
"""
|
||||
logger.debug("Exchange object destroyed, closing async loop")
|
||||
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||
|
||||
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt) -> ccxt.Exchange:
|
||||
"""
|
||||
Initialize ccxt with given config and return valid
|
||||
ccxt instance.
|
||||
@@ -78,19 +122,21 @@ class Exchange(object):
|
||||
# Find matching class for the given exchange name
|
||||
name = exchange_config['name']
|
||||
|
||||
if name not in ccxt.exchanges:
|
||||
if name not in ccxt_module.exchanges:
|
||||
raise OperationalException(f'Exchange {name} is not supported')
|
||||
try:
|
||||
api = getattr(ccxt, name.lower())({
|
||||
api = getattr(ccxt_module, name.lower())({
|
||||
'apiKey': exchange_config.get('key'),
|
||||
'secret': exchange_config.get('secret'),
|
||||
'password': exchange_config.get('password'),
|
||||
'uid': exchange_config.get('uid', ''),
|
||||
'enableRateLimit': True,
|
||||
'enableRateLimit': exchange_config.get('ccxt_rate_limit', True)
|
||||
})
|
||||
except (KeyError, AttributeError):
|
||||
raise OperationalException(f'Exchange {name} is not supported')
|
||||
|
||||
self.set_sandbox(api, exchange_config, name)
|
||||
|
||||
return api
|
||||
|
||||
@property
|
||||
@@ -103,6 +149,35 @@ class Exchange(object):
|
||||
"""exchange ccxt id"""
|
||||
return self._api.id
|
||||
|
||||
def set_sandbox(self, api, exchange_config: dict, name: str):
|
||||
if exchange_config.get('sandbox'):
|
||||
if api.urls.get('test'):
|
||||
api.urls['api'] = api.urls['test']
|
||||
logger.info("Enabled Sandbox API on %s", name)
|
||||
else:
|
||||
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
|
||||
"Please check your config.json")
|
||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||
|
||||
def _load_async_markets(self) -> None:
|
||||
try:
|
||||
if self._api_async:
|
||||
asyncio.get_event_loop().run_until_complete(self._api_async.load_markets())
|
||||
|
||||
except ccxt.BaseError as e:
|
||||
logger.warning('Could not load async markets. Reason: %s', e)
|
||||
return
|
||||
|
||||
def _load_markets(self) -> Dict[str, Any]:
|
||||
""" Initialize markets both sync and async """
|
||||
try:
|
||||
markets = self._api.load_markets()
|
||||
self._load_async_markets()
|
||||
return markets
|
||||
except ccxt.BaseError as e:
|
||||
logger.warning('Unable to initialize markets. Reason: %s', e)
|
||||
return {}
|
||||
|
||||
def validate_pairs(self, pairs: List[str]) -> None:
|
||||
"""
|
||||
Checks if all given pairs are tradable on the current exchange.
|
||||
@@ -111,11 +186,9 @@ class Exchange(object):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
try:
|
||||
markets = self._api.load_markets()
|
||||
except ccxt.BaseError as e:
|
||||
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
|
||||
return
|
||||
if not self.markets:
|
||||
logger.warning('Unable to validate pairs (assuming they are correct).')
|
||||
# return
|
||||
|
||||
stake_cur = self._conf['stake_currency']
|
||||
for pair in pairs:
|
||||
@@ -124,10 +197,19 @@ class Exchange(object):
|
||||
if not pair.endswith(stake_cur):
|
||||
raise OperationalException(
|
||||
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
||||
if pair not in markets:
|
||||
if self.markets and pair not in self.markets:
|
||||
raise OperationalException(
|
||||
f'Pair {pair} is not available at {self.name}')
|
||||
|
||||
def validate_timeframes(self, timeframe: List[str]) -> None:
|
||||
"""
|
||||
Checks if ticker interval from config is a supported timeframe on the exchange
|
||||
"""
|
||||
timeframes = self._api.timeframes
|
||||
if timeframe not in timeframes:
|
||||
raise OperationalException(
|
||||
f'Invalid ticker {timeframe}, this Exchange supports {timeframes}')
|
||||
|
||||
def exchange_has(self, endpoint: str) -> bool:
|
||||
"""
|
||||
Checks if exchange implements a specific API endpoint.
|
||||
@@ -137,6 +219,28 @@ class Exchange(object):
|
||||
"""
|
||||
return endpoint in self._api.has and self._api.has[endpoint]
|
||||
|
||||
def symbol_amount_prec(self, pair, amount: float):
|
||||
'''
|
||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||
Rounded down
|
||||
'''
|
||||
if self._api.markets[pair]['precision']['amount']:
|
||||
symbol_prec = self._api.markets[pair]['precision']['amount']
|
||||
big_amount = amount * pow(10, symbol_prec)
|
||||
amount = floor(big_amount) / pow(10, symbol_prec)
|
||||
return amount
|
||||
|
||||
def symbol_price_prec(self, pair, price: float):
|
||||
'''
|
||||
Returns the price buying or selling with to the precision the Exchange accepts
|
||||
Rounds up
|
||||
'''
|
||||
if self._api.markets[pair]['precision']['price']:
|
||||
symbol_prec = self._api.markets[pair]['precision']['price']
|
||||
big_price = price * pow(10, symbol_prec)
|
||||
price = ceil(big_price) / pow(10, symbol_prec)
|
||||
return price
|
||||
|
||||
def buy(self, pair: str, rate: float, amount: float) -> Dict:
|
||||
if self._conf['dry_run']:
|
||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||
@@ -154,6 +258,10 @@ class Exchange(object):
|
||||
return {'id': order_id}
|
||||
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
rate = self.symbol_price_prec(pair, rate)
|
||||
|
||||
return self._api.create_limit_buy_order(pair, amount, rate)
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
@@ -187,6 +295,10 @@ class Exchange(object):
|
||||
return {'id': order_id}
|
||||
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
rate = self.symbol_price_prec(pair, rate)
|
||||
|
||||
return self._api.create_limit_sell_order(pair, amount, rate)
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
@@ -266,15 +378,111 @@ class Exchange(object):
|
||||
return data
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
else:
|
||||
logger.info("returning cached ticker-data for %s", pair)
|
||||
return self._cached_ticker[pair]
|
||||
|
||||
def get_history(self, pair: str, tick_interval: str,
|
||||
since_ms: int) -> List:
|
||||
"""
|
||||
Gets candle history using asyncio and returns the list of candles.
|
||||
Handles all async doing.
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
self._async_get_history(pair=pair, tick_interval=tick_interval,
|
||||
since_ms=since_ms))
|
||||
|
||||
async def _async_get_history(self, pair: str,
|
||||
tick_interval: str,
|
||||
since_ms: int) -> List:
|
||||
# Assume exchange returns 500 candles
|
||||
_LIMIT = 500
|
||||
|
||||
one_call = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60 * _LIMIT * 1000
|
||||
logger.debug("one_call: %s", one_call)
|
||||
input_coroutines = [self._async_get_candle_history(
|
||||
pair, tick_interval, since) for since in
|
||||
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
||||
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
|
||||
# Combine tickers
|
||||
data: List = []
|
||||
for tick in tickers:
|
||||
if tick[0] == pair:
|
||||
data.extend(tick[1])
|
||||
# Sort data again after extending the result - above calls return in "async order" order
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
logger.info("downloaded %s with length %s.", pair, len(data))
|
||||
return data
|
||||
|
||||
def refresh_tickers(self, pair_list: List[str], ticker_interval: str) -> None:
|
||||
"""
|
||||
Refresh tickers asyncronously and return the result.
|
||||
"""
|
||||
logger.debug("Refreshing klines for %d pairs", len(pair_list))
|
||||
asyncio.get_event_loop().run_until_complete(
|
||||
self.async_get_candles_history(pair_list, ticker_interval))
|
||||
|
||||
async def async_get_candles_history(self, pairs: List[str],
|
||||
tick_interval: str) -> List[Tuple[str, List]]:
|
||||
"""Download ohlcv history for pair-list asyncronously """
|
||||
input_coroutines = [self._async_get_candle_history(
|
||||
symbol, tick_interval) for symbol in pairs]
|
||||
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
return tickers
|
||||
|
||||
@retrier_async
|
||||
async def _async_get_candle_history(self, pair: str, tick_interval: str,
|
||||
since_ms: Optional[int] = None) -> Tuple[str, List]:
|
||||
try:
|
||||
# fetch ohlcv asynchronously
|
||||
logger.debug("fetching %s since %s ...", pair, since_ms)
|
||||
|
||||
# Calculating ticker interval in second
|
||||
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60
|
||||
|
||||
# If (last update time) + (interval in second) is greater or equal than now
|
||||
# that means we don't have to hit the API as there is no new candle
|
||||
# so we fetch it from local cache
|
||||
if (not since_ms and
|
||||
self._pairs_last_refresh_time.get(pair, 0) + interval_in_sec >=
|
||||
arrow.utcnow().timestamp):
|
||||
data = self.klines[pair]
|
||||
logger.debug("Using cached klines data for %s ...", pair)
|
||||
else:
|
||||
data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval,
|
||||
since=since_ms)
|
||||
|
||||
# Because some exchange sort Tickers ASC and other DESC.
|
||||
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
||||
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if data:
|
||||
self._pairs_last_refresh_time[pair] = data[-1][0] // 1000
|
||||
|
||||
# keeping candles in cache
|
||||
self.klines[pair] = data
|
||||
|
||||
logger.debug("done fetching %s ...", pair)
|
||||
return pair, data
|
||||
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
||||
|
||||
@retrier
|
||||
def get_ticker_history(self, pair: str, tick_interval: str,
|
||||
def get_candle_history(self, pair: str, tick_interval: str,
|
||||
since_ms: Optional[int] = None) -> List[Dict]:
|
||||
try:
|
||||
# last item should be in the time interval [now - tick_interval, now]
|
||||
@@ -353,6 +561,37 @@ class Exchange(object):
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@retrier
|
||||
def get_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
"""
|
||||
get order book level 2 from exchange
|
||||
|
||||
Notes:
|
||||
20180619: bittrex doesnt support limits -.-
|
||||
20180619: binance support limits but only on specific range
|
||||
"""
|
||||
try:
|
||||
if self._api.name == 'Binance':
|
||||
limit_range = [5, 10, 20, 50, 100, 500, 1000]
|
||||
# get next-higher step in the limit_range list
|
||||
limit = min(list(filter(lambda x: limit <= x, limit_range)))
|
||||
# above script works like loop below (but with slightly better performance):
|
||||
# for limitx in limit_range:
|
||||
# if limit <= limitx:
|
||||
# limit = limitx
|
||||
# break
|
||||
|
||||
return self._api.fetch_l2_order_book(pair, limit)
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching order book.'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order book due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
@retrier
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
||||
if self._conf['dry_run']:
|
||||
@@ -360,7 +599,8 @@ class Exchange(object):
|
||||
if not self.exchange_has('fetchMyTrades'):
|
||||
return []
|
||||
try:
|
||||
my_trades = self._api.fetch_my_trades(pair, since.timestamp())
|
||||
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||
my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5)
|
||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||
|
||||
return matched_trades
|
||||
@@ -406,12 +646,3 @@ class Exchange(object):
|
||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
def get_amount_lots(self, pair: str, amount: float) -> float:
|
||||
"""
|
||||
get buyable amount rounding, ..
|
||||
"""
|
||||
# validate that markets are loaded before trying to get fee
|
||||
if not self._api.markets:
|
||||
self._api.load_markets()
|
||||
return self._api.amount_to_lots(pair, amount)
|
||||
|
||||
58
freqtrade/exchange/exchange_helpers.py
Normal file
58
freqtrade/exchange/exchange_helpers.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||
"""
|
||||
import logging
|
||||
import pandas as pd
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||
"""
|
||||
Analyses the trend for the given ticker history
|
||||
:param ticker: See exchange.get_candle_history
|
||||
:return: DataFrame
|
||||
"""
|
||||
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
frame = DataFrame(ticker, columns=cols)
|
||||
|
||||
frame['date'] = to_datetime(frame['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
|
||||
# group by index and aggregate results to eliminate duplicate ticks
|
||||
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
||||
'open': 'first',
|
||||
'high': 'max',
|
||||
'low': 'min',
|
||||
'close': 'last',
|
||||
'volume': 'max',
|
||||
})
|
||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||
return frame
|
||||
|
||||
|
||||
def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
||||
"""
|
||||
Gets order book list, returns dataframe with below format per suggested by creslin
|
||||
-------------------------------------------------------------------
|
||||
b_sum b_size bids asks a_size a_sum
|
||||
-------------------------------------------------------------------
|
||||
"""
|
||||
cols = ['bids', 'b_size']
|
||||
|
||||
bids_frame = DataFrame(bids, columns=cols)
|
||||
# add cumulative sum column
|
||||
bids_frame['b_sum'] = bids_frame['b_size'].cumsum()
|
||||
cols2 = ['asks', 'a_size']
|
||||
asks_frame = DataFrame(asks, columns=cols2)
|
||||
# add cumulative sum column
|
||||
asks_frame['a_sum'] = asks_frame['a_size'].cumsum()
|
||||
|
||||
frame = pd.concat([bids_frame['b_sum'], bids_frame['b_size'], bids_frame['bids'],
|
||||
asks_frame['asks'], asks_frame['a_size'], asks_frame['a_sum']], axis=1,
|
||||
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
|
||||
# logger.info('order book %s', frame )
|
||||
return frame
|
||||
@@ -8,9 +8,10 @@ import time
|
||||
from typing import Dict, List
|
||||
|
||||
from coinmarketcap import Market
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from freqtrade.constants import SUPPORTED_FIAT
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -88,10 +89,10 @@ class CryptoToFiatConverter(object):
|
||||
coinlistings = self._coinmarketcap.listings()
|
||||
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
|
||||
coinlistings["data"]))
|
||||
except (ValueError, RequestException) as exception:
|
||||
except (BaseException) as exception:
|
||||
logger.error(
|
||||
"Could not load FIAT Cryptocurrency map for the following problem: %s",
|
||||
exception
|
||||
type(exception).__name__
|
||||
)
|
||||
|
||||
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||
|
||||
@@ -7,22 +7,22 @@ import logging
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Any, Callable
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from cachetools import TTLCache, cached
|
||||
|
||||
from freqtrade import (
|
||||
DependencyException, OperationalException, TemporaryError, persistence, __version__,
|
||||
)
|
||||
from freqtrade import constants
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade import (DependencyException, OperationalException,
|
||||
TemporaryError, __version__, constants, persistence)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.rpc_manager import RPCManager
|
||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||
from freqtrade.state import State
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
|
||||
from freqtrade.exchange.exchange_helpers import order_book_to_dataframe
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,12 +50,10 @@ class FreqtradeBot(object):
|
||||
|
||||
# Init objects
|
||||
self.config = config
|
||||
self.analyze = Analyze(self.config)
|
||||
self.fiat_converter = CryptoToFiatConverter()
|
||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||
self.rpc: RPCManager = RPCManager(self)
|
||||
self.persistence = None
|
||||
self.exchange = Exchange(self.config)
|
||||
|
||||
self._init_modules()
|
||||
|
||||
def _init_modules(self) -> None:
|
||||
@@ -93,8 +91,13 @@ class FreqtradeBot(object):
|
||||
# Log state transition
|
||||
state = self.state
|
||||
if state != old_state:
|
||||
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'{state.name.lower()}'
|
||||
})
|
||||
logger.info('Changing state to: %s', state.name)
|
||||
if state == State.RUNNING:
|
||||
self._startup_messages()
|
||||
|
||||
if state == State.STOPPED:
|
||||
time.sleep(1)
|
||||
@@ -111,6 +114,38 @@ class FreqtradeBot(object):
|
||||
nb_assets=nb_assets)
|
||||
return state
|
||||
|
||||
def _startup_messages(self) -> None:
|
||||
if self.config.get('dry_run', False):
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||
'status': 'Dry run is enabled. All trades are simulated.'
|
||||
})
|
||||
stake_currency = self.config['stake_currency']
|
||||
stake_amount = self.config['stake_amount']
|
||||
minimal_roi = self.config['minimal_roi']
|
||||
ticker_interval = self.config['ticker_interval']
|
||||
exchange_name = self.config['exchange']['name']
|
||||
strategy_name = self.config.get('strategy', '')
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
||||
'status': f'*Exchange:* `{exchange_name}`\n'
|
||||
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
||||
f'*Minimum ROI:* `{minimal_roi}`\n'
|
||||
f'*Ticker Interval:* `{ticker_interval}`\n'
|
||||
f'*Strategy:* `{strategy_name}`'
|
||||
})
|
||||
if self.config.get('dynamic_whitelist', False):
|
||||
top_pairs = 'top ' + str(self.config.get('dynamic_whitelist', 20))
|
||||
specific_pairs = ''
|
||||
else:
|
||||
top_pairs = 'whitelisted'
|
||||
specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', ''))
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...'
|
||||
f'{specific_pairs}'
|
||||
})
|
||||
|
||||
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||
"""
|
||||
Throttles the given callable that it
|
||||
@@ -147,6 +182,9 @@ class FreqtradeBot(object):
|
||||
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
||||
self.config['exchange']['pair_whitelist'] = final_list
|
||||
|
||||
# Refreshing candles
|
||||
self.exchange.refresh_tickers(final_list, self.strategy.ticker_interval)
|
||||
|
||||
# Query trades from persistence layer
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
|
||||
@@ -160,7 +198,7 @@ class FreqtradeBot(object):
|
||||
|
||||
if 'unfilledtimeout' in self.config:
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout(self.config['unfilledtimeout'])
|
||||
self.check_handle_timedout()
|
||||
Trade.session.flush()
|
||||
|
||||
except TemporaryError as error:
|
||||
@@ -169,9 +207,10 @@ class FreqtradeBot(object):
|
||||
except OperationalException:
|
||||
tb = traceback.format_exc()
|
||||
hint = 'Issue `/start` if you think it is safe to restart.'
|
||||
self.rpc.send_msg(
|
||||
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
|
||||
)
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'OperationalException:\n```\n{tb}```{hint}'
|
||||
})
|
||||
logger.exception('OperationalException. Stopping trader ...')
|
||||
self.state = State.STOPPED
|
||||
return state_changed
|
||||
@@ -233,18 +272,47 @@ class FreqtradeBot(object):
|
||||
|
||||
return final_list
|
||||
|
||||
def get_target_bid(self, ticker: Dict[str, float]) -> float:
|
||||
def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float:
|
||||
"""
|
||||
Calculates bid target between current ask price and last price
|
||||
:param ticker: Ticker to use for getting Ask and Last Price
|
||||
:return: float: Price
|
||||
"""
|
||||
if ticker['ask'] < ticker['last']:
|
||||
return ticker['ask']
|
||||
balance = self.config['bid_strategy']['ask_last_balance']
|
||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||
ticker_rate = ticker['ask']
|
||||
else:
|
||||
balance = self.config['bid_strategy']['ask_last_balance']
|
||||
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||
|
||||
used_rate = ticker_rate
|
||||
config_bid_strategy = self.config.get('bid_strategy', {})
|
||||
if 'use_order_book' in config_bid_strategy and\
|
||||
config_bid_strategy.get('use_order_book', False):
|
||||
logger.info('Getting price from order book')
|
||||
order_book_top = config_bid_strategy.get('order_book_top', 1)
|
||||
order_book = self.exchange.get_order_book(pair, order_book_top)
|
||||
logger.debug('order_book %s', order_book)
|
||||
# top 1 = index 0
|
||||
order_book_rate = order_book['bids'][order_book_top - 1][0]
|
||||
# if ticker has lower rate, then use ticker ( usefull if down trending )
|
||||
logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate)
|
||||
if ticker_rate < order_book_rate:
|
||||
logger.info('...using ticker rate instead %0.8f', ticker_rate)
|
||||
used_rate = ticker_rate
|
||||
else:
|
||||
used_rate = order_book_rate
|
||||
else:
|
||||
logger.info('Using Last Ask / Last Price')
|
||||
used_rate = ticker_rate
|
||||
|
||||
return used_rate
|
||||
|
||||
def _get_trade_stake_amount(self) -> Optional[float]:
|
||||
"""
|
||||
Check if stake amount can be fulfilled with the available balance
|
||||
for the stake currency
|
||||
:return: float: Stake Amount
|
||||
"""
|
||||
stake_amount = self.config['stake_amount']
|
||||
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
|
||||
|
||||
@@ -277,21 +345,24 @@ class FreqtradeBot(object):
|
||||
return None
|
||||
|
||||
min_stake_amounts = []
|
||||
if 'cost' in market['limits'] and 'min' in market['limits']['cost']:
|
||||
min_stake_amounts.append(market['limits']['cost']['min'])
|
||||
limits = market['limits']
|
||||
if ('cost' in limits and 'min' in limits['cost']
|
||||
and limits['cost']['min'] is not None):
|
||||
min_stake_amounts.append(limits['cost']['min'])
|
||||
|
||||
if 'amount' in market['limits'] and 'min' in market['limits']['amount']:
|
||||
min_stake_amounts.append(market['limits']['amount']['min'] * price)
|
||||
if ('amount' in limits and 'min' in limits['amount']
|
||||
and limits['amount']['min'] is not None):
|
||||
min_stake_amounts.append(limits['amount']['min'] * price)
|
||||
|
||||
if not min_stake_amounts:
|
||||
return None
|
||||
|
||||
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
|
||||
if self.analyze.get_stoploss() is not None:
|
||||
amount_reserve_percent += self.analyze.get_stoploss()
|
||||
if self.strategy.stoploss is not None:
|
||||
amount_reserve_percent += self.strategy.stoploss
|
||||
# it should not be more than 50%
|
||||
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
||||
return min(min_stake_amounts)/amount_reserve_percent
|
||||
return min(min_stake_amounts) / amount_reserve_percent
|
||||
|
||||
def create_trade(self) -> bool:
|
||||
"""
|
||||
@@ -299,14 +370,11 @@ class FreqtradeBot(object):
|
||||
if one pair triggers the buy_signal a new trade record gets created
|
||||
:return: True if a trade object has been created and persisted, False otherwise
|
||||
"""
|
||||
interval = self.analyze.get_ticker_interval()
|
||||
interval = self.strategy.ticker_interval
|
||||
stake_amount = self._get_trade_stake_amount()
|
||||
|
||||
if not stake_amount:
|
||||
return False
|
||||
stake_currency = self.config['stake_currency']
|
||||
fiat_currency = self.config['fiat_display_currency']
|
||||
exc_name = self.exchange.name
|
||||
|
||||
logger.info(
|
||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||
@@ -323,19 +391,53 @@ class FreqtradeBot(object):
|
||||
if not whitelist:
|
||||
raise DependencyException('No currency pairs in whitelist')
|
||||
|
||||
# Pick pair based on buy signals
|
||||
# running get_signal on historical data fetched
|
||||
# to find buy signals
|
||||
for _pair in whitelist:
|
||||
(buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval)
|
||||
(buy, sell) = self.strategy.get_signal(_pair, interval, self.exchange.klines.get(_pair))
|
||||
if buy and not sell:
|
||||
pair = _pair
|
||||
break
|
||||
else:
|
||||
return False
|
||||
bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
|
||||
get('check_depth_of_market', {})
|
||||
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
|
||||
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
|
||||
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
|
||||
return self.execute_buy(_pair, stake_amount)
|
||||
else:
|
||||
return False
|
||||
return self.execute_buy(_pair, stake_amount)
|
||||
|
||||
return False
|
||||
|
||||
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
|
||||
"""
|
||||
Checks depth of market before executing a buy
|
||||
"""
|
||||
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
|
||||
logger.info('checking depth of market for %s', pair)
|
||||
order_book = self.exchange.get_order_book(pair, 1000)
|
||||
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
|
||||
order_book_bids = order_book_data_frame['b_size'].sum()
|
||||
order_book_asks = order_book_data_frame['a_size'].sum()
|
||||
bids_ask_delta = order_book_bids / order_book_asks
|
||||
logger.info('bids: %s, asks: %s, delta: %s', order_book_bids,
|
||||
order_book_asks, bids_ask_delta)
|
||||
if bids_ask_delta >= conf_bids_to_ask_delta:
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute_buy(self, pair: str, stake_amount: float) -> bool:
|
||||
"""
|
||||
Executes a limit buy for the given pair
|
||||
:param pair: pair for which we want to create a LIMIT_BUY
|
||||
:return: None
|
||||
"""
|
||||
pair_s = pair.replace('_', '/')
|
||||
pair_url = self.exchange.get_pair_detail_url(pair)
|
||||
stake_currency = self.config['stake_currency']
|
||||
fiat_currency = self.config.get('fiat_display_currency', None)
|
||||
|
||||
# Calculate amount
|
||||
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
|
||||
buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
||||
|
||||
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||
@@ -349,18 +451,16 @@ class FreqtradeBot(object):
|
||||
|
||||
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
|
||||
|
||||
stake_amount_fiat = self.fiat_converter.convert_amount(
|
||||
stake_amount,
|
||||
stake_currency,
|
||||
fiat_currency
|
||||
)
|
||||
|
||||
# Create trade entity and return
|
||||
self.rpc.send_msg(
|
||||
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
|
||||
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
|
||||
)
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': pair_s,
|
||||
'market_url': pair_url,
|
||||
'limit': buy_limit,
|
||||
'stake_amount': stake_amount,
|
||||
'stake_currency': stake_currency,
|
||||
'fiat_currency': fiat_currency
|
||||
})
|
||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||
trade = Trade(
|
||||
@@ -373,7 +473,9 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
open_rate_requested=buy_limit,
|
||||
open_date=datetime.utcnow(),
|
||||
exchange=self.exchange.id,
|
||||
open_order_id=order_id
|
||||
open_order_id=order_id,
|
||||
strategy=self.strategy.get_strategy_name(),
|
||||
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
@@ -478,27 +580,62 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
raise ValueError(f'attempt to handle closed trade: {trade}')
|
||||
|
||||
logger.debug('Handling %s ...', trade)
|
||||
current_rate = self.exchange.get_ticker(trade.pair)['bid']
|
||||
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
|
||||
|
||||
(buy, sell) = (False, False)
|
||||
experimental = self.config.get('experimental', {})
|
||||
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
||||
(buy, sell) = self.analyze.get_signal(self.exchange,
|
||||
trade.pair, self.analyze.get_ticker_interval())
|
||||
ticker = self.exchange.klines.get(trade.pair)
|
||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
|
||||
ticker)
|
||||
|
||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||
if config_ask_strategy.get('use_order_book', False):
|
||||
logger.info('Using order book for selling...')
|
||||
# logger.debug('Order book %s',orderBook)
|
||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
||||
|
||||
order_book = self.exchange.get_order_book(trade.pair, order_book_max)
|
||||
|
||||
for i in range(order_book_min, order_book_max + 1):
|
||||
order_book_rate = order_book['asks'][i - 1][0]
|
||||
|
||||
# if orderbook has higher rate (high profit),
|
||||
# use orderbook, otherwise just use bids rate
|
||||
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||
if sell_rate < order_book_rate:
|
||||
sell_rate = order_book_rate
|
||||
|
||||
if self.check_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
break
|
||||
else:
|
||||
logger.info('checking sell')
|
||||
if self.check_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
|
||||
self.execute_sell(trade, current_rate)
|
||||
return True
|
||||
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
||||
return False
|
||||
|
||||
def check_handle_timedout(self, timeoutvalue: int) -> None:
|
||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell)
|
||||
if should_sell.sell_flag:
|
||||
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
||||
logger.info('excuted sell')
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_handle_timedout(self) -> None:
|
||||
"""
|
||||
Check if any orders are timed out and cancel if neccessary
|
||||
:param timeoutvalue: Number of minutes until order is considered timed out
|
||||
:return: None
|
||||
"""
|
||||
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
|
||||
buy_timeout = self.config['unfilledtimeout']['buy']
|
||||
sell_timeout = self.config['unfilledtimeout']['sell']
|
||||
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
|
||||
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
|
||||
|
||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||
try:
|
||||
@@ -509,7 +646,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
if not trade.open_order_id:
|
||||
continue
|
||||
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
except requests.exceptions.RequestException:
|
||||
except (RequestException, DependencyException):
|
||||
logger.info(
|
||||
'Cannot query order for %s due to %s',
|
||||
trade,
|
||||
@@ -521,10 +658,12 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
if int(order['remaining']) == 0:
|
||||
continue
|
||||
|
||||
if order['side'] == 'buy' and ordertime < timeoutthreashold:
|
||||
self.handle_timedout_limit_buy(trade, order)
|
||||
elif order['side'] == 'sell' and ordertime < timeoutthreashold:
|
||||
self.handle_timedout_limit_sell(trade, order)
|
||||
# Check if trade is still actually open
|
||||
if order['status'] == 'open':
|
||||
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
|
||||
self.handle_timedout_limit_buy(trade, order)
|
||||
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
|
||||
self.handle_timedout_limit_sell(trade, order)
|
||||
|
||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||
# it is conditionally called in the
|
||||
@@ -540,7 +679,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
Trade.session.delete(trade)
|
||||
Trade.session.flush()
|
||||
logger.info('Buy order timeout for %s.', trade)
|
||||
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'Unfilled buy order for {pair_s} cancelled due to timeout'
|
||||
})
|
||||
return True
|
||||
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
@@ -549,7 +691,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
trade.stake_amount = trade.amount * trade.open_rate
|
||||
trade.open_order_id = None
|
||||
logger.info('Partial buy order timeout for %s.', trade)
|
||||
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'Remaining buy order for {pair_s} cancelled due to timeout'
|
||||
})
|
||||
return False
|
||||
|
||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||
@@ -567,65 +712,59 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'Unfilled sell order for {pair_s} cancelled due to timeout'
|
||||
})
|
||||
logger.info('Sell order timeout for %s.', trade)
|
||||
return True
|
||||
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
return False
|
||||
|
||||
def execute_sell(self, trade: Trade, limit: float) -> None:
|
||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
|
||||
"""
|
||||
Executes a limit sell for the given trade and limit
|
||||
:param trade: Trade instance
|
||||
:param limit: limit rate for the sell order
|
||||
:param sellreason: Reason the sell was triggered
|
||||
:return: None
|
||||
"""
|
||||
exc = trade.exchange
|
||||
pair = trade.pair
|
||||
# Execute sell and update trade record
|
||||
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
||||
trade.open_order_id = order_id
|
||||
trade.close_rate_requested = limit
|
||||
trade.sell_reason = sell_reason.value
|
||||
|
||||
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
||||
profit_trade = trade.calc_profit(rate=limit)
|
||||
current_rate = self.exchange.get_ticker(trade.pair)['bid']
|
||||
profit = trade.calc_profit_percent(limit)
|
||||
profit_percent = trade.calc_profit_percent(limit)
|
||||
pair_url = self.exchange.get_pair_detail_url(trade.pair)
|
||||
gain = "profit" if fmt_exp_profit > 0 else "loss"
|
||||
gain = "profit" if profit_percent > 0 else "loss"
|
||||
|
||||
message = f"*{exc}:* Selling\n" \
|
||||
f"*Current Pair:* [{pair}]({pair_url})\n" \
|
||||
f"*Limit:* `{limit}`\n" \
|
||||
f"*Amount:* `{round(trade.amount, 8)}`\n" \
|
||||
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
|
||||
f"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
|
||||
""
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'market_url': pair_url,
|
||||
'limit': limit,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_percent': profit_percent,
|
||||
}
|
||||
|
||||
# For regular case, when the configuration exists
|
||||
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
||||
stake = self.config['stake_currency']
|
||||
fiat = self.config['fiat_display_currency']
|
||||
fiat_converter = CryptoToFiatConverter()
|
||||
profit_fiat = fiat_converter.convert_amount(
|
||||
profit_trade,
|
||||
stake,
|
||||
fiat
|
||||
)
|
||||
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
||||
f'` / {profit_fiat:.3f} {fiat})`'\
|
||||
''
|
||||
# Because telegram._forcesell does not have the configuration
|
||||
# Ignore the FIAT value and does not show the stake_currency as well
|
||||
else:
|
||||
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
|
||||
gain="profit" if fmt_exp_profit > 0 else "loss",
|
||||
profit_percent=fmt_exp_profit,
|
||||
profit_coin=profit_trade
|
||||
)
|
||||
stake_currency = self.config['stake_currency']
|
||||
fiat_currency = self.config['fiat_display_currency']
|
||||
msg.update({
|
||||
'stake_currency': stake_currency,
|
||||
'fiat_currency': fiat_currency,
|
||||
})
|
||||
|
||||
# Send the message
|
||||
self.rpc.send_msg(message)
|
||||
self.rpc.send_msg(msg)
|
||||
Trade.session.flush()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from math import exp, pi, sqrt, cos
|
||||
from math import cos, exp, pi, sqrt
|
||||
|
||||
import numpy as np
|
||||
import talib as ta
|
||||
|
||||
@@ -10,9 +10,10 @@ from typing import List
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.configuration import Configuration, set_loggers
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.state import State
|
||||
from freqtrade.rpc import RPCMessageType
|
||||
|
||||
logger = logging.getLogger('freqtrade')
|
||||
|
||||
@@ -59,7 +60,10 @@ def main(sysargv: List[str]) -> None:
|
||||
logger.exception('Fatal exception!')
|
||||
finally:
|
||||
if freqtrade:
|
||||
freqtrade.rpc.send_msg('*Status:* `Process died ...`')
|
||||
freqtrade.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': 'process died'
|
||||
})
|
||||
freqtrade.cleanup()
|
||||
sys.exit(return_code)
|
||||
|
||||
@@ -73,24 +77,13 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
|
||||
|
||||
# Create new instance
|
||||
freqtrade = FreqtradeBot(Configuration(args).get_config())
|
||||
freqtrade.rpc.send_msg(
|
||||
'*Status:* `Config reloaded ...`'.format(
|
||||
freqtrade.state.name.lower()
|
||||
)
|
||||
)
|
||||
freqtrade.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': 'config reloaded'
|
||||
})
|
||||
return freqtrade
|
||||
|
||||
|
||||
def set_loggers() -> None:
|
||||
"""
|
||||
Set the logger level for Third party libs
|
||||
:return: None
|
||||
"""
|
||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(logging.INFO)
|
||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
set_loggers()
|
||||
main(sys.argv[1:])
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
Various tool function for Freqtrade and scripts
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import gzip
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
|
||||
import gzip
|
||||
import json
|
||||
try:
|
||||
import ujson as json
|
||||
_UJSON = True
|
||||
except ImportError:
|
||||
# see mypy/issues/1153
|
||||
import json # type: ignore
|
||||
_UJSON = False
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional, List, Dict, Tuple, Any
|
||||
@@ -14,6 +20,14 @@ from freqtrade.arguments import TimeRange
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def json_load(data):
|
||||
"""Try to load data with ujson"""
|
||||
if _UJSON:
|
||||
return json.load(data, precise_float=True)
|
||||
else:
|
||||
return json.load(data)
|
||||
|
||||
|
||||
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||
if not tickerlist:
|
||||
return tickerlist
|
||||
@@ -54,11 +68,8 @@ def load_tickerdata_file(
|
||||
:return dict OR empty if unsuccesful
|
||||
"""
|
||||
path = make_testdata_path(datadir)
|
||||
pair_file_string = pair.replace('/', '_')
|
||||
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format(
|
||||
pair=pair_file_string,
|
||||
ticker_interval=ticker_interval,
|
||||
))
|
||||
pair_s = pair.replace('/', '_')
|
||||
file = os.path.join(path, f'{pair_s}-{ticker_interval}.json')
|
||||
gzipfile = file + '.gz'
|
||||
|
||||
# If the file does not exist we download it when None is returned.
|
||||
@@ -166,7 +177,7 @@ def load_cached_data_for_updating(filename: str,
|
||||
# read the cached file
|
||||
if os.path.isfile(filename):
|
||||
with open(filename, "rt") as file:
|
||||
data = json.load(file)
|
||||
data = json_load(file)
|
||||
# remove the last item, because we are not sure if it is correct
|
||||
# it could be fetched when the candle was incompleted
|
||||
if data:
|
||||
@@ -194,19 +205,18 @@ def download_backtesting_testdata(datadir: str,
|
||||
timerange: Optional[TimeRange] = None) -> None:
|
||||
|
||||
"""
|
||||
Download the latest ticker intervals from the exchange for the pairs passed in parameters
|
||||
Download the latest ticker intervals from the exchange for the pair passed in parameters
|
||||
The data is downloaded starting from the last correct ticker interval data that
|
||||
esists in a cache. If timerange starts earlier than the data in the cache,
|
||||
exists in a cache. If timerange starts earlier than the data in the cache,
|
||||
the full data will be redownloaded
|
||||
|
||||
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
||||
:param pairs: list of pairs to download
|
||||
:param pair: pair to download
|
||||
:param tick_interval: ticker interval
|
||||
:param timerange: range of time to download
|
||||
:return: None
|
||||
|
||||
"""
|
||||
|
||||
path = make_testdata_path(datadir)
|
||||
filepair = pair.replace("/", "_")
|
||||
filename = os.path.join(path, f'{filepair}-{tick_interval}.json')
|
||||
@@ -222,8 +232,11 @@ def download_backtesting_testdata(datadir: str,
|
||||
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
|
||||
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
||||
|
||||
new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval,
|
||||
since_ms=since_ms)
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
new_data = exchange.get_history(pair=pair, tick_interval=tick_interval,
|
||||
since_ms=since_ms if since_ms
|
||||
else
|
||||
int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000)
|
||||
data.extend(new_data)
|
||||
|
||||
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
|
||||
|
||||
@@ -6,21 +6,24 @@ This module contains the backtesting logic
|
||||
import logging
|
||||
import operator
|
||||
from argparse import Namespace
|
||||
from datetime import datetime
|
||||
from typing import Dict, Tuple, Any, List, Optional, NamedTuple
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
from tabulate import tabulate
|
||||
|
||||
import freqtrade.optimize as optimize
|
||||
from freqtrade import constants, DependencyException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade import DependencyException, constants
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.misc import file_dump_json
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,6 +41,9 @@ class BacktestResult(NamedTuple):
|
||||
close_index: int
|
||||
trade_duration: float
|
||||
open_at_end: bool
|
||||
open_rate: float
|
||||
close_rate: float
|
||||
sell_reason: SellType
|
||||
|
||||
|
||||
class Backtesting(object):
|
||||
@@ -48,13 +54,9 @@ class Backtesting(object):
|
||||
backtesting = Backtesting(config)
|
||||
backtesting.start()
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
self.analyze = Analyze(self.config)
|
||||
self.ticker_interval = self.analyze.strategy.ticker_interval
|
||||
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
|
||||
self.populate_buy_trend = self.analyze.populate_buy_trend
|
||||
self.populate_sell_trend = self.analyze.populate_sell_trend
|
||||
|
||||
# Reset keys for backtesting
|
||||
self.config['exchange']['key'] = ''
|
||||
@@ -62,9 +64,34 @@ class Backtesting(object):
|
||||
self.config['exchange']['password'] = ''
|
||||
self.config['exchange']['uid'] = ''
|
||||
self.config['dry_run'] = True
|
||||
self.strategylist: List[IStrategy] = []
|
||||
if self.config.get('strategy_list', None):
|
||||
# Force one interval
|
||||
self.ticker_interval = str(self.config.get('ticker_interval'))
|
||||
for strat in list(self.config['strategy_list']):
|
||||
stratconf = deepcopy(self.config)
|
||||
stratconf['strategy'] = strat
|
||||
self.strategylist.append(StrategyResolver(stratconf).strategy)
|
||||
|
||||
else:
|
||||
# only one strategy
|
||||
self.strategylist.append(StrategyResolver(self.config).strategy)
|
||||
# Load one strategy
|
||||
self._set_strategy(self.strategylist[0])
|
||||
|
||||
self.exchange = Exchange(self.config)
|
||||
self.fee = self.exchange.get_fee()
|
||||
|
||||
def _set_strategy(self, strategy):
|
||||
"""
|
||||
Load strategy into backtesting
|
||||
"""
|
||||
self.strategy = strategy
|
||||
self.ticker_interval = self.config.get('ticker_interval')
|
||||
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
|
||||
self.advise_buy = strategy.advise_buy
|
||||
self.advise_sell = strategy.advise_sell
|
||||
|
||||
@staticmethod
|
||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||
"""
|
||||
@@ -73,31 +100,37 @@ class Backtesting(object):
|
||||
:return: tuple containing min_date, max_date
|
||||
"""
|
||||
timeframe = [
|
||||
(arrow.get(min(frame.date)), arrow.get(max(frame.date)))
|
||||
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
|
||||
for frame in data.values()
|
||||
]
|
||||
return min(timeframe, key=operator.itemgetter(0))[0], \
|
||||
max(timeframe, key=operator.itemgetter(1))[1]
|
||||
|
||||
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
||||
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame,
|
||||
skip_nan: bool = False) -> str:
|
||||
"""
|
||||
Generates and returns a text table for the given backtest data and the results dataframe
|
||||
:return: pretty printed table with tabulate as str
|
||||
"""
|
||||
stake_currency = str(self.config.get('stake_currency'))
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = ['pair', 'buy count', 'avg profit %',
|
||||
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||
for pair in data:
|
||||
result = results[results.pair == pair]
|
||||
if skip_nan and result.profit_abs.isnull().all():
|
||||
continue
|
||||
|
||||
tabular_data.append([
|
||||
pair,
|
||||
len(result.index),
|
||||
result.profit_percent.mean() * 100.0,
|
||||
result.profit_percent.sum() * 100.0,
|
||||
result.profit_abs.sum(),
|
||||
result.trade_duration.mean(),
|
||||
str(timedelta(
|
||||
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
|
||||
len(result[result.profit_abs > 0]),
|
||||
len(result[result.profit_abs < 0])
|
||||
])
|
||||
@@ -107,22 +140,63 @@ class Backtesting(object):
|
||||
'TOTAL',
|
||||
len(results.index),
|
||||
results.profit_percent.mean() * 100.0,
|
||||
results.profit_percent.sum() * 100.0,
|
||||
results.profit_abs.sum(),
|
||||
results.trade_duration.mean(),
|
||||
str(timedelta(
|
||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||
len(results[results.profit_abs > 0]),
|
||||
len(results[results.profit_abs < 0])
|
||||
])
|
||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||
|
||||
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
|
||||
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
||||
"""
|
||||
Generate small table outlining Backtest results
|
||||
"""
|
||||
tabular_data = []
|
||||
headers = ['Sell Reason', 'Count']
|
||||
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||
tabular_data.append([reason.value, count])
|
||||
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
||||
|
||||
records = [(trade_entry.pair, trade_entry.profit_percent,
|
||||
trade_entry.open_time.timestamp(),
|
||||
trade_entry.close_time.timestamp(),
|
||||
trade_entry.open_index - 1, trade_entry.trade_duration)
|
||||
for index, trade_entry in results.iterrows()]
|
||||
def _generate_text_table_strategy(self, all_results: dict) -> str:
|
||||
"""
|
||||
Generate summary table per strategy
|
||||
"""
|
||||
stake_currency = str(self.config.get('stake_currency'))
|
||||
|
||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
|
||||
tabular_data = []
|
||||
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||
for strategy, results in all_results.items():
|
||||
tabular_data.append([
|
||||
strategy,
|
||||
len(results.index),
|
||||
results.profit_percent.mean() * 100.0,
|
||||
results.profit_percent.sum() * 100.0,
|
||||
results.profit_abs.sum(),
|
||||
str(timedelta(
|
||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||
len(results[results.profit_abs > 0]),
|
||||
len(results[results.profit_abs < 0])
|
||||
])
|
||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||
|
||||
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
|
||||
strategyname: Optional[str] = None) -> None:
|
||||
|
||||
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
|
||||
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
|
||||
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
|
||||
for index, t in results.iterrows()]
|
||||
|
||||
if records:
|
||||
if strategyname:
|
||||
# Inject strategyname to filename
|
||||
recname = Path(recordfilename)
|
||||
recordfilename = str(Path.joinpath(
|
||||
recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix))
|
||||
logger.info('Dumping backtest results to %s', recordfilename)
|
||||
file_dump_json(recordfilename, records)
|
||||
|
||||
@@ -133,7 +207,7 @@ class Backtesting(object):
|
||||
stake_amount = args['stake_amount']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
trade = Trade(
|
||||
open_rate=buy_row.close,
|
||||
open_rate=buy_row.open,
|
||||
open_date=buy_row.date,
|
||||
stake_amount=stake_amount,
|
||||
amount=stake_amount / buy_row.open,
|
||||
@@ -148,31 +222,40 @@ class Backtesting(object):
|
||||
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
||||
|
||||
buy_signal = sell_row.buy
|
||||
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
||||
sell_row.sell):
|
||||
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
|
||||
sell_row.sell)
|
||||
if sell.sell_flag:
|
||||
|
||||
return BacktestResult(pair=pair,
|
||||
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.close),
|
||||
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||
open_time=buy_row.date,
|
||||
close_time=sell_row.date,
|
||||
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
||||
trade_duration=int((
|
||||
sell_row.date - buy_row.date).total_seconds() // 60),
|
||||
open_index=buy_row.Index,
|
||||
close_index=sell_row.Index,
|
||||
open_at_end=False
|
||||
open_at_end=False,
|
||||
open_rate=buy_row.open,
|
||||
close_rate=sell_row.open,
|
||||
sell_reason=sell.sell_type
|
||||
)
|
||||
if partial_ticker:
|
||||
# no sell condition found - trade stil open at end of backtest period
|
||||
sell_row = partial_ticker[-1]
|
||||
btr = BacktestResult(pair=pair,
|
||||
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.close),
|
||||
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||
open_time=buy_row.date,
|
||||
close_time=sell_row.date,
|
||||
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
||||
trade_duration=int((
|
||||
sell_row.date - buy_row.date).total_seconds() // 60),
|
||||
open_index=buy_row.Index,
|
||||
close_index=sell_row.Index,
|
||||
open_at_end=True
|
||||
open_at_end=True,
|
||||
open_rate=buy_row.open,
|
||||
close_rate=sell_row.open,
|
||||
sell_reason=SellType.FORCE_SELL
|
||||
)
|
||||
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
|
||||
btr.profit_percent, btr.profit_abs)
|
||||
@@ -191,20 +274,20 @@ class Backtesting(object):
|
||||
stake_amount: btc amount to use for each trade
|
||||
processed: a processed dictionary with format {pair, data}
|
||||
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||
realistic: do we try to simulate realistic trades? (default: True)
|
||||
position_stacking: do we allow position stacking? (default: False)
|
||||
:return: DataFrame
|
||||
"""
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell']
|
||||
processed = args['processed']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
realistic = args.get('realistic', False)
|
||||
position_stacking = args.get('position_stacking', False)
|
||||
trades = []
|
||||
trade_count_lock: Dict = {}
|
||||
for pair, pair_data in processed.items():
|
||||
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||
|
||||
ticker_data = self.populate_sell_trend(
|
||||
self.populate_buy_trend(pair_data))[headers].copy()
|
||||
ticker_data = self.advise_sell(
|
||||
self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||
|
||||
# to avoid using data from future, we buy/sell with signal from previous candle
|
||||
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
||||
@@ -221,7 +304,7 @@ class Backtesting(object):
|
||||
if row.buy == 0 or row.sell == 1:
|
||||
continue # skip rows where no buy signal or that would immediately sell off
|
||||
|
||||
if realistic:
|
||||
if not position_stacking:
|
||||
if lock_pair_until is not None and row.date <= lock_pair_until:
|
||||
continue
|
||||
if max_open_trades > 0:
|
||||
@@ -249,15 +332,15 @@ class Backtesting(object):
|
||||
Run a backtesting end-to-end
|
||||
:return: None
|
||||
"""
|
||||
data = {}
|
||||
data: Dict[str, Any] = {}
|
||||
pairs = self.config['exchange']['pair_whitelist']
|
||||
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
||||
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
|
||||
|
||||
if self.config.get('live'):
|
||||
logger.info('Downloading data for all pairs in whitelist ...')
|
||||
for pair in pairs:
|
||||
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval)
|
||||
self.exchange.refresh_tickers(pairs, self.ticker_interval)
|
||||
data = self.exchange.klines
|
||||
else:
|
||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||
|
||||
@@ -275,58 +358,61 @@ class Backtesting(object):
|
||||
if not data:
|
||||
logger.critical("No data found. Terminating.")
|
||||
return
|
||||
# Ignore max_open_trades in backtesting, except realistic flag was passed
|
||||
if self.config.get('realistic_simulation', False):
|
||||
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
|
||||
if self.config.get('use_max_market_positions', True):
|
||||
max_open_trades = self.config['max_open_trades']
|
||||
else:
|
||||
logger.info('Ignoring max_open_trades (realistic_simulation not set) ...')
|
||||
logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
||||
max_open_trades = 0
|
||||
all_results = {}
|
||||
|
||||
preprocessed = self.tickerdata_to_dataframe(data)
|
||||
for strat in self.strategylist:
|
||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||
self._set_strategy(strat)
|
||||
|
||||
# Print timeframe
|
||||
min_date, max_date = self.get_timeframe(preprocessed)
|
||||
logger.info(
|
||||
'Measuring data from %s up to %s (%s days)..',
|
||||
min_date.isoformat(),
|
||||
max_date.isoformat(),
|
||||
(max_date - min_date).days
|
||||
)
|
||||
# need to reprocess data every time to populate signals
|
||||
preprocessed = self.tickerdata_to_dataframe(data)
|
||||
|
||||
# Execute backtest and print results
|
||||
results = self.backtest(
|
||||
{
|
||||
'stake_amount': self.config.get('stake_amount'),
|
||||
'processed': preprocessed,
|
||||
'max_open_trades': max_open_trades,
|
||||
'realistic': self.config.get('realistic_simulation', False),
|
||||
}
|
||||
)
|
||||
|
||||
if self.config.get('export', False):
|
||||
self._store_backtest_result(self.config.get('exportfilename'), results)
|
||||
|
||||
logger.info(
|
||||
'\n======================================== '
|
||||
'BACKTESTING REPORT'
|
||||
' =========================================\n'
|
||||
'%s',
|
||||
self._generate_text_table(
|
||||
data,
|
||||
results
|
||||
# Print timeframe
|
||||
min_date, max_date = self.get_timeframe(preprocessed)
|
||||
logger.info(
|
||||
'Measuring data from %s up to %s (%s days)..',
|
||||
min_date.isoformat(),
|
||||
max_date.isoformat(),
|
||||
(max_date - min_date).days
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'\n====================================== '
|
||||
'LEFT OPEN TRADES REPORT'
|
||||
' ======================================\n'
|
||||
'%s',
|
||||
self._generate_text_table(
|
||||
data,
|
||||
results.loc[results.open_at_end]
|
||||
# Execute backtest and print results
|
||||
all_results[self.strategy.get_strategy_name()] = self.backtest(
|
||||
{
|
||||
'stake_amount': self.config.get('stake_amount'),
|
||||
'processed': preprocessed,
|
||||
'max_open_trades': max_open_trades,
|
||||
'position_stacking': self.config.get('position_stacking', False),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for strategy, results in all_results.items():
|
||||
|
||||
if self.config.get('export', False):
|
||||
self._store_backtest_result(self.config['exportfilename'], results,
|
||||
strategy if len(self.strategylist) > 1 else None)
|
||||
|
||||
print(f"Result for strategy {strategy}")
|
||||
print(' BACKTESTING REPORT '.center(119, '='))
|
||||
print(self._generate_text_table(data, results))
|
||||
|
||||
print(' SELL REASON STATS '.center(119, '='))
|
||||
print(self._generate_text_table_sell_reason(data, results))
|
||||
|
||||
print(' LEFT OPEN TRADES REPORT '.center(119, '='))
|
||||
print(self._generate_text_table(data, results.loc[results.open_at_end], True))
|
||||
print()
|
||||
if len(all_results) > 1:
|
||||
# Print Strategy summary table
|
||||
print(' Strategy Summary '.center(119, '='))
|
||||
print(self._generate_text_table_strategy(all_results))
|
||||
print('\nFor more details, please look at the detail tables above')
|
||||
|
||||
|
||||
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||
|
||||
@@ -4,22 +4,21 @@
|
||||
This module contains the hyperopt logic
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import pickle
|
||||
import signal
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from functools import reduce
|
||||
from math import exp
|
||||
from operator import itemgetter
|
||||
from typing import Dict, Any, Callable, Optional
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
import numpy
|
||||
import talib.abstract as ta
|
||||
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
|
||||
from pandas import DataFrame
|
||||
from sklearn.externals.joblib import Parallel, delayed, dump, load
|
||||
from skopt import Optimizer
|
||||
from skopt.space import Categorical, Dimension, Integer, Real
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.arguments import Arguments
|
||||
@@ -29,6 +28,9 @@ from freqtrade.optimize.backtesting import Backtesting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
||||
TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl')
|
||||
|
||||
|
||||
class Hyperopt(Backtesting):
|
||||
"""
|
||||
@@ -44,7 +46,6 @@ class Hyperopt(Backtesting):
|
||||
# to the number of days
|
||||
self.target_trades = 600
|
||||
self.total_tries = config.get('epochs', 0)
|
||||
self.current_tries = 0
|
||||
self.current_best_loss = 100
|
||||
|
||||
# max average trade duration in minutes
|
||||
@@ -56,130 +57,38 @@ class Hyperopt(Backtesting):
|
||||
# check that the reported Σ% values do not exceed this!
|
||||
self.expected_max_profit = 3.0
|
||||
|
||||
# Configuration and data used by hyperopt
|
||||
self.processed: Optional[Dict[str, Any]] = None
|
||||
# Previous evaluations
|
||||
self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle')
|
||||
self.trials: List = []
|
||||
|
||||
# Hyperopt Trials
|
||||
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
||||
self.trials = Trials()
|
||||
def get_args(self, params):
|
||||
dimensions = self.hyperopt_space()
|
||||
# Ensure the number of dimensions match
|
||||
# the number of parameters in the list x.
|
||||
if len(params) != len(dimensions):
|
||||
raise ValueError('Mismatch in number of search-space dimensions. '
|
||||
f'len(dimensions)=={len(dimensions)} and len(x)=={len(params)}')
|
||||
|
||||
# Create a dict where the keys are the names of the dimensions
|
||||
# and the values are taken from the list of parameters x.
|
||||
arg_dict = {dim.name: value for dim, value in zip(dimensions, params)}
|
||||
return arg_dict
|
||||
|
||||
@staticmethod
|
||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
"""
|
||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
# SAR Parabolic
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
|
||||
return dataframe
|
||||
|
||||
@@ -187,15 +96,16 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
Save hyperopt trials to file
|
||||
"""
|
||||
logger.info('Saving Trials to \'%s\'', self.trials_file)
|
||||
pickle.dump(self.trials, open(self.trials_file, 'wb'))
|
||||
if self.trials:
|
||||
logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file)
|
||||
dump(self.trials, self.trials_file)
|
||||
|
||||
def read_trials(self) -> Trials:
|
||||
def read_trials(self) -> List:
|
||||
"""
|
||||
Read hyperopt trials file
|
||||
"""
|
||||
logger.info('Reading Trials from \'%s\'', self.trials_file)
|
||||
trials = pickle.load(open(self.trials_file, 'rb'))
|
||||
trials = load(self.trials_file)
|
||||
os.remove(self.trials_file)
|
||||
return trials
|
||||
|
||||
@@ -203,22 +113,27 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
Display Best hyperopt result
|
||||
"""
|
||||
vals = json.dumps(self.trials.best_trial['misc']['vals'], indent=4)
|
||||
results = self.trials.best_trial['result']['result']
|
||||
logger.info('Best result:\n%s\nwith values:\n%s', results, vals)
|
||||
results = sorted(self.trials, key=itemgetter('loss'))
|
||||
best_result = results[0]
|
||||
logger.info(
|
||||
'Best result:\n%s\nwith values:\n%s',
|
||||
best_result['result'],
|
||||
best_result['params']
|
||||
)
|
||||
if 'roi_t1' in best_result['params']:
|
||||
logger.info('ROI table:\n%s', self.generate_roi_table(best_result['params']))
|
||||
|
||||
def log_results(self, results) -> None:
|
||||
"""
|
||||
Log results if it is better than any previous evaluation
|
||||
"""
|
||||
if results['loss'] < self.current_best_loss:
|
||||
current = results['current_tries']
|
||||
total = results['total_tries']
|
||||
res = results['result']
|
||||
loss = results['loss']
|
||||
self.current_best_loss = results['loss']
|
||||
log_msg = '\n{:5d}/{}: {}. Loss {:.5f}'.format(
|
||||
results['current_tries'],
|
||||
results['total_tries'],
|
||||
results['result'],
|
||||
results['loss']
|
||||
)
|
||||
log_msg = f'\n{current:5d}/{total}: {res}. Loss {loss:.5f}'
|
||||
print(log_msg)
|
||||
else:
|
||||
print('.', end='')
|
||||
@@ -231,12 +146,13 @@ class Hyperopt(Backtesting):
|
||||
trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8)
|
||||
profit_loss = max(0, 1 - total_profit / self.expected_max_profit)
|
||||
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1)
|
||||
return trade_loss + profit_loss + duration_loss
|
||||
result = trade_loss + profit_loss + duration_loss
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||
"""
|
||||
Generate the ROI table thqt will be used by Hyperopt
|
||||
Generate the ROI table that will be used by Hyperopt
|
||||
"""
|
||||
roi_table = {}
|
||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||
@@ -247,87 +163,44 @@ class Hyperopt(Backtesting):
|
||||
return roi_table
|
||||
|
||||
@staticmethod
|
||||
def roi_space() -> Dict[str, Any]:
|
||||
def roi_space() -> List[Dimension]:
|
||||
"""
|
||||
Values to search for each ROI steps
|
||||
"""
|
||||
return {
|
||||
'roi_t1': hp.quniform('roi_t1', 10, 120, 20),
|
||||
'roi_t2': hp.quniform('roi_t2', 10, 60, 15),
|
||||
'roi_t3': hp.quniform('roi_t3', 10, 40, 10),
|
||||
'roi_p1': hp.quniform('roi_p1', 0.01, 0.04, 0.01),
|
||||
'roi_p2': hp.quniform('roi_p2', 0.01, 0.07, 0.01),
|
||||
'roi_p3': hp.quniform('roi_p3', 0.01, 0.20, 0.01),
|
||||
}
|
||||
return [
|
||||
Integer(10, 120, name='roi_t1'),
|
||||
Integer(10, 60, name='roi_t2'),
|
||||
Integer(10, 40, name='roi_t3'),
|
||||
Real(0.01, 0.04, name='roi_p1'),
|
||||
Real(0.01, 0.07, name='roi_p2'),
|
||||
Real(0.01, 0.20, name='roi_p3'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def stoploss_space() -> Dict[str, Any]:
|
||||
def stoploss_space() -> List[Dimension]:
|
||||
"""
|
||||
Stoploss Value to search
|
||||
Stoploss search space
|
||||
"""
|
||||
return {
|
||||
'stoploss': hp.quniform('stoploss', -0.5, -0.02, 0.02),
|
||||
}
|
||||
return [
|
||||
Real(-0.5, -0.02, name='stoploss'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def indicator_space() -> Dict[str, Any]:
|
||||
def indicator_space() -> List[Dimension]:
|
||||
"""
|
||||
Define your Hyperopt space for searching strategy parameters
|
||||
"""
|
||||
return {
|
||||
'macd_below_zero': hp.choice('macd_below_zero', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'mfi': hp.choice('mfi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('mfi-value', 10, 25, 5)}
|
||||
]),
|
||||
'fastd': hp.choice('fastd', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('fastd-value', 15, 45, 5)}
|
||||
]),
|
||||
'adx': hp.choice('adx', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('adx-value', 20, 50, 5)}
|
||||
]),
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 5)}
|
||||
]),
|
||||
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'over_sar': hp.choice('over_sar', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'green_candle': hp.choice('green_candle', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'uptrend_sma': hp.choice('uptrend_sma', [
|
||||
{'enabled': False},
|
||||
{'enabled': True}
|
||||
]),
|
||||
'trigger': hp.choice('trigger', [
|
||||
{'type': 'lower_bb'},
|
||||
{'type': 'lower_bb_tema'},
|
||||
{'type': 'faststoch10'},
|
||||
{'type': 'ao_cross_zero'},
|
||||
{'type': 'ema3_cross_ema10'},
|
||||
{'type': 'macd_cross_signal'},
|
||||
{'type': 'sar_reversal'},
|
||||
{'type': 'ht_sine'},
|
||||
{'type': 'heiken_reversal_bull'},
|
||||
{'type': 'di_cross'},
|
||||
]),
|
||||
}
|
||||
return [
|
||||
Integer(10, 25, name='mfi-value'),
|
||||
Integer(15, 45, name='fastd-value'),
|
||||
Integer(20, 50, name='adx-value'),
|
||||
Integer(20, 40, name='rsi-value'),
|
||||
Categorical([True, False], name='mfi-enabled'),
|
||||
Categorical([True, False], name='fastd-enabled'),
|
||||
Categorical([True, False], name='adx-enabled'),
|
||||
Categorical([True, False], name='rsi-enabled'),
|
||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
||||
]
|
||||
|
||||
def has_space(self, space: str) -> bool:
|
||||
"""
|
||||
@@ -337,17 +210,17 @@ class Hyperopt(Backtesting):
|
||||
return True
|
||||
return False
|
||||
|
||||
def hyperopt_space(self) -> Dict[str, Any]:
|
||||
def hyperopt_space(self) -> List[Dimension]:
|
||||
"""
|
||||
Return the space to use during Hyperopt
|
||||
"""
|
||||
spaces: Dict = {}
|
||||
spaces: List[Dimension] = []
|
||||
if self.has_space('buy'):
|
||||
spaces = {**spaces, **Hyperopt.indicator_space()}
|
||||
spaces += Hyperopt.indicator_space()
|
||||
if self.has_space('roi'):
|
||||
spaces = {**spaces, **Hyperopt.roi_space()}
|
||||
spaces += Hyperopt.roi_space()
|
||||
if self.has_space('stoploss'):
|
||||
spaces = {**spaces, **Hyperopt.stoploss_space()}
|
||||
spaces += Hyperopt.stoploss_space()
|
||||
return spaces
|
||||
|
||||
@staticmethod
|
||||
@@ -355,69 +228,32 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
Define the buy strategy parameters to be used by hyperopt
|
||||
"""
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Buy strategy Hyperopt will build and use
|
||||
"""
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if 'uptrend_long_ema' in params and params['uptrend_long_ema']['enabled']:
|
||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||
if 'macd_below_zero' in params and params['macd_below_zero']['enabled']:
|
||||
conditions.append(dataframe['macd'] < 0)
|
||||
if 'uptrend_short_ema' in params and params['uptrend_short_ema']['enabled']:
|
||||
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
||||
if 'mfi' in params and params['mfi']['enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||
if 'fastd' in params and params['fastd']['enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
||||
if 'adx' in params and params['adx']['enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||
if 'rsi' in params and params['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||
if 'over_sar' in params and params['over_sar']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||
if 'green_candle' in params and params['green_candle']['enabled']:
|
||||
conditions.append(dataframe['close'] > dataframe['open'])
|
||||
if 'uptrend_sma' in params and params['uptrend_sma']['enabled']:
|
||||
prevsma = dataframe['sma'].shift(1)
|
||||
conditions.append(dataframe['sma'] > prevsma)
|
||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||
if 'fastd-enabled' in params and params['fastd-enabled']:
|
||||
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
||||
if 'adx-enabled' in params and params['adx-enabled']:
|
||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||
|
||||
# TRIGGERS
|
||||
triggers = {
|
||||
'lower_bb': (
|
||||
dataframe['close'] < dataframe['bb_lowerband']
|
||||
),
|
||||
'lower_bb_tema': (
|
||||
dataframe['tema'] < dataframe['bb_lowerband']
|
||||
),
|
||||
'faststoch10': (qtpylib.crossed_above(
|
||||
dataframe['fastd'], 10.0
|
||||
)),
|
||||
'ao_cross_zero': (qtpylib.crossed_above(
|
||||
dataframe['ao'], 0.0
|
||||
)),
|
||||
'ema3_cross_ema10': (qtpylib.crossed_above(
|
||||
dataframe['ema3'], dataframe['ema10']
|
||||
)),
|
||||
'macd_cross_signal': (qtpylib.crossed_above(
|
||||
if params['trigger'] == 'bb_lower':
|
||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||
if params['trigger'] == 'macd_cross_signal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['macd'], dataframe['macdsignal']
|
||||
)),
|
||||
'sar_reversal': (qtpylib.crossed_above(
|
||||
))
|
||||
if params['trigger'] == 'sar_reversal':
|
||||
conditions.append(qtpylib.crossed_above(
|
||||
dataframe['close'], dataframe['sar']
|
||||
)),
|
||||
'ht_sine': (qtpylib.crossed_above(
|
||||
dataframe['htleadsine'], dataframe['htsine']
|
||||
)),
|
||||
'heiken_reversal_bull': (
|
||||
(qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) &
|
||||
(dataframe['ha_low'] == dataframe['ha_open'])
|
||||
),
|
||||
'di_cross': (qtpylib.crossed_above(
|
||||
dataframe['plus_di'], dataframe['minus_di']
|
||||
)),
|
||||
}
|
||||
conditions.append(triggers.get(params['trigger']['type']))
|
||||
))
|
||||
|
||||
dataframe.loc[
|
||||
reduce(lambda x, y: x & y, conditions),
|
||||
@@ -427,21 +263,24 @@ class Hyperopt(Backtesting):
|
||||
|
||||
return populate_buy_trend
|
||||
|
||||
def generate_optimizer(self, params: Dict) -> Dict:
|
||||
def generate_optimizer(self, _params) -> Dict:
|
||||
params = self.get_args(_params)
|
||||
|
||||
if self.has_space('roi'):
|
||||
self.analyze.strategy.minimal_roi = self.generate_roi_table(params)
|
||||
self.strategy.minimal_roi = self.generate_roi_table(params)
|
||||
|
||||
if self.has_space('buy'):
|
||||
self.populate_buy_trend = self.buy_strategy_generator(params)
|
||||
self.advise_buy = self.buy_strategy_generator(params)
|
||||
|
||||
if self.has_space('stoploss'):
|
||||
self.analyze.strategy.stoploss = params['stoploss']
|
||||
self.strategy.stoploss = params['stoploss']
|
||||
|
||||
processed = load(TICKERDATA_PICKLE)
|
||||
results = self.backtest(
|
||||
{
|
||||
'stake_amount': self.config['stake_amount'],
|
||||
'processed': self.processed,
|
||||
'realistic': self.config.get('realistic_simulation', False),
|
||||
'processed': processed,
|
||||
'position_stacking': self.config.get('position_stacking', True),
|
||||
}
|
||||
)
|
||||
result_explanation = self.format_results(results)
|
||||
@@ -450,30 +289,18 @@ class Hyperopt(Backtesting):
|
||||
trade_count = len(results.index)
|
||||
trade_duration = results.trade_duration.mean()
|
||||
|
||||
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
||||
print('.', end='')
|
||||
sys.stdout.flush()
|
||||
if trade_count == 0:
|
||||
return {
|
||||
'status': STATUS_FAIL,
|
||||
'loss': float('inf')
|
||||
'loss': MAX_LOSS,
|
||||
'params': params,
|
||||
'result': result_explanation,
|
||||
}
|
||||
|
||||
loss = self.calculate_loss(total_profit, trade_count, trade_duration)
|
||||
|
||||
self.current_tries += 1
|
||||
|
||||
self.log_results(
|
||||
{
|
||||
'loss': loss,
|
||||
'current_tries': self.current_tries,
|
||||
'total_tries': self.total_tries,
|
||||
'result': result_explanation,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
'loss': loss,
|
||||
'status': STATUS_OK,
|
||||
'params': params,
|
||||
'result': result_explanation,
|
||||
}
|
||||
|
||||
@@ -481,15 +308,37 @@ class Hyperopt(Backtesting):
|
||||
"""
|
||||
Return the format result in a string
|
||||
"""
|
||||
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
||||
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
||||
len(results.index),
|
||||
results.profit_percent.mean() * 100.0,
|
||||
results.profit_abs.sum(),
|
||||
self.config['stake_currency'],
|
||||
results.profit_percent.sum(),
|
||||
results.trade_duration.mean(),
|
||||
)
|
||||
trades = len(results.index)
|
||||
avg_profit = results.profit_percent.mean() * 100.0
|
||||
total_profit = results.profit_abs.sum()
|
||||
stake_cur = self.config['stake_currency']
|
||||
profit = results.profit_percent.sum()
|
||||
duration = results.trade_duration.mean()
|
||||
|
||||
return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. '
|
||||
f'Total profit {total_profit: 11.8f} {stake_cur} '
|
||||
f'({profit:.4f}Σ%). Avg duration {duration:5.1f} mins.')
|
||||
|
||||
def get_optimizer(self, cpu_count) -> Optimizer:
|
||||
return Optimizer(
|
||||
self.hyperopt_space(),
|
||||
base_estimator="ET",
|
||||
acq_optimizer="auto",
|
||||
n_initial_points=30,
|
||||
acq_optimizer_kwargs={'n_jobs': cpu_count}
|
||||
)
|
||||
|
||||
def run_optimizer_parallel(self, parallel, asked) -> List:
|
||||
return parallel(delayed(self.generate_optimizer)(v) for v in asked)
|
||||
|
||||
def load_previous_results(self):
|
||||
""" read trials file if we have one """
|
||||
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
||||
self.trials = self.read_trials()
|
||||
logger.info(
|
||||
'Loaded %d previous evaluations from disk.',
|
||||
len(self.trials)
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||
@@ -502,68 +351,36 @@ class Hyperopt(Backtesting):
|
||||
)
|
||||
|
||||
if self.has_space('buy'):
|
||||
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
|
||||
self.processed = self.tickerdata_to_dataframe(data)
|
||||
self.strategy.advise_indicators = Hyperopt.populate_indicators # type: ignore
|
||||
dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
|
||||
self.exchange = None # type: ignore
|
||||
self.load_previous_results()
|
||||
|
||||
logger.info('Preparing Trials..')
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
# read trials file if we have one
|
||||
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
||||
self.trials = self.read_trials()
|
||||
|
||||
self.current_tries = len(self.trials.results)
|
||||
self.total_tries += self.current_tries
|
||||
logger.info(
|
||||
'Continuing with trials. Current: %d, Total: %d',
|
||||
self.current_tries,
|
||||
self.total_tries
|
||||
)
|
||||
cpus = multiprocessing.cpu_count()
|
||||
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
|
||||
|
||||
opt = self.get_optimizer(cpus)
|
||||
EVALS = max(self.total_tries // cpus, 1)
|
||||
try:
|
||||
best_parameters = fmin(
|
||||
fn=self.generate_optimizer,
|
||||
space=self.hyperopt_space(),
|
||||
algo=tpe.suggest,
|
||||
max_evals=self.total_tries,
|
||||
trials=self.trials
|
||||
)
|
||||
with Parallel(n_jobs=cpus) as parallel:
|
||||
for i in range(EVALS):
|
||||
asked = opt.ask(n_points=cpus)
|
||||
f_val = self.run_optimizer_parallel(parallel, asked)
|
||||
opt.tell(asked, [i['loss'] for i in f_val])
|
||||
|
||||
results = sorted(self.trials.results, key=itemgetter('loss'))
|
||||
best_result = results[0]['result']
|
||||
|
||||
except ValueError:
|
||||
best_parameters = {}
|
||||
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \
|
||||
'try with more epochs (param: -e).'
|
||||
|
||||
# Improve best parameter logging display
|
||||
if best_parameters:
|
||||
best_parameters = space_eval(
|
||||
self.hyperopt_space(),
|
||||
best_parameters
|
||||
)
|
||||
|
||||
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
|
||||
if 'roi_t1' in best_parameters:
|
||||
logger.info('ROI table:\n%s', self.generate_roi_table(best_parameters))
|
||||
|
||||
logger.info('Best Result:\n%s', best_result)
|
||||
|
||||
# Store trials result to file to resume next time
|
||||
self.save_trials()
|
||||
|
||||
def signal_handler(self, sig, frame) -> None:
|
||||
"""
|
||||
Hyperopt SIGINT handler
|
||||
"""
|
||||
logger.info(
|
||||
'Hyperopt received %s',
|
||||
signal.Signals(sig).name
|
||||
)
|
||||
self.trials += f_val
|
||||
for j in range(cpus):
|
||||
self.log_results({
|
||||
'loss': f_val[j]['loss'],
|
||||
'current_tries': i * cpus + j,
|
||||
'total_tries': self.total_tries,
|
||||
'result': f_val[j]['result'],
|
||||
})
|
||||
except KeyboardInterrupt:
|
||||
print('User interrupted..')
|
||||
|
||||
self.save_trials()
|
||||
self.log_trials_result()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def start(args: Namespace) -> None:
|
||||
@@ -585,6 +402,13 @@ def start(args: Namespace) -> None:
|
||||
config['exchange']['key'] = ''
|
||||
config['exchange']['secret'] = ''
|
||||
|
||||
if config.get('strategy') and config.get('strategy') != 'DefaultStrategy':
|
||||
logger.error("Please don't use --strategy for hyperopt.")
|
||||
logger.error(
|
||||
"Read the documentation at "
|
||||
"https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md "
|
||||
"to understand how to configure hyperopt.")
|
||||
raise ValueError("--strategy configured but not supported for hyperopt")
|
||||
# Initialize backtesting object
|
||||
hyperopt = Hyperopt(config)
|
||||
hyperopt.start()
|
||||
|
||||
@@ -5,12 +5,11 @@ This module contains the class to persist trades into SQLite
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, getcontext
|
||||
from typing import Dict, Optional, Any
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
||||
create_engine)
|
||||
from sqlalchemy import inspect
|
||||
create_engine, inspect)
|
||||
from sqlalchemy.exc import NoSuchModuleError
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
@@ -22,6 +21,7 @@ from freqtrade import OperationalException
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DECL_BASE: Any = declarative_base()
|
||||
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||
|
||||
|
||||
def init(config: Dict) -> None:
|
||||
@@ -46,10 +46,8 @@ def init(config: Dict) -> None:
|
||||
try:
|
||||
engine = create_engine(db_url, **kwargs)
|
||||
except NoSuchModuleError:
|
||||
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format(
|
||||
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||
)
|
||||
raise OperationalException(error)
|
||||
raise OperationalException(f'Given value for db_url: \'{db_url}\' '
|
||||
f'is no valid database URL! (See {_SQL_DOCS_URL})')
|
||||
|
||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||
Trade.session = session()
|
||||
@@ -66,6 +64,10 @@ def has_column(columns, searchname: str) -> bool:
|
||||
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
||||
|
||||
|
||||
def get_column_def(columns, column: str, default: str) -> str:
|
||||
return default if not has_column(columns, column) else column
|
||||
|
||||
|
||||
def check_migrate(engine) -> None:
|
||||
"""
|
||||
Checks if migration is necessary and migrates if necessary
|
||||
@@ -73,18 +75,40 @@ def check_migrate(engine) -> None:
|
||||
inspector = inspect(engine)
|
||||
|
||||
cols = inspector.get_columns('trades')
|
||||
tabs = inspector.get_table_names()
|
||||
table_back_name = 'trades_bak'
|
||||
for i, table_back_name in enumerate(tabs):
|
||||
table_back_name = f'trades_bak{i}'
|
||||
logger.debug(f'trying {table_back_name}')
|
||||
|
||||
# Check for latest column
|
||||
if not has_column(cols, 'ticker_interval'):
|
||||
logger.info(f'Running database migration - backup available as {table_back_name}')
|
||||
|
||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||
fee_close = get_column_def(cols, 'fee_close', 'fee')
|
||||
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
|
||||
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
|
||||
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
||||
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
||||
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||
strategy = get_column_def(cols, 'strategy', 'null')
|
||||
ticker_interval = get_column_def(cols, 'ticker_interval', 'null')
|
||||
|
||||
if not has_column(cols, 'fee_open'):
|
||||
# Schema migration necessary
|
||||
engine.execute("alter table trades rename to trades_bak")
|
||||
engine.execute(f"alter table trades rename to {table_back_name}")
|
||||
# let SQLAlchemy create the schema as required
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
|
||||
# Copy data back - following the correct schema
|
||||
engine.execute("""insert into trades
|
||||
engine.execute(f"""insert into trades
|
||||
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||
stake_amount, amount, open_date, close_date, open_order_id)
|
||||
stake_amount, amount, open_date, close_date, open_order_id,
|
||||
stop_loss, initial_stop_loss, max_rate, sell_reason, strategy,
|
||||
ticker_interval
|
||||
)
|
||||
select id, lower(exchange),
|
||||
case
|
||||
when instr(pair, '_') != 0 then
|
||||
@@ -93,22 +117,20 @@ def check_migrate(engine) -> None:
|
||||
else pair
|
||||
end
|
||||
pair,
|
||||
is_open, fee fee_open, fee fee_close,
|
||||
open_rate, null open_rate_requested, close_rate,
|
||||
null close_rate_requested, close_profit,
|
||||
stake_amount, amount, open_date, close_date, open_order_id
|
||||
from trades_bak
|
||||
is_open, {fee_open} fee_open, {fee_close} fee_close,
|
||||
open_rate, {open_rate_requested} open_rate_requested, close_rate,
|
||||
{close_rate_requested} close_rate_requested, close_profit,
|
||||
stake_amount, amount, open_date, close_date, open_order_id,
|
||||
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
||||
{max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy,
|
||||
{ticker_interval} ticker_interval
|
||||
from {table_back_name}
|
||||
""")
|
||||
|
||||
# Reread columns - the above recreated the table!
|
||||
inspector = inspect(engine)
|
||||
cols = inspector.get_columns('trades')
|
||||
|
||||
if not has_column(cols, 'open_rate_requested'):
|
||||
engine.execute("alter table trades add open_rate_requested float")
|
||||
if not has_column(cols, 'close_rate_requested'):
|
||||
engine.execute("alter table trades add close_rate_requested float")
|
||||
|
||||
|
||||
def cleanup() -> None:
|
||||
"""
|
||||
@@ -137,8 +159,8 @@ class Trade(_DECL_BASE):
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
exchange = Column(String, nullable=False)
|
||||
pair = Column(String, nullable=False)
|
||||
is_open = Column(Boolean, nullable=False, default=True)
|
||||
pair = Column(String, nullable=False, index=True)
|
||||
is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||
fee_open = Column(Float, nullable=False, default=0.0)
|
||||
fee_close = Column(Float, nullable=False, default=0.0)
|
||||
open_rate = Column(Float)
|
||||
@@ -151,15 +173,60 @@ class Trade(_DECL_BASE):
|
||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
close_date = Column(DateTime)
|
||||
open_order_id = Column(String)
|
||||
# absolute value of the stop loss
|
||||
stop_loss = Column(Float, nullable=True, default=0.0)
|
||||
# absolute value of the initial stop loss
|
||||
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||
# absolute value of the highest reached price
|
||||
max_rate = Column(Float, nullable=True, default=0.0)
|
||||
sell_reason = Column(String, nullable=True)
|
||||
strategy = Column(String, nullable=True)
|
||||
ticker_interval = Column(Integer, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
||||
self.id,
|
||||
self.pair,
|
||||
self.amount,
|
||||
self.open_rate,
|
||||
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||
)
|
||||
open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||
|
||||
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
||||
|
||||
def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
|
||||
"""this adjusts the stop loss to it's most recently observed setting"""
|
||||
|
||||
if initial and not (self.stop_loss is None or self.stop_loss == 0):
|
||||
# Don't modify if called with initial and nothing to do
|
||||
return
|
||||
|
||||
new_loss = float(current_price * (1 - abs(stoploss)))
|
||||
|
||||
# keeping track of the highest observed rate for this trade
|
||||
if self.max_rate is None:
|
||||
self.max_rate = current_price
|
||||
else:
|
||||
if current_price > self.max_rate:
|
||||
self.max_rate = current_price
|
||||
|
||||
# no stop loss assigned yet
|
||||
if not self.stop_loss:
|
||||
logger.debug("assigning new stop loss")
|
||||
self.stop_loss = new_loss
|
||||
self.initial_stop_loss = new_loss
|
||||
|
||||
# evaluate if the stop loss needs to be updated
|
||||
else:
|
||||
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||
self.stop_loss = new_loss
|
||||
logger.debug("adjusted stop loss")
|
||||
else:
|
||||
logger.debug("keeping current stop loss")
|
||||
|
||||
logger.debug(
|
||||
f"{self.pair} - current price {current_price:.8f}, "
|
||||
f"bought at {self.open_rate:.8f} and calculated "
|
||||
f"stop loss is at: {self.initial_stop_loss:.8f} initial "
|
||||
f"stop at {self.stop_loss:.8f}. "
|
||||
f"trailing stop loss saved us: "
|
||||
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} "
|
||||
f"and max observed rate was {self.max_rate:.8f}")
|
||||
|
||||
def update(self, order: Dict) -> None:
|
||||
"""
|
||||
@@ -167,6 +234,7 @@ class Trade(_DECL_BASE):
|
||||
:param order: order retrieved by exchange.get_order()
|
||||
:return: None
|
||||
"""
|
||||
order_type = order['type']
|
||||
# Ignore open and cancelled orders
|
||||
if order['status'] == 'open' or order['price'] is None:
|
||||
return
|
||||
@@ -174,16 +242,16 @@ class Trade(_DECL_BASE):
|
||||
logger.info('Updating trade (id=%d) ...', self.id)
|
||||
|
||||
getcontext().prec = 8 # Bittrex do not go above 8 decimal
|
||||
if order['type'] == 'limit' and order['side'] == 'buy':
|
||||
if order_type == 'limit' and order['side'] == 'buy':
|
||||
# Update open rate and actual amount
|
||||
self.open_rate = Decimal(order['price'])
|
||||
self.amount = Decimal(order['amount'])
|
||||
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
||||
self.open_order_id = None
|
||||
elif order['type'] == 'limit' and order['side'] == 'sell':
|
||||
elif order_type == 'limit' and order['side'] == 'sell':
|
||||
self.close(order['price'])
|
||||
else:
|
||||
raise ValueError('Unknown order type: {}'.format(order['type']))
|
||||
raise ValueError(f'Unknown order type: {order_type}')
|
||||
cleanup()
|
||||
|
||||
def close(self, rate: float) -> None:
|
||||
@@ -254,7 +322,8 @@ class Trade(_DECL_BASE):
|
||||
rate=(rate or self.close_rate),
|
||||
fee=(fee or self.fee_close)
|
||||
)
|
||||
return float("{0:.8f}".format(close_trade_price - open_trade_price))
|
||||
profit = close_trade_price - open_trade_price
|
||||
return float(f"{profit:.8f}")
|
||||
|
||||
def calc_profit_percent(
|
||||
self,
|
||||
@@ -274,5 +343,5 @@ class Trade(_DECL_BASE):
|
||||
rate=(rate or self.close_rate),
|
||||
fee=(fee or self.fee_close)
|
||||
)
|
||||
|
||||
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
|
||||
profit_percent = (close_trade_price / open_trade_price) - 1
|
||||
return float(f"{profit_percent:.8f}")
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from .rpc import RPC, RPCMessageType, RPCException # noqa
|
||||
from .rpc_manager import RPCManager # noqa
|
||||
|
||||
@@ -3,22 +3,37 @@ This module contains class to define a RPC communications
|
||||
"""
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime, timedelta, date
|
||||
from datetime import timedelta, datetime, date
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Tuple, Any, List
|
||||
from enum import Enum
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
import arrow
|
||||
import sqlalchemy as sql
|
||||
from numpy import mean, nan_to_num
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import TemporaryError
|
||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.misc import shorten_date
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.state import State
|
||||
from freqtrade.strategy.interface import SellType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RPCMessageType(Enum):
|
||||
STATUS_NOTIFICATION = 'status'
|
||||
WARNING_NOTIFICATION = 'warning'
|
||||
CUSTOM_NOTIFICATION = 'custom'
|
||||
BUY_NOTIFICATION = 'buy'
|
||||
SELL_NOTIFICATION = 'sell'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class RPCException(Exception):
|
||||
"""
|
||||
Should be raised with a rpc-formatted message in an _rpc_* method
|
||||
@@ -26,13 +41,21 @@ class RPCException(Exception):
|
||||
|
||||
raise RPCException('*Status:* `no active trade`')
|
||||
"""
|
||||
pass
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(self)
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
|
||||
class RPC(object):
|
||||
"""
|
||||
RPC class can be used to have extra feature, like bot data, and access to DB data
|
||||
"""
|
||||
# Bind _fiat_converter if needed in each RPC handler
|
||||
_fiat_converter: Optional[CryptoToFiatConverter] = None
|
||||
|
||||
def __init__(self, freqtrade) -> None:
|
||||
"""
|
||||
Initializes all enabled rpc modules
|
||||
@@ -41,20 +64,20 @@ class RPC(object):
|
||||
"""
|
||||
self._freqtrade = freqtrade
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
""" Returns the lowercase name of the implementation """
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
@abstractmethod
|
||||
def cleanup(self) -> None:
|
||||
""" Cleanup pending module resources """
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
""" Returns the lowercase name of this module """
|
||||
|
||||
@abstractmethod
|
||||
def send_msg(self, msg: str) -> None:
|
||||
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||
""" Sends a message to all registered rpc modules """
|
||||
|
||||
def _rpc_trade_status(self) -> List[str]:
|
||||
def _rpc_trade_status(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
||||
a remotely exposed function
|
||||
@@ -62,11 +85,11 @@ class RPC(object):
|
||||
# Fetch open trade
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('*Status:* `trader is not running`')
|
||||
raise RPCException('trader is not running')
|
||||
elif not trades:
|
||||
raise RPCException('*Status:* `no active trade`')
|
||||
raise RPCException('no active trade')
|
||||
else:
|
||||
result = []
|
||||
results = []
|
||||
for trade in trades:
|
||||
order = None
|
||||
if trade.open_order_id:
|
||||
@@ -74,53 +97,42 @@ class RPC(object):
|
||||
# calculate profit and send message to user
|
||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
fmt_close_profit = '{:.2f}%'.format(
|
||||
round(trade.close_profit * 100, 2)
|
||||
) if trade.close_profit else None
|
||||
message = "*Trade ID:* `{trade_id}`\n" \
|
||||
"*Current Pair:* [{pair}]({market_url})\n" \
|
||||
"*Open Since:* `{date}`\n" \
|
||||
"*Amount:* `{amount}`\n" \
|
||||
"*Open Rate:* `{open_rate:.8f}`\n" \
|
||||
"*Close Rate:* `{close_rate}`\n" \
|
||||
"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||
"*Close Profit:* `{close_profit}`\n" \
|
||||
"*Current Profit:* `{current_profit:.2f}%`\n" \
|
||||
"*Open Order:* `{open_order}`"\
|
||||
.format(
|
||||
trade_id=trade.id,
|
||||
pair=trade.pair,
|
||||
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
|
||||
date=arrow.get(trade.open_date).humanize(),
|
||||
open_rate=trade.open_rate,
|
||||
close_rate=trade.close_rate,
|
||||
current_rate=current_rate,
|
||||
amount=round(trade.amount, 8),
|
||||
close_profit=fmt_close_profit,
|
||||
current_profit=round(current_profit * 100, 2),
|
||||
open_order='({} {} rem={:.8f})'.format(
|
||||
order['type'], order['side'], order['remaining']
|
||||
) if order else None,
|
||||
)
|
||||
result.append(message)
|
||||
return result
|
||||
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
||||
if trade.close_profit else None)
|
||||
results.append(dict(
|
||||
trade_id=trade.id,
|
||||
pair=trade.pair,
|
||||
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
|
||||
date=arrow.get(trade.open_date),
|
||||
open_rate=trade.open_rate,
|
||||
close_rate=trade.close_rate,
|
||||
current_rate=current_rate,
|
||||
amount=round(trade.amount, 8),
|
||||
close_profit=fmt_close_profit,
|
||||
current_profit=round(current_profit * 100, 2),
|
||||
open_order='({} {} rem={:.8f})'.format(
|
||||
order['type'], order['side'], order['remaining']
|
||||
) if order else None,
|
||||
))
|
||||
return results
|
||||
|
||||
def _rpc_status_table(self) -> DataFrame:
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('*Status:* `trader is not running`')
|
||||
raise RPCException('trader is not running')
|
||||
elif not trades:
|
||||
raise RPCException('*Status:* `no active order`')
|
||||
raise RPCException('no active order')
|
||||
else:
|
||||
trades_list = []
|
||||
for trade in trades:
|
||||
# calculate profit and send message to user
|
||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||
trade_perc = (100 * trade.calc_profit_percent(current_rate))
|
||||
trades_list.append([
|
||||
trade.id,
|
||||
trade.pair,
|
||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
|
||||
f'{trade_perc:.2f}%'
|
||||
])
|
||||
|
||||
columns = ['ID', 'Pair', 'Since', 'Profit']
|
||||
@@ -135,9 +147,8 @@ class RPC(object):
|
||||
profit_days: Dict[date, Dict] = {}
|
||||
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
|
||||
raise RPCException('timescale must be an integer greater than 0')
|
||||
|
||||
fiat = self._freqtrade.fiat_converter
|
||||
for day in range(0, timescale):
|
||||
profitday = today - timedelta(days=day)
|
||||
trades = Trade.query \
|
||||
@@ -148,7 +159,7 @@ class RPC(object):
|
||||
.all()
|
||||
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
||||
profit_days[profitday] = {
|
||||
'amount': format(curdayprofit, '.8f'),
|
||||
'amount': f'{curdayprofit:.8f}',
|
||||
'trades': len(trades)
|
||||
}
|
||||
|
||||
@@ -160,11 +171,11 @@ class RPC(object):
|
||||
symbol=stake_currency
|
||||
),
|
||||
'{value:.3f} {symbol}'.format(
|
||||
value=fiat.convert_amount(
|
||||
value=self._fiat_converter.convert_amount(
|
||||
value['amount'],
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
),
|
||||
) if self._fiat_converter else 0,
|
||||
symbol=fiat_display_currency
|
||||
),
|
||||
'{value} trade{s}'.format(
|
||||
@@ -215,34 +226,33 @@ class RPC(object):
|
||||
.order_by(sql.text('profit_sum DESC')).first()
|
||||
|
||||
if not best_pair:
|
||||
raise RPCException('*Status:* `no closed trade`')
|
||||
raise RPCException('no closed trade')
|
||||
|
||||
bp_pair, bp_rate = best_pair
|
||||
|
||||
# FIX: we want to keep fiatconverter in a state/environment,
|
||||
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
||||
fiat = self._freqtrade.fiat_converter
|
||||
# Prepare data to display
|
||||
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
||||
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
||||
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
||||
profit_closed_fiat = fiat.convert_amount(
|
||||
profit_closed_coin,
|
||||
profit_closed_fiat = self._fiat_converter.convert_amount(
|
||||
profit_closed_coin_sum,
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
)
|
||||
profit_all_coin = round(sum(profit_all_coin), 8)
|
||||
) if self._fiat_converter else 0
|
||||
|
||||
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
||||
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
|
||||
profit_all_fiat = fiat.convert_amount(
|
||||
profit_all_coin,
|
||||
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||
profit_all_coin_sum,
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
)
|
||||
) if self._fiat_converter else 0
|
||||
|
||||
num = float(len(durations) or 1)
|
||||
return {
|
||||
'profit_closed_coin': profit_closed_coin,
|
||||
'profit_closed_coin': profit_closed_coin_sum,
|
||||
'profit_closed_percent': profit_closed_percent,
|
||||
'profit_closed_fiat': profit_closed_fiat,
|
||||
'profit_all_coin': profit_all_coin,
|
||||
'profit_all_coin': profit_all_coin_sum,
|
||||
'profit_all_percent': profit_all_percent,
|
||||
'profit_all_fiat': profit_all_fiat,
|
||||
'trade_count': len(trades),
|
||||
@@ -253,7 +263,7 @@ class RPC(object):
|
||||
'best_rate': round(bp_rate * 100, 2),
|
||||
}
|
||||
|
||||
def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
|
||||
def _rpc_balance(self, fiat_display_currency: str) -> Dict:
|
||||
""" Returns current account balance per crypto """
|
||||
output = []
|
||||
total = 0.0
|
||||
@@ -264,51 +274,56 @@ class RPC(object):
|
||||
if coin == 'BTC':
|
||||
rate = 1.0
|
||||
else:
|
||||
if coin == 'USDT':
|
||||
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
|
||||
else:
|
||||
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
||||
try:
|
||||
if coin == 'USDT':
|
||||
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
|
||||
else:
|
||||
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
||||
except TemporaryError:
|
||||
continue
|
||||
est_btc: float = rate * balance['total']
|
||||
total = total + est_btc
|
||||
output.append(
|
||||
{
|
||||
'currency': coin,
|
||||
'available': balance['free'],
|
||||
'balance': balance['total'],
|
||||
'pending': balance['used'],
|
||||
'est_btc': est_btc
|
||||
}
|
||||
)
|
||||
output.append({
|
||||
'currency': coin,
|
||||
'available': balance['free'],
|
||||
'balance': balance['total'],
|
||||
'pending': balance['used'],
|
||||
'est_btc': est_btc,
|
||||
})
|
||||
if total == 0.0:
|
||||
raise RPCException('`All balances are zero.`')
|
||||
raise RPCException('all balances are zero')
|
||||
|
||||
fiat = self._freqtrade.fiat_converter
|
||||
symbol = fiat_display_currency
|
||||
value = fiat.convert_amount(total, 'BTC', symbol)
|
||||
return output, total, symbol, value
|
||||
value = self._fiat_converter.convert_amount(total, 'BTC',
|
||||
symbol) if self._fiat_converter else 0
|
||||
return {
|
||||
'currencies': output,
|
||||
'total': total,
|
||||
'symbol': symbol,
|
||||
'value': value,
|
||||
}
|
||||
|
||||
def _rpc_start(self) -> str:
|
||||
def _rpc_start(self) -> Dict[str, str]:
|
||||
""" Handler for start """
|
||||
if self._freqtrade.state == State.RUNNING:
|
||||
return '*Status:* `already running`'
|
||||
return {'status': 'already running'}
|
||||
|
||||
self._freqtrade.state = State.RUNNING
|
||||
return '`Starting trader ...`'
|
||||
return {'status': 'starting trader ...'}
|
||||
|
||||
def _rpc_stop(self) -> str:
|
||||
def _rpc_stop(self) -> Dict[str, str]:
|
||||
""" Handler for stop """
|
||||
if self._freqtrade.state == State.RUNNING:
|
||||
self._freqtrade.state = State.STOPPED
|
||||
return '`Stopping trader ...`'
|
||||
return {'status': 'stopping trader ...'}
|
||||
|
||||
return '*Status:* `already stopped`'
|
||||
return {'status': 'already stopped'}
|
||||
|
||||
def _rpc_reload_conf(self) -> str:
|
||||
def _rpc_reload_conf(self) -> Dict[str, str]:
|
||||
""" Handler for reload_conf. """
|
||||
self._freqtrade.state = State.RELOAD_CONF
|
||||
return '*Status:* `Reloading config ...`'
|
||||
return {'status': 'reloading config ...'}
|
||||
|
||||
# FIX: no test for this!!!!
|
||||
def _rpc_forcesell(self, trade_id) -> None:
|
||||
"""
|
||||
Handler for forcesell <id>.
|
||||
@@ -338,11 +353,11 @@ class RPC(object):
|
||||
|
||||
# Get current rate and execute sell
|
||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||
self._freqtrade.execute_sell(trade, current_rate)
|
||||
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
|
||||
# ---- EOF def _exec_forcesell ----
|
||||
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('`trader is not running`')
|
||||
raise RPCException('trader is not running')
|
||||
|
||||
if trade_id == 'all':
|
||||
# Execute sell for all open orders
|
||||
@@ -359,7 +374,7 @@ class RPC(object):
|
||||
).first()
|
||||
if not trade:
|
||||
logger.warning('forcesell: Invalid argument received')
|
||||
raise RPCException('Invalid argument.')
|
||||
raise RPCException('invalid argument')
|
||||
|
||||
_exec_forcesell(trade)
|
||||
Trade.session.flush()
|
||||
@@ -370,7 +385,7 @@ class RPC(object):
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('`trader is not running`')
|
||||
raise RPCException('trader is not running')
|
||||
|
||||
pair_rates = Trade.session.query(Trade.pair,
|
||||
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
||||
@@ -387,6 +402,6 @@ class RPC(object):
|
||||
def _rpc_count(self) -> List[Trade]:
|
||||
""" Returns the number of trades running """
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('`trader is not running`')
|
||||
raise RPCException('trader is not running')
|
||||
|
||||
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from freqtrade.rpc.rpc import RPC
|
||||
from freqtrade.rpc import RPC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,6 +23,12 @@ class RPCManager(object):
|
||||
from freqtrade.rpc.telegram import Telegram
|
||||
self.registered_modules.append(Telegram(freqtrade))
|
||||
|
||||
# Enable Webhook
|
||||
if freqtrade.config.get('webhook', {}).get('enabled', False):
|
||||
logger.info('Enabling rpc.webhook ...')
|
||||
from freqtrade.rpc.webhook import Webhook
|
||||
self.registered_modules.append(Webhook(freqtrade))
|
||||
|
||||
def cleanup(self) -> None:
|
||||
""" Stops all enabled rpc modules """
|
||||
logger.info('Cleaning up rpc modules ...')
|
||||
@@ -32,11 +38,14 @@ class RPCManager(object):
|
||||
mod.cleanup()
|
||||
del mod
|
||||
|
||||
def send_msg(self, msg: str) -> None:
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Send given markdown message to all registered rpc modules
|
||||
:param msg: message
|
||||
:return: None
|
||||
Send given message to all registered rpc modules.
|
||||
A message consists of one or more key value pairs of strings.
|
||||
e.g.:
|
||||
{
|
||||
'status': 'stopping bot'
|
||||
}
|
||||
"""
|
||||
logger.info('Sending rpc message: %s', msg)
|
||||
for mod in self.registered_modules:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
This module manage Telegram communication
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from tabulate import tabulate
|
||||
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
|
||||
@@ -12,7 +12,8 @@ from telegram.error import NetworkError, TelegramError
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
|
||||
from freqtrade.__init__ import __version__
|
||||
from freqtrade.rpc.rpc import RPC, RPCException
|
||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.rpc import RPC, RPCException, RPCMessageType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,10 +56,6 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
|
||||
class Telegram(RPC):
|
||||
""" This class handles all telegram communication """
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "telegram"
|
||||
|
||||
def __init__(self, freqtrade) -> None:
|
||||
"""
|
||||
Init the Telegram call, and init the super class RPC
|
||||
@@ -70,6 +67,8 @@ class Telegram(RPC):
|
||||
self._updater: Updater = None
|
||||
self._config = freqtrade.config
|
||||
self._init()
|
||||
if self._config.get('fiat_display_currency', None):
|
||||
self._fiat_converter = CryptoToFiatConverter()
|
||||
|
||||
def _init(self) -> None:
|
||||
"""
|
||||
@@ -114,9 +113,57 @@ class Telegram(RPC):
|
||||
"""
|
||||
self._updater.stop()
|
||||
|
||||
def send_msg(self, msg: str) -> None:
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
""" Send a message to telegram channel """
|
||||
self._send_msg(msg)
|
||||
|
||||
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
|
||||
if self._fiat_converter:
|
||||
msg['stake_amount_fiat'] = self._fiat_converter.convert_amount(
|
||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
else:
|
||||
msg['stake_amount_fiat'] = 0
|
||||
|
||||
message = "*{exchange}:* Buying [{pair}]({market_url})\n" \
|
||||
"with limit `{limit:.8f}\n" \
|
||||
"({stake_amount:.6f} {stake_currency}".format(**msg)
|
||||
|
||||
if msg.get('fiat_currency', None):
|
||||
message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
|
||||
message += ")`"
|
||||
|
||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||
msg['amount'] = round(msg['amount'], 8)
|
||||
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
||||
|
||||
message = "*{exchange}:* Selling [{pair}]({market_url})\n" \
|
||||
"*Limit:* `{limit:.8f}`\n" \
|
||||
"*Amount:* `{amount:.8f}`\n" \
|
||||
"*Open Rate:* `{open_rate:.8f}`\n" \
|
||||
"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||
"*Profit:* `{profit_percent:.2f}%`".format(**msg)
|
||||
|
||||
# Check if all sell properties are available.
|
||||
# This might not be the case if the message origin is triggered by /forcesell
|
||||
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
||||
and self._fiat_converter):
|
||||
msg['profit_fiat'] = self._fiat_converter.convert_amount(
|
||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
message += '` ({gain}: {profit_amount:.8f} {stake_currency}`' \
|
||||
'` / {profit_fiat:.3f} {fiat_currency})`'.format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||
message = '*Status:* `{status}`'.format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
|
||||
message = '*Warning:* `{status}`'.format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
|
||||
message = '{status}'.format(**msg)
|
||||
|
||||
else:
|
||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||
|
||||
self._send_msg(message)
|
||||
|
||||
@authorized_only
|
||||
def _status(self, bot: Bot, update: Update) -> None:
|
||||
@@ -136,8 +183,26 @@ class Telegram(RPC):
|
||||
return
|
||||
|
||||
try:
|
||||
for trade_msg in self._rpc_trade_status():
|
||||
self._send_msg(trade_msg, bot=bot)
|
||||
results = self._rpc_trade_status()
|
||||
# pre format data
|
||||
for result in results:
|
||||
result['date'] = result['date'].humanize()
|
||||
|
||||
messages = [
|
||||
"*Trade ID:* `{trade_id}`\n"
|
||||
"*Current Pair:* [{pair}]({market_url})\n"
|
||||
"*Open Since:* `{date}`\n"
|
||||
"*Amount:* `{amount}`\n"
|
||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||
"*Close Rate:* `{close_rate}`\n"
|
||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||
"*Close Profit:* `{close_profit}`\n"
|
||||
"*Current Profit:* `{current_profit:.2f}%`\n"
|
||||
"*Open Order:* `{open_order}`".format(**result)
|
||||
for result in results
|
||||
]
|
||||
for msg in messages:
|
||||
self._send_msg(msg, bot=bot)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
|
||||
@@ -153,7 +218,7 @@ class Telegram(RPC):
|
||||
try:
|
||||
df_statuses = self._rpc_status_table()
|
||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||
self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML)
|
||||
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
|
||||
@@ -166,6 +231,8 @@ class Telegram(RPC):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(update.message.text.replace('/daily', '').strip())
|
||||
except (TypeError, ValueError):
|
||||
@@ -173,18 +240,17 @@ class Telegram(RPC):
|
||||
try:
|
||||
stats = self._rpc_daily_profit(
|
||||
timescale,
|
||||
self._config['stake_currency'],
|
||||
self._config['fiat_display_currency']
|
||||
stake_cur,
|
||||
fiat_disp_cur
|
||||
)
|
||||
stats = tabulate(stats,
|
||||
headers=[
|
||||
'Day',
|
||||
'Profit {}'.format(self._config['stake_currency']),
|
||||
'Profit {}'.format(self._config['fiat_display_currency'])
|
||||
f'Profit {stake_cur}',
|
||||
f'Profit {fiat_disp_cur}'
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
|
||||
.format(timescale, stats)
|
||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>'
|
||||
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
@@ -198,39 +264,38 @@ class Telegram(RPC):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
|
||||
try:
|
||||
stats = self._rpc_trade_statistics(
|
||||
self._config['stake_currency'],
|
||||
self._config['fiat_display_currency'])
|
||||
|
||||
stake_cur,
|
||||
fiat_disp_cur)
|
||||
profit_closed_coin = stats['profit_closed_coin']
|
||||
profit_closed_percent = stats['profit_closed_percent']
|
||||
profit_closed_fiat = stats['profit_closed_fiat']
|
||||
profit_all_coin = stats['profit_all_coin']
|
||||
profit_all_percent = stats['profit_all_percent']
|
||||
profit_all_fiat = stats['profit_all_fiat']
|
||||
trade_count = stats['trade_count']
|
||||
first_trade_date = stats['first_trade_date']
|
||||
latest_trade_date = stats['latest_trade_date']
|
||||
avg_duration = stats['avg_duration']
|
||||
best_pair = stats['best_pair']
|
||||
best_rate = stats['best_rate']
|
||||
# Message to display
|
||||
markdown_msg = "*ROI:* Close trades\n" \
|
||||
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
|
||||
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
|
||||
"*ROI:* All trades\n" \
|
||||
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
|
||||
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
|
||||
"*Total Trade Count:* `{trade_count}`\n" \
|
||||
"*First Trade opened:* `{first_trade_date}`\n" \
|
||||
"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
||||
"*Avg. Duration:* `{avg_duration}`\n" \
|
||||
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
|
||||
.format(
|
||||
coin=self._config['stake_currency'],
|
||||
fiat=self._config['fiat_display_currency'],
|
||||
profit_closed_coin=stats['profit_closed_coin'],
|
||||
profit_closed_percent=stats['profit_closed_percent'],
|
||||
profit_closed_fiat=stats['profit_closed_fiat'],
|
||||
profit_all_coin=stats['profit_all_coin'],
|
||||
profit_all_percent=stats['profit_all_percent'],
|
||||
profit_all_fiat=stats['profit_all_fiat'],
|
||||
trade_count=stats['trade_count'],
|
||||
first_trade_date=stats['first_trade_date'],
|
||||
latest_trade_date=stats['latest_trade_date'],
|
||||
avg_duration=stats['avg_duration'],
|
||||
best_pair=stats['best_pair'],
|
||||
best_rate=stats['best_rate']
|
||||
)
|
||||
f"∙ `{profit_closed_coin:.8f} {stake_cur} "\
|
||||
f"({profit_closed_percent:.2f}%)`\n" \
|
||||
f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \
|
||||
f"*ROI:* All trades\n" \
|
||||
f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \
|
||||
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \
|
||||
f"*Total Trade Count:* `{trade_count}`\n" \
|
||||
f"*First Trade opened:* `{first_trade_date}`\n" \
|
||||
f"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
||||
f"*Avg. Duration:* `{avg_duration}`\n" \
|
||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
|
||||
self._send_msg(markdown_msg, bot=bot)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
@@ -239,10 +304,9 @@ class Telegram(RPC):
|
||||
def _balance(self, bot: Bot, update: Update) -> None:
|
||||
""" Handler for /balance """
|
||||
try:
|
||||
currencys, total, symbol, value = \
|
||||
self._rpc_balance(self._config['fiat_display_currency'])
|
||||
result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
|
||||
output = ''
|
||||
for currency in currencys:
|
||||
for currency in result['currencies']:
|
||||
output += "*{currency}:*\n" \
|
||||
"\t`Available: {available: .8f}`\n" \
|
||||
"\t`Balance: {balance: .8f}`\n" \
|
||||
@@ -250,8 +314,8 @@ class Telegram(RPC):
|
||||
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
||||
|
||||
output += "\n*Estimated Value*:\n" \
|
||||
"\t`BTC: {0: .8f}`\n" \
|
||||
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
|
||||
"\t`BTC: {total: .8f}`\n" \
|
||||
"\t`{symbol}: {value: .2f}`\n".format(**result)
|
||||
self._send_msg(output, bot=bot)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
@@ -266,7 +330,7 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_start()
|
||||
self._send_msg(msg, bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
|
||||
@authorized_only
|
||||
def _stop(self, bot: Bot, update: Update) -> None:
|
||||
@@ -278,7 +342,7 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_stop()
|
||||
self._send_msg(msg, bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
|
||||
@authorized_only
|
||||
def _reload_conf(self, bot: Bot, update: Update) -> None:
|
||||
@@ -290,7 +354,7 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
msg = self._rpc_reload_conf()
|
||||
self._send_msg(msg, bot=bot)
|
||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||
|
||||
@authorized_only
|
||||
def _forcesell(self, bot: Bot, update: Update) -> None:
|
||||
|
||||
66
freqtrade/rpc/webhook.py
Normal file
66
freqtrade/rpc/webhook.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
This module manages webhook communication
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from requests import post, RequestException
|
||||
|
||||
from freqtrade.rpc import RPC, RPCMessageType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.debug('Included module rpc.webhook ...')
|
||||
|
||||
|
||||
class Webhook(RPC):
|
||||
""" This class handles all webhook communication """
|
||||
|
||||
def __init__(self, freqtrade) -> None:
|
||||
"""
|
||||
Init the Webhook class, and init the super class RPC
|
||||
:param freqtrade: Instance of a freqtrade bot
|
||||
:return: None
|
||||
"""
|
||||
super().__init__(freqtrade)
|
||||
|
||||
self._config = freqtrade.config
|
||||
self._url = self._config['webhook']['url']
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Cleanup pending module resources.
|
||||
This will do nothing for webhooks, they will simply not be called anymore
|
||||
"""
|
||||
pass
|
||||
|
||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||
""" Send a message to telegram channel """
|
||||
try:
|
||||
|
||||
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhookbuy', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhookstatus', None)
|
||||
else:
|
||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||
if not valuedict:
|
||||
logger.info("Message type %s not configured for webhooks", msg['type'])
|
||||
return
|
||||
|
||||
payload = {key: value.format(**msg) for (key, value) in valuedict.items()}
|
||||
self._send_msg(payload)
|
||||
except KeyError as exc:
|
||||
logger.exception("Problem calling Webhook. Please check your webhook configuration. "
|
||||
"Exception: %s", exc)
|
||||
|
||||
def _send_msg(self, payload: dict) -> None:
|
||||
"""do the actual call to the webhook"""
|
||||
|
||||
try:
|
||||
post(self._url, data=payload)
|
||||
except RequestException as exc:
|
||||
logger.warning("Could not call webhook url. Exception: %s", exc)
|
||||
@@ -1,19 +1,31 @@
|
||||
import logging
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
# Import Default-Strategy to have hyperopt correctly resolve
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_strategy(strategy: IStrategy) -> IStrategy:
|
||||
def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
|
||||
"""
|
||||
Imports given Strategy instance to global scope
|
||||
of freqtrade.strategy and returns an instance of it
|
||||
"""
|
||||
|
||||
# Copy all attributes from base class and class
|
||||
attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__})
|
||||
|
||||
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
|
||||
|
||||
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
|
||||
# `TypeError: can't pickle _abc_data objects``
|
||||
# This will only apply to python 3.7
|
||||
if sys.version_info.major == 3 and sys.version_info.minor == 7 and '_abc_impl' in comb:
|
||||
del comb['_abc_impl']
|
||||
|
||||
attr = deepcopy(comb)
|
||||
# Adjust module name
|
||||
attr['__module__'] = 'freqtrade.strategy'
|
||||
|
||||
@@ -29,4 +41,4 @@ def import_strategy(strategy: IStrategy) -> IStrategy:
|
||||
# Modify global scope to declare class
|
||||
globals()[name] = clazz
|
||||
|
||||
return clazz()
|
||||
return clazz(config)
|
||||
|
||||
@@ -28,13 +28,16 @@ class DefaultStrategy(IStrategy):
|
||||
# Optimal ticker interval for the strategy
|
||||
ticker_interval = '5m'
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
@@ -196,10 +199,11 @@ class DefaultStrategy(IStrategy):
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
@@ -217,10 +221,11 @@ class DefaultStrategy(IStrategy):
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
|
||||
@@ -2,11 +2,50 @@
|
||||
IStrategy interface
|
||||
This module defines the interface to apply for strategies
|
||||
"""
|
||||
from typing import Dict
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
||||
import warnings
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SignalType(Enum):
|
||||
"""
|
||||
Enum to distinguish between buy and sell signals
|
||||
"""
|
||||
BUY = "buy"
|
||||
SELL = "sell"
|
||||
|
||||
|
||||
class SellType(Enum):
|
||||
"""
|
||||
Enum to distinguish between sell reasons
|
||||
"""
|
||||
ROI = "roi"
|
||||
STOP_LOSS = "stop_loss"
|
||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||
SELL_SIGNAL = "sell_signal"
|
||||
FORCE_SELL = "force_sell"
|
||||
NONE = ""
|
||||
|
||||
|
||||
class SellCheckTuple(NamedTuple):
|
||||
"""
|
||||
NamedTuple for Sell type + reason
|
||||
"""
|
||||
sell_flag: bool
|
||||
sell_type: SellType
|
||||
|
||||
|
||||
class IStrategy(ABC):
|
||||
"""
|
||||
@@ -19,30 +58,296 @@ class IStrategy(ABC):
|
||||
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||
"""
|
||||
|
||||
_populate_fun_len: int = 0
|
||||
_buy_fun_len: int = 0
|
||||
_sell_fun_len: int = 0
|
||||
# associated minimal roi
|
||||
minimal_roi: Dict
|
||||
|
||||
# associated stoploss
|
||||
stoploss: float
|
||||
|
||||
# associated ticker interval
|
||||
ticker_interval: str
|
||||
|
||||
# run "populate_indicators" only for new candle
|
||||
process_only_new_candles: bool = False
|
||||
|
||||
# Dict to determine if analysis is necessary
|
||||
_last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
self.config = config
|
||||
self._last_candle_seen_per_pair = {}
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with sell column
|
||||
"""
|
||||
|
||||
def get_strategy_name(self) -> str:
|
||||
"""
|
||||
Returns strategy class name
|
||||
"""
|
||||
return self.__class__.__name__
|
||||
|
||||
def analyze_ticker(self, ticker_history: List[Dict], metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
|
||||
dataframe = parse_ticker_dataframe(ticker_history)
|
||||
|
||||
pair = str(metadata.get('pair'))
|
||||
|
||||
# Test if seen this pair and last candle before.
|
||||
# always run if process_only_new_candles is set to true
|
||||
if (not self.process_only_new_candles or
|
||||
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
||||
# Defs that only make change on new candle data.
|
||||
logging.debug("TA Analysis Launched")
|
||||
dataframe = self.advise_indicators(dataframe, metadata)
|
||||
dataframe = self.advise_buy(dataframe, metadata)
|
||||
dataframe = self.advise_sell(dataframe, metadata)
|
||||
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
||||
else:
|
||||
logging.debug("Skippinig TA Analysis for already analyzed candle")
|
||||
dataframe['buy'] = 0
|
||||
dataframe['sell'] = 0
|
||||
|
||||
# Other Defs in strategy that want to be called every loop here
|
||||
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
||||
logging.debug("Loop Analysis Launched")
|
||||
|
||||
return dataframe
|
||||
|
||||
def get_signal(self, pair: str, interval: str,
|
||||
ticker_hist: Optional[List[Dict]]) -> Tuple[bool, bool]:
|
||||
"""
|
||||
Calculates current signal based several technical analysis indicators
|
||||
:param pair: pair in format ANT/BTC
|
||||
:param interval: Interval to use (in min)
|
||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||
"""
|
||||
if not ticker_hist:
|
||||
logger.warning('Empty ticker history for pair %s', pair)
|
||||
return False, False
|
||||
|
||||
try:
|
||||
dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
|
||||
except ValueError as error:
|
||||
logger.warning(
|
||||
'Unable to analyze ticker for pair %s: %s',
|
||||
pair,
|
||||
str(error)
|
||||
)
|
||||
return False, False
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
'Unexpected error when analyzing ticker for pair %s: %s',
|
||||
pair,
|
||||
str(error)
|
||||
)
|
||||
return False, False
|
||||
|
||||
if dataframe.empty:
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return False, False
|
||||
|
||||
latest = dataframe.iloc[-1]
|
||||
|
||||
# Check if dataframe is out of date
|
||||
signal_date = arrow.get(latest['date'])
|
||||
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||
offset = self.config.get('exchange', {}).get('outdated_offset', 5)
|
||||
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
|
||||
logger.warning(
|
||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||
pair,
|
||||
(arrow.utcnow() - signal_date).seconds // 60
|
||||
)
|
||||
return False, False
|
||||
|
||||
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
||||
logger.debug(
|
||||
'trigger: %s (pair=%s) buy=%s sell=%s',
|
||||
latest['date'],
|
||||
pair,
|
||||
str(buy),
|
||||
str(sell)
|
||||
)
|
||||
return buy, sell
|
||||
|
||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
||||
sell: bool) -> SellCheckTuple:
|
||||
"""
|
||||
This function evaluate if on the condition required to trigger a sell has been reached
|
||||
if the threshold is reached and updates the trade record.
|
||||
:return: True if trade should be sold, False otherwise
|
||||
"""
|
||||
current_profit = trade.calc_profit_percent(rate)
|
||||
stoplossflag = self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date,
|
||||
current_profit=current_profit)
|
||||
if stoplossflag.sell_flag:
|
||||
return stoplossflag
|
||||
|
||||
experimental = self.config.get('experimental', {})
|
||||
|
||||
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
||||
logger.debug('Buy signal still active - not selling.')
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
|
||||
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
||||
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
|
||||
logger.debug('Required profit reached. Selling..')
|
||||
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
|
||||
|
||||
if experimental.get('sell_profit_only', False):
|
||||
logger.debug('Checking if trade is profitable..')
|
||||
if trade.calc_profit(rate=rate) <= 0:
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
if sell and not buy and experimental.get('use_sell_signal', False):
|
||||
logger.debug('Sell signal received. Selling..')
|
||||
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
|
||||
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
|
||||
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime,
|
||||
current_profit: float) -> SellCheckTuple:
|
||||
"""
|
||||
Based on current profit of the trade and configured (trailing) stoploss,
|
||||
decides to sell or not
|
||||
:param current_profit: current profit in percent
|
||||
"""
|
||||
|
||||
trailing_stop = self.config.get('trailing_stop', False)
|
||||
|
||||
trade.adjust_stop_loss(trade.open_rate, self.stoploss, initial=True)
|
||||
|
||||
# evaluate if the stoploss was hit
|
||||
if self.stoploss is not None and trade.stop_loss >= current_rate:
|
||||
selltype = SellType.STOP_LOSS
|
||||
if trailing_stop:
|
||||
selltype = SellType.TRAILING_STOP_LOSS
|
||||
logger.debug(
|
||||
f"HIT STOP: current price at {current_rate:.6f}, "
|
||||
f"stop loss is {trade.stop_loss:.6f}, "
|
||||
f"initial stop loss was at {trade.initial_stop_loss:.6f}, "
|
||||
f"trade opened at {trade.open_rate:.6f}")
|
||||
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")
|
||||
|
||||
logger.debug('Stop loss hit.')
|
||||
return SellCheckTuple(sell_flag=True, sell_type=selltype)
|
||||
|
||||
# update the stop loss afterwards, after all by definition it's supposed to be hanging
|
||||
if trailing_stop:
|
||||
|
||||
# check if we have a special stop loss for positive condition
|
||||
# and if profit is positive
|
||||
stop_loss_value = self.stoploss
|
||||
sl_offset = self.config.get('trailing_stop_positive_offset', 0.0)
|
||||
|
||||
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
|
||||
|
||||
# Ignore mypy error check in configuration that this is a float
|
||||
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
|
||||
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
|
||||
f"with offset {sl_offset:.4g} "
|
||||
f"since we have profit {current_profit:.4f}%")
|
||||
|
||||
trade.adjust_stop_loss(current_rate, stop_loss_value)
|
||||
|
||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
|
||||
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||
"""
|
||||
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
||||
sell
|
||||
:return True if bot should sell at current rate
|
||||
"""
|
||||
|
||||
# Check if time matches and current rate is above threshold
|
||||
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||
for duration, threshold in self.minimal_roi.items():
|
||||
if time_diff <= duration:
|
||||
return False
|
||||
if current_profit > threshold:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
||||
"""
|
||||
Creates a dataframe and populates indicators for given ticker data
|
||||
"""
|
||||
return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair})
|
||||
for pair, pair_data in tickerdata.items()}
|
||||
|
||||
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Populate indicators that will be used in the Buy and Sell strategy
|
||||
This method should not be overridden.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
if self._populate_fun_len == 2:
|
||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||
"the current function headers!", DeprecationWarning)
|
||||
return self.populate_indicators(dataframe) # type: ignore
|
||||
else:
|
||||
return self.populate_indicators(dataframe, metadata)
|
||||
|
||||
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
This method should not be overridden.
|
||||
:param dataframe: DataFrame
|
||||
:param pair: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
if self._buy_fun_len == 2:
|
||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||
"the current function headers!", DeprecationWarning)
|
||||
return self.populate_buy_trend(dataframe) # type: ignore
|
||||
else:
|
||||
return self.populate_buy_trend(dataframe, metadata)
|
||||
|
||||
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
This method should not be overridden.
|
||||
:param dataframe: DataFrame
|
||||
:param pair: Additional information, like the currently traded pair
|
||||
:return: DataFrame with sell column
|
||||
"""
|
||||
if self._sell_fun_len == 2:
|
||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||
"the current function headers!", DeprecationWarning)
|
||||
return self.populate_sell_trend(dataframe) # type: ignore
|
||||
else:
|
||||
return self.populate_sell_trend(dataframe, metadata)
|
||||
|
||||
@@ -7,14 +7,16 @@ import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from base64 import urlsafe_b64decode
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, Dict, Type
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Type
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.strategy import import_strategy
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -35,26 +37,43 @@ class StrategyResolver(object):
|
||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
||||
self.strategy: IStrategy = self._load_strategy(strategy_name,
|
||||
config=config,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
|
||||
# Set attributes
|
||||
# Check if we need to override configuration
|
||||
if 'minimal_roi' in config:
|
||||
self.strategy.minimal_roi = config['minimal_roi']
|
||||
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
||||
logger.info("Override strategy 'minimal_roi' with value in config file: %s.",
|
||||
config['minimal_roi'])
|
||||
else:
|
||||
config['minimal_roi'] = self.strategy.minimal_roi
|
||||
|
||||
if 'stoploss' in config:
|
||||
self.strategy.stoploss = config['stoploss']
|
||||
logger.info(
|
||||
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
||||
"Override strategy 'stoploss' with value in config file: %s.", config['stoploss']
|
||||
)
|
||||
else:
|
||||
config['stoploss'] = self.strategy.stoploss
|
||||
|
||||
if 'ticker_interval' in config:
|
||||
self.strategy.ticker_interval = config['ticker_interval']
|
||||
logger.info(
|
||||
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
||||
"Override strategy 'ticker_interval' with value in config file: %s.",
|
||||
config['ticker_interval']
|
||||
)
|
||||
else:
|
||||
config['ticker_interval'] = self.strategy.ticker_interval
|
||||
|
||||
if 'process_only_new_candles' in config:
|
||||
self.strategy.process_only_new_candles = config['process_only_new_candles']
|
||||
logger.info(
|
||||
"Override process_only_new_candles 'process_only_new_candles' "
|
||||
"with value in config file: %s.", config['process_only_new_candles']
|
||||
)
|
||||
else:
|
||||
config['process_only_new_candles'] = self.strategy.process_only_new_candles
|
||||
|
||||
# Sort and apply type conversions
|
||||
self.strategy.minimal_roi = OrderedDict(sorted(
|
||||
@@ -63,10 +82,11 @@ class StrategyResolver(object):
|
||||
self.strategy.stoploss = float(self.strategy.stoploss)
|
||||
|
||||
def _load_strategy(
|
||||
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
|
||||
self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy:
|
||||
"""
|
||||
Search and loads the specified strategy.
|
||||
:param strategy_name: name of the module to import
|
||||
:param config: configuration for the strategy
|
||||
:param extra_dir: additional directory to search for the given strategy
|
||||
:return: Strategy instance or None
|
||||
"""
|
||||
@@ -80,12 +100,35 @@ class StrategyResolver(object):
|
||||
# Add extra strategy directory on top of search paths
|
||||
abs_paths.insert(0, extra_dir)
|
||||
|
||||
if ":" in strategy_name:
|
||||
logger.info("loading base64 endocded strategy")
|
||||
strat = strategy_name.split(":")
|
||||
|
||||
if len(strat) == 2:
|
||||
temp = Path(tempfile.mkdtemp("freq", "strategy"))
|
||||
name = strat[0] + ".py"
|
||||
|
||||
temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
|
||||
temp.joinpath("__init__.py").touch()
|
||||
|
||||
strategy_name = os.path.splitext(name)[0]
|
||||
|
||||
# register temp path with the bot
|
||||
abs_paths.insert(0, str(temp.resolve()))
|
||||
|
||||
for path in abs_paths:
|
||||
try:
|
||||
strategy = self._search_strategy(path, strategy_name)
|
||||
strategy = self._search_strategy(path, strategy_name=strategy_name, config=config)
|
||||
if strategy:
|
||||
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
||||
return import_strategy(strategy)
|
||||
strategy._populate_fun_len = len(
|
||||
inspect.getfullargspec(strategy.populate_indicators).args)
|
||||
strategy._buy_fun_len = len(
|
||||
inspect.getfullargspec(strategy.populate_buy_trend).args)
|
||||
strategy._sell_fun_len = len(
|
||||
inspect.getfullargspec(strategy.populate_sell_trend).args)
|
||||
|
||||
return import_strategy(strategy, config=config)
|
||||
except FileNotFoundError:
|
||||
logger.warning('Path "%s" does not exist', path)
|
||||
|
||||
@@ -115,7 +158,7 @@ class StrategyResolver(object):
|
||||
return next(valid_strategies_gen, None)
|
||||
|
||||
@staticmethod
|
||||
def _search_strategy(directory: str, strategy_name: str) -> Optional[IStrategy]:
|
||||
def _search_strategy(directory: str, strategy_name: str, config: dict) -> Optional[IStrategy]:
|
||||
"""
|
||||
Search for the strategy_name in the given directory
|
||||
:param directory: relative or absolute directory path
|
||||
@@ -131,5 +174,5 @@ class StrategyResolver(object):
|
||||
os.path.abspath(os.path.join(directory, entry)), strategy_name
|
||||
)
|
||||
if strategy:
|
||||
return strategy()
|
||||
return strategy(config)
|
||||
return None
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
from functools import reduce
|
||||
from unittest.mock import MagicMock
|
||||
from typing import Dict, Optional
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
from telegram import Chat, Message, Update
|
||||
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade import constants
|
||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
|
||||
@@ -20,7 +18,7 @@ logging.getLogger('').setLevel(logging.INFO)
|
||||
|
||||
|
||||
def log_has(line, logs):
|
||||
# caplog mocker returns log as a tuple: ('freqtrade.analyze', logging.WARNING, 'foobar')
|
||||
# caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar')
|
||||
# and we want to match line against foobar in the tuple
|
||||
return reduce(lambda a, b: a or b,
|
||||
filter(lambda x: x[2] == line, logs),
|
||||
@@ -28,7 +26,10 @@ def log_has(line, logs):
|
||||
|
||||
|
||||
def patch_exchange(mocker, api_mock=None) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value="Bittrex"))
|
||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value="bittrex"))
|
||||
if api_mock:
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
else:
|
||||
@@ -51,13 +52,11 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||
"""
|
||||
# mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
|
||||
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
|
||||
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||
patch_exchange(mocker, None)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
||||
|
||||
return FreqtradeBot(config)
|
||||
|
||||
@@ -100,9 +99,23 @@ def default_conf():
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.10,
|
||||
"unfilledtimeout": 600,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
"ask_last_balance": 0.0,
|
||||
"use_order_book": False,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
"enabled": False,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"ask_strategy": {
|
||||
"use_order_book": False,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
@@ -125,7 +138,6 @@ def default_conf():
|
||||
"db_url": "sqlite://",
|
||||
"loglevel": logging.DEBUG,
|
||||
}
|
||||
validate(configuration, constants.CONF_SCHEMA)
|
||||
return configuration
|
||||
|
||||
|
||||
@@ -404,6 +416,39 @@ def limit_sell_order():
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def order_book_l2():
|
||||
return MagicMock(return_value={
|
||||
'bids': [
|
||||
[0.043936, 10.442],
|
||||
[0.043935, 31.865],
|
||||
[0.043933, 11.212],
|
||||
[0.043928, 0.088],
|
||||
[0.043925, 10.0],
|
||||
[0.043921, 10.0],
|
||||
[0.04392, 37.64],
|
||||
[0.043899, 0.066],
|
||||
[0.043885, 0.676],
|
||||
[0.04387, 22.758]
|
||||
],
|
||||
'asks': [
|
||||
[0.043949, 0.346],
|
||||
[0.04395, 0.608],
|
||||
[0.043951, 3.948],
|
||||
[0.043954, 0.288],
|
||||
[0.043958, 9.277],
|
||||
[0.043995, 1.566],
|
||||
[0.044, 0.588],
|
||||
[0.044002, 0.992],
|
||||
[0.044003, 0.095],
|
||||
[0.04402, 37.64]
|
||||
],
|
||||
'timestamp': None,
|
||||
'datetime': None,
|
||||
'nonce': 288004540
|
||||
})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ticker_history():
|
||||
return [
|
||||
@@ -613,7 +658,7 @@ def tickers():
|
||||
@pytest.fixture
|
||||
def result():
|
||||
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
||||
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||
return parse_ticker_dataframe(json.load(data_file))
|
||||
|
||||
# FIX:
|
||||
# Create an fixture/function
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||
# pragma pylint: disable=protected-access
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from random import randint
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
from random import randint
|
||||
from unittest.mock import Mock, MagicMock, PropertyMock
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade import OperationalException, DependencyException, TemporaryError
|
||||
from freqtrade.exchange import Exchange, API_RETRY_COUNT
|
||||
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
||||
from freqtrade import DependencyException, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import API_RETRY_COUNT, Exchange
|
||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||
|
||||
|
||||
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
||||
def get_mock_coro(return_value):
|
||||
async def mock_coro(*args, **kwargs):
|
||||
return return_value
|
||||
|
||||
return Mock(wraps=mock_coro)
|
||||
|
||||
|
||||
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
getattr(exchange, fun)(**kwargs)
|
||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
getattr(exchange, fun)(**kwargs)
|
||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
||||
|
||||
|
||||
async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
await getattr(exchange, fun)(**kwargs)
|
||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
await getattr(exchange, fun)(**kwargs)
|
||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
||||
|
||||
|
||||
def test_init(default_conf, mocker, caplog):
|
||||
@@ -20,7 +56,13 @@ def test_init(default_conf, mocker, caplog):
|
||||
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_init_exception(default_conf):
|
||||
def test_destroy(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
get_patched_exchange(mocker, default_conf)
|
||||
assert log_has('Exchange object destroyed, closing async loop', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_init_exception(default_conf, mocker):
|
||||
default_conf['exchange']['name'] = 'wrong_exchange_name'
|
||||
|
||||
with pytest.raises(
|
||||
@@ -28,6 +70,141 @@ def test_init_exception(default_conf):
|
||||
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
|
||||
Exchange(default_conf)
|
||||
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
|
||||
mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError))
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_symbol_amount_prec(default_conf, mocker):
|
||||
'''
|
||||
Test rounds down to 4 Decimal places
|
||||
'''
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value={
|
||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||
})
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance'))
|
||||
|
||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}})
|
||||
type(api_mock).markets = markets
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
exchange = Exchange(default_conf)
|
||||
|
||||
amount = 2.34559
|
||||
pair = 'ETH/BTC'
|
||||
amount = exchange.symbol_amount_prec(pair, amount)
|
||||
assert amount == 2.3455
|
||||
|
||||
|
||||
def test_symbol_price_prec(default_conf, mocker):
|
||||
'''
|
||||
Test rounds up to 4 decimal places
|
||||
'''
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value={
|
||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||
})
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance'))
|
||||
|
||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}})
|
||||
type(api_mock).markets = markets
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
exchange = Exchange(default_conf)
|
||||
|
||||
price = 2.34559
|
||||
pair = 'ETH/BTC'
|
||||
price = exchange.symbol_price_prec(pair, price)
|
||||
assert price == 2.3456
|
||||
|
||||
|
||||
def test_set_sandbox(default_conf, mocker):
|
||||
"""
|
||||
Test working scenario
|
||||
"""
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value={
|
||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||
})
|
||||
url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com",
|
||||
'api': 'https://api.gdax.com'})
|
||||
type(api_mock).urls = url_mock
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
|
||||
exchange = Exchange(default_conf)
|
||||
liveurl = exchange._api.urls['api']
|
||||
default_conf['exchange']['sandbox'] = True
|
||||
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
||||
assert exchange._api.urls['api'] != liveurl
|
||||
|
||||
|
||||
def test_set_sandbox_exception(default_conf, mocker):
|
||||
"""
|
||||
Test Fail scenario
|
||||
"""
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value={
|
||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||
})
|
||||
url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'})
|
||||
type(api_mock).urls = url_mock
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
|
||||
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
|
||||
exchange = Exchange(default_conf)
|
||||
default_conf['exchange']['sandbox'] = True
|
||||
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
||||
|
||||
|
||||
def test__load_async_markets(default_conf, mocker, caplog):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange._api_async.load_markets = get_mock_coro(None)
|
||||
exchange._load_async_markets()
|
||||
assert exchange._api_async.load_markets.call_count == 1
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
|
||||
exchange._load_async_markets()
|
||||
|
||||
assert log_has('Could not load async markets. Reason: deadbeef',
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test__load_markets(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
api_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
|
||||
|
||||
api_mock.load_markets = MagicMock(return_value={})
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
|
||||
expected_return = {'ETH/BTC': 'available'}
|
||||
api_mock.load_markets = MagicMock(return_value=expected_return)
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
default_conf['exchange']['pair_whitelist'] = ['ETH/BTC']
|
||||
ex = Exchange(default_conf)
|
||||
assert ex.markets == expected_return
|
||||
|
||||
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
Exchange(default_conf)
|
||||
assert log_has('Unable to initialize markets. Reason: ', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_validate_pairs(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
@@ -38,13 +215,17 @@ def test_validate_pairs(default_conf, mocker):
|
||||
type(api_mock).id = id_mock
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_validate_pairs_not_available(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
api_mock.load_markets = MagicMock(return_value={})
|
||||
api_mock.load_markets = MagicMock(return_value={'XRP/BTC': 'inactive'})
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
|
||||
with pytest.raises(OperationalException, match=r'not available'):
|
||||
Exchange(default_conf)
|
||||
@@ -55,12 +236,12 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
||||
api_mock.load_markets = MagicMock(return_value={
|
||||
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
|
||||
})
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_currency'] = 'ETH'
|
||||
default_conf['stake_currency'] = 'ETH'
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
with pytest.raises(OperationalException, match=r'not compatible'):
|
||||
Exchange(conf)
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_validate_pairs_exception(default_conf, mocker, caplog):
|
||||
@@ -70,31 +251,95 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
||||
|
||||
api_mock.load_markets = MagicMock(return_value={})
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
|
||||
Exchange(default_conf)
|
||||
|
||||
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
Exchange(default_conf)
|
||||
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ',
|
||||
assert log_has('Unable to validate pairs (assuming they are correct).',
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_currency'] = 'ETH'
|
||||
default_conf['stake_currency'] = 'ETH'
|
||||
api_mock = MagicMock()
|
||||
api_mock.name = MagicMock(return_value='binance')
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
|
||||
):
|
||||
Exchange(conf)
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_validate_timeframes(default_conf, mocker):
|
||||
default_conf["ticker_interval"] = "5m"
|
||||
api_mock = MagicMock()
|
||||
id_mock = PropertyMock(return_value='test_exchange')
|
||||
type(api_mock).id = id_mock
|
||||
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||
'5m': '5m',
|
||||
'15m': '15m',
|
||||
'1h': '1h'})
|
||||
type(api_mock).timeframes = timeframes
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_validate_timeframes_failed(default_conf, mocker):
|
||||
default_conf["ticker_interval"] = "3m"
|
||||
api_mock = MagicMock()
|
||||
id_mock = PropertyMock(return_value='test_exchange')
|
||||
type(api_mock).id = id_mock
|
||||
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||
'5m': '5m',
|
||||
'15m': '15m',
|
||||
'1h': '1h'})
|
||||
type(api_mock).timeframes = timeframes
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'):
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_validate_timeframes_not_in_config(default_conf, mocker):
|
||||
del default_conf["ticker_interval"]
|
||||
api_mock = MagicMock()
|
||||
id_mock = PropertyMock(return_value='test_exchange')
|
||||
type(api_mock).id = id_mock
|
||||
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||
'5m': '5m',
|
||||
'15m': '15m',
|
||||
'1h': '1h'})
|
||||
type(api_mock).timeframes = timeframes
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
Exchange(default_conf)
|
||||
|
||||
|
||||
def test_exchange_has(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
assert not exchange.exchange_has('ASDFASDF')
|
||||
api_mock = MagicMock()
|
||||
|
||||
type(api_mock).has = PropertyMock(return_value={'deadbeef': True})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
assert exchange.exchange_has("deadbeef")
|
||||
|
||||
type(api_mock).has = PropertyMock(return_value={'deadbeef': False})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
assert not exchange.exchange_has("deadbeef")
|
||||
|
||||
|
||||
def test_buy_dry_run(default_conf, mocker):
|
||||
@@ -216,6 +461,11 @@ def test_get_balance_prod(default_conf, mocker):
|
||||
|
||||
exchange.get_balance(currency='BTC')
|
||||
|
||||
with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={}))
|
||||
exchange.get_balance(currency='BTC')
|
||||
|
||||
|
||||
def test_get_balances_dry_run(default_conf, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
@@ -243,17 +493,8 @@ def test_get_balances_prod(default_conf, mocker):
|
||||
assert exchange.get_balances()['1ST']['total'] == 10.0
|
||||
assert exchange.get_balances()['1ST']['used'] == 0.0
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_balances()
|
||||
assert api_mock.fetch_balance.call_count == API_RETRY_COUNT + 1
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_balances()
|
||||
assert api_mock.fetch_balance.call_count == 1
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
"get_balances", "fetch_balance")
|
||||
|
||||
|
||||
def test_get_tickers(default_conf, mocker):
|
||||
@@ -282,15 +523,8 @@ def test_get_tickers(default_conf, mocker):
|
||||
assert tickers['BCH/BTC']['bid'] == 0.6
|
||||
assert tickers['BCH/BTC']['ask'] == 0.5
|
||||
|
||||
with pytest.raises(TemporaryError): # test retrier
|
||||
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_tickers()
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_tickers()
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
"get_tickers", "fetch_tickers")
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
|
||||
@@ -345,21 +579,198 @@ def test_get_ticker(default_conf, mocker):
|
||||
exchange.get_ticker(pair='ETH/BTC', refresh=False)
|
||||
assert api_mock.fetch_ticker.call_count == 0
|
||||
|
||||
with pytest.raises(TemporaryError): # test retrier
|
||||
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
"get_ticker", "fetch_ticker",
|
||||
pair='ETH/BTC', refresh=True)
|
||||
|
||||
api_mock.fetch_ticker = MagicMock(return_value={})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||
|
||||
|
||||
def test_get_history(default_conf, mocker, caplog):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
tick = [
|
||||
[
|
||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
4, # close
|
||||
5, # volume (in quote currency)
|
||||
]
|
||||
]
|
||||
pair = 'ETH/BTC'
|
||||
|
||||
async def mock_candle_hist(pair, tick_interval, since_ms):
|
||||
return pair, tick
|
||||
|
||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||
# one_call calculation * 1.8 should do 2 calls
|
||||
since = 5 * 60 * 500 * 1.8
|
||||
print(f"since = {since}")
|
||||
ret = exchange.get_history(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
||||
|
||||
assert exchange._async_get_candle_history.call_count == 2
|
||||
# Returns twice the above tick
|
||||
assert len(ret) == 2
|
||||
|
||||
|
||||
def test_refresh_tickers(mocker, default_conf, caplog) -> None:
|
||||
tick = [
|
||||
[
|
||||
1511686200000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
4, # close
|
||||
5, # volume (in quote currency)
|
||||
]
|
||||
]
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||
|
||||
pairs = ['IOTA/ETH', 'XRP/ETH']
|
||||
# empty dicts
|
||||
assert not exchange.klines
|
||||
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m')
|
||||
|
||||
assert log_has(f'Refreshing klines for {len(pairs)} pairs', caplog.record_tuples)
|
||||
assert exchange.klines
|
||||
for pair in pairs:
|
||||
assert exchange.klines[pair]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test__async_get_candle_history(default_conf, mocker, caplog):
|
||||
tick = [
|
||||
[
|
||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
4, # close
|
||||
5, # volume (in quote currency)
|
||||
]
|
||||
]
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
# Monkey-patch async function
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||
|
||||
exchange = Exchange(default_conf)
|
||||
pair = 'ETH/BTC'
|
||||
res = await exchange._async_get_candle_history(pair, "5m")
|
||||
assert type(res) is tuple
|
||||
assert len(res) == 2
|
||||
assert res[0] == pair
|
||||
assert res[1] == tick
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||
assert not log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
|
||||
# test caching
|
||||
res = await exchange._async_get_candle_history(pair, "5m")
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||
assert log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
|
||||
|
||||
# exchange = Exchange(default_conf)
|
||||
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||
"_async_get_candle_history", "fetch_ohlcv",
|
||||
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
|
||||
|
||||
api_mock = MagicMock()
|
||||
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'):
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
await exchange._async_get_candle_history(pair, "5m",
|
||||
(arrow.utcnow().timestamp - 2000) * 1000)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
|
||||
""" Test empty exchange result """
|
||||
tick = []
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
# Monkey-patch async function
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro([])
|
||||
|
||||
exchange = Exchange(default_conf)
|
||||
pair = 'ETH/BTC'
|
||||
res = await exchange._async_get_candle_history(pair, "5m")
|
||||
assert type(res) is tuple
|
||||
assert len(res) == 2
|
||||
assert res[0] == pair
|
||||
assert res[1] == tick
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_get_candles_history(default_conf, mocker):
|
||||
tick = [
|
||||
[
|
||||
1511686200000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
4, # close
|
||||
5, # volume (in quote currency)
|
||||
]
|
||||
]
|
||||
|
||||
async def mock_get_candle_hist(pair, tick_interval, since_ms=None):
|
||||
return (pair, tick)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
# Monkey-patch async function
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||
|
||||
exchange._async_get_candle_history = Mock(wraps=mock_get_candle_hist)
|
||||
|
||||
pairs = ['ETH/BTC', 'XRP/BTC']
|
||||
res = await exchange.async_get_candles_history(pairs, "5m")
|
||||
assert type(res) is list
|
||||
assert len(res) == 2
|
||||
assert type(res[0]) is tuple
|
||||
assert res[0][0] == pairs[0]
|
||||
assert res[0][1] == tick
|
||||
assert res[1][0] == pairs[1]
|
||||
assert res[1][1] == tick
|
||||
assert exchange._async_get_candle_history.call_count == 2
|
||||
|
||||
|
||||
def test_get_order_book(default_conf, mocker, order_book_l2):
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
api_mock = MagicMock()
|
||||
|
||||
api_mock.fetch_l2_order_book = order_book_l2
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
order_book = exchange.get_order_book(pair='ETH/BTC', limit=10)
|
||||
assert 'bids' in order_book
|
||||
assert 'asks' in order_book
|
||||
assert len(order_book['bids']) == 10
|
||||
assert len(order_book['asks']) == 10
|
||||
|
||||
|
||||
def test_get_order_book_exception(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||
|
||||
|
||||
def make_fetch_ohlcv_mock(data):
|
||||
def fetch_ohlcv_mock(pair, timeframe, since):
|
||||
if since:
|
||||
@@ -369,7 +780,7 @@ def make_fetch_ohlcv_mock(data):
|
||||
return fetch_ohlcv_mock
|
||||
|
||||
|
||||
def test_get_ticker_history(default_conf, mocker):
|
||||
def test_get_candle_history(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
tick = [
|
||||
[
|
||||
@@ -386,7 +797,7 @@ def test_get_ticker_history(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
|
||||
# retrieve original ticker
|
||||
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
assert ticks[0][0] == 1511686200000
|
||||
assert ticks[0][1] == 1
|
||||
assert ticks[0][2] == 2
|
||||
@@ -408,7 +819,7 @@ def test_get_ticker_history(default_conf, mocker):
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
|
||||
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
assert ticks[0][0] == 1511686210000
|
||||
assert ticks[0][1] == 6
|
||||
assert ticks[0][2] == 7
|
||||
@@ -416,20 +827,17 @@ def test_get_ticker_history(default_conf, mocker):
|
||||
assert ticks[0][4] == 9
|
||||
assert ticks[0][5] == 10
|
||||
|
||||
with pytest.raises(TemporaryError): # test retrier
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError)
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
"get_candle_history", "fetch_ohlcv",
|
||||
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'):
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
# new symbol to get around cache
|
||||
exchange.get_ticker_history('ABCD/BTC', default_conf['ticker_interval'])
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
# new symbol to get around cache
|
||||
exchange.get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
|
||||
exchange.get_candle_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
|
||||
|
||||
|
||||
def test_get_ticker_history_sort(default_conf, mocker):
|
||||
def test_get_candle_history_sort(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
|
||||
# GDAX use-case (real data from GDAX)
|
||||
@@ -452,7 +860,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
|
||||
# Test the ticker history sort
|
||||
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
assert ticks[0][0] == 1527830400000
|
||||
assert ticks[0][1] == 0.07649
|
||||
assert ticks[0][2] == 0.07651
|
||||
@@ -485,7 +893,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
# Test the ticker history sort
|
||||
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||
assert ticks[0][0] == 1527827700000
|
||||
assert ticks[0][1] == 0.07659999
|
||||
assert ticks[0][2] == 0.0766
|
||||
@@ -515,24 +923,15 @@ def test_cancel_order(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
|
||||
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.cancel_order.call_count == 1
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
"cancel_order", "cancel_order",
|
||||
order_id='_', pair='TKN/BTC')
|
||||
|
||||
|
||||
def test_get_order(default_conf, mocker):
|
||||
@@ -550,28 +949,19 @@ def test_get_order(default_conf, mocker):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
assert exchange.get_order('X', 'TKN/BTC') == 456
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
||||
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.fetch_order.call_count == 1
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
'get_order', 'fetch_order',
|
||||
order_id='_', pair='TKN/BTC')
|
||||
|
||||
|
||||
def test_name(default_conf, mocker):
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
||||
side_effect=lambda s: True)
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
exchange = Exchange(default_conf)
|
||||
|
||||
@@ -579,16 +969,14 @@ def test_name(default_conf, mocker):
|
||||
|
||||
|
||||
def test_id(default_conf, mocker):
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
||||
side_effect=lambda s: True)
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
exchange = Exchange(default_conf)
|
||||
assert exchange.id == 'binance'
|
||||
|
||||
|
||||
def test_get_pair_detail_url(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
||||
side_effect=lambda s: True)
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
exchange = Exchange(default_conf)
|
||||
|
||||
@@ -651,19 +1039,12 @@ def test_get_trades_for_order(default_conf, mocker):
|
||||
assert len(orders) == 1
|
||||
assert orders[0]['price'] == 165
|
||||
|
||||
# test Exceptions
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
'get_trades_for_order', 'fetch_my_trades',
|
||||
order_id=order_id, pair='LTC/BTC', since=since)
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
||||
assert api_mock.fetch_my_trades.call_count == API_RETRY_COUNT + 1
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
|
||||
assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == []
|
||||
|
||||
|
||||
def test_get_markets(default_conf, mocker, markets):
|
||||
@@ -677,19 +1058,8 @@ def test_get_markets(default_conf, mocker, markets):
|
||||
assert ret[0]["id"] == "ethbtc"
|
||||
assert ret[0]["symbol"] == "ETH/BTC"
|
||||
|
||||
# test Exceptions
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_markets = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_markets()
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_markets = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_markets()
|
||||
assert api_mock.fetch_markets.call_count == API_RETRY_COUNT + 1
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
'get_markets', 'fetch_markets')
|
||||
|
||||
|
||||
def test_get_fee(default_conf, mocker):
|
||||
@@ -704,28 +1074,5 @@ def test_get_fee(default_conf, mocker):
|
||||
|
||||
assert exchange.get_fee() == 0.025
|
||||
|
||||
# test Exceptions
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock = MagicMock()
|
||||
api_mock.calculate_fee = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_fee()
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock = MagicMock()
|
||||
api_mock.calculate_fee = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange.get_fee()
|
||||
assert api_mock.calculate_fee.call_count == API_RETRY_COUNT + 1
|
||||
|
||||
|
||||
def test_get_amount_lots(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
api_mock.amount_to_lots = MagicMock(return_value=1.0)
|
||||
api_mock.markets = None
|
||||
marketmock = MagicMock()
|
||||
api_mock.load_markets = marketmock
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
|
||||
assert exchange.get_amount_lots('LTC/BTC', 1.54) == 1
|
||||
assert marketmock.call_count == 1
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
'get_fee', 'calculate_fee')
|
||||
|
||||
21
freqtrade/tests/exchange/test_exchange_helpers.py
Normal file
21
freqtrade/tests/exchange/test_exchange_helpers.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||
|
||||
|
||||
def test_dataframe_correct_length(result):
|
||||
dataframe = parse_ticker_dataframe(result)
|
||||
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
|
||||
|
||||
|
||||
def test_dataframe_correct_columns(result):
|
||||
assert result.columns.tolist() == \
|
||||
['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
|
||||
|
||||
def test_parse_ticker_dataframe(ticker_history):
|
||||
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
|
||||
# Test file with BV data
|
||||
dataframe = parse_ticker_dataframe(ticker_history)
|
||||
assert dataframe.columns.tolist() == columns
|
||||
@@ -3,20 +3,21 @@
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import pytest
|
||||
from copy import deepcopy
|
||||
from typing import List
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from arrow import Arrow
|
||||
|
||||
from freqtrade import optimize, constants, DependencyException
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade import DependencyException, constants, optimize
|
||||
from freqtrade.arguments import Arguments, TimeRange
|
||||
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
||||
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
||||
start)
|
||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
|
||||
|
||||
def get_args(args) -> List[str]:
|
||||
@@ -95,7 +96,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||
'stake_amount': config['stake_amount'],
|
||||
'processed': processed,
|
||||
'max_open_trades': 1,
|
||||
'realistic': True
|
||||
'position_stacking': False
|
||||
}
|
||||
)
|
||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||
@@ -109,7 +110,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
|
||||
return pairdata
|
||||
|
||||
|
||||
# use for mock freqtrade.exchange.get_ticker_history'
|
||||
# use for mock ccxt.fetch_ohlvc'
|
||||
def _load_pair_as_ticks(pair, tickfreq):
|
||||
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
||||
ticks = trim_dictlist(ticks, -201)
|
||||
@@ -126,7 +127,7 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
||||
'stake_amount': conf['stake_amount'],
|
||||
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True,
|
||||
'position_stacking': False,
|
||||
'record': record
|
||||
}
|
||||
|
||||
@@ -144,7 +145,7 @@ def _trend(signals, buy_value, sell_value):
|
||||
return signals
|
||||
|
||||
|
||||
def _trend_alternate(dataframe=None):
|
||||
def _trend_alternate(dataframe=None, metadata=None):
|
||||
signals = dataframe
|
||||
low = signals['low']
|
||||
n = len(low)
|
||||
@@ -162,9 +163,6 @@ def _trend_alternate(dataframe=None):
|
||||
|
||||
# Unit tests
|
||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
@@ -192,8 +190,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
assert 'live' not in config
|
||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'realistic_simulation' not in config
|
||||
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||
assert 'position_stacking' not in config
|
||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'refresh_pairs' not in config
|
||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||
@@ -203,9 +201,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
|
||||
|
||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
@@ -217,7 +212,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--live',
|
||||
'--realistic-simulation',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--refresh-pairs-cached',
|
||||
'--timerange', ':100',
|
||||
'--export', '/bar/foo',
|
||||
@@ -245,9 +241,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
assert 'live' in config
|
||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'realistic_simulation' in config
|
||||
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
||||
assert 'position_stacking' in config
|
||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'use_max_market_positions' in config
|
||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
||||
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
||||
|
||||
assert 'refresh_pairs' in config
|
||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||
@@ -270,15 +269,10 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
|
||||
|
||||
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = [
|
||||
@@ -292,9 +286,6 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog
|
||||
|
||||
|
||||
def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test start() function
|
||||
"""
|
||||
start_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
@@ -317,26 +308,19 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_backtesting_init(mocker, default_conf) -> None:
|
||||
"""
|
||||
Test Backtesting._init() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
backtesting = Backtesting(default_conf)
|
||||
assert backtesting.config == default_conf
|
||||
assert isinstance(backtesting.analyze, Analyze)
|
||||
assert backtesting.ticker_interval == '5m'
|
||||
assert callable(backtesting.tickerdata_to_dataframe)
|
||||
assert callable(backtesting.populate_buy_trend)
|
||||
assert callable(backtesting.populate_sell_trend)
|
||||
assert callable(backtesting.advise_buy)
|
||||
assert callable(backtesting.advise_sell)
|
||||
get_fee.assert_called()
|
||||
assert backtesting.fee == 0.5
|
||||
|
||||
|
||||
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test Backtesting.tickerdata_to_dataframe() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
@@ -346,16 +330,13 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
||||
assert len(data['UNITTEST/BTC']) == 99
|
||||
|
||||
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
||||
analyze = Analyze(default_conf)
|
||||
data2 = analyze.tickerdata_to_dataframe(tickerlist)
|
||||
# Load strategy to compare the result between Backtesting function and strategy are the same
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
data2 = strategy.tickerdata_to_dataframe(tickerlist)
|
||||
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
||||
|
||||
|
||||
def test_get_timeframe(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test Backtesting.get_timeframe() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
@@ -372,9 +353,6 @@ def test_get_timeframe(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_generate_text_table(default_conf, mocker):
|
||||
"""
|
||||
Test Backtesting.generate_text_table() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
@@ -390,29 +368,94 @@ def test_generate_text_table(default_conf, mocker):
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| pair | buy count | avg profit % | '
|
||||
'total profit BTC | avg duration | profit | loss |\n'
|
||||
'|:--------|------------:|---------------:|'
|
||||
'-------------------:|---------------:|---------:|-------:|\n'
|
||||
'| ETH/BTC | 2 | 15.00 | '
|
||||
'0.60000000 | 20.0 | 2 | 0 |\n'
|
||||
'| TOTAL | 2 | 15.00 | '
|
||||
'0.60000000 | 20.0 | 2 | 0 |'
|
||||
'| pair | buy count | avg profit % | cum profit % | '
|
||||
'total profit BTC | avg duration | profit | loss |\n'
|
||||
'|:--------|------------:|---------------:|---------------:|'
|
||||
'-------------------:|:---------------|---------:|-------:|\n'
|
||||
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
||||
'0.60000000 | 0:20:00 | 2 | 0 |\n'
|
||||
'| TOTAL | 2 | 15.00 | 30.00 | '
|
||||
'0.60000000 | 0:20:00 | 2 | 0 |'
|
||||
)
|
||||
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
||||
|
||||
|
||||
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test Backtesting.start() method
|
||||
"""
|
||||
def test_generate_text_table_sell_reason(default_conf, mocker):
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
results = pd.DataFrame(
|
||||
{
|
||||
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||
'profit_percent': [0.1, 0.2, 0.3],
|
||||
'profit_abs': [0.2, 0.4, 0.5],
|
||||
'trade_duration': [10, 30, 10],
|
||||
'profit': [2, 0, 0],
|
||||
'loss': [0, 0, 1],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||
}
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| Sell Reason | Count |\n'
|
||||
'|:--------------|--------:|\n'
|
||||
'| roi | 2 |\n'
|
||||
'| stop_loss | 1 |'
|
||||
)
|
||||
assert backtesting._generate_text_table_sell_reason(
|
||||
data={'ETH/BTC': {}}, results=results) == result_str
|
||||
|
||||
|
||||
def test_generate_text_table_strategyn(default_conf, mocker):
|
||||
"""
|
||||
Test Backtesting.generate_text_table_sell_reason() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
results = {}
|
||||
results['ETH/BTC'] = pd.DataFrame(
|
||||
{
|
||||
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||
'profit_percent': [0.1, 0.2, 0.3],
|
||||
'profit_abs': [0.2, 0.4, 0.5],
|
||||
'trade_duration': [10, 30, 10],
|
||||
'profit': [2, 0, 0],
|
||||
'loss': [0, 0, 1],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||
}
|
||||
)
|
||||
results['LTC/BTC'] = pd.DataFrame(
|
||||
{
|
||||
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
|
||||
'profit_percent': [0.4, 0.2, 0.3],
|
||||
'profit_abs': [0.4, 0.4, 0.5],
|
||||
'trade_duration': [15, 30, 15],
|
||||
'profit': [4, 1, 0],
|
||||
'loss': [0, 0, 1],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||
}
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| Strategy | buy count | avg profit % | cum profit % '
|
||||
'| total profit BTC | avg duration | profit | loss |\n'
|
||||
'|:-----------|------------:|---------------:|---------------:'
|
||||
'|-------------------:|:---------------|---------:|-------:|\n'
|
||||
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
||||
'| 1.10000000 | 0:17:00 | 3 | 0 |\n'
|
||||
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
||||
'| 1.30000000 | 0:20:00 | 3 | 0 |'
|
||||
)
|
||||
print(backtesting._generate_text_table_strategy(all_results=results))
|
||||
assert backtesting._generate_text_table_strategy(all_results=results) == result_str
|
||||
|
||||
|
||||
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||
def get_timeframe(input1, input2):
|
||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
|
||||
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.optimize.backtesting.Backtesting',
|
||||
@@ -421,15 +464,14 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||
get_timeframe=get_timeframe,
|
||||
)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
conf['ticker_interval'] = 1
|
||||
conf['live'] = False
|
||||
conf['datadir'] = None
|
||||
conf['export'] = None
|
||||
conf['timerange'] = '-100'
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
default_conf['ticker_interval'] = 1
|
||||
default_conf['live'] = False
|
||||
default_conf['datadir'] = None
|
||||
default_conf['export'] = None
|
||||
default_conf['timerange'] = '-100'
|
||||
|
||||
backtesting = Backtesting(conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.start()
|
||||
# check the logs, that will contain the backtest result
|
||||
exists = [
|
||||
@@ -444,16 +486,11 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||
|
||||
|
||||
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test Backtesting.start() method if no data is found
|
||||
"""
|
||||
|
||||
def get_timeframe(input1, input2):
|
||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
|
||||
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.optimize.backtesting.Backtesting',
|
||||
@@ -462,15 +499,14 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||
get_timeframe=get_timeframe,
|
||||
)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
conf['ticker_interval'] = "1m"
|
||||
conf['live'] = False
|
||||
conf['datadir'] = None
|
||||
conf['export'] = None
|
||||
conf['timerange'] = '20180101-20180102'
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
default_conf['ticker_interval'] = "1m"
|
||||
default_conf['live'] = False
|
||||
default_conf['datadir'] = None
|
||||
default_conf['export'] = None
|
||||
default_conf['timerange'] = '20180101-20180102'
|
||||
|
||||
backtesting = Backtesting(conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.start()
|
||||
# check the logs, that will contain the backtest result
|
||||
|
||||
@@ -478,31 +514,53 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||
|
||||
|
||||
def test_backtest(default_conf, fee, mocker) -> None:
|
||||
"""
|
||||
Test Backtesting.backtest() method
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
pair = 'UNITTEST/BTC'
|
||||
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
|
||||
data = trim_dictlist(data, -200)
|
||||
data_processed = backtesting.tickerdata_to_dataframe(data)
|
||||
results = backtesting.backtest(
|
||||
{
|
||||
'stake_amount': default_conf['stake_amount'],
|
||||
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||
'processed': data_processed,
|
||||
'max_open_trades': 10,
|
||||
'realistic': True
|
||||
'position_stacking': False
|
||||
}
|
||||
)
|
||||
assert not results.empty
|
||||
assert len(results) == 2
|
||||
|
||||
expected = pd.DataFrame(
|
||||
{'pair': [pair, pair],
|
||||
'profit_percent': [0.00029975, 0.00056708],
|
||||
'profit_abs': [1.49e-06, 7.6e-07],
|
||||
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||
Arrow(2018, 1, 30, 3, 30, 0).datetime],
|
||||
'close_time': [Arrow(2018, 1, 29, 22, 40, 0).datetime,
|
||||
Arrow(2018, 1, 30, 4, 20, 0).datetime],
|
||||
'open_index': [77, 183],
|
||||
'close_index': [125, 193],
|
||||
'trade_duration': [240, 50],
|
||||
'open_at_end': [False, False],
|
||||
'open_rate': [0.104445, 0.10302485],
|
||||
'close_rate': [0.105, 0.10359999],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI]
|
||||
})
|
||||
pd.testing.assert_frame_equal(results, expected)
|
||||
data_pair = data_processed[pair]
|
||||
for _, t in results.iterrows():
|
||||
ln = data_pair.loc[data_pair["date"] == t["open_time"]]
|
||||
# Check open trade rate alignes to open rate
|
||||
assert ln is not None
|
||||
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
||||
# check close trade rate alignes to close rate
|
||||
ln = data_pair.loc[data_pair["date"] == t["close_time"]]
|
||||
assert round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6)
|
||||
|
||||
|
||||
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||
"""
|
||||
Test Backtesting.backtest() method with 1 min ticker
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
@@ -515,7 +573,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||
'stake_amount': default_conf['stake_amount'],
|
||||
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||
'max_open_trades': 1,
|
||||
'realistic': True
|
||||
'position_stacking': False
|
||||
}
|
||||
)
|
||||
assert not results.empty
|
||||
@@ -523,9 +581,6 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||
|
||||
|
||||
def test_processed(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test Backtesting.backtest() method with offline data
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
@@ -551,42 +606,42 @@ def test_backtest_ticks(default_conf, fee, mocker):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
ticks = [1, 5]
|
||||
fun = Backtesting(default_conf).populate_buy_trend
|
||||
fun = Backtesting(default_conf).advise_buy
|
||||
for _ in ticks:
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.populate_buy_trend = fun # Override
|
||||
backtesting.populate_sell_trend = fun # Override
|
||||
backtesting.advise_buy = fun # Override
|
||||
backtesting.advise_sell = fun # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
assert not results.empty
|
||||
|
||||
|
||||
def test_backtest_clash_buy_sell(mocker, default_conf):
|
||||
# Override the default buy trend function in our default_strategy
|
||||
def fun(dataframe=None):
|
||||
def fun(dataframe=None, pair=None):
|
||||
buy_value = 1
|
||||
sell_value = 1
|
||||
return _trend(dataframe, buy_value, sell_value)
|
||||
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.populate_buy_trend = fun # Override
|
||||
backtesting.populate_sell_trend = fun # Override
|
||||
backtesting.advise_buy = fun # Override
|
||||
backtesting.advise_sell = fun # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
assert results.empty
|
||||
|
||||
|
||||
def test_backtest_only_sell(mocker, default_conf):
|
||||
# Override the default buy trend function in our default_strategy
|
||||
def fun(dataframe=None):
|
||||
def fun(dataframe=None, pair=None):
|
||||
buy_value = 0
|
||||
sell_value = 1
|
||||
return _trend(dataframe, buy_value, sell_value)
|
||||
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.populate_buy_trend = fun # Override
|
||||
backtesting.populate_sell_trend = fun # Override
|
||||
backtesting.advise_buy = fun # Override
|
||||
backtesting.advise_sell = fun # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
assert results.empty
|
||||
|
||||
@@ -595,8 +650,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.populate_buy_trend = _trend_alternate # Override
|
||||
backtesting.populate_sell_trend = _trend_alternate # Override
|
||||
backtesting.advise_buy = _trend_alternate # Override
|
||||
backtesting.advise_sell = _trend_alternate # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
backtesting._store_backtest_result("test_.json", results)
|
||||
assert len(results) == 4
|
||||
@@ -627,9 +682,15 @@ def test_backtest_record(default_conf, fee, mocker):
|
||||
Arrow(2017, 11, 14, 22, 10, 00).datetime,
|
||||
Arrow(2017, 11, 14, 22, 43, 00).datetime,
|
||||
Arrow(2017, 11, 14, 22, 58, 00).datetime],
|
||||
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
|
||||
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
|
||||
"open_index": [1, 119, 153, 185],
|
||||
"close_index": [118, 151, 184, 199],
|
||||
"trade_duration": [123, 34, 31, 14]})
|
||||
"trade_duration": [123, 34, 31, 14],
|
||||
"open_at_end": [False, False, False, True],
|
||||
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
|
||||
SellType.ROI, SellType.FORCE_SELL]
|
||||
})
|
||||
backtesting._store_backtest_result("backtest-result.json", results)
|
||||
assert len(results) == 4
|
||||
# Assert file_dump_json was only called once
|
||||
@@ -637,15 +698,32 @@ def test_backtest_record(default_conf, fee, mocker):
|
||||
records = records[0]
|
||||
# Ensure records are of correct type
|
||||
assert len(records) == 4
|
||||
|
||||
# reset test to test with strategy name
|
||||
names = []
|
||||
records = []
|
||||
backtesting._store_backtest_result("backtest-result.json", results, "DefStrat")
|
||||
assert len(results) == 4
|
||||
# Assert file_dump_json was only called once
|
||||
assert names == ['backtest-result-DefStrat.json']
|
||||
records = records[0]
|
||||
# Ensure records are of correct type
|
||||
assert len(records) == 4
|
||||
|
||||
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
||||
# Below follows just a typecheck of the schema/type of trade-records
|
||||
oix = None
|
||||
for (pair, profit, date_buy, date_sell, buy_index, dur) in records:
|
||||
for (pair, profit, date_buy, date_sell, buy_index, dur,
|
||||
openr, closer, open_at_end, sell_reason) in records:
|
||||
assert pair == 'UNITTEST/BTC'
|
||||
isinstance(profit, float)
|
||||
assert isinstance(profit, float)
|
||||
# FIX: buy/sell should be converted to ints
|
||||
isinstance(date_buy, str)
|
||||
isinstance(date_sell, str)
|
||||
assert isinstance(date_buy, float)
|
||||
assert isinstance(date_sell, float)
|
||||
assert isinstance(openr, float)
|
||||
assert isinstance(closer, float)
|
||||
assert isinstance(open_at_end, bool)
|
||||
assert isinstance(sell_reason, str)
|
||||
isinstance(buy_index, pd._libs.tslib.Timestamp)
|
||||
if oix:
|
||||
assert buy_index > oix
|
||||
@@ -654,26 +732,21 @@ def test_backtest_record(default_conf, fee, mocker):
|
||||
|
||||
|
||||
def test_backtest_start_live(default_conf, mocker, caplog):
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history',
|
||||
new=lambda s, n, i: _load_pair_as_ticks(n, i))
|
||||
patch_exchange(mocker)
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
|
||||
async def load_pairs(pair, timeframe, since):
|
||||
return _load_pair_as_ticks(pair, timeframe)
|
||||
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_ohlcv = load_pairs
|
||||
|
||||
patch_exchange(mocker, api_mock)
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = MagicMock()
|
||||
args.ticker_interval = 1
|
||||
args.level = 10
|
||||
args.live = True
|
||||
args.datadir = None
|
||||
args.export = None
|
||||
args.strategy = 'DefaultStrategy'
|
||||
args.timerange = '-100' # needed due to MagicMock malleability
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
@@ -682,7 +755,8 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
||||
'--ticker-interval', '1m',
|
||||
'--live',
|
||||
'--timerange', '-100',
|
||||
'--realistic-simulation'
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions'
|
||||
]
|
||||
args = get_args(args)
|
||||
start(args)
|
||||
@@ -691,14 +765,75 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
||||
'Parameter -i/--ticker-interval detected ...',
|
||||
'Using ticker_interval: 1m ...',
|
||||
'Parameter -l/--live detected ...',
|
||||
'Using max_open_trades: 1 ...',
|
||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||
'Parameter --timerange detected: -100 ...',
|
||||
'Using data folder: freqtrade/tests/testdata ...',
|
||||
'Using stake_currency: BTC ...',
|
||||
'Using stake_amount: 0.001 ...',
|
||||
'Downloading data for all pairs in whitelist ...',
|
||||
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||
'Parameter --realistic-simulation detected ...'
|
||||
'Parameter --enable-position-stacking detected ...'
|
||||
]
|
||||
|
||||
for line in exists:
|
||||
assert log_has(line, caplog.record_tuples)
|
||||
|
||||
|
||||
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
|
||||
async def load_pairs(pair, timeframe, since):
|
||||
return _load_pair_as_ticks(pair, timeframe)
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_ohlcv = load_pairs
|
||||
|
||||
patch_exchange(mocker, api_mock)
|
||||
backtestmock = MagicMock()
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||
gen_table_mock = MagicMock()
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock)
|
||||
gen_strattable_mock = MagicMock()
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy',
|
||||
gen_strattable_mock)
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--datadir', 'freqtrade/tests/testdata',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--live',
|
||||
'--timerange', '-100',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
'TestStrategy',
|
||||
]
|
||||
args = get_args(args)
|
||||
start(args)
|
||||
# 2 backtests, 4 tables
|
||||
assert backtestmock.call_count == 2
|
||||
assert gen_table_mock.call_count == 4
|
||||
assert gen_strattable_mock.call_count == 1
|
||||
|
||||
# check the logs, that will contain the backtest result
|
||||
exists = [
|
||||
'Parameter -i/--ticker-interval detected ...',
|
||||
'Using ticker_interval: 1m ...',
|
||||
'Parameter -l/--live detected ...',
|
||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||
'Parameter --timerange detected: -100 ...',
|
||||
'Using data folder: freqtrade/tests/testdata ...',
|
||||
'Using stake_currency: BTC ...',
|
||||
'Using stake_amount: 0.001 ...',
|
||||
'Downloading data for all pairs in whitelist ...',
|
||||
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||
'Parameter --enable-position-stacking detected ...',
|
||||
'Running backtesting for Strategy DefaultStrategy',
|
||||
'Running backtesting for Strategy TestStrategy',
|
||||
]
|
||||
|
||||
for line in exists:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||
import os
|
||||
import signal
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pandas as pd
|
||||
@@ -13,52 +11,32 @@ from freqtrade.strategy.resolver import StrategyResolver
|
||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||
from freqtrade.tests.optimize.test_backtesting import get_args
|
||||
|
||||
# Avoid to reinit the same object again and again
|
||||
_HYPEROPT_INITIALIZED = False
|
||||
_HYPEROPT = None
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def init_hyperopt(default_conf, mocker):
|
||||
global _HYPEROPT_INITIALIZED, _HYPEROPT
|
||||
if not _HYPEROPT_INITIALIZED:
|
||||
patch_exchange(mocker)
|
||||
_HYPEROPT = Hyperopt(default_conf)
|
||||
_HYPEROPT_INITIALIZED = True
|
||||
def hyperopt(default_conf, mocker):
|
||||
patch_exchange(mocker)
|
||||
return Hyperopt(default_conf)
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
def create_trials(mocker) -> None:
|
||||
def create_trials(mocker, hyperopt) -> None:
|
||||
"""
|
||||
When creating trials, mock the hyperopt Trials so that *by default*
|
||||
- we don't create any pickle'd files in the filesystem
|
||||
- we might have a pickle'd file so make sure that we return
|
||||
false when looking for it
|
||||
"""
|
||||
_HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
|
||||
return mocker.Mock(
|
||||
results=[
|
||||
{
|
||||
'loss': 1,
|
||||
'result': 'foo',
|
||||
'status': 'ok'
|
||||
}
|
||||
],
|
||||
best_trial={'misc': {'vals': {'adx': 999}}}
|
||||
)
|
||||
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
||||
|
||||
|
||||
# Unit tests
|
||||
def test_start(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test start() function
|
||||
"""
|
||||
start_mock = MagicMock()
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
@@ -87,11 +65,32 @@ def test_start(mocker, default_conf, caplog) -> None:
|
||||
assert start_mock.call_count == 1
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
|
||||
"""
|
||||
Test Hyperopt.calculate_loss()
|
||||
"""
|
||||
hyperopt = _HYPEROPT
|
||||
def test_start_failure(mocker, default_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'TestStrategy',
|
||||
'hyperopt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
with pytest.raises(ValueError):
|
||||
start(args)
|
||||
assert log_has(
|
||||
"Please don't use --strategy for hyperopt.",
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
|
||||
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
|
||||
@@ -101,20 +100,13 @@ def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None:
|
||||
"""
|
||||
Test Hyperopt.calculate_loss()
|
||||
"""
|
||||
hyperopt = _HYPEROPT
|
||||
|
||||
def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None:
|
||||
shorter = hyperopt.calculate_loss(1, 100, 20)
|
||||
longer = hyperopt.calculate_loss(1, 100, 30)
|
||||
assert shorter < longer
|
||||
|
||||
|
||||
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
|
||||
hyperopt = _HYPEROPT
|
||||
|
||||
def test_loss_calculation_has_limited_profit(hyperopt) -> None:
|
||||
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
|
||||
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
|
||||
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
|
||||
@@ -122,8 +114,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
||||
hyperopt = _HYPEROPT
|
||||
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
||||
hyperopt.current_best_loss = 2
|
||||
hyperopt.log_results(
|
||||
{
|
||||
@@ -134,11 +125,10 @@ def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
||||
}
|
||||
)
|
||||
out, err = capsys.readouterr()
|
||||
assert ' 1/2: foo. Loss 1.00000'in out
|
||||
assert ' 1/2: foo. Loss 1.00000' in out
|
||||
|
||||
|
||||
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
||||
hyperopt = _HYPEROPT
|
||||
def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
|
||||
hyperopt.current_best_loss = 2
|
||||
hyperopt.log_results(
|
||||
{
|
||||
@@ -148,166 +138,23 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
||||
assert caplog.record_tuples == []
|
||||
|
||||
|
||||
def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
|
||||
fmin_result = {
|
||||
"macd_below_zero": 0,
|
||||
"adx": 1,
|
||||
"adx-value": 15.0,
|
||||
"fastd": 1,
|
||||
"fastd-value": 40.0,
|
||||
"green_candle": 1,
|
||||
"mfi": 0,
|
||||
"over_sar": 0,
|
||||
"rsi": 1,
|
||||
"rsi-value": 37.0,
|
||||
"trigger": 2,
|
||||
"uptrend_long_ema": 1,
|
||||
"uptrend_short_ema": 0,
|
||||
"uptrend_sma": 0,
|
||||
"stoploss": -0.1,
|
||||
"roi_t1": 1,
|
||||
"roi_t2": 2,
|
||||
"roi_t3": 3,
|
||||
"roi_p1": 1,
|
||||
"roi_p2": 2,
|
||||
"roi_p3": 3,
|
||||
}
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'epochs': 1})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
||||
patch_exchange(mocker)
|
||||
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt.trials = create_trials(mocker)
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.start()
|
||||
|
||||
exists = [
|
||||
'Best parameters:',
|
||||
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
|
||||
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
|
||||
'"green_candle": {\n "enabled": true\n },',
|
||||
'"macd_below_zero": {\n "enabled": false\n },',
|
||||
'"mfi": {\n "enabled": false\n },',
|
||||
'"over_sar": {\n "enabled": false\n },',
|
||||
'"roi_p1": 1.0,',
|
||||
'"roi_p2": 2.0,',
|
||||
'"roi_p3": 3.0,',
|
||||
'"roi_t1": 1.0,',
|
||||
'"roi_t2": 2.0,',
|
||||
'"roi_t3": 3.0,',
|
||||
'"rsi": {\n "enabled": true,\n "value": 37.0\n },',
|
||||
'"stoploss": -0.1,',
|
||||
'"trigger": {\n "type": "faststoch10"\n },',
|
||||
'"uptrend_long_ema": {\n "enabled": true\n },',
|
||||
'"uptrend_short_ema": {\n "enabled": false\n },',
|
||||
'"uptrend_sma": {\n "enabled": false\n }',
|
||||
'ROI table:\n{0: 6.0, 3.0: 3.0, 5.0: 1.0, 6.0: 0}',
|
||||
'Best Result:\nfoo'
|
||||
]
|
||||
for line in exists:
|
||||
assert line in caplog.text
|
||||
|
||||
|
||||
def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'epochs': 1})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
patch_exchange(mocker)
|
||||
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt.trials = create_trials(mocker)
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
exists = [
|
||||
'Best Result:',
|
||||
'Sorry, Hyperopt was not able to find good parameters. Please try with more epochs '
|
||||
'(param: -e).',
|
||||
]
|
||||
|
||||
for line in exists:
|
||||
assert line in caplog.text
|
||||
|
||||
|
||||
def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, default_conf) -> None:
|
||||
trials = create_trials(mocker)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'epochs': 1})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
|
||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=True)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.len', return_value=len(trials.results))
|
||||
mock_read = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.read_trials',
|
||||
return_value=trials
|
||||
)
|
||||
mock_save = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.save_trials',
|
||||
return_value=None
|
||||
)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||
patch_exchange(mocker)
|
||||
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
hyperopt = Hyperopt(conf)
|
||||
def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker, hyperopt)
|
||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
hyperopt.trials = trials
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
mock_read.assert_called_once()
|
||||
mock_save.assert_called_once()
|
||||
|
||||
current_tries = hyperopt.current_tries
|
||||
total_tries = hyperopt.total_tries
|
||||
|
||||
assert current_tries == len(trials.results)
|
||||
assert total_tries == (current_tries + len(trials.results))
|
||||
|
||||
|
||||
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
|
||||
create_trials(mocker)
|
||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
mocker.patch('freqtrade.optimize.hyperopt.open', return_value=hyperopt.trials_file)
|
||||
|
||||
hyperopt.save_trials()
|
||||
|
||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
assert log_has(
|
||||
'Saving Trials to \'{}\''.format(trials_file),
|
||||
'Saving 1 evaluations to \'{}\''.format(trials_file),
|
||||
caplog.record_tuples
|
||||
)
|
||||
mock_dump.assert_called_once()
|
||||
|
||||
|
||||
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker)
|
||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
|
||||
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker, hyperopt)
|
||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
|
||||
hyperopt_trial = hyperopt.read_trials()
|
||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
assert log_has(
|
||||
@@ -315,11 +162,10 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert hyperopt_trial == trials
|
||||
mock_open.assert_called_once()
|
||||
mock_load.assert_called_once()
|
||||
|
||||
|
||||
def test_roi_table_generation(init_hyperopt) -> None:
|
||||
def test_roi_table_generation(hyperopt) -> None:
|
||||
params = {
|
||||
'roi_t1': 5,
|
||||
'roi_t2': 10,
|
||||
@@ -329,36 +175,35 @@ def test_roi_table_generation(init_hyperopt) -> None:
|
||||
'roi_p3': 3,
|
||||
}
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||
|
||||
|
||||
def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
|
||||
trials = create_trials(mocker)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||
def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1))
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'epochs': 1})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
default_conf.update({'config': 'config.json.example'})
|
||||
default_conf.update({'epochs': 1})
|
||||
default_conf.update({'timerange': None})
|
||||
default_conf.update({'spaces': 'all'})
|
||||
|
||||
hyperopt = Hyperopt(conf)
|
||||
hyperopt.trials = trials
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||
|
||||
hyperopt.start()
|
||||
mock_fmin.assert_called_once()
|
||||
parallel.assert_called_once()
|
||||
|
||||
assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text
|
||||
assert dumper.called
|
||||
|
||||
|
||||
def test_format_results(init_hyperopt):
|
||||
"""
|
||||
Test Hyperopt.format_results()
|
||||
"""
|
||||
|
||||
def test_format_results(hyperopt):
|
||||
# Test with BTC as stake_currency
|
||||
trades = [
|
||||
('ETH/BTC', 2, 2, 123),
|
||||
@@ -368,7 +213,7 @@ def test_format_results(init_hyperopt):
|
||||
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
result = _HYPEROPT.format_results(df)
|
||||
result = hyperopt.format_results(df)
|
||||
assert result.find(' 66.67%')
|
||||
assert result.find('Total profit 1.00000000 BTC')
|
||||
assert result.find('2.0000Σ %')
|
||||
@@ -380,117 +225,61 @@ def test_format_results(init_hyperopt):
|
||||
('XPR/EUR', -1, -2, -246)
|
||||
]
|
||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||
result = _HYPEROPT.format_results(df)
|
||||
result = hyperopt.format_results(df)
|
||||
assert result.find('Total profit 1.00000000 EUR')
|
||||
|
||||
|
||||
def test_signal_handler(mocker, init_hyperopt):
|
||||
"""
|
||||
Test Hyperopt.signal_handler()
|
||||
"""
|
||||
m = MagicMock()
|
||||
mocker.patch('sys.exit', m)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.save_trials', m)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
|
||||
def test_has_space(hyperopt):
|
||||
hyperopt.config.update({'spaces': ['buy', 'roi']})
|
||||
assert hyperopt.has_space('roi')
|
||||
assert hyperopt.has_space('buy')
|
||||
assert not hyperopt.has_space('stoploss')
|
||||
|
||||
hyperopt = _HYPEROPT
|
||||
hyperopt.signal_handler(signal.SIGTERM, None)
|
||||
assert m.call_count == 3
|
||||
hyperopt.config.update({'spaces': ['all']})
|
||||
assert hyperopt.has_space('buy')
|
||||
|
||||
|
||||
def test_has_space(init_hyperopt):
|
||||
"""
|
||||
Test Hyperopt.has_space() method
|
||||
"""
|
||||
_HYPEROPT.config.update({'spaces': ['buy', 'roi']})
|
||||
assert _HYPEROPT.has_space('roi')
|
||||
assert _HYPEROPT.has_space('buy')
|
||||
assert not _HYPEROPT.has_space('stoploss')
|
||||
|
||||
_HYPEROPT.config.update({'spaces': ['all']})
|
||||
assert _HYPEROPT.has_space('buy')
|
||||
|
||||
|
||||
def test_populate_indicators(init_hyperopt) -> None:
|
||||
"""
|
||||
Test Hyperopt.populate_indicators()
|
||||
"""
|
||||
def test_populate_indicators(hyperopt) -> None:
|
||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
|
||||
dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
|
||||
|
||||
# Check if some indicators are generated. We will not test all of them
|
||||
assert 'adx' in dataframe
|
||||
assert 'ao' in dataframe
|
||||
assert 'cci' in dataframe
|
||||
assert 'mfi' in dataframe
|
||||
assert 'rsi' in dataframe
|
||||
|
||||
|
||||
def test_buy_strategy_generator(init_hyperopt) -> None:
|
||||
"""
|
||||
Test Hyperopt.buy_strategy_generator()
|
||||
"""
|
||||
def test_buy_strategy_generator(hyperopt) -> None:
|
||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
|
||||
dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
|
||||
|
||||
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
|
||||
populate_buy_trend = hyperopt.buy_strategy_generator(
|
||||
{
|
||||
'uptrend_long_ema': {
|
||||
'enabled': True
|
||||
},
|
||||
'macd_below_zero': {
|
||||
'enabled': True
|
||||
},
|
||||
'uptrend_short_ema': {
|
||||
'enabled': True
|
||||
},
|
||||
'mfi': {
|
||||
'enabled': True,
|
||||
'value': 20
|
||||
},
|
||||
'fastd': {
|
||||
'enabled': True,
|
||||
'value': 20
|
||||
},
|
||||
'adx': {
|
||||
'enabled': True,
|
||||
'value': 20
|
||||
},
|
||||
'rsi': {
|
||||
'enabled': True,
|
||||
'value': 20
|
||||
},
|
||||
'over_sar': {
|
||||
'enabled': True,
|
||||
},
|
||||
'green_candle': {
|
||||
'enabled': True,
|
||||
},
|
||||
'uptrend_sma': {
|
||||
'enabled': True,
|
||||
},
|
||||
|
||||
'trigger': {
|
||||
'type': 'lower_bb'
|
||||
}
|
||||
'adx-value': 20,
|
||||
'fastd-value': 20,
|
||||
'mfi-value': 20,
|
||||
'rsi-value': 20,
|
||||
'adx-enabled': True,
|
||||
'fastd-enabled': True,
|
||||
'mfi-enabled': True,
|
||||
'rsi-enabled': True,
|
||||
'trigger': 'bb_lower'
|
||||
}
|
||||
)
|
||||
result = populate_buy_trend(dataframe)
|
||||
result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'})
|
||||
# Check if some indicators are generated. We will not test all of them
|
||||
assert 'buy' in result
|
||||
assert 1 in result['buy']
|
||||
|
||||
|
||||
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
||||
"""
|
||||
Test Hyperopt.generate_optimizer() function
|
||||
"""
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'config': 'config.json.example'})
|
||||
conf.update({'timerange': None})
|
||||
conf.update({'spaces': 'all'})
|
||||
def test_generate_optimizer(mocker, default_conf) -> None:
|
||||
default_conf.update({'config': 'config.json.example'})
|
||||
default_conf.update({'timerange': None})
|
||||
default_conf.update({'spaces': 'all'})
|
||||
|
||||
trades = [
|
||||
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||
@@ -503,35 +292,33 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
||||
MagicMock(return_value=backtest_result)
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
||||
|
||||
optimizer_param = {
|
||||
'adx': {'enabled': False},
|
||||
'fastd': {'enabled': True, 'value': 35.0},
|
||||
'green_candle': {'enabled': True},
|
||||
'macd_below_zero': {'enabled': True},
|
||||
'mfi': {'enabled': False},
|
||||
'over_sar': {'enabled': False},
|
||||
'roi_p1': 0.01,
|
||||
'roi_p2': 0.01,
|
||||
'roi_p3': 0.1,
|
||||
'adx-value': 0,
|
||||
'fastd-value': 35,
|
||||
'mfi-value': 0,
|
||||
'rsi-value': 0,
|
||||
'adx-enabled': False,
|
||||
'fastd-enabled': True,
|
||||
'mfi-enabled': False,
|
||||
'rsi-enabled': False,
|
||||
'trigger': 'macd_cross_signal',
|
||||
'roi_t1': 60.0,
|
||||
'roi_t2': 30.0,
|
||||
'roi_t3': 20.0,
|
||||
'rsi': {'enabled': False},
|
||||
'roi_p1': 0.01,
|
||||
'roi_p2': 0.01,
|
||||
'roi_p3': 0.1,
|
||||
'stoploss': -0.4,
|
||||
'trigger': {'type': 'macd_cross_signal'},
|
||||
'uptrend_long_ema': {'enabled': False},
|
||||
'uptrend_short_ema': {'enabled': True},
|
||||
'uptrend_sma': {'enabled': True}
|
||||
}
|
||||
|
||||
response_expected = {
|
||||
'loss': 1.9840569076926293,
|
||||
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||
'(0.0231Σ%). Avg duration 100.0 mins.',
|
||||
'status': 'ok'
|
||||
'params': optimizer_param
|
||||
}
|
||||
|
||||
hyperopt = Hyperopt(conf)
|
||||
generate_optimizer_value = hyperopt.generate_optimizer(optimizer_param)
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
|
||||
assert generate_optimizer_value == response_expected
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
import arrow
|
||||
from shutil import copyfile
|
||||
|
||||
import arrow
|
||||
|
||||
from freqtrade import optimize
|
||||
from freqtrade.misc import file_dump_json
|
||||
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
||||
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
|
||||
load_cached_data_for_updating
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
||||
from freqtrade.misc import file_dump_json
|
||||
from freqtrade.optimize.__init__ import (download_backtesting_testdata,
|
||||
download_pairs,
|
||||
load_cached_data_for_updating,
|
||||
load_tickerdata_file,
|
||||
make_testdata_path, trim_tickerlist)
|
||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||
|
||||
# Change this if modifying UNITTEST/BTC testdatafile
|
||||
_BTC_UNITTEST_LENGTH = 13681
|
||||
@@ -50,10 +53,7 @@ def _clean_test_file(file: str) -> None:
|
||||
|
||||
|
||||
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
||||
"""
|
||||
Test load_data() with 30 min ticker
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
|
||||
_backup_file(file, copy_file=True)
|
||||
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
|
||||
@@ -63,10 +63,7 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) ->
|
||||
|
||||
|
||||
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
||||
"""
|
||||
Test load_data() with 5 min ticker
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
|
||||
_backup_file(file, copy_file=True)
|
||||
@@ -77,11 +74,7 @@ def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) ->
|
||||
|
||||
|
||||
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
||||
"""
|
||||
Test load_data() with 1 min ticker
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||
_backup_file(file, copy_file=True)
|
||||
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||
@@ -94,7 +87,7 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_co
|
||||
"""
|
||||
Test load_data() with 1 min ticker
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||
|
||||
@@ -125,7 +118,7 @@ def test_testdata_path() -> None:
|
||||
|
||||
|
||||
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||
@@ -268,7 +261,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||
|
||||
|
||||
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
||||
side_effect=BaseException('File Error'))
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
@@ -286,7 +279,7 @@ def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf)
|
||||
|
||||
|
||||
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
|
||||
# Download a 1 min ticker file
|
||||
@@ -311,7 +304,7 @@ def test_download_backtesting_testdata2(mocker, default_conf) -> None:
|
||||
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
||||
]
|
||||
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m')
|
||||
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
|
||||
@@ -418,10 +411,6 @@ def test_trim_tickerlist() -> None:
|
||||
|
||||
|
||||
def test_file_dump_json() -> None:
|
||||
"""
|
||||
Test file_dump_json()
|
||||
:return: None
|
||||
"""
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
||||
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
||||
data = {'bar': 'foo'}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
||||
|
||||
"""
|
||||
Unit test file for rpc/rpc.py
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, ANY
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade import TemporaryError
|
||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.rpc import RPC, RPCException
|
||||
from freqtrade.rpc import RPC, RPCException
|
||||
from freqtrade.state import State
|
||||
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
||||
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
||||
from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
@@ -26,21 +26,18 @@ def prec_satoshi(a, b) -> float:
|
||||
|
||||
# Unit tests
|
||||
def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||
"""
|
||||
Test rpc_trade_status() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
_load_markets=MagicMock(return_value={}),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -52,50 +49,44 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||
rpc._rpc_trade_status()
|
||||
|
||||
freqtradebot.create_trade()
|
||||
trades = rpc._rpc_trade_status()
|
||||
trade = trades[0]
|
||||
results = rpc._rpc_trade_status()
|
||||
|
||||
result_message = [
|
||||
'*Trade ID:* `1`\n'
|
||||
'*Current Pair:* '
|
||||
'[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
|
||||
'*Open Since:* `just now`\n'
|
||||
'*Amount:* `90.99181074`\n'
|
||||
'*Open Rate:* `0.00001099`\n'
|
||||
'*Close Rate:* `None`\n'
|
||||
'*Current Rate:* `0.00001098`\n'
|
||||
'*Close Profit:* `None`\n'
|
||||
'*Current Profit:* `-0.59%`\n'
|
||||
'*Open Order:* `(limit buy rem=0.00000000)`'
|
||||
]
|
||||
assert trades == result_message
|
||||
assert trade.find('[ETH/BTC]') >= 0
|
||||
assert {
|
||||
'trade_id': 1,
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'date': ANY,
|
||||
'open_rate': 1.099e-05,
|
||||
'close_rate': None,
|
||||
'current_rate': 1.098e-05,
|
||||
'amount': 90.99181074,
|
||||
'close_profit': None,
|
||||
'current_profit': -0.59,
|
||||
'open_order': '(limit buy rem=0.00000000)'
|
||||
} == results[0]
|
||||
|
||||
|
||||
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||
"""
|
||||
Test rpc_status_table() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
|
||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||
rpc._rpc_status_table()
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
|
||||
with pytest.raises(RPCException, match=r'.*no active order*'):
|
||||
rpc._rpc_status_table()
|
||||
|
||||
freqtradebot.create_trade()
|
||||
@@ -107,26 +98,23 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||
|
||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
"""
|
||||
Test rpc_daily_profit() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
# Create some test data
|
||||
freqtradebot.create_trade()
|
||||
trade = Trade.query.first()
|
||||
@@ -159,29 +147,28 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
|
||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
"""
|
||||
Test rpc_trade_statistics() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.fiat_convert.Market',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
)
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
|
||||
with pytest.raises(RPCException, match=r'.*no closed trade*'):
|
||||
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
@@ -195,7 +182,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_up
|
||||
)
|
||||
trade.update(limit_sell_order)
|
||||
@@ -210,7 +196,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_up
|
||||
)
|
||||
trade.update(limit_sell_order)
|
||||
@@ -236,10 +221,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
# trade.open_rate (it is set to None)
|
||||
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||
ticker_sell_up, limit_buy_order, limit_sell_order):
|
||||
"""
|
||||
Test rpc_trade_statistics() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.fiat_convert.Market',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
@@ -248,13 +230,13 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
@@ -268,7 +250,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_up,
|
||||
get_fee=fee
|
||||
)
|
||||
@@ -295,9 +276,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||
|
||||
|
||||
def test_rpc_balance_handle(default_conf, mocker):
|
||||
"""
|
||||
Test rpc_balance() method
|
||||
"""
|
||||
mock_balance = {
|
||||
'BTC': {
|
||||
'free': 10.0,
|
||||
@@ -305,104 +283,101 @@ def test_rpc_balance_handle(default_conf, mocker):
|
||||
'used': 2.0,
|
||||
},
|
||||
'ETH': {
|
||||
'free': 0.0,
|
||||
'total': 0.0,
|
||||
'used': 0.0,
|
||||
'free': 1.0,
|
||||
'total': 5.0,
|
||||
'used': 4.0,
|
||||
}
|
||||
}
|
||||
# ETH will be skipped due to mocked Error below
|
||||
|
||||
patch_get_signal(mocker, (True, False))
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.fiat_convert.Market',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
)
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_balances=MagicMock(return_value=mock_balance)
|
||||
get_balances=MagicMock(return_value=mock_balance),
|
||||
get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
|
||||
output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
|
||||
assert prec_satoshi(total, 12)
|
||||
assert prec_satoshi(value, 180000)
|
||||
assert 'USD' in symbol
|
||||
assert len(output) == 1
|
||||
assert 'BTC' in output[0]['currency']
|
||||
assert prec_satoshi(output[0]['available'], 10)
|
||||
assert prec_satoshi(output[0]['balance'], 12)
|
||||
assert prec_satoshi(output[0]['pending'], 2)
|
||||
assert prec_satoshi(output[0]['est_btc'], 12)
|
||||
result = rpc._rpc_balance(default_conf['fiat_display_currency'])
|
||||
assert prec_satoshi(result['total'], 12)
|
||||
assert prec_satoshi(result['value'], 180000)
|
||||
assert 'USD' == result['symbol']
|
||||
assert result['currencies'] == [{
|
||||
'currency': 'BTC',
|
||||
'available': 10.0,
|
||||
'balance': 12.0,
|
||||
'pending': 2.0,
|
||||
'est_btc': 12.0,
|
||||
}]
|
||||
assert result['total'] == 12.0
|
||||
|
||||
|
||||
def test_rpc_start(mocker, default_conf) -> None:
|
||||
"""
|
||||
Test rpc_start() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock()
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
freqtradebot.state = State.STOPPED
|
||||
|
||||
result = rpc._rpc_start()
|
||||
assert '`Starting trader ...`' in result
|
||||
assert {'status': 'starting trader ...'} == result
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
|
||||
result = rpc._rpc_start()
|
||||
assert '*Status:* `already running`' in result
|
||||
assert {'status': 'already running'} == result
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
|
||||
|
||||
def test_rpc_stop(mocker, default_conf) -> None:
|
||||
"""
|
||||
Test rpc_stop() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock()
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
freqtradebot.state = State.RUNNING
|
||||
|
||||
result = rpc._rpc_stop()
|
||||
assert '`Stopping trader ...`' in result
|
||||
assert {'status': 'stopping trader ...'} == result
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
|
||||
result = rpc._rpc_stop()
|
||||
assert '*Status:* `already stopped`' in result
|
||||
|
||||
assert {'status': 'already stopped'} == result
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
|
||||
|
||||
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||
"""
|
||||
Test rpc_forcesell() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
cancel_order=cancel_order_mock,
|
||||
get_order=MagicMock(
|
||||
@@ -417,14 +392,15 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||
rpc._rpc_forcesell(None)
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
|
||||
with pytest.raises(RPCException, match=r'.*invalid argument*'):
|
||||
rpc._rpc_forcesell(None)
|
||||
|
||||
rpc._rpc_forcesell('all')
|
||||
@@ -435,10 +411,10 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||
rpc._rpc_forcesell('1')
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||
rpc._rpc_forcesell(None)
|
||||
|
||||
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||
rpc._rpc_forcesell('all')
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
@@ -496,15 +472,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||
|
||||
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, markets, mocker) -> None:
|
||||
"""
|
||||
Test rpc_performance() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -512,6 +484,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -535,15 +508,11 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
|
||||
|
||||
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
||||
"""
|
||||
Test rpc_count() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
@@ -551,6 +520,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
trades = rpc._rpc_count()
|
||||
|
||||
@@ -1,50 +1,31 @@
|
||||
"""
|
||||
Unit test file for rpc/rpc_manager.py
|
||||
"""
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.rpc.rpc_manager import RPCManager
|
||||
from freqtrade.rpc import RPCMessageType, RPCManager
|
||||
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
||||
|
||||
|
||||
def test_rpc_manager_object() -> None:
|
||||
""" Test the Arguments object has the mandatory methods """
|
||||
assert hasattr(RPCManager, 'send_msg')
|
||||
assert hasattr(RPCManager, 'cleanup')
|
||||
|
||||
|
||||
def test__init__(mocker, default_conf) -> None:
|
||||
""" Test __init__() method """
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
assert rpc_manager.registered_modules == []
|
||||
|
||||
|
||||
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||
""" Test _init() method with Telegram disabled """
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
||||
default_conf['telegram']['enabled'] = False
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||
assert rpc_manager.registered_modules == []
|
||||
|
||||
|
||||
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test _init() method with Telegram enabled
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||
@@ -54,16 +35,11 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test cleanup() method with Telegram disabled
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.cleanup()
|
||||
|
||||
@@ -72,9 +48,6 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test cleanup() method with Telegram enabled
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||
@@ -92,32 +65,51 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test send_msg() method with Telegram disabled
|
||||
"""
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.send_msg('test')
|
||||
rpc_manager.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': 'test'
|
||||
})
|
||||
|
||||
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
||||
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
|
||||
assert telegram_mock.call_count == 0
|
||||
|
||||
|
||||
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test send_msg() method with Telegram disabled
|
||||
"""
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.send_msg('test')
|
||||
rpc_manager.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': 'test'
|
||||
})
|
||||
|
||||
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
||||
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
|
||||
assert telegram_mock.call_count == 1
|
||||
|
||||
|
||||
def test_init_webhook_disabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
default_conf['webhook'] = {'enabled': False}
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
||||
assert rpc_manager.registered_modules == []
|
||||
|
||||
|
||||
def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
||||
assert len(rpc_manager.registered_modules) == 1
|
||||
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
# pragma pylint: disable=protected-access, unused-argument, invalid-name
|
||||
# pragma pylint: disable=too-many-lines, too-many-arguments
|
||||
|
||||
"""
|
||||
Unit test file for rpc/telegram.py
|
||||
"""
|
||||
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, ANY
|
||||
|
||||
from telegram import Update, Message, Chat
|
||||
import arrow
|
||||
import pytest
|
||||
from telegram import Chat, Message, Update
|
||||
from telegram.error import NetworkError
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.telegram import Telegram
|
||||
from freqtrade.rpc.telegram import authorized_only
|
||||
from freqtrade.rpc import RPCMessageType
|
||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||
from freqtrade.state import State
|
||||
from freqtrade.tests.conftest import get_patched_freqtradebot, patch_exchange, log_has
|
||||
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
||||
from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
|
||||
patch_exchange)
|
||||
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
||||
from freqtrade.tests.conftest import patch_coinmarketcap
|
||||
|
||||
|
||||
class DummyCls(Telegram):
|
||||
@@ -51,9 +51,6 @@ class DummyCls(Telegram):
|
||||
|
||||
|
||||
def test__init__(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test __init__() method
|
||||
"""
|
||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
|
||||
@@ -63,7 +60,6 @@ def test__init__(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_init(default_conf, mocker, caplog) -> None:
|
||||
""" Test _init() method """
|
||||
start_polling = MagicMock()
|
||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
||||
|
||||
@@ -82,9 +78,6 @@ def test_init(default_conf, mocker, caplog) -> None:
|
||||
|
||||
|
||||
def test_cleanup(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test cleanup() method
|
||||
"""
|
||||
updater_mock = MagicMock()
|
||||
updater_mock.stop = MagicMock()
|
||||
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
|
||||
@@ -95,10 +88,6 @@ def test_cleanup(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test authorized_only() method when we are authorized
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker, None)
|
||||
|
||||
@@ -106,9 +95,10 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||
update = Update(randint(1, 100))
|
||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
dummy = DummyCls(FreqtradeBot(conf))
|
||||
default_conf['telegram']['enabled'] = False
|
||||
bot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(bot, (True, False))
|
||||
dummy = DummyCls(bot)
|
||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||
assert dummy.state['called'] is True
|
||||
assert log_has(
|
||||
@@ -126,19 +116,16 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||
|
||||
|
||||
def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test authorized_only() method when we are unauthorized
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker, None)
|
||||
chat = Chat(0xdeadbeef, 0)
|
||||
update = Update(randint(1, 100))
|
||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
dummy = DummyCls(FreqtradeBot(conf))
|
||||
default_conf['telegram']['enabled'] = False
|
||||
bot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(bot, (True, False))
|
||||
dummy = DummyCls(bot)
|
||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||
assert dummy.state['called'] is False
|
||||
assert not log_has(
|
||||
@@ -156,19 +143,18 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
||||
|
||||
|
||||
def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test authorized_only() method when an exception is thrown
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
|
||||
update = Update(randint(1, 100))
|
||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
dummy = DummyCls(FreqtradeBot(conf))
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
bot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(bot, (True, False))
|
||||
dummy = DummyCls(bot)
|
||||
|
||||
dummy.dummy_exception(bot=MagicMock(), update=update)
|
||||
assert dummy.state['called'] is False
|
||||
assert not log_has(
|
||||
@@ -186,19 +172,14 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
||||
|
||||
|
||||
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||
"""
|
||||
Test _status() method
|
||||
"""
|
||||
update.message.chat.id = 123
|
||||
conf = deepcopy(default_conf)
|
||||
conf['telegram']['enabled'] = False
|
||||
conf['telegram']['chat_id'] = 123
|
||||
default_conf['telegram']['enabled'] = False
|
||||
default_conf['telegram']['chat_id'] = 123
|
||||
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_pair_detail_url=MagicMock(),
|
||||
get_fee=fee,
|
||||
@@ -209,13 +190,26 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
|
||||
_rpc_trade_status=MagicMock(return_value=[{
|
||||
'trade_id': 1,
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'date': arrow.utcnow(),
|
||||
'open_rate': 1.099e-05,
|
||||
'close_rate': None,
|
||||
'current_rate': 1.098e-05,
|
||||
'amount': 90.99181074,
|
||||
'close_profit': None,
|
||||
'current_profit': -0.59,
|
||||
'open_order': '(limit buy rem=0.00000000)'
|
||||
}]),
|
||||
_status_table=status_table,
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(conf)
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -223,7 +217,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||
freqtradebot.create_trade()
|
||||
|
||||
telegram._status(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 3
|
||||
assert msg_mock.call_count == 1
|
||||
|
||||
update.message.text = MagicMock()
|
||||
update.message.text.replace = MagicMock(return_value='table 2 3')
|
||||
@@ -232,14 +226,10 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||
|
||||
|
||||
def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||
"""
|
||||
Test _status() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
@@ -255,6 +245,8 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -279,14 +271,10 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
||||
|
||||
|
||||
def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||
"""
|
||||
Test _status_table() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
|
||||
get_fee=fee,
|
||||
@@ -300,9 +288,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = 15.0
|
||||
freqtradebot = FreqtradeBot(conf)
|
||||
default_conf['stake_amount'] = 15.0
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -333,18 +322,14 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
||||
|
||||
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, markets, mocker) -> None:
|
||||
"""
|
||||
Test _daily() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.fiat_convert.CryptoToFiatConverter._find_price',
|
||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||
return_value=15000.0
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
@@ -358,6 +343,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -407,14 +393,10 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||
|
||||
|
||||
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
"""
|
||||
Test _daily() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker
|
||||
)
|
||||
msg_mock = MagicMock()
|
||||
@@ -426,6 +408,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Try invalid data
|
||||
@@ -446,15 +429,11 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||
|
||||
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
"""
|
||||
Test _profit() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
@@ -468,6 +447,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._profit(bot=MagicMock(), update=update)
|
||||
@@ -507,10 +487,6 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||
|
||||
|
||||
def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _balance() method
|
||||
"""
|
||||
|
||||
mock_balance = {
|
||||
'BTC': {
|
||||
'total': 12.0,
|
||||
@@ -535,9 +511,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||
}
|
||||
|
||||
def mock_ticker(symbol, refresh):
|
||||
"""
|
||||
Mock Bittrex.get_ticker() response
|
||||
"""
|
||||
if symbol == 'BTC/USDT':
|
||||
return {
|
||||
'bid': 10000.00,
|
||||
@@ -551,7 +524,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||
'last': 0.1,
|
||||
}
|
||||
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
|
||||
@@ -564,6 +536,8 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._balance(bot=MagicMock(), update=update)
|
||||
@@ -577,11 +551,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||
assert 'BTC: 14.00000000' in result
|
||||
|
||||
|
||||
def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _balance() method when the Exchange platform returns nothing
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
|
||||
|
||||
msg_mock = MagicMock()
|
||||
@@ -592,18 +562,17 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
||||
)
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._balance(bot=MagicMock(), update=update)
|
||||
result = msg_mock.call_args_list[0][0][0]
|
||||
assert msg_mock.call_count == 1
|
||||
assert '`All balances are zero.`' in result
|
||||
assert 'all balances are zero' in result
|
||||
|
||||
|
||||
def test_start_handle(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _start() method
|
||||
"""
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
@@ -622,9 +591,6 @@ def test_start_handle(default_conf, update, mocker) -> None:
|
||||
|
||||
|
||||
def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _start() method
|
||||
"""
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
@@ -644,9 +610,6 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
||||
|
||||
|
||||
def test_stop_handle(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _stop() method
|
||||
"""
|
||||
patch_coinmarketcap(mocker)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@@ -663,13 +626,10 @@ def test_stop_handle(default_conf, update, mocker) -> None:
|
||||
telegram._stop(bot=MagicMock(), update=update)
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'stopping trader' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _stop() method
|
||||
"""
|
||||
patch_coinmarketcap(mocker)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@@ -690,7 +650,6 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
||||
|
||||
|
||||
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
||||
""" Test _reload_conf() method """
|
||||
patch_coinmarketcap(mocker)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@@ -707,28 +666,25 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
||||
telegram._reload_conf(bot=MagicMock(), update=update)
|
||||
assert freqtradebot.state == State.RELOAD_CONF
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_forcesell_handle(default_conf, update, ticker, fee,
|
||||
ticker_sell_up, markets, mocker) -> None:
|
||||
"""
|
||||
Test _forcesell() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
_load_markets=MagicMock(return_value={}),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -744,33 +700,40 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'profit',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.172e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
'current_rate': 1.172e-05,
|
||||
'profit_amount': 6.126e-05,
|
||||
'profit_percent': 0.06110514,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'USD',
|
||||
} == last_msg
|
||||
|
||||
|
||||
def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||
ticker_sell_down, markets, mocker) -> None:
|
||||
"""
|
||||
Test _forcesell() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
_load_markets=MagicMock(return_value={}),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -779,7 +742,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||
# Decrease the price and sell it
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_down
|
||||
)
|
||||
|
||||
@@ -790,33 +752,41 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||
|
||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.044e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
'current_rate': 1.044e-05,
|
||||
'profit_amount': -5.492e-05,
|
||||
'profit_percent': -0.05478343,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'USD',
|
||||
} == last_msg
|
||||
|
||||
|
||||
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||
"""
|
||||
Test _forcesell() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -828,17 +798,25 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
|
||||
assert rpc_mock.call_count == 4
|
||||
for args in rpc_mock.call_args_list:
|
||||
assert '0.00001098' in args[0][0]
|
||||
assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0]
|
||||
assert '-0.089 USD' in args[0][0]
|
||||
msg = rpc_mock.call_args_list[0][0][0]
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
'market_url': ANY,
|
||||
'limit': 1.098e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
'current_rate': 1.098e-05,
|
||||
'profit_amount': -5.91e-06,
|
||||
'profit_percent': -0.00589292,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'USD',
|
||||
} == msg
|
||||
|
||||
|
||||
def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _forcesell() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
msg_mock = MagicMock()
|
||||
@@ -847,9 +825,10 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||
patch_exchange(mocker)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Trader is not running
|
||||
@@ -865,7 +844,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
update.message.text = '/forcesell'
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Invalid argument
|
||||
msg_mock.reset_mock()
|
||||
@@ -873,16 +852,13 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
update.message.text = '/forcesell 123456'
|
||||
telegram._forcesell(bot=MagicMock(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_performance_handle(default_conf, update, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
"""
|
||||
Test _performance() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
@@ -891,13 +867,13 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
@@ -920,19 +896,16 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
||||
|
||||
|
||||
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _performance() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
# Trader is not running
|
||||
@@ -943,11 +916,8 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
||||
|
||||
|
||||
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||
"""
|
||||
Test _count() method
|
||||
"""
|
||||
patch_get_signal(mocker, (True, False))
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
@@ -956,13 +926,13 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
||||
)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
|
||||
get_markets=markets
|
||||
)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
@@ -987,9 +957,6 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
||||
|
||||
|
||||
def test_help_handle(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _help() method
|
||||
"""
|
||||
patch_coinmarketcap(mocker)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@@ -1007,9 +974,6 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
||||
|
||||
|
||||
def test_version_handle(default_conf, update, mocker) -> None:
|
||||
"""
|
||||
Test _version() method
|
||||
"""
|
||||
patch_coinmarketcap(mocker)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@@ -1025,15 +989,224 @@ def test_version_handle(default_conf, update, mocker) -> None:
|
||||
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_send_msg(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test send_msg() method
|
||||
"""
|
||||
def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.099e-05,
|
||||
'stake_amount': 0.001,
|
||||
'stake_amount_fiat': 0.0,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'USD'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
|
||||
'with limit `0.00001099\n' \
|
||||
'(0.001000 BTC,0.000 USD)`'
|
||||
|
||||
|
||||
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
old_convamount = telegram._fiat_converter.convert_amount
|
||||
telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||
'limit': 3.201e-05,
|
||||
'amount': 1333.3333333333335,
|
||||
'open_rate': 7.5e-05,
|
||||
'current_rate': 3.201e-05,
|
||||
'profit_amount': -0.05746268,
|
||||
'profit_percent': -0.57405275,
|
||||
'stake_currency': 'ETH',
|
||||
'fiat_currency': 'USD'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Binance:* Selling [KEY/ETH]' \
|
||||
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
||||
'*Limit:* `0.00003201`\n' \
|
||||
'*Amount:* `1333.33333333`\n' \
|
||||
'*Open Rate:* `0.00007500`\n' \
|
||||
'*Current Rate:* `0.00003201`\n' \
|
||||
'*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`'
|
||||
|
||||
msg_mock.reset_mock()
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||
'limit': 3.201e-05,
|
||||
'amount': 1333.3333333333335,
|
||||
'open_rate': 7.5e-05,
|
||||
'current_rate': 3.201e-05,
|
||||
'profit_amount': -0.05746268,
|
||||
'profit_percent': -0.57405275,
|
||||
'stake_currency': 'ETH',
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Binance:* Selling [KEY/ETH]' \
|
||||
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
||||
'*Limit:* `0.00003201`\n' \
|
||||
'*Amount:* `1333.33333333`\n' \
|
||||
'*Open Rate:* `0.00007500`\n' \
|
||||
'*Current Rate:* `0.00003201`\n' \
|
||||
'*Profit:* `-57.41%`'
|
||||
# Reset singleton function to avoid random breaks
|
||||
telegram._fiat_converter.convert_amount = old_convamount
|
||||
|
||||
|
||||
def test_send_msg_status_notification(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': 'running'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] == '*Status:* `running`'
|
||||
|
||||
|
||||
def test_warning_notification(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||
'status': 'message'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] == '*Warning:* `message`'
|
||||
|
||||
|
||||
def test_custom_notification(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
||||
'status': '*Custom:* `Hello World`'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
|
||||
|
||||
|
||||
def test_send_msg_unknown_type(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
with pytest.raises(NotImplementedError, match=r'Unknown message type: None'):
|
||||
telegram.send_msg({
|
||||
'type': None,
|
||||
})
|
||||
|
||||
|
||||
def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
||||
del default_conf['fiat_display_currency']
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.099e-05,
|
||||
'stake_amount': 0.001,
|
||||
'stake_amount_fiat': 0.0,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': None
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
|
||||
'with limit `0.00001099\n' \
|
||||
'(0.001000 BTC)`'
|
||||
|
||||
|
||||
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||
del default_conf['fiat_display_currency']
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||
'limit': 3.201e-05,
|
||||
'amount': 1333.3333333333335,
|
||||
'open_rate': 7.5e-05,
|
||||
'current_rate': 3.201e-05,
|
||||
'profit_amount': -0.05746268,
|
||||
'profit_percent': -0.57405275,
|
||||
'stake_currency': 'ETH',
|
||||
'fiat_currency': 'USD'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Binance:* Selling [KEY/ETH]' \
|
||||
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
||||
'*Limit:* `0.00003201`\n' \
|
||||
'*Amount:* `1333.33333333`\n' \
|
||||
'*Open Rate:* `0.00007500`\n' \
|
||||
'*Current Rate:* `0.00003201`\n' \
|
||||
'*Profit:* `-57.41%`'
|
||||
|
||||
|
||||
def test__send_msg(default_conf, mocker) -> None:
|
||||
patch_coinmarketcap(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
conf = deepcopy(default_conf)
|
||||
bot = MagicMock()
|
||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._config['telegram']['enabled'] = True
|
||||
@@ -1041,16 +1214,12 @@ def test_send_msg(default_conf, mocker) -> None:
|
||||
assert len(bot.method_calls) == 1
|
||||
|
||||
|
||||
def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test send_msg() method
|
||||
"""
|
||||
def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
|
||||
patch_coinmarketcap(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
conf = deepcopy(default_conf)
|
||||
bot = MagicMock()
|
||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._config['telegram']['enabled'] = True
|
||||
|
||||
166
freqtrade/tests/rpc/test_rpc_webhook.py
Normal file
166
freqtrade/tests/rpc/test_rpc_webhook.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from requests import RequestException
|
||||
|
||||
from freqtrade.rpc import RPCMessageType
|
||||
from freqtrade.rpc.webhook import Webhook
|
||||
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
||||
|
||||
|
||||
def get_webhook_dict() -> dict:
|
||||
return {
|
||||
"enabled": True,
|
||||
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
|
||||
"webhookbuy": {
|
||||
"value1": "Buying {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "{stake_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhooksell": {
|
||||
"value1": "Selling {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhookstatus": {
|
||||
"value1": "Status: {status}",
|
||||
"value2": "",
|
||||
"value3": ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test__init__(mocker, default_conf):
|
||||
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
assert webhook._config == default_conf
|
||||
|
||||
|
||||
def test_send_msg(default_conf, mocker):
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
msg = {
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': "http://mockedurl/ETH_BTC",
|
||||
'limit': 0.005,
|
||||
'stake_amount': 0.8,
|
||||
'stake_amount_fiat': 500,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'EUR'
|
||||
}
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook.send_msg(msg=msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
default_conf["webhook"]["webhookbuy"]["value1"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
|
||||
# Test sell
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': "profit",
|
||||
'market_url': "http://mockedurl/ETH_BTC",
|
||||
'limit': 0.005,
|
||||
'amount': 0.8,
|
||||
'open_rate': 0.004,
|
||||
'current_rate': 0.005,
|
||||
'profit_amount': 0.001,
|
||||
'profit_percent': 0.20,
|
||||
'stake_currency': 'BTC',
|
||||
}
|
||||
webhook.send_msg(msg=msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
default_conf["webhook"]["webhooksell"]["value1"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||
default_conf["webhook"]["webhooksell"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
|
||||
|
||||
# Test notification
|
||||
msg = {
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': 'Unfilled sell order for BTC cancelled due to timeout'
|
||||
}
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook.send_msg(msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
default_conf["webhook"]["webhookstatus"]["value1"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||
default_conf["webhook"]["webhookstatus"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhookstatus"]["value3"].format(**msg))
|
||||
|
||||
|
||||
def test_exception_send_msg(default_conf, mocker, caplog):
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
default_conf["webhook"]["webhookbuy"] = None
|
||||
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
|
||||
assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks",
|
||||
caplog.record_tuples)
|
||||
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}"
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
msg = {
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': "http://mockedurl/ETH_BTC",
|
||||
'limit': 0.005,
|
||||
'stake_amount': 0.8,
|
||||
'stake_amount_fiat': 500,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'EUR'
|
||||
}
|
||||
webhook.send_msg(msg)
|
||||
assert log_has("Problem calling Webhook. Please check your webhook configuration. "
|
||||
"Exception: 'DEADBEEF'", caplog.record_tuples)
|
||||
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
msg = {
|
||||
'type': 'DEADBEEF',
|
||||
'status': 'whatever'
|
||||
}
|
||||
with pytest.raises(NotImplementedError):
|
||||
webhook.send_msg(msg)
|
||||
|
||||
|
||||
def test__send_msg(default_conf, mocker, caplog):
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
msg = {'value1': 'DEADBEEF',
|
||||
'value2': 'ALIVEBEEF',
|
||||
'value3': 'FREQTRADE'}
|
||||
post = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||
webhook._send_msg(msg)
|
||||
|
||||
assert post.call_count == 1
|
||||
assert post.call_args[1] == {'data': msg}
|
||||
assert post.call_args[0] == (default_conf['webhook']['url'], )
|
||||
|
||||
post = MagicMock(side_effect=RequestException)
|
||||
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||
webhook._send_msg(msg)
|
||||
assert log_has('Could not call webhook url. Exception: ', caplog.record_tuples)
|
||||
235
freqtrade/tests/strategy/legacy_strategy.py
Normal file
235
freqtrade/tests/strategy/legacy_strategy.py
Normal file
@@ -0,0 +1,235 @@
|
||||
|
||||
# --- Do not remove these libs ---
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from pandas import DataFrame
|
||||
# --------------------------------
|
||||
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
import numpy # noqa
|
||||
|
||||
|
||||
# This class is a sample. Feel free to customize it.
|
||||
class TestStrategyLegacy(IStrategy):
|
||||
"""
|
||||
This is a test strategy using the legacy function headers, which will be
|
||||
removed in a future update.
|
||||
Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py
|
||||
for a uptodate version of this template.
|
||||
|
||||
"""
|
||||
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||
minimal_roi = {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
# This attribute will be overridden if the config file contains "stoploss"
|
||||
stoploss = -0.10
|
||||
|
||||
# Optimal ticker interval for the strategy
|
||||
ticker_interval = '5m'
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
# ------------------------------------
|
||||
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
"""
|
||||
# Awesome oscillator
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
|
||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
# MFI
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
|
||||
# Minus Directional Indicator / Movement
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# Plus Directional Indicator / Movement
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# ROC
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
"""
|
||||
|
||||
# Overlap Studies
|
||||
# ------------------------------------
|
||||
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
|
||||
"""
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
|
||||
# SAR Parabol
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
"""
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# Cycle Indicator
|
||||
# ------------------------------------
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
"""
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
@@ -3,14 +3,14 @@ import json
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
||||
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||
return parse_ticker_dataframe(json.load(data_file))
|
||||
|
||||
|
||||
def test_default_strategy_structure():
|
||||
@@ -23,12 +23,13 @@ def test_default_strategy_structure():
|
||||
|
||||
|
||||
def test_default_strategy(result):
|
||||
strategy = DefaultStrategy()
|
||||
strategy = DefaultStrategy({})
|
||||
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
assert type(strategy.minimal_roi) is dict
|
||||
assert type(strategy.stoploss) is float
|
||||
assert type(strategy.ticker_interval) is str
|
||||
indicators = strategy.populate_indicators(result)
|
||||
indicators = strategy.populate_indicators(result, metadata)
|
||||
assert type(indicators) is DataFrame
|
||||
assert type(strategy.populate_buy_trend(indicators)) is DataFrame
|
||||
assert type(strategy.populate_sell_trend(indicators)) is DataFrame
|
||||
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
|
||||
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
|
||||
|
||||
202
freqtrade/tests/strategy/test_interface.py
Normal file
202
freqtrade/tests/strategy/test_interface.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
|
||||
# Avoid to reinit the same object again and again
|
||||
_STRATEGY = DefaultStrategy(config={})
|
||||
|
||||
|
||||
def test_returns_latest_buy_signal(mocker, default_conf):
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False)
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True)
|
||||
|
||||
|
||||
def test_returns_latest_sell_signal(mocker, default_conf):
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True)
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False)
|
||||
|
||||
|
||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||
None)
|
||||
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
side_effect=ValueError('xyz')
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], 1)
|
||||
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
return_value=DataFrame([])
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1)
|
||||
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||
# this is necessary as the last candle is removed (partial candles) by default
|
||||
oldtime = arrow.utcnow().shift(minutes=-16)
|
||||
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
return_value=DataFrame(ticks)
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1)
|
||||
assert log_has(
|
||||
'Outdated history for pair xyz. Last tick is 16 minutes old',
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_get_signal_handles_exceptions(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
side_effect=Exception('invalid ticker history ')
|
||||
)
|
||||
assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
|
||||
|
||||
|
||||
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
data = strategy.tickerdata_to_dataframe(tickerlist)
|
||||
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
||||
|
||||
|
||||
def test_min_roi_reached(default_conf, fee) -> None:
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
strategy.minimal_roi = {0: 0.1, 20: 0.05, 55: 0.01}
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
open_rate=1,
|
||||
)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.01, arrow.utcnow().shift(minutes=-55).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-55).datetime)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, -0.01, arrow.utcnow().shift(minutes=-1).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
|
||||
|
||||
|
||||
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
buy_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
sell_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.strategy.interface.IStrategy',
|
||||
advise_indicators=ind_mock,
|
||||
advise_buy=buy_mock,
|
||||
advise_sell=sell_mock,
|
||||
|
||||
)
|
||||
strategy = DefaultStrategy({})
|
||||
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||
assert ind_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
|
||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
||||
caplog.record_tuples)
|
||||
caplog.clear()
|
||||
|
||||
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||
# No analysis happens as process_only_new_candles is true
|
||||
assert ind_mock.call_count == 2
|
||||
assert buy_mock.call_count == 2
|
||||
assert buy_mock.call_count == 2
|
||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
buy_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
sell_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.strategy.interface.IStrategy',
|
||||
advise_indicators=ind_mock,
|
||||
advise_buy=buy_mock,
|
||||
advise_sell=sell_mock,
|
||||
|
||||
)
|
||||
strategy = DefaultStrategy({})
|
||||
strategy.process_only_new_candles = True
|
||||
|
||||
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||
assert ind_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
||||
caplog.record_tuples)
|
||||
caplog.clear()
|
||||
|
||||
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||
# No analysis happens as process_only_new_candles is true
|
||||
assert ind_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
# only skipped analyze adds buy and sell columns, otherwise it's all mocked
|
||||
assert 'buy' in ret
|
||||
assert 'sell' in ret
|
||||
assert ret['buy'].sum() == 0
|
||||
assert ret['sell'].sum() == 0
|
||||
assert not log_has('TA Analysis Launched', caplog.record_tuples)
|
||||
assert log_has('Skippinig TA Analysis for already analyzed candle',
|
||||
caplog.record_tuples)
|
||||
@@ -1,8 +1,11 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||
import logging
|
||||
import os
|
||||
from base64 import urlsafe_b64encode
|
||||
from os import path
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.strategy import import_strategy
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
@@ -12,14 +15,15 @@ from freqtrade.strategy.resolver import StrategyResolver
|
||||
|
||||
def test_import_strategy(caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_config = {}
|
||||
|
||||
strategy = DefaultStrategy()
|
||||
strategy = DefaultStrategy(default_config)
|
||||
strategy.some_method = lambda *args, **kwargs: 42
|
||||
|
||||
assert strategy.__module__ == 'freqtrade.strategy.default_strategy'
|
||||
assert strategy.some_method() == 42
|
||||
|
||||
imported_strategy = import_strategy(strategy)
|
||||
imported_strategy = import_strategy(strategy, default_config)
|
||||
|
||||
assert dir(strategy) == dir(imported_strategy)
|
||||
|
||||
@@ -35,25 +39,42 @@ def test_import_strategy(caplog):
|
||||
|
||||
|
||||
def test_search_strategy():
|
||||
default_location = os.path.join(os.path.dirname(
|
||||
os.path.realpath(__file__)), '..', '..', 'strategy'
|
||||
default_config = {}
|
||||
default_location = path.join(path.dirname(
|
||||
path.realpath(__file__)), '..', '..', 'strategy'
|
||||
)
|
||||
assert isinstance(
|
||||
StrategyResolver._search_strategy(default_location, 'DefaultStrategy'), IStrategy
|
||||
StrategyResolver._search_strategy(
|
||||
default_location,
|
||||
config=default_config,
|
||||
strategy_name='DefaultStrategy'
|
||||
),
|
||||
IStrategy
|
||||
)
|
||||
assert StrategyResolver._search_strategy(default_location, 'NotFoundStrategy') is None
|
||||
assert StrategyResolver._search_strategy(
|
||||
default_location,
|
||||
config=default_config,
|
||||
strategy_name='NotFoundStrategy'
|
||||
) is None
|
||||
|
||||
|
||||
def test_load_strategy(result):
|
||||
resolver = StrategyResolver({'strategy': 'TestStrategy'})
|
||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||
|
||||
|
||||
def test_load_strategy_byte64(result):
|
||||
with open("freqtrade/tests/strategy/test_strategy.py", "r") as file:
|
||||
encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8")
|
||||
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)})
|
||||
assert 'adx' in resolver.strategy.advise_indicators(result, 'ETH/BTC')
|
||||
|
||||
|
||||
def test_load_strategy_invalid_directory(result, caplog):
|
||||
resolver = StrategyResolver()
|
||||
extra_dir = os.path.join('some', 'path')
|
||||
resolver._load_strategy('TestStrategy', extra_dir)
|
||||
extra_dir = path.join('some', 'path')
|
||||
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
|
||||
|
||||
assert (
|
||||
'freqtrade.strategy.resolver',
|
||||
@@ -61,8 +82,7 @@ def test_load_strategy_invalid_directory(result, caplog):
|
||||
'Path "{}" does not exist'.format(extra_dir),
|
||||
) in caplog.record_tuples
|
||||
|
||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||
|
||||
|
||||
def test_load_not_found_strategy():
|
||||
@@ -70,27 +90,30 @@ def test_load_not_found_strategy():
|
||||
with pytest.raises(ImportError,
|
||||
match=r'Impossible to load Strategy \'NotFoundStrategy\'.'
|
||||
r' This class does not exist or contains Python code errors'):
|
||||
strategy._load_strategy('NotFoundStrategy')
|
||||
strategy._load_strategy(strategy_name='NotFoundStrategy', config={})
|
||||
|
||||
|
||||
def test_strategy(result):
|
||||
resolver = StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
config = {'strategy': 'DefaultStrategy'}
|
||||
|
||||
assert hasattr(resolver.strategy, 'minimal_roi')
|
||||
resolver = StrategyResolver(config)
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
assert resolver.strategy.minimal_roi[0] == 0.04
|
||||
assert config["minimal_roi"]['0'] == 0.04
|
||||
|
||||
assert hasattr(resolver.strategy, 'stoploss')
|
||||
assert resolver.strategy.stoploss == -0.10
|
||||
assert config['stoploss'] == -0.10
|
||||
|
||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||
assert resolver.strategy.ticker_interval == '5m'
|
||||
assert config['ticker_interval'] == '5m'
|
||||
|
||||
assert hasattr(resolver.strategy, 'populate_buy_trend')
|
||||
dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result))
|
||||
df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||
assert 'adx' in df_indicators
|
||||
|
||||
dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata)
|
||||
assert 'buy' in dataframe.columns
|
||||
|
||||
assert hasattr(resolver.strategy, 'populate_sell_trend')
|
||||
dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result))
|
||||
dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata)
|
||||
assert 'sell' in dataframe.columns
|
||||
|
||||
|
||||
@@ -104,11 +127,10 @@ def test_strategy_override_minimal_roi(caplog):
|
||||
}
|
||||
resolver = StrategyResolver(config)
|
||||
|
||||
assert hasattr(resolver.strategy, 'minimal_roi')
|
||||
assert resolver.strategy.minimal_roi[0] == 0.5
|
||||
assert ('freqtrade.strategy.resolver',
|
||||
logging.INFO,
|
||||
'Override strategy \'minimal_roi\' with value in config file.'
|
||||
"Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
@@ -120,11 +142,10 @@ def test_strategy_override_stoploss(caplog):
|
||||
}
|
||||
resolver = StrategyResolver(config)
|
||||
|
||||
assert hasattr(resolver.strategy, 'stoploss')
|
||||
assert resolver.strategy.stoploss == -0.5
|
||||
assert ('freqtrade.strategy.resolver',
|
||||
logging.INFO,
|
||||
'Override strategy \'stoploss\' with value in config file: -0.5.'
|
||||
"Override strategy 'stoploss' with value in config file: -0.5."
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
@@ -137,9 +158,81 @@ def test_strategy_override_ticker_interval(caplog):
|
||||
}
|
||||
resolver = StrategyResolver(config)
|
||||
|
||||
assert hasattr(resolver.strategy, 'ticker_interval')
|
||||
assert resolver.strategy.ticker_interval == 60
|
||||
assert ('freqtrade.strategy.resolver',
|
||||
logging.INFO,
|
||||
'Override strategy \'ticker_interval\' with value in config file: 60.'
|
||||
"Override strategy 'ticker_interval' with value in config file: 60."
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_strategy_override_process_only_new_candles(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
config = {
|
||||
'strategy': 'DefaultStrategy',
|
||||
'process_only_new_candles': True
|
||||
}
|
||||
resolver = StrategyResolver(config)
|
||||
|
||||
assert resolver.strategy.process_only_new_candles
|
||||
assert ('freqtrade.strategy.resolver',
|
||||
logging.INFO,
|
||||
"Override process_only_new_candles 'process_only_new_candles' "
|
||||
"with value in config file: True."
|
||||
) in caplog.record_tuples
|
||||
|
||||
|
||||
def test_deprecate_populate_indicators(result):
|
||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
||||
'strategy_path': default_location})
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Cause all warnings to always be triggered.
|
||||
warnings.simplefilter("always")
|
||||
indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC')
|
||||
assert len(w) == 1
|
||||
assert issubclass(w[-1].category, DeprecationWarning)
|
||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||
in str(w[-1].message)
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Cause all warnings to always be triggered.
|
||||
warnings.simplefilter("always")
|
||||
resolver.strategy.advise_buy(indicators, 'ETH/BTC')
|
||||
assert len(w) == 1
|
||||
assert issubclass(w[-1].category, DeprecationWarning)
|
||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||
in str(w[-1].message)
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Cause all warnings to always be triggered.
|
||||
warnings.simplefilter("always")
|
||||
resolver.strategy.advise_sell(indicators, 'ETH_BTC')
|
||||
assert len(w) == 1
|
||||
assert issubclass(w[-1].category, DeprecationWarning)
|
||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||
in str(w[-1].message)
|
||||
|
||||
|
||||
def test_call_deprecated_function(result, monkeypatch):
|
||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
||||
'strategy_path': default_location})
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
|
||||
# Make sure we are using a legacy function
|
||||
assert resolver.strategy._populate_fun_len == 2
|
||||
assert resolver.strategy._buy_fun_len == 2
|
||||
assert resolver.strategy._sell_fun_len == 2
|
||||
|
||||
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||
assert type(indicator_df) is DataFrame
|
||||
assert 'adx' in indicator_df.columns
|
||||
|
||||
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
|
||||
assert type(buydf) is DataFrame
|
||||
assert 'buy' in buydf.columns
|
||||
|
||||
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
|
||||
assert type(selldf) is DataFrame
|
||||
assert 'sell' in selldf
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
||||
|
||||
import freqtrade.tests.conftest as tt # test tools
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.tests.conftest import get_patched_freqtradebot
|
||||
|
||||
import pytest
|
||||
|
||||
# whitelist, blacklist, filtering, all of that will
|
||||
# eventually become some rules to run on a generic ACL engine
|
||||
# perhaps try to anticipate that by using some python package
|
||||
|
||||
|
||||
def whitelist_conf():
|
||||
config = tt.default_conf()
|
||||
|
||||
config['stake_currency'] = 'BTC'
|
||||
config['exchange']['pair_whitelist'] = [
|
||||
@pytest.fixture(scope="function")
|
||||
def whitelist_conf(default_conf):
|
||||
default_conf['stake_currency'] = 'BTC'
|
||||
default_conf['exchange']['pair_whitelist'] = [
|
||||
'ETH/BTC',
|
||||
'TKN/BTC',
|
||||
'TRST/BTC',
|
||||
'SWT/BTC',
|
||||
'BCC/BTC'
|
||||
]
|
||||
|
||||
config['exchange']['pair_blacklist'] = [
|
||||
default_conf['exchange']['pair_blacklist'] = [
|
||||
'BLK/BTC'
|
||||
]
|
||||
|
||||
return config
|
||||
return default_conf
|
||||
|
||||
|
||||
def test_refresh_market_pair_not_in_whitelist(mocker, markets):
|
||||
conf = whitelist_conf()
|
||||
def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf):
|
||||
|
||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||
conf['exchange']['pair_whitelist'] + ['XXX/BTC']
|
||||
whitelist_conf['exchange']['pair_whitelist'] + ['XXX/BTC']
|
||||
)
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
@@ -42,12 +42,12 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets):
|
||||
assert whitelist == refreshedwhitelist
|
||||
|
||||
|
||||
def test_refresh_whitelist(mocker, markets):
|
||||
conf = whitelist_conf()
|
||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||
def test_refresh_whitelist(mocker, markets, whitelist_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
|
||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||
whitelist_conf['exchange']['pair_whitelist'])
|
||||
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
@@ -55,9 +55,8 @@ def test_refresh_whitelist(mocker, markets):
|
||||
assert whitelist == refreshedwhitelist
|
||||
|
||||
|
||||
def test_refresh_whitelist_dynamic(mocker, markets, tickers):
|
||||
conf = whitelist_conf()
|
||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||
def test_refresh_whitelist_dynamic(mocker, markets, tickers, whitelist_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_markets=markets,
|
||||
@@ -69,21 +68,20 @@ def test_refresh_whitelist_dynamic(mocker, markets, tickers):
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
|
||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
|
||||
freqtradebot._gen_pair_whitelist(whitelist_conf['stake_currency'])
|
||||
)
|
||||
|
||||
assert whitelist == refreshedwhitelist
|
||||
|
||||
|
||||
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty):
|
||||
conf = whitelist_conf()
|
||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty, whitelist_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
|
||||
|
||||
# argument: use the whitelist dynamically by exchange-volume
|
||||
whitelist = []
|
||||
conf['exchange']['pair_whitelist'] = []
|
||||
whitelist_conf['exchange']['pair_whitelist'] = []
|
||||
freqtradebot._refresh_whitelist(whitelist)
|
||||
pairslist = conf['exchange']['pair_whitelist']
|
||||
pairslist = whitelist_conf['exchange']['pair_whitelist']
|
||||
|
||||
assert set(whitelist) == set(pairslist)
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
"""
|
||||
Unit test file for analyse.py
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import Analyze, SignalType
|
||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
||||
|
||||
# Avoid to reinit the same object again and again
|
||||
_ANALYZE = Analyze({'strategy': 'DefaultStrategy'})
|
||||
|
||||
|
||||
def test_signaltype_object() -> None:
|
||||
"""
|
||||
Test the SignalType object has the mandatory Constants
|
||||
:return: None
|
||||
"""
|
||||
assert hasattr(SignalType, 'BUY')
|
||||
assert hasattr(SignalType, 'SELL')
|
||||
|
||||
|
||||
def test_analyze_object() -> None:
|
||||
"""
|
||||
Test the Analyze object has the mandatory methods
|
||||
:return: None
|
||||
"""
|
||||
assert hasattr(Analyze, 'parse_ticker_dataframe')
|
||||
assert hasattr(Analyze, 'populate_indicators')
|
||||
assert hasattr(Analyze, 'populate_buy_trend')
|
||||
assert hasattr(Analyze, 'populate_sell_trend')
|
||||
assert hasattr(Analyze, 'analyze_ticker')
|
||||
assert hasattr(Analyze, 'get_signal')
|
||||
assert hasattr(Analyze, 'should_sell')
|
||||
assert hasattr(Analyze, 'min_roi_reached')
|
||||
|
||||
|
||||
def test_dataframe_correct_length(result):
|
||||
dataframe = Analyze.parse_ticker_dataframe(result)
|
||||
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
|
||||
|
||||
|
||||
def test_dataframe_correct_columns(result):
|
||||
assert result.columns.tolist() == \
|
||||
['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
|
||||
|
||||
def test_populates_buy_trend(result):
|
||||
# Load the default strategy for the unit test, because this logic is done in main.py
|
||||
dataframe = _ANALYZE.populate_buy_trend(_ANALYZE.populate_indicators(result))
|
||||
assert 'buy' in dataframe.columns
|
||||
|
||||
|
||||
def test_populates_sell_trend(result):
|
||||
# Load the default strategy for the unit test, because this logic is done in main.py
|
||||
dataframe = _ANALYZE.populate_sell_trend(_ANALYZE.populate_indicators(result))
|
||||
assert 'sell' in dataframe.columns
|
||||
|
||||
|
||||
def test_returns_latest_buy_signal(mocker, default_conf):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.analyze.Analyze',
|
||||
analyze_ticker=MagicMock(
|
||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
)
|
||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.analyze.Analyze',
|
||||
analyze_ticker=MagicMock(
|
||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
)
|
||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
|
||||
|
||||
|
||||
def test_returns_latest_sell_signal(mocker, default_conf):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.analyze.Analyze',
|
||||
analyze_ticker=MagicMock(
|
||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
)
|
||||
|
||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.analyze.Analyze',
|
||||
analyze_ticker=MagicMock(
|
||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
)
|
||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
|
||||
|
||||
|
||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=None)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval'])
|
||||
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.analyze.Analyze',
|
||||
analyze_ticker=MagicMock(
|
||||
side_effect=ValueError('xyz')
|
||||
)
|
||||
)
|
||||
assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval'])
|
||||
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.analyze.Analyze',
|
||||
analyze_ticker=MagicMock(
|
||||
return_value=DataFrame([])
|
||||
)
|
||||
)
|
||||
assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval'])
|
||||
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
|
||||
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
|
||||
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.analyze.Analyze',
|
||||
analyze_ticker=MagicMock(
|
||||
return_value=DataFrame(ticks)
|
||||
)
|
||||
)
|
||||
assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval'])
|
||||
assert log_has(
|
||||
'Outdated history for pair xyz. Last tick is 11 minutes old',
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_get_signal_handles_exceptions(mocker, default_conf):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.analyze.Analyze',
|
||||
analyze_ticker=MagicMock(
|
||||
side_effect=Exception('invalid ticker history ')
|
||||
)
|
||||
)
|
||||
|
||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
|
||||
|
||||
|
||||
def test_parse_ticker_dataframe(ticker_history):
|
||||
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
|
||||
# Test file with BV data
|
||||
dataframe = Analyze.parse_ticker_dataframe(ticker_history)
|
||||
assert dataframe.columns.tolist() == columns
|
||||
|
||||
|
||||
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||
"""
|
||||
Test Analyze.tickerdata_to_dataframe() method
|
||||
"""
|
||||
analyze = Analyze(default_conf)
|
||||
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
data = analyze.tickerdata_to_dataframe(tickerlist)
|
||||
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
||||
@@ -1,41 +1,24 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
"""
|
||||
Unit test file for arguments.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.arguments import Arguments, TimeRange
|
||||
|
||||
|
||||
def test_arguments_object() -> None:
|
||||
"""
|
||||
Test the Arguments object has the mandatory methods
|
||||
:return: None
|
||||
"""
|
||||
assert hasattr(Arguments, 'get_parsed_arg')
|
||||
assert hasattr(Arguments, 'parse_args')
|
||||
assert hasattr(Arguments, 'parse_timerange')
|
||||
assert hasattr(Arguments, 'scripts_options')
|
||||
|
||||
|
||||
# Parse common command-line-arguments. Used for all tools
|
||||
def test_parse_args_none() -> None:
|
||||
arguments = Arguments([], '')
|
||||
assert isinstance(arguments, Arguments)
|
||||
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||
|
||||
|
||||
def test_parse_args_defaults() -> None:
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
assert args.config == 'config.json'
|
||||
assert args.dynamic_whitelist is None
|
||||
assert args.loglevel == logging.INFO
|
||||
assert args.loglevel == 0
|
||||
|
||||
|
||||
def test_parse_args_config() -> None:
|
||||
@@ -53,10 +36,10 @@ def test_parse_args_db_url() -> None:
|
||||
|
||||
def test_parse_args_verbose() -> None:
|
||||
args = Arguments(['-v'], '').get_parsed_arg()
|
||||
assert args.loglevel == logging.DEBUG
|
||||
assert args.loglevel == 1
|
||||
|
||||
args = Arguments(['--verbose'], '').get_parsed_arg()
|
||||
assert args.loglevel == logging.DEBUG
|
||||
assert args.loglevel == 1
|
||||
|
||||
|
||||
def test_scripts_options() -> None:
|
||||
@@ -149,15 +132,21 @@ def test_parse_args_backtesting_custom() -> None:
|
||||
'backtesting',
|
||||
'--live',
|
||||
'--ticker-interval', '1m',
|
||||
'--refresh-pairs-cached']
|
||||
'--refresh-pairs-cached',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
'TestStrategy'
|
||||
]
|
||||
call_args = Arguments(args, '').get_parsed_arg()
|
||||
assert call_args.config == 'test_conf.json'
|
||||
assert call_args.live is True
|
||||
assert call_args.loglevel == logging.INFO
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'backtesting'
|
||||
assert call_args.func is not None
|
||||
assert call_args.ticker_interval == '1m'
|
||||
assert call_args.refresh_pairs is True
|
||||
assert type(call_args.strategy_list) is list
|
||||
assert len(call_args.strategy_list) == 2
|
||||
|
||||
|
||||
def test_parse_args_hyperopt_custom() -> None:
|
||||
@@ -170,7 +159,7 @@ def test_parse_args_hyperopt_custom() -> None:
|
||||
call_args = Arguments(args, '').get_parsed_arg()
|
||||
assert call_args.config == 'test_conf.json'
|
||||
assert call_args.epochs == 20
|
||||
assert call_args.loglevel == logging.INFO
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'hyperopt'
|
||||
assert call_args.spaces == ['buy']
|
||||
assert call_args.func is not None
|
||||
|
||||
@@ -1,76 +1,46 @@
|
||||
# pragma pylint: disable=protected-access, invalid-name
|
||||
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||
|
||||
"""
|
||||
Unit test file for configuration.py
|
||||
"""
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
from argparse import Namespace
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from jsonschema import ValidationError
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.constants import DEFAULT_DB_PROD_URL, DEFAULT_DB_DRYRUN_URL
|
||||
from freqtrade.tests.conftest import log_has
|
||||
from freqtrade import constants
|
||||
from freqtrade import OperationalException
|
||||
|
||||
|
||||
def test_configuration_object() -> None:
|
||||
"""
|
||||
Test the Constants object has the mandatory Constants
|
||||
"""
|
||||
assert hasattr(Configuration, 'load_config')
|
||||
assert hasattr(Configuration, '_load_config_file')
|
||||
assert hasattr(Configuration, '_validate_config')
|
||||
assert hasattr(Configuration, '_load_common_config')
|
||||
assert hasattr(Configuration, '_load_backtesting_config')
|
||||
assert hasattr(Configuration, '_load_hyperopt_config')
|
||||
assert hasattr(Configuration, 'get_config')
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import Configuration, set_loggers
|
||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
|
||||
def test_load_config_invalid_pair(default_conf) -> None:
|
||||
"""
|
||||
Test the configuration validator with an invalid PAIR format
|
||||
"""
|
||||
conf = deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||
default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config(conf)
|
||||
configuration._validate_config(default_conf)
|
||||
|
||||
|
||||
def test_load_config_missing_attributes(default_conf) -> None:
|
||||
"""
|
||||
Test the configuration validator with a missing attribute
|
||||
"""
|
||||
conf = deepcopy(default_conf)
|
||||
conf.pop('exchange')
|
||||
default_conf.pop('exchange')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config(conf)
|
||||
configuration._validate_config(default_conf)
|
||||
|
||||
|
||||
def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
||||
"""
|
||||
Test the configuration validator with a missing attribute
|
||||
"""
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] = 'fake'
|
||||
default_conf['stake_amount'] = 'fake'
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
|
||||
configuration = Configuration(Namespace())
|
||||
configuration._validate_config(conf)
|
||||
configuration._validate_config(default_conf)
|
||||
|
||||
|
||||
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test Configuration._load_config_file() method
|
||||
"""
|
||||
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
@@ -84,13 +54,9 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
|
||||
|
||||
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test Configuration._load_config_file() method
|
||||
"""
|
||||
conf = deepcopy(default_conf)
|
||||
conf['max_open_trades'] = 0
|
||||
default_conf['max_open_trades'] = 0
|
||||
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(conf)
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
Configuration(Namespace())._load_config_file('somefile')
|
||||
@@ -99,9 +65,6 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||
|
||||
|
||||
def test_load_config_file_exception(mocker) -> None:
|
||||
"""
|
||||
Test Configuration._load_config_file() method
|
||||
"""
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.open',
|
||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||
@@ -113,9 +76,6 @@ def test_load_config_file_exception(mocker) -> None:
|
||||
|
||||
|
||||
def test_load_config(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test Configuration.load_config() without any cli params
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
@@ -130,13 +90,9 @@ def test_load_config(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test Configuration.load_config() with cli params used
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--dynamic-whitelist', '10',
|
||||
'--strategy', 'TestStrategy',
|
||||
@@ -144,7 +100,6 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
'--db-url', 'sqlite:///someurl',
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@@ -161,10 +116,10 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--dynamic-whitelist', '10',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
'--dynamic-whitelist', '10',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
@@ -192,16 +147,12 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
"""
|
||||
Test Configuration.load_config() without any cli params
|
||||
"""
|
||||
custom_conf = deepcopy(default_conf)
|
||||
custom_conf.update({
|
||||
default_conf.update({
|
||||
'strategy': 'CustomStrategy',
|
||||
'strategy_path': '/tmp/strategies',
|
||||
})
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(custom_conf)
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
@@ -213,13 +164,9 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_show_info(default_conf, mocker, caplog) -> None:
|
||||
"""
|
||||
Test Configuration.show_info()
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--dynamic-whitelist', '10',
|
||||
'--strategy', 'TestStrategy',
|
||||
@@ -236,19 +183,14 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
||||
'(not applicable with Backtesting and Hyperopt)',
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
|
||||
assert log_has('Dry run is enabled', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
@@ -275,8 +217,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
assert 'live' not in config
|
||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'realistic_simulation' not in config
|
||||
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||
assert 'position_stacking' not in config
|
||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'refresh_pairs' not in config
|
||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||
@@ -286,9 +228,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
|
||||
|
||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
@@ -300,7 +239,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--live',
|
||||
'--realistic-simulation',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--refresh-pairs-cached',
|
||||
'--timerange', ':100',
|
||||
'--export', '/bar/foo'
|
||||
@@ -330,9 +270,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
assert 'live' in config
|
||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'realistic_simulation'in config
|
||||
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
||||
assert 'position_stacking'in config
|
||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||
|
||||
assert 'use_max_market_positions' in config
|
||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
||||
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
||||
|
||||
assert 'refresh_pairs'in config
|
||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||
@@ -349,7 +292,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||
)
|
||||
|
||||
|
||||
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
@@ -357,12 +300,62 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--export', '/bar/foo',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
'TestStrategy'
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has(
|
||||
'Using data folder: {} ...'.format(config['datadir']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||
assert log_has(
|
||||
'Using ticker_interval: 1m ...',
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
assert 'strategy_list' in config
|
||||
assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
|
||||
assert 'use_max_market_positions' not in config
|
||||
|
||||
assert 'timerange' not in config
|
||||
|
||||
assert 'export' in config
|
||||
assert log_has(
|
||||
'Parameter --export detected: {} ...'.format(config['export']),
|
||||
caplog.record_tuples
|
||||
)
|
||||
|
||||
|
||||
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
arglist = [
|
||||
'hyperopt',
|
||||
'--epochs', '10',
|
||||
'--spaces', 'all',
|
||||
]
|
||||
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
@@ -379,26 +372,79 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_check_exchange(default_conf) -> None:
|
||||
"""
|
||||
Test the configuration validator with a missing attribute
|
||||
"""
|
||||
conf = deepcopy(default_conf)
|
||||
configuration = Configuration(Namespace())
|
||||
|
||||
# Test a valid exchange
|
||||
conf.get('exchange').update({'name': 'BITTREX'})
|
||||
assert configuration.check_exchange(conf)
|
||||
default_conf.get('exchange').update({'name': 'BITTREX'})
|
||||
assert configuration.check_exchange(default_conf)
|
||||
|
||||
# Test a valid exchange
|
||||
conf.get('exchange').update({'name': 'binance'})
|
||||
assert configuration.check_exchange(conf)
|
||||
default_conf.get('exchange').update({'name': 'binance'})
|
||||
assert configuration.check_exchange(default_conf)
|
||||
|
||||
# Test a invalid exchange
|
||||
conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||
configuration.config = conf
|
||||
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||
configuration.config = default_conf
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r'.*Exchange "unknown_exchange" not supported.*'
|
||||
):
|
||||
configuration.check_exchange(conf)
|
||||
configuration.check_exchange(default_conf)
|
||||
|
||||
|
||||
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)))
|
||||
# Prevent setting loggers
|
||||
mocker.patch('freqtrade.configuration.set_loggers', MagicMock)
|
||||
arglist = ['-vvv']
|
||||
args = Arguments(arglist, '').get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('verbosity') == 3
|
||||
assert log_has('Verbosity set to 3', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_set_loggers() -> None:
|
||||
# Reset Logging to Debug, otherwise this fails randomly as it's set globally
|
||||
logging.getLogger('requests').setLevel(logging.DEBUG)
|
||||
logging.getLogger("urllib3").setLevel(logging.DEBUG)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(logging.DEBUG)
|
||||
logging.getLogger('telegram').setLevel(logging.DEBUG)
|
||||
|
||||
previous_value1 = logging.getLogger('requests').level
|
||||
previous_value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
previous_value3 = logging.getLogger('telegram').level
|
||||
|
||||
set_loggers()
|
||||
|
||||
value1 = logging.getLogger('requests').level
|
||||
assert previous_value1 is not value1
|
||||
assert value1 is logging.INFO
|
||||
|
||||
value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
assert previous_value2 is not value2
|
||||
assert value2 is logging.INFO
|
||||
|
||||
value3 = logging.getLogger('telegram').level
|
||||
assert previous_value3 is not value3
|
||||
assert value3 is logging.INFO
|
||||
|
||||
set_loggers(log_level=2)
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
|
||||
set_loggers(log_level=3)
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
|
||||
|
||||
def test_validate_default_conf(default_conf) -> None:
|
||||
validate(default_conf, constants.CONF_SCHEMA)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
Unit test file for constants.py
|
||||
"""
|
||||
|
||||
from freqtrade import constants
|
||||
|
||||
|
||||
def test_constant_object() -> None:
|
||||
"""
|
||||
Test the Constants object has the mandatory Constants
|
||||
"""
|
||||
assert hasattr(constants, 'CONF_SCHEMA')
|
||||
assert hasattr(constants, 'DYNAMIC_WHITELIST')
|
||||
assert hasattr(constants, 'PROCESS_THROTTLE_SECS')
|
||||
assert hasattr(constants, 'TICKER_INTERVAL')
|
||||
assert hasattr(constants, 'HYPEROPT_EPOCH')
|
||||
assert hasattr(constants, 'RETRY_TIMEOUT')
|
||||
assert hasattr(constants, 'DEFAULT_STRATEGY')
|
||||
|
||||
|
||||
def test_conf_schema() -> None:
|
||||
"""
|
||||
Test the CONF_SCHEMA is from the right type
|
||||
"""
|
||||
assert isinstance(constants.CONF_SCHEMA, dict)
|
||||
@@ -2,33 +2,31 @@
|
||||
|
||||
import pandas
|
||||
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade.optimize import load_data
|
||||
from freqtrade.strategy.resolver import StrategyResolver
|
||||
|
||||
_pairs = ['ETH/BTC']
|
||||
|
||||
|
||||
def load_dataframe_pair(pairs):
|
||||
def load_dataframe_pair(pairs, strategy):
|
||||
ld = load_data(None, ticker_interval='5m', pairs=pairs)
|
||||
assert isinstance(ld, dict)
|
||||
assert isinstance(pairs[0], str)
|
||||
dataframe = ld[pairs[0]]
|
||||
|
||||
analyze = Analyze({'strategy': 'DefaultStrategy'})
|
||||
dataframe = analyze.analyze_ticker(dataframe)
|
||||
dataframe = strategy.analyze_ticker(dataframe, {'pair': pairs[0]})
|
||||
return dataframe
|
||||
|
||||
|
||||
def test_dataframe_load():
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
dataframe = load_dataframe_pair(_pairs)
|
||||
strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy
|
||||
dataframe = load_dataframe_pair(_pairs, strategy)
|
||||
assert isinstance(dataframe, pandas.core.frame.DataFrame)
|
||||
|
||||
|
||||
def test_dataframe_columns_exists():
|
||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||
dataframe = load_dataframe_pair(_pairs)
|
||||
strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy
|
||||
dataframe = load_dataframe_pair(_pairs, strategy)
|
||||
assert 'high' in dataframe.columns
|
||||
assert 'low' in dataframe.columns
|
||||
assert 'close' in dataframe.columns
|
||||
|
||||
@@ -5,7 +5,6 @@ import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
||||
@@ -184,6 +183,24 @@ def test_fiat_convert_without_network(mocker):
|
||||
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
||||
|
||||
|
||||
def test_fiat_invalid_response(mocker, caplog):
|
||||
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
||||
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.fiat_convert.Market',
|
||||
listings=listmock,
|
||||
)
|
||||
# with pytest.raises(RequestEsxception):
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
fiat_convert._cryptomap = {}
|
||||
fiat_convert._load_cryptomap()
|
||||
|
||||
length_cryptomap = len(fiat_convert._cryptomap)
|
||||
assert length_cryptomap == 0
|
||||
assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError',
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_convert_amount(mocker):
|
||||
patch_coinmarketcap(mocker)
|
||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.indicator_helpers import went_up, went_down
|
||||
from freqtrade.indicator_helpers import went_down, went_up
|
||||
|
||||
|
||||
def test_went_up():
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""
|
||||
Unit test file for main.py
|
||||
"""
|
||||
# pragma pylint: disable=missing-docstring
|
||||
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -11,7 +8,7 @@ import pytest
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.main import main, set_loggers, reconfigure
|
||||
from freqtrade.main import main, reconfigure
|
||||
from freqtrade.state import State
|
||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||
|
||||
@@ -27,49 +24,24 @@ def test_parse_args_backtesting(mocker) -> None:
|
||||
call_args = backtesting_mock.call_args[0][0]
|
||||
assert call_args.config == 'config.json'
|
||||
assert call_args.live is False
|
||||
assert call_args.loglevel == 20
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'backtesting'
|
||||
assert call_args.func is not None
|
||||
assert call_args.ticker_interval is None
|
||||
|
||||
|
||||
def test_main_start_hyperopt(mocker) -> None:
|
||||
"""
|
||||
Test that main() can start hyperopt
|
||||
"""
|
||||
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
|
||||
main(['hyperopt'])
|
||||
assert hyperopt_mock.call_count == 1
|
||||
call_args = hyperopt_mock.call_args[0][0]
|
||||
assert call_args.config == 'config.json'
|
||||
assert call_args.loglevel == 20
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'hyperopt'
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
def test_set_loggers() -> None:
|
||||
"""
|
||||
Test set_loggers() update the logger level for third-party libraries
|
||||
"""
|
||||
previous_value1 = logging.getLogger('requests.packages.urllib3').level
|
||||
previous_value2 = logging.getLogger('telegram').level
|
||||
|
||||
set_loggers()
|
||||
|
||||
value1 = logging.getLogger('requests.packages.urllib3').level
|
||||
assert previous_value1 is not value1
|
||||
assert value1 is logging.INFO
|
||||
|
||||
value2 = logging.getLogger('telegram').level
|
||||
assert previous_value2 is not value2
|
||||
assert value2 is logging.INFO
|
||||
|
||||
|
||||
def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test main() function
|
||||
In this test we are skipping the while True loop by throwing an exception.
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
@@ -81,7 +53,6 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
@@ -94,10 +65,6 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test main() function
|
||||
In this test we are skipping the while True loop by throwing an exception.
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
@@ -109,7 +76,6 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
@@ -122,10 +88,6 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test main() function
|
||||
In this test we are skipping the while True loop by throwing an exception.
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
@@ -137,7 +99,6 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
@@ -150,10 +111,6 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test main() function
|
||||
In this test we are skipping the while True loop by throwing an exception.
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
@@ -165,7 +122,6 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
# Raise exception as side effect to avoid endless loop
|
||||
@@ -181,7 +137,6 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
def test_reconfigure(mocker, default_conf) -> None:
|
||||
""" Test recreate() function """
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
@@ -193,7 +148,6 @@ def test_reconfigure(mocker, default_conf) -> None:
|
||||
'freqtrade.configuration.Configuration._load_config_file',
|
||||
lambda *args, **kwargs: default_conf
|
||||
)
|
||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
# pragma pylint: disable=missing-docstring,C0103
|
||||
|
||||
"""
|
||||
Unit test file for misc.py
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
|
||||
common_datearray, file_dump_json, format_ms_time)
|
||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
|
||||
file_dump_json, format_ms_time, shorten_date)
|
||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
|
||||
|
||||
def test_shorten_date() -> None:
|
||||
"""
|
||||
Test shorten_date() function
|
||||
:return: None
|
||||
"""
|
||||
str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
|
||||
str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
|
||||
assert shorten_date(str_data) == str_shorten_data
|
||||
|
||||
|
||||
def test_datesarray_to_datetimearray(ticker_history):
|
||||
"""
|
||||
Test datesarray_to_datetimearray() function
|
||||
:return: None
|
||||
"""
|
||||
dataframes = Analyze.parse_ticker_dataframe(ticker_history)
|
||||
dataframes = parse_ticker_dataframe(ticker_history)
|
||||
dates = datesarray_to_datetimearray(dataframes['date'])
|
||||
|
||||
assert isinstance(dates[0], datetime.datetime)
|
||||
@@ -43,14 +32,10 @@ def test_datesarray_to_datetimearray(ticker_history):
|
||||
|
||||
|
||||
def test_common_datearray(default_conf) -> None:
|
||||
"""
|
||||
Test common_datearray()
|
||||
:return: None
|
||||
"""
|
||||
analyze = Analyze(default_conf)
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||
tickerlist = {'UNITTEST/BTC': tick}
|
||||
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
|
||||
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
|
||||
|
||||
dates = common_datearray(dataframes)
|
||||
|
||||
@@ -60,10 +45,6 @@ def test_common_datearray(default_conf) -> None:
|
||||
|
||||
|
||||
def test_file_dump_json(mocker) -> None:
|
||||
"""
|
||||
Test file_dump_json()
|
||||
:return: None
|
||||
"""
|
||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||
json_dump = mocker.patch('json.dump', MagicMock())
|
||||
file_dump_json('somefile', [1, 2, 3])
|
||||
@@ -77,10 +58,6 @@ def test_file_dump_json(mocker) -> None:
|
||||
|
||||
|
||||
def test_format_ms_time() -> None:
|
||||
"""
|
||||
test format_ms_time()
|
||||
:return: None
|
||||
"""
|
||||
# Date 2018-04-10 18:02:01
|
||||
date_in_epoch_ms = 1523383321000
|
||||
date = format_ms_time(date_in_epoch_ms)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from freqtrade import constants, OperationalException
|
||||
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.persistence import Trade, clean_dry_run_db, init
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
@@ -22,46 +23,40 @@ def test_init_create_session(default_conf):
|
||||
|
||||
|
||||
def test_init_custom_db_url(default_conf, mocker):
|
||||
conf = deepcopy(default_conf)
|
||||
|
||||
# Update path to a value other than default, but still in-memory
|
||||
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
||||
default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
|
||||
init(conf)
|
||||
init(default_conf)
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
||||
|
||||
|
||||
def test_init_invalid_db_url(default_conf):
|
||||
conf = deepcopy(default_conf)
|
||||
|
||||
# Update path to a value other than default, but still in-memory
|
||||
conf.update({'db_url': 'unknown:///some.url'})
|
||||
default_conf.update({'db_url': 'unknown:///some.url'})
|
||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||
init(conf)
|
||||
init(default_conf)
|
||||
|
||||
|
||||
def test_init_prod_db(default_conf, mocker):
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'dry_run': False})
|
||||
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||
default_conf.update({'dry_run': False})
|
||||
default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
|
||||
init(conf)
|
||||
init(default_conf)
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||
|
||||
|
||||
def test_init_dryrun_db(default_conf, mocker):
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({'dry_run': True})
|
||||
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||
default_conf.update({'dry_run': True})
|
||||
default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
|
||||
init(conf)
|
||||
init(default_conf)
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
||||
|
||||
@@ -400,13 +395,18 @@ def test_migrate_old(mocker, default_conf, fee):
|
||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||
assert trade.pair == "ETC/BTC"
|
||||
assert trade.exchange == "bittrex"
|
||||
assert trade.max_rate == 0.0
|
||||
assert trade.stop_loss == 0.0
|
||||
assert trade.initial_stop_loss == 0.0
|
||||
|
||||
|
||||
def test_migrate_new(mocker, default_conf, fee):
|
||||
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
amount = 103.223
|
||||
# Always create all columns apart from the last!
|
||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||
id INTEGER NOT NULL,
|
||||
exchange VARCHAR NOT NULL,
|
||||
@@ -421,14 +421,21 @@ def test_migrate_new(mocker, default_conf, fee):
|
||||
open_date DATETIME NOT NULL,
|
||||
close_date DATETIME,
|
||||
open_order_id VARCHAR,
|
||||
stop_loss FLOAT,
|
||||
initial_stop_loss FLOAT,
|
||||
max_rate FLOAT,
|
||||
sell_reason VARCHAR,
|
||||
strategy VARCHAR,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (is_open IN (0, 1))
|
||||
);"""
|
||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
||||
open_rate, stake_amount, amount, open_date)
|
||||
open_rate, stake_amount, amount, open_date,
|
||||
stop_loss, initial_stop_loss, max_rate)
|
||||
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
||||
0.00258580, {stake}, {amount},
|
||||
'2019-11-28 12:44:24.000000')
|
||||
'2019-11-28 12:44:24.000000',
|
||||
0.0, 0.0, 0.0)
|
||||
""".format(fee=fee.return_value,
|
||||
stake=default_conf.get("stake_amount"),
|
||||
amount=amount
|
||||
@@ -439,6 +446,11 @@ def test_migrate_new(mocker, default_conf, fee):
|
||||
# Create table using the old format
|
||||
engine.execute(create_table_old)
|
||||
engine.execute(insert_table_old)
|
||||
|
||||
# fake previous backup
|
||||
engine.execute("create table trades_bak as select * from trades")
|
||||
|
||||
engine.execute("create table trades_bak1 as select * from trades")
|
||||
# Run init to test migration
|
||||
init(default_conf)
|
||||
|
||||
@@ -453,3 +465,121 @@ def test_migrate_new(mocker, default_conf, fee):
|
||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||
assert trade.pair == "ETC/BTC"
|
||||
assert trade.exchange == "binance"
|
||||
assert trade.max_rate == 0.0
|
||||
assert trade.stop_loss == 0.0
|
||||
assert trade.initial_stop_loss == 0.0
|
||||
assert trade.sell_reason is None
|
||||
assert trade.strategy is None
|
||||
assert trade.ticker_interval is None
|
||||
assert log_has("trying trades_bak1", caplog.record_tuples)
|
||||
assert log_has("trying trades_bak2", caplog.record_tuples)
|
||||
assert log_has("Running database migration - backup available as trades_bak2",
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
Test Database migration (starting with new pairformat)
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
amount = 103.223
|
||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||
id INTEGER NOT NULL,
|
||||
exchange VARCHAR NOT NULL,
|
||||
pair VARCHAR NOT NULL,
|
||||
is_open BOOLEAN NOT NULL,
|
||||
fee_open FLOAT NOT NULL,
|
||||
fee_close FLOAT NOT NULL,
|
||||
open_rate FLOAT,
|
||||
close_rate FLOAT,
|
||||
close_profit FLOAT,
|
||||
stake_amount FLOAT NOT NULL,
|
||||
amount FLOAT,
|
||||
open_date DATETIME NOT NULL,
|
||||
close_date DATETIME,
|
||||
open_order_id VARCHAR,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (is_open IN (0, 1))
|
||||
);"""
|
||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
|
||||
open_rate, stake_amount, amount, open_date)
|
||||
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
|
||||
0.00258580, {stake}, {amount},
|
||||
'2019-11-28 12:44:24.000000')
|
||||
""".format(fee=fee.return_value,
|
||||
stake=default_conf.get("stake_amount"),
|
||||
amount=amount
|
||||
)
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
engine.execute(create_table_old)
|
||||
engine.execute(insert_table_old)
|
||||
|
||||
# Run init to test migration
|
||||
init(default_conf)
|
||||
|
||||
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||
trade = Trade.query.filter(Trade.id == 1).first()
|
||||
assert trade.fee_open == fee.return_value
|
||||
assert trade.fee_close == fee.return_value
|
||||
assert trade.open_rate_requested is None
|
||||
assert trade.close_rate_requested is None
|
||||
assert trade.is_open == 1
|
||||
assert trade.amount == amount
|
||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||
assert trade.pair == "ETC/BTC"
|
||||
assert trade.exchange == "binance"
|
||||
assert trade.max_rate == 0.0
|
||||
assert trade.stop_loss == 0.0
|
||||
assert trade.initial_stop_loss == 0.0
|
||||
assert log_has("trying trades_bak0", caplog.record_tuples)
|
||||
assert log_has("Running database migration - backup available as trades_bak0",
|
||||
caplog.record_tuples)
|
||||
|
||||
|
||||
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
open_rate=1,
|
||||
)
|
||||
|
||||
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
|
||||
assert trade.stop_loss == 0.95
|
||||
assert trade.max_rate == 1
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
|
||||
# Get percent of profit with a lowre rate
|
||||
trade.adjust_stop_loss(0.96, 0.05)
|
||||
assert trade.stop_loss == 0.95
|
||||
assert trade.max_rate == 1
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
|
||||
# Get percent of profit with a custom rate (Higher than open rate)
|
||||
trade.adjust_stop_loss(1.3, -0.1)
|
||||
assert round(trade.stop_loss, 8) == 1.17
|
||||
assert trade.max_rate == 1.3
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
|
||||
# current rate lower again ... should not change
|
||||
trade.adjust_stop_loss(1.2, 0.1)
|
||||
assert round(trade.stop_loss, 8) == 1.17
|
||||
assert trade.max_rate == 1.3
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
|
||||
# current rate higher... should raise stoploss
|
||||
trade.adjust_stop_loss(1.4, 0.1)
|
||||
assert round(trade.stop_loss, 8) == 1.26
|
||||
assert trade.max_rate == 1.4
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
|
||||
# Initial is true but stop_loss set - so doesn't do anything
|
||||
trade.adjust_stop_loss(1.7, 0.1, True)
|
||||
assert round(trade.stop_loss, 8) == 1.26
|
||||
assert trade.max_rate == 1.4
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
Unit test file for constants.py
|
||||
"""
|
||||
|
||||
from freqtrade.state import State
|
||||
|
||||
|
||||
def test_state_object() -> None:
|
||||
"""
|
||||
Test the State object has the mandatory states
|
||||
:return: None
|
||||
"""
|
||||
assert hasattr(State, 'RUNNING')
|
||||
assert hasattr(State, 'STOPPED')
|
||||
16
freqtrade/tests/test_talib.py
Normal file
16
freqtrade/tests/test_talib.py
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
import talib.abstract as ta
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def test_talib_bollingerbands_near_zero_values():
|
||||
inputs = pd.DataFrame([
|
||||
{'close': 0.00000010},
|
||||
{'close': 0.00000011},
|
||||
{'close': 0.00000012},
|
||||
{'close': 0.00000013},
|
||||
{'close': 0.00000014}
|
||||
])
|
||||
bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
|
||||
assert (bollinger['upperband'][3] != bollinger['middleband'][3])
|
||||
@@ -1,6 +1,6 @@
|
||||
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||
cd ta-lib && ./configure && make && sudo make install && cd ..
|
||||
cd ta-lib && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && ./configure && make && sudo make install && cd ..
|
||||
else
|
||||
echo "TA-lib already installed, skipping download and build."
|
||||
cd ta-lib && sudo make install && cd ..
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
ccxt==1.14.256
|
||||
SQLAlchemy==1.2.8
|
||||
python-telegram-bot==10.1.0
|
||||
ccxt==1.17.363
|
||||
SQLAlchemy==1.2.12
|
||||
python-telegram-bot==11.1.0
|
||||
arrow==0.12.1
|
||||
cachetools==2.1.0
|
||||
requests==2.19.1
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
pandas==0.23.1
|
||||
scikit-learn==0.19.1
|
||||
pandas==0.23.4
|
||||
scikit-learn==0.20.0
|
||||
scipy==1.1.0
|
||||
jsonschema==2.6.0
|
||||
numpy==1.14.5
|
||||
numpy==1.15.2
|
||||
TA-Lib==0.4.17
|
||||
pytest==3.6.2
|
||||
pytest==3.8.1
|
||||
pytest-mock==1.10.0
|
||||
pytest-cov==2.5.1
|
||||
hyperopt==0.1
|
||||
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
||||
networkx==1.11 # pyup: ignore
|
||||
pytest-asyncio==0.9.0
|
||||
pytest-cov==2.6.0
|
||||
tabulate==0.8.2
|
||||
coinmarketcap==5.0.3
|
||||
|
||||
# Required for hyperopt
|
||||
scikit-optimize==0.5.2
|
||||
|
||||
# Required for plotting data
|
||||
#plotly==2.7.0
|
||||
#plotly==3.1.1
|
||||
|
||||
@@ -143,15 +143,14 @@ def convert_main(args: Namespace) -> None:
|
||||
interval = str_interval
|
||||
break
|
||||
# change order on pairs if old ticker interval found
|
||||
|
||||
filename_new = path.join(path.dirname(filename),
|
||||
"{}_{}-{}.json".format(currencies[1],
|
||||
currencies[0], interval))
|
||||
f"{currencies[1]}_{currencies[0]}-{interval}.json")
|
||||
|
||||
elif ret_string:
|
||||
interval = ret_string.group(0)
|
||||
filename_new = path.join(path.dirname(filename),
|
||||
"{}_{}-{}.json".format(currencies[0],
|
||||
currencies[1], interval))
|
||||
f"{currencies[0]}_{currencies[1]}-{interval}.json")
|
||||
|
||||
else:
|
||||
logger.warning("file %s could not be converted, interval not found", filename)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""This script generate json data from bittrex"""
|
||||
"""This script generate json data"""
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
import arrow
|
||||
|
||||
from freqtrade import (arguments, misc)
|
||||
from freqtrade import arguments
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.optimize import download_backtesting_testdata
|
||||
|
||||
|
||||
DEFAULT_DL_PATH = 'user_data/data'
|
||||
|
||||
@@ -17,25 +20,27 @@ args = arguments.parse_args()
|
||||
|
||||
timeframes = args.timeframes
|
||||
|
||||
dl_path = os.path.join(DEFAULT_DL_PATH, args.exchange)
|
||||
dl_path = Path(DEFAULT_DL_PATH).joinpath(args.exchange)
|
||||
if args.export:
|
||||
dl_path = args.export
|
||||
dl_path = Path(args.export)
|
||||
|
||||
if not os.path.isdir(dl_path):
|
||||
if not dl_path.is_dir():
|
||||
sys.exit(f'Directory {dl_path} does not exist.')
|
||||
|
||||
pairs_file = args.pairs_file if args.pairs_file else os.path.join(dl_path, 'pairs.json')
|
||||
if not os.path.isfile(pairs_file):
|
||||
pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json')
|
||||
if not pairs_file.exists():
|
||||
sys.exit(f'No pairs file found with path {pairs_file}.')
|
||||
|
||||
with open(pairs_file) as file:
|
||||
with pairs_file.open() as file:
|
||||
PAIRS = list(set(json.load(file)))
|
||||
|
||||
PAIRS.sort()
|
||||
|
||||
since_time = None
|
||||
|
||||
timerange = TimeRange()
|
||||
if args.days:
|
||||
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
|
||||
time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d")
|
||||
timerange = arguments.parse_timerange(f'{time_since}-')
|
||||
|
||||
|
||||
print(f'About to download pairs: {PAIRS} to {dl_path}')
|
||||
@@ -47,9 +52,10 @@ exchange = Exchange({'key': '',
|
||||
'stake_currency': '',
|
||||
'dry_run': True,
|
||||
'exchange': {
|
||||
'name': args.exchange,
|
||||
'pair_whitelist': []
|
||||
}
|
||||
'name': args.exchange,
|
||||
'pair_whitelist': [],
|
||||
'ccxt_rate_limit': False
|
||||
}
|
||||
})
|
||||
pairs_not_available = []
|
||||
|
||||
@@ -59,21 +65,18 @@ for pair in PAIRS:
|
||||
print(f"skipping pair {pair}")
|
||||
continue
|
||||
for tick_interval in timeframes:
|
||||
print(f'downloading pair {pair}, interval {tick_interval}')
|
||||
|
||||
data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time)
|
||||
if not data:
|
||||
print('\tNo data was downloaded')
|
||||
break
|
||||
|
||||
print('\tData was downloaded for period %s - %s' % (
|
||||
arrow.get(data[0][0] / 1000).format(),
|
||||
arrow.get(data[-1][0] / 1000).format()))
|
||||
|
||||
# save data
|
||||
pair_print = pair.replace('/', '_')
|
||||
filename = f'{pair_print}-{tick_interval}.json'
|
||||
misc.file_dump_json(os.path.join(dl_path, filename), data)
|
||||
dl_file = dl_path.joinpath(filename)
|
||||
if args.erase and dl_file.exists():
|
||||
print(f'Deleting existing data for pair {pair}, interval {tick_interval}')
|
||||
dl_file.unlink()
|
||||
|
||||
print(f'downloading pair {pair}, interval {tick_interval}')
|
||||
download_backtesting_testdata(str(dl_path), exchange=exchange,
|
||||
pair=pair,
|
||||
tick_interval=tick_interval,
|
||||
timerange=timerange)
|
||||
|
||||
|
||||
if pairs_not_available:
|
||||
|
||||
93
scripts/get_market_pairs.py
Normal file
93
scripts/get_market_pairs.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.append(root + '/python')
|
||||
|
||||
import ccxt # noqa: E402
|
||||
|
||||
|
||||
def style(s, style):
|
||||
return style + s + '\033[0m'
|
||||
|
||||
|
||||
def green(s):
|
||||
return style(s, '\033[92m')
|
||||
|
||||
|
||||
def blue(s):
|
||||
return style(s, '\033[94m')
|
||||
|
||||
|
||||
def yellow(s):
|
||||
return style(s, '\033[93m')
|
||||
|
||||
|
||||
def red(s):
|
||||
return style(s, '\033[91m')
|
||||
|
||||
|
||||
def pink(s):
|
||||
return style(s, '\033[95m')
|
||||
|
||||
|
||||
def bold(s):
|
||||
return style(s, '\033[1m')
|
||||
|
||||
|
||||
def underline(s):
|
||||
return style(s, '\033[4m')
|
||||
|
||||
|
||||
def dump(*args):
|
||||
print(' '.join([str(arg) for arg in args]))
|
||||
|
||||
|
||||
def print_supported_exchanges():
|
||||
dump('Supported exchanges:', green(', '.join(ccxt.exchanges)))
|
||||
|
||||
|
||||
try:
|
||||
|
||||
id = sys.argv[1] # get exchange id from command line arguments
|
||||
|
||||
|
||||
# check if the exchange is supported by ccxt
|
||||
exchange_found = id in ccxt.exchanges
|
||||
|
||||
if exchange_found:
|
||||
dump('Instantiating', green(id), 'exchange')
|
||||
|
||||
# instantiate the exchange by id
|
||||
exchange = getattr(ccxt, id)({
|
||||
# 'proxy':'https://cors-anywhere.herokuapp.com/',
|
||||
})
|
||||
|
||||
# load all markets from the exchange
|
||||
markets = exchange.load_markets()
|
||||
|
||||
# output a list of all market symbols
|
||||
dump(green(id), 'has', len(exchange.symbols), 'symbols:', exchange.symbols)
|
||||
|
||||
tuples = list(ccxt.Exchange.keysort(markets).items())
|
||||
|
||||
# debug
|
||||
for (k, v) in tuples:
|
||||
print(v)
|
||||
|
||||
# output a table of all markets
|
||||
dump(pink('{:<15} {:<15} {:<15} {:<15}'.format('id', 'symbol', 'base', 'quote')))
|
||||
|
||||
for (k, v) in tuples:
|
||||
dump('{:<15} {:<15} {:<15} {:<15}'.format(v['id'], v['symbol'], v['base'], v['quote']))
|
||||
|
||||
else:
|
||||
|
||||
dump('Exchange ' + red(id) + ' not found')
|
||||
print_supported_exchanges()
|
||||
|
||||
except Exception as e:
|
||||
dump('[' + type(e).__name__ + ']', str(e))
|
||||
dump("Usage: python " + sys.argv[0], green('id'))
|
||||
print_supported_exchanges()
|
||||
|
||||
@@ -24,27 +24,76 @@ Example of usage:
|
||||
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3
|
||||
--indicators2 fastk,fastd
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
|
||||
import pandas as pd
|
||||
import plotly.graph_objs as go
|
||||
import pytz
|
||||
|
||||
from plotly import tools
|
||||
from plotly.offline import plot
|
||||
|
||||
import freqtrade.optimize as optimize
|
||||
from freqtrade import persistence
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.arguments import Arguments, TimeRange
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.optimize.backtesting import setup_configuration
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.resolver import StrategyResolver
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_CONF: Dict[str, Any] = {}
|
||||
|
||||
timeZone = pytz.UTC
|
||||
|
||||
|
||||
def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame:
|
||||
trades: pd.DataFrame = pd.DataFrame()
|
||||
if args.db_url:
|
||||
persistence.init(_CONF)
|
||||
columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"]
|
||||
|
||||
for x in Trade.query.all():
|
||||
print("date: {}".format(x.open_date))
|
||||
|
||||
trades = pd.DataFrame([(t.pair, t.calc_profit(),
|
||||
t.open_date.replace(tzinfo=timeZone),
|
||||
t.close_date.replace(tzinfo=timeZone) if t.close_date else None,
|
||||
t.open_rate, t.close_rate,
|
||||
t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None)
|
||||
for t in Trade.query.filter(Trade.pair.is_(pair)).all()],
|
||||
columns=columns)
|
||||
|
||||
elif args.exportfilename:
|
||||
file = Path(args.exportfilename)
|
||||
# must align with columns in backtest.py
|
||||
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||
with file.open() as f:
|
||||
data = json.load(f)
|
||||
trades = pd.DataFrame(data, columns=columns)
|
||||
trades = trades.loc[trades["pair"] == pair]
|
||||
if timerange:
|
||||
if timerange.starttype == 'date':
|
||||
trades = trades.loc[trades["opents"] >= timerange.startts]
|
||||
if timerange.stoptype == 'date':
|
||||
trades = trades.loc[trades["opents"] <= timerange.stopts]
|
||||
|
||||
trades['opents'] = pd.to_datetime(trades['opents'],
|
||||
unit='s',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
trades['closets'] = pd.to_datetime(trades['closets'],
|
||||
unit='s',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
return trades
|
||||
|
||||
|
||||
def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||
"""
|
||||
@@ -56,6 +105,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||
# Load the configuration
|
||||
_CONF.update(setup_configuration(args))
|
||||
|
||||
print(_CONF)
|
||||
# Set the pair to audit
|
||||
pair = args.pair
|
||||
|
||||
@@ -72,7 +122,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||
|
||||
# Load the strategy
|
||||
try:
|
||||
analyze = Analyze(_CONF)
|
||||
strategy = StrategyResolver(_CONF).strategy
|
||||
exchange = Exchange(_CONF)
|
||||
except AttributeError:
|
||||
logger.critical(
|
||||
@@ -82,20 +132,22 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||
exit()
|
||||
|
||||
# Set the ticker to use
|
||||
tick_interval = analyze.get_ticker_interval()
|
||||
tick_interval = strategy.ticker_interval
|
||||
|
||||
# Load pair tickers
|
||||
tickers = {}
|
||||
if args.live:
|
||||
logger.info('Downloading pair.')
|
||||
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
||||
exchange.refresh_tickers([pair], tick_interval)
|
||||
tickers[pair] = exchange.klines[pair]
|
||||
else:
|
||||
tickers = optimize.load_data(
|
||||
datadir=_CONF.get("datadir"),
|
||||
pairs=[pair],
|
||||
ticker_interval=tick_interval,
|
||||
refresh_pairs=_CONF.get('refresh_pairs', False),
|
||||
timerange=timerange
|
||||
timerange=timerange,
|
||||
exchange=Exchange(_CONF)
|
||||
)
|
||||
|
||||
# No ticker found, or impossible to download
|
||||
@@ -103,30 +155,31 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||
exit()
|
||||
|
||||
# Get trades already made from the DB
|
||||
trades: List[Trade] = []
|
||||
if args.db_url:
|
||||
persistence.init(_CONF)
|
||||
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
|
||||
trades = load_trades(args, pair, timerange)
|
||||
|
||||
dataframes = strategy.tickerdata_to_dataframe(tickers)
|
||||
|
||||
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||
dataframe = dataframes[pair]
|
||||
dataframe = analyze.populate_buy_trend(dataframe)
|
||||
dataframe = analyze.populate_sell_trend(dataframe)
|
||||
dataframe = strategy.advise_buy(dataframe, {'pair': pair})
|
||||
dataframe = strategy.advise_sell(dataframe, {'pair': pair})
|
||||
|
||||
if len(dataframe.index) > 750:
|
||||
logger.warning('Ticker contained more than 750 candles, clipping.')
|
||||
if len(dataframe.index) > args.plot_limit:
|
||||
logger.warning('Ticker contained more than %s candles as defined '
|
||||
'with --plot-limit, clipping.', args.plot_limit)
|
||||
dataframe = dataframe.tail(args.plot_limit)
|
||||
|
||||
trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']]
|
||||
fig = generate_graph(
|
||||
pair=pair,
|
||||
trades=trades,
|
||||
data=dataframe.tail(750),
|
||||
data=dataframe,
|
||||
args=args
|
||||
)
|
||||
|
||||
plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html'))
|
||||
plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')))
|
||||
|
||||
|
||||
def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
||||
def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots:
|
||||
"""
|
||||
Generate the graph from the data generated by Backtesting or from DB
|
||||
:param pair: Pair to Display on the graph
|
||||
@@ -187,8 +240,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
||||
)
|
||||
|
||||
trade_buys = go.Scattergl(
|
||||
x=[t.open_date.isoformat() for t in trades],
|
||||
y=[t.open_rate for t in trades],
|
||||
x=trades["opents"],
|
||||
y=trades["open_rate"],
|
||||
mode='markers',
|
||||
name='trade_buy',
|
||||
marker=dict(
|
||||
@@ -199,8 +252,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
||||
)
|
||||
)
|
||||
trade_sells = go.Scattergl(
|
||||
x=[t.close_date.isoformat() for t in trades],
|
||||
y=[t.close_rate for t in trades],
|
||||
x=trades["closets"],
|
||||
y=trades["close_rate"],
|
||||
mode='markers',
|
||||
name='trade_sell',
|
||||
marker=dict(
|
||||
@@ -219,7 +272,7 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
||||
x=data.date,
|
||||
y=data.bb_lowerband,
|
||||
name='BB lower',
|
||||
line={'color': "transparent"},
|
||||
line={'color': 'rgba(255,255,255,0)'},
|
||||
)
|
||||
bb_upper = go.Scatter(
|
||||
x=data.date,
|
||||
@@ -227,7 +280,7 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
||||
name='BB upper',
|
||||
fill="tonexty",
|
||||
fillcolor="rgba(0,176,246,0.2)",
|
||||
line={'color': "transparent"},
|
||||
line={'color': 'rgba(255,255,255,0)'},
|
||||
)
|
||||
fig.append_trace(bb_lower, 1, 1)
|
||||
fig.append_trace(bb_upper, 1, 1)
|
||||
@@ -299,11 +352,17 @@ def plot_parse_args(args: List[str]) -> Namespace:
|
||||
default='macd',
|
||||
dest='indicators2',
|
||||
)
|
||||
|
||||
arguments.parser.add_argument(
|
||||
'--plot-limit',
|
||||
help='Specify tick limit for plotting - too high values cause huge files - '
|
||||
'Default: %(default)s',
|
||||
dest='plot_limit',
|
||||
default=750,
|
||||
type=int,
|
||||
)
|
||||
arguments.common_args_parser()
|
||||
arguments.optimizer_shared_options(arguments.parser)
|
||||
arguments.backtesting_options(arguments.parser)
|
||||
|
||||
return arguments.parse_args()
|
||||
|
||||
|
||||
|
||||
@@ -26,9 +26,8 @@ import plotly.graph_objs as go
|
||||
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.analyze import Analyze
|
||||
from freqtrade import constants
|
||||
|
||||
from freqtrade.strategy.resolver import StrategyResolver
|
||||
import freqtrade.optimize as optimize
|
||||
import freqtrade.misc as misc
|
||||
|
||||
@@ -87,7 +86,8 @@ def plot_profit(args: Namespace) -> None:
|
||||
|
||||
# Init strategy
|
||||
try:
|
||||
analyze = Analyze({'strategy': config.get('strategy')})
|
||||
strategy = StrategyResolver({'strategy': config.get('strategy')}).strategy
|
||||
|
||||
except AttributeError:
|
||||
logger.critical(
|
||||
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||
@@ -113,7 +113,7 @@ def plot_profit(args: Namespace) -> None:
|
||||
else:
|
||||
filter_pairs = config['exchange']['pair_whitelist']
|
||||
|
||||
tick_interval = analyze.strategy.ticker_interval
|
||||
tick_interval = strategy.ticker_interval
|
||||
pairs = config['exchange']['pair_whitelist']
|
||||
|
||||
if filter_pairs:
|
||||
@@ -127,7 +127,7 @@ def plot_profit(args: Namespace) -> None:
|
||||
refresh_pairs=False,
|
||||
timerange=timerange
|
||||
)
|
||||
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||
dataframes = strategy.tickerdata_to_dataframe(tickers)
|
||||
|
||||
# NOTE: the dataframes are of unequal length,
|
||||
# 'dates' is an merged date array of them all.
|
||||
|
||||
3
setup.py
3
setup.py
@@ -18,7 +18,7 @@ setup(name='freqtrade',
|
||||
license='GPLv3',
|
||||
packages=['freqtrade'],
|
||||
scripts=['bin/freqtrade'],
|
||||
setup_requires=['pytest-runner'],
|
||||
setup_requires=['pytest-runner', 'numpy'],
|
||||
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
||||
install_requires=[
|
||||
'ccxt',
|
||||
@@ -36,6 +36,7 @@ setup(name='freqtrade',
|
||||
'tabulate',
|
||||
'cachetools',
|
||||
'coinmarketcap',
|
||||
'scikit-optimize',
|
||||
],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
|
||||
29
setup.sh
29
setup.sh
@@ -1,13 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
#encoding=utf8
|
||||
|
||||
# Check which python version is installed
|
||||
function check_installed_python() {
|
||||
which python3.7
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "using Python 3.7"
|
||||
PYTHON=python3.7
|
||||
return
|
||||
fi
|
||||
|
||||
which python3.6
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "using Python 3.6"
|
||||
PYTHON=python3.6
|
||||
return
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function updateenv () {
|
||||
echo "-------------------------"
|
||||
echo "Update your virtual env"
|
||||
echo "-------------------------"
|
||||
source .env/bin/activate
|
||||
echo "pip3 install in-progress. Please wait..."
|
||||
pip3.6 install --quiet --upgrade pip
|
||||
pip3 install --quiet --upgrade pip
|
||||
pip3 install --quiet -r requirements.txt --upgrade
|
||||
pip3 install --quiet -r requirements.txt
|
||||
pip3 install --quiet -e .
|
||||
@@ -79,7 +97,7 @@ function reset () {
|
||||
fi
|
||||
|
||||
echo
|
||||
python3.6 -m venv .env
|
||||
${PYTHON} -m venv .env
|
||||
updateenv
|
||||
}
|
||||
|
||||
@@ -183,7 +201,7 @@ function install () {
|
||||
install_debian
|
||||
else
|
||||
echo "This script does not support your OS."
|
||||
echo "If you have Python3.6, pip, virtualenv, ta-lib you can continue."
|
||||
echo "If you have Python3.6 or Python3.7, pip, virtualenv, ta-lib you can continue."
|
||||
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
||||
sleep 10
|
||||
fi
|
||||
@@ -193,7 +211,7 @@ function install () {
|
||||
echo "-------------------------"
|
||||
echo "Run the bot"
|
||||
echo "-------------------------"
|
||||
echo "You can now use the bot by executing 'source .env/bin/activate; python3.6 freqtrade/main.py'."
|
||||
echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'."
|
||||
}
|
||||
|
||||
function plot () {
|
||||
@@ -214,6 +232,9 @@ function help () {
|
||||
echo " -p,--plot Install dependencies for Plotting scripts."
|
||||
}
|
||||
|
||||
# Verify if 3.6 or 3.7 is installed
|
||||
check_installed_python
|
||||
|
||||
case $* in
|
||||
--install|-i)
|
||||
install
|
||||
|
||||
@@ -12,11 +12,13 @@ import numpy # noqa
|
||||
|
||||
# This class is a sample. Feel free to customize it.
|
||||
class TestStrategy(IStrategy):
|
||||
__test__ = False # pytest expects to find tests here because of the name
|
||||
"""
|
||||
This is a test strategy to inspire you.
|
||||
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
|
||||
|
||||
You can:
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
- Rename the class name (Do not forget to update class_name)
|
||||
- Add any methods you want to build your strategy
|
||||
- Add any lib you need to build your strategy
|
||||
@@ -43,13 +45,19 @@ class TestStrategy(IStrategy):
|
||||
# Optimal ticker interval for the strategy
|
||||
ticker_interval = '5m'
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
# run "populate_indicators" only for new candle
|
||||
ta_on_candle = False
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
@@ -210,10 +218,11 @@ class TestStrategy(IStrategy):
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
@@ -226,10 +235,11 @@ class TestStrategy(IStrategy):
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param dataframe: DataFrame populated with indicators
|
||||
:param metadata: Additional information, like the currently traded pair
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
|
||||
Reference in New Issue
Block a user