Compare commits
1501 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2e397a88e1 | ||
|
fe6c62e144 | ||
|
f0db721f05 | ||
|
60f2a12bd9 | ||
|
a682faf6b1 | ||
|
81957e004d | ||
|
2cb24ed310 | ||
|
a55bc9c1e4 | ||
|
3f98fcb0db | ||
|
d94b84e38c | ||
|
2442257856 | ||
|
dae9f4d877 | ||
|
094676def4 | ||
|
e7418cdcdb | ||
|
00287febc6 | ||
|
487d3e891e | ||
|
4e52055a17 | ||
|
7eddc09c1c | ||
|
4db713318b | ||
|
0a977291b6 | ||
|
c28e0b0d0c | ||
|
7170b585f9 | ||
|
47516ed353 | ||
|
4d8d30ea39 | ||
|
e90e3cead0 | ||
|
334452d3ee | ||
|
9d756bc6a1 | ||
|
c9dc07bdaa | ||
|
2b63adf6c3 | ||
|
057db5aaab | ||
|
371f6bf90e | ||
|
03090d8f3f | ||
|
1a37100bd4 | ||
|
5a136f04df | ||
|
59c7403b12 | ||
|
1bc5d449a2 | ||
|
f5e71a67fa | ||
|
e9c4e6a69d | ||
|
6ec7b84b92 | ||
|
49e087df5b | ||
|
fcec071a08 | ||
|
8556e6a053 | ||
|
aceaa3faec | ||
|
0f76b23733 | ||
|
eb08b92180 | ||
|
95f69b905a | ||
|
73fc344eb1 | ||
|
f55db8e262 | ||
|
96bf82dbc6 | ||
|
6024fa482e | ||
|
47317e0f06 | ||
|
89aae71c32 | ||
|
ebd61ebdef | ||
|
18030a30e7 | ||
|
3b53ffb22f | ||
|
8cb3158810 | ||
|
717a4b82fe | ||
|
5462ff0ebc | ||
|
af1543ea37 | ||
|
edda5b4ceb | ||
|
a419e7012e | ||
|
404d700a74 | ||
|
c5cb617c92 | ||
|
999a154213 | ||
|
3fbe4a9944 | ||
|
a7133f1974 | ||
|
4cbdc9a74f | ||
|
3fc1c94aba | ||
|
3a0ad2f26e | ||
|
7764ad1541 | ||
|
be5b0acfbd | ||
|
c63b5fbbbf | ||
|
32c06f4a05 | ||
|
3133be19e3 | ||
|
7146122f4a | ||
|
b8b56d95f3 | ||
|
9107819c95 | ||
|
843606c9cb | ||
|
91549d3254 | ||
|
7e7e596372 | ||
|
bf5afbcdbd | ||
|
2ba79a32a0 | ||
|
f343036e66 | ||
|
7825d855cd | ||
|
11c76c3c89 | ||
|
24f480b4ce | ||
|
9ff52c0a93 | ||
|
6f4d607902 | ||
|
7db28b1b16 | ||
|
6bb93bdc25 | ||
|
6e10439f90 | ||
|
d47274066e | ||
|
ae4742afcb | ||
|
e3ced55f5c | ||
|
61182f849b | ||
|
f2ed6165e9 | ||
|
17041b78fc | ||
|
f0252cf79d | ||
|
98acff8169 | ||
|
26f6d8076d | ||
|
82595f3a5d | ||
|
805a04a6cb | ||
|
07524e9f37 | ||
|
749e0dd5a0 | ||
|
3c83d8c74a | ||
|
46c3f56bf5 | ||
|
25964f70d8 | ||
|
0c8dd7e502 | ||
|
f1d2cb9ce4 | ||
|
1d63bb66a9 | ||
|
67a8b8b631 | ||
|
708def3d96 | ||
|
d74e3091de | ||
|
f571fee899 | ||
|
be4bc4955c | ||
|
c0e12d632f | ||
|
e9456cdf15 | ||
|
17c9c3caf3 | ||
|
d4fbb785b5 | ||
|
b18256c231 | ||
|
71be547d82 | ||
|
abc8854b5a | ||
|
f74de1cca3 | ||
|
54165662ce | ||
|
69cfb0b278 | ||
|
c2b90afa61 | ||
|
a2c9879375 | ||
|
f26247e8e0 | ||
|
dca83b070d | ||
|
68bc2a6107 | ||
|
a922c4df70 | ||
|
cf22926cee | ||
|
151841965a | ||
|
207b211e5e | ||
|
42fbec4172 | ||
|
faf6a35ad7 | ||
|
590944a600 | ||
|
1ac360674c | ||
|
0ebf40f390 | ||
|
fdbd75501f | ||
|
41316abb55 | ||
|
14d49e85af | ||
|
a1f2f6ddeb | ||
|
f3421dfa9f | ||
|
1b91be08fe | ||
|
292c350885 | ||
|
9504b3eb05 | ||
|
ee7bc55727 | ||
|
2ec1a7b370 | ||
|
f181cdeecd | ||
|
a568548192 | ||
|
f9d10a7fad | ||
|
cbc2b00ee6 | ||
|
3942b30ebf | ||
|
6caa5f7131 | ||
|
0749199097 | ||
|
020729cf50 | ||
|
768b526c38 | ||
|
7ba92086c9 | ||
|
ea197b79ca | ||
|
1d57ce19eb | ||
|
7883160ce0 | ||
|
018c620057 | ||
|
a0b42c7aa2 | ||
|
8f7b857ae9 | ||
|
5826698c04 | ||
|
e88b022cd4 | ||
|
bfb738f69f | ||
|
3c88b4cf0c | ||
|
00dd8e76ee | ||
|
4b7271df46 | ||
|
3b1b66bee8 | ||
|
df726a54f8 | ||
|
42df65d4ec | ||
|
53452c8d64 | ||
|
731eb99713 | ||
|
afd2be06d8 | ||
|
5a4f30d1bd | ||
|
1f9ed0beff | ||
|
02ce0dc02e | ||
|
a2960d8505 | ||
|
2b16606dbc | ||
|
c6a9c0805c | ||
|
fdad14d852 | ||
|
b9a99bd0b7 | ||
|
7b6a0f7a19 | ||
|
0bd621ece8 | ||
|
df04612549 | ||
|
d354f1f84c | ||
|
317487fefc | ||
|
dc8e9bab44 | ||
|
d1cded3532 | ||
|
21b5f56f7d | ||
|
fddacfedaa | ||
|
a24586cd41 | ||
|
6fb5b22a8e | ||
|
dc7bcf5dda | ||
|
db540dc990 | ||
|
e9f451406c | ||
|
874c161f78 | ||
|
508e677d70 | ||
|
1b1216fc87 | ||
|
c13eed2178 | ||
|
d610b6305d | ||
|
a7a25bb285 | ||
|
42bb33811c | ||
|
a32aed2225 | ||
|
3785f04be7 | ||
|
0bbbe2e96c | ||
|
5705ff7f82 | ||
|
60d1e7fc65 | ||
|
95d4a11bb1 | ||
|
eb88c0f71b | ||
|
e60553b8f7 | ||
|
877a0750ce | ||
|
e7bfb4fd5c | ||
|
a77c11c7e0 | ||
|
b043697d70 | ||
|
78a93b6052 | ||
|
3787b747ae | ||
|
64b98989d2 | ||
|
dfd5d3b8b2 | ||
|
7b2e33b0bc | ||
|
30f6dbfc40 | ||
|
acd7f26a9d | ||
|
cd54f1536e | ||
|
35e800a84b | ||
|
7e2e9272cc | ||
|
8ba149a2af | ||
|
1ad41f0efc | ||
|
9c62ffe4f6 | ||
|
5cc6c2afe1 | ||
|
6290fb6d10 | ||
|
067378d7fc | ||
|
0c632555d6 | ||
|
e63ef86e9e | ||
|
ecb93f14b1 | ||
|
5062c17ac0 | ||
|
04c20afece | ||
|
7f8e956b44 | ||
|
22036d69d8 | ||
|
b18e44bc43 | ||
|
1674beed91 | ||
|
03d4002be8 | ||
|
be8accebd8 | ||
|
ca62914794 | ||
|
b1b8167b5e | ||
|
109440a6bf | ||
|
c769e9757d | ||
|
119d4d5204 | ||
|
08803524bd | ||
|
d0adc4ee62 | ||
|
c9cfc246f1 | ||
|
6511b3bec2 | ||
|
d563bfc3d0 | ||
|
6a59103869 | ||
|
be84a028c1 | ||
|
af984bdc0d | ||
|
2e41d80a2c | ||
|
7252cf47fb | ||
|
9f47853661 | ||
|
45c03f1440 | ||
|
a6a041526a | ||
|
1ba9b70afc | ||
|
6d3803fa22 | ||
|
4e2f06fe9c | ||
|
6191288ff9 | ||
|
1d10d2c87c | ||
|
172e018d2d | ||
|
dcf8ad36f9 | ||
|
926b017981 | ||
|
118ae8a3d0 | ||
|
b192c82731 | ||
|
d2dbe8f8d0 | ||
|
535bbd681f | ||
|
85767d0d70 | ||
|
036c2888b4 | ||
|
380e383eee | ||
|
3a60709f16 | ||
|
4bce64b427 | ||
|
5f886e7ffe | ||
|
92d1f2b945 | ||
|
7811a36ae9 | ||
|
5047492f5a | ||
|
36dad186fd | ||
|
2d979b84bf | ||
|
48ff2b3baa | ||
|
8cdb6e0774 | ||
|
39a0cef922 | ||
|
f31fa07b3f | ||
|
548b9e75f3 | ||
|
37ea07a45d | ||
|
5221194318 | ||
|
2893d0b50d | ||
|
94b546228b | ||
|
b8af4bf8fe | ||
|
110a270a0b | ||
|
576d5a5b48 | ||
|
1e43683283 | ||
|
22e395af87 | ||
|
e24c837e1f | ||
|
099a03f190 | ||
|
6d91a5ecbd | ||
|
7d3b80fbde | ||
|
fe33b86308 | ||
|
6b5f63d4d6 | ||
|
ee2a7a968b | ||
|
5eb5029856 | ||
|
ef086d438c | ||
|
c19f3950da | ||
|
0b01fcf047 | ||
|
303b12efd8 | ||
|
b657d2d8de | ||
|
7232324eb7 | ||
|
da73e754b4 | ||
|
8f2425e49f | ||
|
644442e2f9 | ||
|
17d748dd4c | ||
|
c5e0daf2d3 | ||
|
2a3ab1ef61 | ||
|
6b9696057d | ||
|
4cf514e293 | ||
|
131b2d68d8 | ||
|
0477070faa | ||
|
c4a54cc9cd | ||
|
cfaf13c90f | ||
|
82006ff1db | ||
|
22173851d6 | ||
|
2a59ef7311 | ||
|
808cefe526 | ||
|
9bf86bbe27 | ||
|
58fad72778 | ||
|
e08006ea25 | ||
|
4ea79a32e4 | ||
|
1e603985c5 | ||
|
6637dacd7f | ||
|
7ac44380f7 | ||
|
090554f197 | ||
|
f4149ee462 | ||
|
44e616c264 | ||
|
49cecf1cb2 | ||
|
9140679bf4 | ||
|
15698dd1ca | ||
|
f7a1cabe23 | ||
|
c12e5a3b6c | ||
|
6ed237a72a | ||
|
06387478b5 | ||
|
761f7fdefb | ||
|
e84a58de28 | ||
|
a3e045f69d | ||
|
f8faf748df | ||
|
1e6362debf | ||
|
29879bb415 | ||
|
d6482066ef | ||
|
a733a74dd9 | ||
|
a4e1aaa9bd | ||
|
2d45163f8f | ||
|
e95fb7ef3e | ||
|
0058abcc2d | ||
|
5aa683006c | ||
|
64d0c75bbb | ||
|
d96a354a3e | ||
|
479b560549 | ||
|
1a838680e7 | ||
|
2c492abc1e | ||
|
f35c6545c1 | ||
|
b3e36def34 | ||
|
c53122ed02 | ||
|
d10d84adf3 | ||
|
aac6d15a1d | ||
|
faa23949b5 | ||
|
677a14ddde | ||
|
d4ca2e5767 | ||
|
ab3c6a7ee4 | ||
|
bc5adc0188 | ||
|
65526e9803 | ||
|
8a9b70cc49 | ||
|
ab5c1e6c1e | ||
|
15dbdfe130 | ||
|
dbf2226841 | ||
|
7d066d81c1 | ||
|
287e90af8e | ||
|
cafda31869 | ||
|
7aae9565c7 | ||
|
da39ca6650 | ||
|
aea84dc117 | ||
|
326ba46bf8 | ||
|
d1d520769e | ||
|
e7409e74c2 | ||
|
fb3c67d86b | ||
|
571ddceaf6 | ||
|
9ae14f0b56 | ||
|
2ba2144df1 | ||
|
15d5389564 | ||
|
660f474ab8 | ||
|
e062188a18 | ||
|
5d0c2bcb44 | ||
|
138e867a68 | ||
|
9df7014de3 | ||
|
bf8ef58439 | ||
|
b8f29802e5 | ||
|
cbd213bc0a | ||
|
bd1b991448 | ||
|
31211a33fd | ||
|
82e193d9f0 | ||
|
4b9d55dbe2 | ||
|
002226f5fd | ||
|
18168cba7a | ||
|
4a2914d72e | ||
|
396ebebdc1 | ||
|
ed71f777a3 | ||
|
1f26709aca | ||
|
4408f97a00 | ||
|
b6943f3bca | ||
|
12c79967f5 | ||
|
30b27ae736 | ||
|
2e2b1e2470 | ||
|
f7a5b2cb71 | ||
|
6e47d06733 | ||
|
d9347e9900 | ||
|
eb677ad9b6 | ||
|
0fa7986369 | ||
|
7c975df42a | ||
|
aacddf64cf | ||
|
e72c3ec19f | ||
|
78986a0def | ||
|
acf6e94591 | ||
|
1d59a6b7e3 | ||
|
ac71d79364 | ||
|
b8377b9e30 | ||
|
2823da977d | ||
|
45a2298929 | ||
|
381bda1e4a | ||
|
194a5ce3cc | ||
|
e252830229 | ||
|
d3d4894ec5 | ||
|
6d91ceb28c | ||
|
ce7dff405f | ||
|
1aa7c193ba | ||
|
0990e5d472 | ||
|
9ae6bbb8d2 | ||
|
b8413410d1 | ||
|
fd1828c283 | ||
|
cf79cef7ba | ||
|
95c7d48684 | ||
|
12fabba784 | ||
|
013262f7e2 | ||
|
138fd9440a | ||
|
bf62fc9b25 | ||
|
451eca51c8 | ||
|
e67a54f7a9 | ||
|
daee59f4f1 | ||
|
57067ce88d | ||
|
7429f535c1 | ||
|
62df044618 | ||
|
6492e1cd76 | ||
|
4b6f9121ca | ||
|
6613e3757a | ||
|
09db4bcadd | ||
|
51b94889b2 | ||
|
8c79d55739 | ||
|
480ed90a02 | ||
|
821a9d9cdc | ||
|
cc3852daf3 | ||
|
56daafd6b7 | ||
|
01b331ee42 | ||
|
7bef9a9b3e | ||
|
82f0d4d056 | ||
|
bd4014e1e6 | ||
|
a35b0b519a | ||
|
fe5f61694b | ||
|
1505ad451c | ||
|
9ecd7400c8 | ||
|
314a544881 | ||
|
f79decdb9c | ||
|
05046b9eef | ||
|
a43c088448 | ||
|
3d94d7df5c | ||
|
c265f39323 | ||
|
19948a6f89 | ||
|
5dca183b7b | ||
|
bb1d8fb54f | ||
|
3249f9fb98 | ||
|
f3a152a5a2 | ||
|
730d2e3574 | ||
|
d02acb21c2 | ||
|
f4487c7711 | ||
|
748381c5cd | ||
|
e35a1e4a01 | ||
|
a9f14ac119 | ||
|
8ce5536dd8 | ||
|
4e9f0d89af | ||
|
d549905856 | ||
|
a6c7f45545 | ||
|
e9baabce6f | ||
|
f30580e5f2 | ||
|
5fb9511556 | ||
|
62ea1a445e | ||
|
2e537df358 | ||
|
847e8977ca | ||
|
afe46a55f7 | ||
|
d319204dea | ||
|
7c010b3058 | ||
|
ac93eea585 | ||
|
5fffc5033a | ||
|
5525fdae1a | ||
|
3925e8a7e3 | ||
|
a4dbdb549d | ||
|
a6a127f596 | ||
|
407c20412d | ||
|
301b2e8a0f | ||
|
d918d24f08 | ||
|
3c06d31bbf | ||
|
d813fef95b | ||
|
9c9c9f0171 | ||
|
91236c1876 | ||
|
a156101d5c | ||
|
3de843ab2c | ||
|
8d67caafb3 | ||
|
f9a935b9a3 | ||
|
d0dc9e26b0 | ||
|
3a5841bc03 | ||
|
cf41f71f39 | ||
|
f2984e9d0e | ||
|
fa605e6a50 | ||
|
0f51192575 | ||
|
0629dc866d | ||
|
da134d3ad1 | ||
|
4f9af81b50 | ||
|
543a561019 | ||
|
9092596a1f | ||
|
096d2d7313 | ||
|
81b8008047 | ||
|
7073b36421 | ||
|
108f79ad39 | ||
|
d384184784 | ||
|
9fbb9332f9 | ||
|
c403464bb4 | ||
|
3d9f34b064 | ||
|
e4af162f38 | ||
|
5afa975839 | ||
|
6be6515ecc | ||
|
b6ad0f52e9 | ||
|
edd2ea3699 | ||
|
3cdb672ac3 | ||
|
c02497e4b8 | ||
|
2bcfc0c90c | ||
|
d08885ed92 | ||
|
69c00db7cd | ||
|
b96b0f89bd | ||
|
acda9571d1 | ||
|
6c4b261469 | ||
|
eabeb87ceb | ||
|
39184e1f95 | ||
|
270d7ebbf5 | ||
|
062d00e8f2 | ||
|
2b7405470a | ||
|
9becce9897 | ||
|
526ed7fa9a | ||
|
16861db653 | ||
|
6684bff963 | ||
|
caea8967d5 | ||
|
66a479c26a | ||
|
6c0eef94bb | ||
|
9f9e2a8722 | ||
|
93adb436f8 | ||
|
766c69734d | ||
|
320c9ccf90 | ||
|
1e324d208e | ||
|
08cae6f067 | ||
|
7699fde380 | ||
|
ffe69535d8 | ||
|
13bc5c5d8f | ||
|
678be0b773 | ||
|
faa35cb167 | ||
|
13651fd3be | ||
|
c826c9c2b9 | ||
|
814a343ed3 | ||
|
a22e1b6500 | ||
|
33cb9e9002 | ||
|
ffab70d869 | ||
|
7344f88ad5 | ||
|
7cd8448656 | ||
|
8643b20a0e | ||
|
af3d220ffc | ||
|
db3483c827 | ||
|
775b1201d2 | ||
|
e50b07ecb4 | ||
|
fec95277bb | ||
|
94f2c99989 | ||
|
73840e1d91 | ||
|
58b77bd15f | ||
|
438a083602 | ||
|
fbf026ac43 | ||
|
3b7167ab07 | ||
|
26f2db4777 | ||
|
30d293bfec | ||
|
0dc7c389a0 | ||
|
78921824c6 | ||
|
0d00da8dab | ||
|
e0b05c4df2 | ||
|
ef06ede3bd | ||
|
42b5e8dac6 | ||
|
11d74da1e0 | ||
|
fc1069cfe0 | ||
|
1e35f54709 | ||
|
d80216ca05 | ||
|
d2c7ff3f0f | ||
|
d90745651a | ||
|
130275faff | ||
|
29457078e7 | ||
|
8d554585f1 | ||
|
000e29113f | ||
|
8057929817 | ||
|
858a65e308 | ||
|
fe067994e3 | ||
|
d349d2743a | ||
|
2d930d081c | ||
|
96f8338496 | ||
|
7bc50dff7a | ||
|
a4d2cf2f06 | ||
|
af60b9db59 | ||
|
bc95e1e151 | ||
|
626970b32e | ||
|
a0d378fb7e | ||
|
b8e8a31f84 | ||
|
3fc44aa1bd | ||
|
59ffb98779 | ||
|
cbd449f710 | ||
|
91b89c8c42 | ||
|
0424b44667 | ||
|
195d601b8e | ||
|
c929d428b2 | ||
|
0bca07a32a | ||
|
813a2cd23b | ||
|
cf077b15c2 | ||
|
94631c7d64 | ||
|
8e424f7c73 | ||
|
43f8087f32 | ||
|
827b8d3e4c | ||
|
04976658da | ||
|
b82e63cb62 | ||
|
11ace0f867 | ||
|
7f20f6834b | ||
|
cd144cdfc9 | ||
|
e540959c27 | ||
|
1203d08d1e | ||
|
b77943af0d | ||
|
560b3d5dbe | ||
|
d64f9030c1 | ||
|
9a3d0528a3 | ||
|
b3a4ecaf77 | ||
|
28011a3907 | ||
|
72f486289a | ||
|
24ec78b11c | ||
|
326e3d1f8e | ||
|
bb29c44462 | ||
|
7451b60501 | ||
|
a0f9c1bf7b | ||
|
e88a1ab209 | ||
|
addba6597a | ||
|
5451972456 | ||
|
2a2392fd73 | ||
|
33d95d245e | ||
|
a9a6cf13f8 | ||
|
4e2b9203d7 | ||
|
2ca90577a6 | ||
|
2ecaf9f8b4 | ||
|
6abd6bceb9 | ||
|
67e4dda5b3 | ||
|
8373a4e713 | ||
|
4d9b4ddc28 | ||
|
09fae25c94 | ||
|
7a2b50ce8b | ||
|
42579c0268 | ||
|
7bf735dbfc | ||
|
937f5e3d0f | ||
|
7ea5b0e359 | ||
|
15cb3792cf | ||
|
fa620d3f7b | ||
|
7adb7f90a6 | ||
|
5536410ed0 | ||
|
de2a7c1956 | ||
|
079dbc7997 | ||
|
05ac09b38e | ||
|
028636b4a9 | ||
|
8ed30fc9c1 | ||
|
2469dc0424 | ||
|
33991e8de9 | ||
|
5407a06254 | ||
|
616d5bbaed | ||
|
3f4c5a7902 | ||
|
a253ad5ec1 | ||
|
ce1780ca3f | ||
|
6d3747d9e6 | ||
|
0da31cff72 | ||
|
f7d3c50213 | ||
|
6b0a7a81a9 | ||
|
8c0f7321c3 | ||
|
fac6956eeb | ||
|
711a6a6dbc | ||
|
2116b0729f | ||
|
209ecc8732 | ||
|
f3784f2149 | ||
|
08ba5b0451 | ||
|
fb06a673e0 | ||
|
78ba2d3fc7 | ||
|
a2d97eecfe | ||
|
45a02beea8 | ||
|
8b49bec649 | ||
|
c29469decf | ||
|
9becd20f20 | ||
|
713b884d9b | ||
|
515e1040c2 | ||
|
670aed06bf | ||
|
0277d93a64 | ||
|
c9296dc9a0 | ||
|
550a1eef91 | ||
|
880ee016a4 | ||
|
39f8c5719b | ||
|
a715083fc0 | ||
|
78ccaae318 | ||
|
ee774f12bd | ||
|
b1b2eebd11 | ||
|
b63491fb9c | ||
|
1bc2c71757 | ||
|
6b22f84d30 | ||
|
505d4bacd5 | ||
|
5b2a1b9e7a | ||
|
8edc84bf25 | ||
|
bd98637ae9 | ||
|
77afb7b5e2 | ||
|
2b94fbfa74 | ||
|
b530600718 | ||
|
043218cc7e | ||
|
c3e9ef27f6 | ||
|
3d336a736e | ||
|
24807515c1 | ||
|
5a546855e6 | ||
|
f965e9177c | ||
|
4b654b2713 | ||
|
093f98d368 | ||
|
2a728c676e | ||
|
05a488a7a0 | ||
|
bb65621134 | ||
|
df53873dab | ||
|
ef2b326262 | ||
|
54858a0bbb | ||
|
314e10596b | ||
|
53ef37d5fc | ||
|
17f037cec6 | ||
|
1b739acc08 | ||
|
3804a17775 | ||
|
c8253790b6 | ||
|
a215e29d2a | ||
|
d58ed0e242 | ||
|
2ab8f467dd | ||
|
c1ec368c0c | ||
|
29fff65598 | ||
|
3cba405b2e | ||
|
24d16d7dab | ||
|
2e84b8f0d5 | ||
|
470ef7c160 | ||
|
1093f22b80 | ||
|
e085058621 | ||
|
81b383fe5c | ||
|
f77b8cbb7a | ||
|
bc8fc3ab09 | ||
|
bd5520bee2 | ||
|
099dc07baf | ||
|
817a65b656 | ||
|
045225beef | ||
|
d3f3c49b13 | ||
|
6509c38717 | ||
|
fbaf46901e | ||
|
96fbf63d0b | ||
|
aa54592ec7 | ||
|
2917cc1f2e | ||
|
6fdad8c6bd | ||
|
356b2d3d91 | ||
|
b1feb69ca9 | ||
|
49aa34c6f3 | ||
|
ea79eb55e9 | ||
|
d11a8928d4 | ||
|
3cbb2ff31f | ||
|
e3181748dc | ||
|
f61aaa8c0d | ||
|
ad247b2f07 | ||
|
de79d25caf | ||
|
58663180e0 | ||
|
98f6d2d722 | ||
|
110e48c541 | ||
|
61dbb6206f | ||
|
ac690e9215 | ||
|
9a9cc31d83 | ||
|
0c4664e8f4 | ||
|
bc60139ae3 | ||
|
8393c99b62 | ||
|
8bf1001b33 | ||
|
ace0a83c0c | ||
|
2e23e88fc1 | ||
|
d70ddeef9a | ||
|
e439ae1fea | ||
|
da2e07b7fe | ||
|
76e7bf6cd2 | ||
|
7df3e7ada4 | ||
|
fa01cbf546 | ||
|
f88b6af26f | ||
|
e5aaef6440 | ||
|
6ba8b17fdd | ||
|
4862cdb296 | ||
|
c9243fb4f6 | ||
|
f6d36ce56b | ||
|
d9f5694965 | ||
|
40036bc710 | ||
|
afad9be53f | ||
|
6fe09b6dee | ||
|
21da01f777 | ||
|
260c627e99 | ||
|
d47167c9c4 | ||
|
b6f8765d3b | ||
|
5b608c9005 | ||
|
cfad873ea7 | ||
|
480eb55721 | ||
|
e754cc09fc | ||
|
cde35509db | ||
|
5a3a5e98d6 | ||
|
44ac002cf0 | ||
|
56d96d6cff | ||
|
36632b48c7 | ||
|
1b3aaffef4 | ||
|
b8b5e93000 | ||
|
f28d95ffb5 | ||
|
5da38f3613 | ||
|
1cbc4da72b | ||
|
58c3d69d14 | ||
|
3aca3a7133 | ||
|
1eb83f9a62 | ||
|
db2f0660fa | ||
|
b094430c26 | ||
|
30673f84f9 | ||
|
cc28f73d7f | ||
|
d10fb95fce | ||
|
cea023399e | ||
|
462270bc5a | ||
|
ea38b58081 | ||
|
337af44901 | ||
|
b2fc3e814e | ||
|
39f0a17e62 | ||
|
7200659b35 | ||
|
f9aa36f291 | ||
|
b80b5ed1ad | ||
|
a7c67e8c7c | ||
|
9d8646072c | ||
|
dda302eea2 | ||
|
9be29c6e92 | ||
|
468076cf54 | ||
|
793d090561 | ||
|
95949bd466 | ||
|
d4b31263ca | ||
|
6f6e7467f5 | ||
|
1d0af074ac | ||
|
f2d55a91cd | ||
|
5371458c99 | ||
|
884a04c7fe | ||
|
172b9383c0 | ||
|
ec4a24649c | ||
|
1362bd9626 | ||
|
2c3e5fa080 | ||
|
1017b68af9 | ||
|
98255c18cf | ||
|
3398469e55 | ||
|
8dd3128ed4 | ||
|
5b998aeca7 | ||
|
878e16545d | ||
|
c6256aba35 | ||
|
8dacd987b9 | ||
|
c12f2378db | ||
|
1a4b403792 | ||
|
b90c5e56fb | ||
|
8fdef2900e | ||
|
2918032dac | ||
|
64558e60d3 | ||
|
2e13893341 | ||
|
06bd8a1540 | ||
|
9176e2f1f6 | ||
|
71147d2899 | ||
|
58cd91bd80 | ||
|
dbe97bcdb1 | ||
|
843eec63f0 | ||
|
0df8786af6 | ||
|
b4ed90788b | ||
|
c871e51dcc | ||
|
857f4ec125 | ||
|
7d42f42405 | ||
|
f11a40f144 | ||
|
783ee633aa | ||
|
fb134c67a9 | ||
|
849ca1ec06 | ||
|
8da79d0ab2 | ||
|
aaf5f4ce39 | ||
|
ae92bf56bf | ||
|
f47cfbd2a9 | ||
|
c9c683f2b0 | ||
|
81cafd090d | ||
|
671b9903d7 | ||
|
cc96db76f0 | ||
|
e729fad99c | ||
|
e9c3f0cbbd | ||
|
f97662e816 | ||
|
b7bf3247b8 | ||
|
1e3fc5e984 | ||
|
c179951cca | ||
|
b2c2852f86 | ||
|
00366c5c88 | ||
|
28d0b5165a | ||
|
fde6779873 | ||
|
88792852e4 | ||
|
be6b1f6f83 | ||
|
b79f2f2981 | ||
|
facb5b3991 | ||
|
79a87649b9 | ||
|
7848e17a49 | ||
|
fd875786fd | ||
|
decaa24f81 | ||
|
f9529c1fb6 | ||
|
3dda0ef2ef | ||
|
50a6eaea22 | ||
|
61211a1194 | ||
|
fbd64d757d | ||
|
4278c5a24a | ||
|
243e59cabb | ||
|
210202a797 | ||
|
c981cc335d | ||
|
d0467b30ba | ||
|
e3190cf8a8 | ||
|
848a2d5383 | ||
|
2080bf0952 | ||
|
68ac8008ec | ||
|
84ad176287 | ||
|
86910b58dc | ||
|
d1209fe415 | ||
|
d09a30cc67 | ||
|
ad5c8f601c | ||
|
d3ad4fb52e | ||
|
294c98ed5e | ||
|
c1fed8a077 | ||
|
0375a08302 | ||
|
5ce1eeecf5 | ||
|
c22f381dfe | ||
|
542963c7a6 | ||
|
f0abe218a2 | ||
|
231b1e2f57 | ||
|
de7e1e6bf7 | ||
|
85b1f6f6b3 | ||
|
60eca8b1f1 | ||
|
06d8217e62 | ||
|
dfb148f8d7 | ||
|
f8cb3d2901 | ||
|
bd8348451e | ||
|
0f15340269 | ||
|
2e51477455 | ||
|
018407852a | ||
|
56b4457a9c | ||
|
2db064d8f7 | ||
|
f0bf9b51dc | ||
|
57e55eb938 | ||
|
5ee5600cb9 | ||
|
828ab874c1 | ||
|
90892e5a89 | ||
|
180df0514f | ||
|
731208936f | ||
|
3b4051488f | ||
|
c126d2530a | ||
|
24997fb36f | ||
|
b81d768eb3 | ||
|
39c3175b69 | ||
|
b0b2fdba70 | ||
|
c2a7b1930b | ||
|
589c9f55e0 | ||
|
e9e8023d73 | ||
|
df09fe5df6 | ||
|
29180a1d2b | ||
|
0fa5bf54cd | ||
|
cf5ff9257d | ||
|
c7d10e2c7e | ||
|
2414c0bd9f | ||
|
fb6ae174b9 | ||
|
fd9bf2adb0 | ||
|
6429205d39 | ||
|
2b3e7eeb21 | ||
|
409a801763 | ||
|
b90303c9a3 | ||
|
4179a1a797 | ||
|
cb95b362ec | ||
|
62d248d182 | ||
|
2f0f576fce | ||
|
8c52ba3360 | ||
|
7e1eedd7df | ||
|
eab4bdd274 | ||
|
a9cdb428d0 | ||
|
3f10430eb5 | ||
|
a629777890 | ||
|
6ca6f62509 | ||
|
bc52b3db56 | ||
|
80ed5283b2 | ||
|
450293878f | ||
|
897788de17 | ||
|
f4bc30c927 | ||
|
5307d2bf3b | ||
|
c23d90e2b8 | ||
|
0c629fc951 | ||
|
0d1e84cf55 | ||
|
338fe333a9 | ||
|
65906d330f | ||
|
e8feac3674 | ||
|
342862a5f3 | ||
|
c23ca35d23 | ||
|
b8cefd687e | ||
|
0d082f7b17 | ||
|
c245a2a897 | ||
|
2c805e53ee | ||
|
259b95074f | ||
|
43dab3ee60 | ||
|
78a00f2518 | ||
|
280a0ec17e | ||
|
64e34f382e | ||
|
ecf2ac3c21 | ||
|
80946cd9d6 | ||
|
965ab3848c | ||
|
6f93f96f18 | ||
|
9f1fdc9931 | ||
|
e0f21a5e35 | ||
|
0ef99206b0 | ||
|
247f855ba9 | ||
|
fdc6ca1bd8 | ||
|
ab93e13682 | ||
|
d4fd13bf50 | ||
|
c0cc3f5f97 | ||
|
b36fe8fe0f | ||
|
0bae1471bd | ||
|
ef67a2adfc | ||
|
d8ee72554f | ||
|
f8f7d81fc2 | ||
|
a239e5f725 | ||
|
06c81b5234 | ||
|
0b6060dd11 | ||
|
5fb0f53539 | ||
|
60cf52aa34 | ||
|
4d45eb0644 | ||
|
056f8c72a1 | ||
|
6a79a04350 | ||
|
d477ccab19 | ||
|
e3bb102dc0 | ||
|
5df5a6f13b | ||
|
f4fd4ecdb9 | ||
|
c8191c4412 | ||
|
46de615b50 | ||
|
003e17bbb2 | ||
|
b1618afef3 | ||
|
7bd384c7fb | ||
|
876b59f477 | ||
|
8ec5f72be4 | ||
|
178e3ac6af | ||
|
2a1c61fb30 | ||
|
c046790727 | ||
|
43120e03f9 | ||
|
4d1d8de9b7 | ||
|
1dc98cc4d5 | ||
|
c70fdea886 | ||
|
5b9cbaf277 | ||
|
33f00d23b9 | ||
|
632c1bc0aa | ||
|
7c11619924 | ||
|
c4c1b301cd | ||
|
0bc9384451 | ||
|
7412b7ba51 | ||
|
a177e58dc4 | ||
|
37d461c6c2 | ||
|
0e70d23bef | ||
|
0e2b5ef6d4 | ||
|
df27499e19 | ||
|
32e3376296 | ||
|
a237667bc9 | ||
|
4d1ce51207 | ||
|
39bb34cdb3 | ||
|
e0fd880c11 | ||
|
4eb9038358 | ||
|
1b271d0840 | ||
|
ce2aa1dc69 | ||
|
f8d30abd79 | ||
|
f7b2c0c5d7 | ||
|
e7d1630c92 | ||
|
d3d17f9f8b | ||
|
23a566b478 | ||
|
c9d974d210 | ||
|
e8b4d44881 | ||
|
b676868ce6 | ||
|
6f0a98229f | ||
|
6267678ca9 | ||
|
f9e5a25b36 | ||
|
2bfec7d549 | ||
|
ae0e72a945 | ||
|
e4cca63163 | ||
|
f2be820f73 | ||
|
63f4221f70 | ||
|
84261237a0 | ||
|
bb2b8efef1 | ||
|
3ce898e4a9 | ||
|
bbd7c6e4fc | ||
|
d003a2b7a3 | ||
|
2b88b3b749 | ||
|
d80dda9caa | ||
|
3cfce605de | ||
|
fdc6053633 | ||
|
b39794f8d2 | ||
|
ab06584a3e | ||
|
a2c12f15f1 | ||
|
d0199b6014 | ||
|
dbc863bcdf | ||
|
c54cf63bae | ||
|
c11e1a84e4 | ||
|
de4bc7204d | ||
|
a08dd17bc1 | ||
|
9fa64c2647 | ||
|
fb6ba62158 | ||
|
1dd6872b80 | ||
|
4595c1e73c | ||
|
25fcab0794 | ||
|
fef7da03b2 | ||
|
66220d6f9f | ||
|
4f5c5b6982 | ||
|
d0e192e20f | ||
|
f7dc47b1c8 | ||
|
d5acd979dc | ||
|
3c33b48fd5 | ||
|
7a907a7636 | ||
|
da4344d216 | ||
|
8eabdd659f | ||
|
77f3dabd15 | ||
|
70253258f0 | ||
|
87634f0409 | ||
|
459ff9692d | ||
|
5f40158c0b | ||
|
a8651b0dcd | ||
|
15616d75ad | ||
|
d99eaccb5a | ||
|
ae3b53014d | ||
|
60a5ded532 | ||
|
2115a3ed12 | ||
|
ffc2de8d33 | ||
|
781f8a059c | ||
|
26e5418519 | ||
|
ae2343db93 | ||
|
eb280798d8 | ||
|
10e839c17e | ||
|
5b9a168ca9 | ||
|
17ecfda2e8 | ||
|
c061b576a9 | ||
|
431b96de98 | ||
|
048db4f509 | ||
|
437e5f0645 | ||
|
6fb0866350 | ||
|
a1e8878030 | ||
|
ce597d12d9 | ||
|
f60d101076 | ||
|
1fefb132e0 | ||
|
161a3fac15 | ||
|
e78df59e30 | ||
|
f365e68706 | ||
|
7ae9b90174 | ||
|
3056be3a1d | ||
|
74e8b28991 | ||
|
a16328f372 | ||
|
6623dfe7da | ||
|
4249fcefba | ||
|
6934f37d16 | ||
|
27dce9eeea | ||
|
e34c62074b | ||
|
2b1373966f | ||
|
46d4418e85 | ||
|
45f7093e52 | ||
|
e2041ddb70 | ||
|
3d59289b09 | ||
|
6b90b4a144 | ||
|
dffe76f109 | ||
|
c15f73aa1f | ||
|
20904f1ca4 | ||
|
72ecb45d86 | ||
|
650d6c276a | ||
|
e8f85aed6b | ||
|
d60001e886 | ||
|
459a2239ce | ||
|
6cf140f8fb | ||
|
851062ca46 | ||
|
f472709438 | ||
|
0f3809345a | ||
|
6f1e719216 | ||
|
c34b8a95d7 | ||
|
c579fcfc19 | ||
|
201fe108bc | ||
|
240923341b | ||
|
5cdae2ce3f | ||
|
e9d71f26b3 | ||
|
658006e7ee | ||
|
dadf015c23 | ||
|
560802c326 | ||
|
02e69e1667 | ||
|
335412a3a8 | ||
|
f280397fd7 | ||
|
98ed7edb11 | ||
|
8dd7d134f2 | ||
|
dc605e29aa | ||
|
92130837a9 | ||
|
892a1ca60c | ||
|
e2b64a750f | ||
|
2e7d08612e | ||
|
2eb33707c9 | ||
|
a50bde10de | ||
|
91b9e5ce68 | ||
|
c1b5dcd756 | ||
|
6b17094c6f | ||
|
51c925f9f3 | ||
|
f80d3d48e4 | ||
|
21ab83163d | ||
|
e4e75d4861 | ||
|
9c6cbc025a | ||
|
9f6e4c6c0e | ||
|
ae06899694 | ||
|
c3f3bdaa2a | ||
|
b51f946ee0 | ||
|
20a61e03da | ||
|
d1e2a53267 | ||
|
7ff16997e9 | ||
|
88b96d5d1b | ||
|
029ddd23c1 | ||
|
262f186a37 | ||
|
33d75e9963 | ||
|
cea251c83c | ||
|
4e88bd07fa | ||
|
538d9e8b37 | ||
|
478013a306 | ||
|
0e72a901cc | ||
|
2979679db4 | ||
|
1a40e02ace | ||
|
826d4eb2f4 | ||
|
3d90305f8e | ||
|
b50b38f049 | ||
|
520c5687aa | ||
|
f7926083ca | ||
|
4539170424 | ||
|
df033d92ef | ||
|
22dd2ca003 | ||
|
17432b2823 | ||
|
5f309627ea | ||
|
dffb4c5d53 | ||
|
b4bedc22d7 | ||
|
fde10f5395 | ||
|
78724e304e | ||
|
0e085298e9 | ||
|
96f99699e0 | ||
|
053fb076e4 | ||
|
e458c9867a | ||
|
7b5346b984 | ||
|
fa028c2134 | ||
|
7197f4ce77 | ||
|
de5497c766 | ||
|
1267374c8a | ||
|
905f3a1a50 | ||
|
5454460227 | ||
|
1fdc4425dd | ||
|
55b0216180 | ||
|
42a4dfed28 | ||
|
00406ea7d5 | ||
|
2995f9a347 | ||
|
5ecdd1d112 | ||
|
69a59cdf37 | ||
|
f863f4fdfc | ||
|
5d2e374099 | ||
|
f9b1667478 | ||
|
0da5ef16e6 | ||
|
ddba4e32d7 | ||
|
52bd761111 | ||
|
711897cffa | ||
|
1c6fc068c6 | ||
|
925df4dfec | ||
|
8a7ea65531 | ||
|
0ef20a5b04 | ||
|
3af55cc8c7 | ||
|
0d7be62e62 | ||
|
75e6a2d276 | ||
|
e00a71a3db | ||
|
ef70ea3997 | ||
|
618f0ffe68 | ||
|
5320ddc382 | ||
|
44e6e13429 | ||
|
4b02749019 | ||
|
ff3bfa3d52 | ||
|
dccce4855e | ||
|
733d254644 | ||
|
82684f5de9 | ||
|
bddbb474aa | ||
|
69c98c4141 | ||
|
035380d8a4 | ||
|
e6bbfaefe5 | ||
|
c58126e9c5 | ||
|
d7756efe8b | ||
|
b60371822f | ||
|
e7a2672f07 | ||
|
9b0171ef37 | ||
|
12a041b466 | ||
|
6be40cb7c3 | ||
|
00fc38a5dc | ||
|
e09ec6e6b3 | ||
|
7d8cd736b8 | ||
|
47bba331c1 | ||
|
28483a7952 | ||
|
d4d57f0002 | ||
|
9bb2dd1851 | ||
|
e23eb99abf | ||
|
abd5c4f278 | ||
|
fb2c8f7621 | ||
|
29ad90f1e3 | ||
|
f61dc6d95a | ||
|
5a9983086a | ||
|
89ca8abea9 | ||
|
b1ee56a5ce | ||
|
5ba1d66be7 | ||
|
dcefb3eb9c | ||
|
7f1080368b | ||
|
de5657a91b | ||
|
f120e0d256 | ||
|
fe9f597eab | ||
|
794862a35a | ||
|
c02a538187 | ||
|
0e7d903a6f | ||
|
fe8374f2a4 | ||
|
0bb7ea10ab | ||
|
ed39b8dab0 | ||
|
8b2c14a6fa | ||
|
d341d85079 | ||
|
96cab22a8c | ||
|
3279ea568c | ||
|
aed919a05f | ||
|
7067c43ff4 | ||
|
0f670189eb | ||
|
3ee9674bb7 | ||
|
af74850e79 | ||
|
b151cf032b | ||
|
02243b1a2b | ||
|
80b71790bc | ||
|
c9edf3bf4a | ||
|
b898f86364 | ||
|
0926beaf73 | ||
|
8798ae5677 | ||
|
59ed11358f | ||
|
ce9debe9fd | ||
|
7e958589b3 | ||
|
396bc9b2e3 | ||
|
7b1e81689a | ||
|
1649c00cb7 | ||
|
fa00b52c47 | ||
|
7d629d45c3 | ||
|
f5a0e78c1b | ||
|
802599bdc9 | ||
|
29371b2f28 | ||
|
c4e967c003 | ||
|
8b2bad3366 | ||
|
acc2760512 | ||
|
90ea3d4440 | ||
|
9cb20241d9 | ||
|
4921a4caec | ||
|
32174f8f90 | ||
|
01e7b08aa9 | ||
|
afc086f33c | ||
|
e467491dbe | ||
|
3fdc62d29c | ||
|
a51084f7a7 | ||
|
5fb0401dca | ||
|
7323ffa25a | ||
|
57095d7167 | ||
|
50439ac441 | ||
|
1a3b41ed97 | ||
|
fa9484a06b | ||
|
cae8ff5949 | ||
|
4d61e3866c | ||
|
2c68342140 | ||
|
91ceaef02f | ||
|
7b1c888665 | ||
|
30bc96cf3f | ||
|
11ec1d9b06 | ||
|
482f4418c6 | ||
|
1327c21d01 | ||
|
f07eeddda0 | ||
|
a1be6124f2 | ||
|
45b7a0c837 | ||
|
29863ad2bf | ||
|
46c320513a | ||
|
93e4cf4006 | ||
|
526bdaa2dc | ||
|
65d4df938d | ||
|
992cef56e6 | ||
|
57ef25789e | ||
|
6ba46b38bd | ||
|
c0d01dbc26 | ||
|
949d616082 | ||
|
0db5c07314 | ||
|
92f8f231af | ||
|
7f4baab420 | ||
|
60e28a30f4 | ||
|
f15922a168 | ||
|
07750518c3 | ||
|
26d2b5b81f | ||
|
ae46b516e6 | ||
|
222a15922e | ||
|
f41fd4e88d | ||
|
5e195bdc0f | ||
|
949f4fbbbf | ||
|
8f4b598ee2 | ||
|
9491564148 | ||
|
2a4fb992c3 | ||
|
2b41066ab7 | ||
|
0071d002b6 | ||
|
35c4a0a188 | ||
|
ff45d52d49 | ||
|
d220c55d40 | ||
|
0d9beaa3f3 | ||
|
1c63d01cec | ||
|
f5e5203388 | ||
|
e73f5ab480 | ||
|
ad6ca3773d | ||
|
126c291988 | ||
|
9e77a739fa | ||
|
058c7b3e99 | ||
|
908dee961d | ||
|
93679db7c4 | ||
|
057a187231 | ||
|
3b5cc5f015 | ||
|
77388eb423 | ||
|
66e19f5775 | ||
|
5fdeca812d | ||
|
022839b728 | ||
|
e5e1e49f53 | ||
|
96d09b5615 | ||
|
6f8e66117b | ||
|
107fa911a5 | ||
|
c820db4c60 | ||
|
8961370269 | ||
|
2ee87f8c66 | ||
|
43339f1660 | ||
|
ba5d78f005 | ||
|
7eebb6bb2d | ||
|
755cc9cda1 | ||
|
26fdad8468 | ||
|
d0e0d0ee01 | ||
|
5ce09c7519 | ||
|
ffa9a3ac7d | ||
|
6fdcf8cd73 | ||
|
d1e3d48075 | ||
|
8d7f75c4de | ||
|
dadd134200 | ||
|
f69cb39a17 | ||
|
bd7d9c0d33 | ||
|
15df5fd9c5 | ||
|
99e3450d30 | ||
|
df45f467c6 | ||
|
95227376b6 | ||
|
08fcd1a0d4 | ||
|
5dd1088d8d | ||
|
5f23af5802 | ||
|
bd27993e79 | ||
|
178db516bf | ||
|
248c61bb26 | ||
|
fc511aac44 | ||
|
656526c007 | ||
|
51c6eb2014 | ||
|
e025576d8c | ||
|
ca973c05d1 | ||
|
626a40252d | ||
|
c3414c3b78 | ||
|
67e9626da1 | ||
|
a1566fe5d7 | ||
|
bc86cb3280 | ||
|
193b22475d | ||
|
bdca3e2343 | ||
|
a77ca22026 | ||
|
e1036d6f58 | ||
|
89b7dfda0e | ||
|
24baad7884 | ||
|
097da448e2 | ||
|
ca20e17d40 | ||
|
b2ac039d5c | ||
|
0f29cbc882 | ||
|
3b99c84b0a | ||
|
c6b684603c | ||
|
b946f8e7f1 | ||
|
3834bb86ff | ||
|
3845d55186 | ||
|
85979c3176 | ||
|
76a02ff70a | ||
|
2bed41da5d |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [xmatthias]
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -9,7 +9,7 @@ assignees: ''
|
||||
<!--
|
||||
Have you searched for similar issues before posting it?
|
||||
|
||||
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
||||
If you have discovered a bug in the bot, please [search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
||||
If it hasn't been reported, please create a new issue.
|
||||
|
||||
Please do not use bug reports to request new features.
|
||||
|
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -22,4 +22,4 @@ Please do not use the question template to report bugs or to request new feature
|
||||
|
||||
## Your question
|
||||
|
||||
*Ask the question you have not been able to find an answer in our [Documentation](https://www.freqtrade.io/en/latest/)*
|
||||
*Ask the question you have not been able to find an answer in the [Documentation](https://www.freqtrade.io/en/latest/)*
|
||||
|
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -5,9 +5,17 @@ updates:
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: develop
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: develop
|
||||
|
125
.github/workflows/ci.yml
vendored
125
.github/workflows/ci.yml
vendored
@@ -3,9 +3,9 @@ name: Freqtrade CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
- develop
|
||||
- ci/*
|
||||
tags:
|
||||
release:
|
||||
types: [published]
|
||||
@@ -20,13 +20,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: pip cache (linux)
|
||||
uses: actions/cache@v2
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
if: runner.os == 'Linux'
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||
@@ -50,8 +50,9 @@ jobs:
|
||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||
|
||||
- name: Installation - *nix
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade pip wheel
|
||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
@@ -69,7 +70,7 @@ jobs:
|
||||
if: matrix.python-version == '3.9'
|
||||
|
||||
- name: Coveralls
|
||||
if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8')
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
|
||||
env:
|
||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
@@ -101,29 +102,26 @@ jobs:
|
||||
run: |
|
||||
mypy freqtrade scripts
|
||||
|
||||
- name: Slack Notification
|
||||
uses: lazy-actions/slatify@v3.0.0
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
type: ${{ job.status }}
|
||||
job_name: '*Freqtrade CI ${{ matrix.os }}*'
|
||||
mention: 'here'
|
||||
mention_if: 'failure'
|
||||
channel: '#notifications'
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
severity: error
|
||||
details: Freqtrade CI failed on ${{ matrix.os }}
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
build_macos:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-latest ]
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -136,7 +134,7 @@ jobs:
|
||||
|
||||
- name: pip cache (macOS)
|
||||
uses: actions/cache@v2
|
||||
if: startsWith(matrix.os, 'macOS')
|
||||
if: runner.os == 'macOS'
|
||||
with:
|
||||
path: ~/Library/Caches/pip
|
||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||
@@ -147,10 +145,11 @@ jobs:
|
||||
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
|
||||
|
||||
- name: Installation - macOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew update
|
||||
brew install hdf5 c-blosc
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade pip wheel
|
||||
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
@@ -162,7 +161,7 @@ jobs:
|
||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||
|
||||
- name: Coveralls
|
||||
if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8')
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
|
||||
env:
|
||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
@@ -194,17 +193,13 @@ jobs:
|
||||
run: |
|
||||
mypy freqtrade scripts
|
||||
|
||||
- name: Slack Notification
|
||||
uses: lazy-actions/slatify@v3.0.0
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
type: ${{ job.status }}
|
||||
job_name: '*Freqtrade CI ${{ matrix.os }}*'
|
||||
mention: 'here'
|
||||
mention_if: 'failure'
|
||||
channel: '#notifications'
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
severity: info
|
||||
details: Test Succeeded!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
build_windows:
|
||||
|
||||
@@ -212,19 +207,18 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest ]
|
||||
python-version: [3.7, 3.8]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Pip cache (Windows)
|
||||
uses: actions/cache@preview
|
||||
if: startsWith(runner.os, 'Windows')
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||
@@ -257,28 +251,25 @@ jobs:
|
||||
run: |
|
||||
mypy freqtrade scripts
|
||||
|
||||
- name: Slack Notification
|
||||
uses: lazy-actions/slatify@v3.0.0
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
type: ${{ job.status }}
|
||||
job_name: '*Freqtrade CI windows*'
|
||||
mention: 'here'
|
||||
mention_if: 'failure'
|
||||
channel: '#notifications'
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
severity: error
|
||||
details: Test Failed
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
docs_check:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Documentation syntax
|
||||
run: |
|
||||
./tests/test_docs.sh
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
@@ -288,14 +279,13 @@ jobs:
|
||||
pip install mkdocs
|
||||
mkdocs build
|
||||
|
||||
- name: Slack Notification
|
||||
uses: lazy-actions/slatify@v3.0.0
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
type: ${{ job.status }}
|
||||
job_name: '*Freqtrade Docs*'
|
||||
channel: '#notifications'
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
severity: error
|
||||
details: Freqtrade doc test failed!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
cleanup-prior-runs:
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -306,7 +296,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
# Notify on slack only once - when CI completes (and after deploy) in case it's successfull
|
||||
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||
notify-complete:
|
||||
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -320,14 +310,13 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Slack Notification
|
||||
uses: lazy-actions/slatify@v3.0.0
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
type: ${{ job.status }}
|
||||
job_name: '*Freqtrade CI*'
|
||||
channel: '#notifications'
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
severity: info
|
||||
details: Test Completed!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
deploy:
|
||||
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
||||
@@ -336,10 +325,10 @@ jobs:
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
@@ -385,7 +374,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
uses: crazy-max/ghaction-docker-buildx@v3.3.1
|
||||
with:
|
||||
buildx-version: latest
|
||||
qemu-version: latest
|
||||
@@ -400,17 +389,13 @@ jobs:
|
||||
run: |
|
||||
build_helpers/publish_docker_multi.sh
|
||||
|
||||
|
||||
- name: Slack Notification
|
||||
uses: lazy-actions/slatify@v3.0.0
|
||||
- name: Discord notification
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
with:
|
||||
type: ${{ job.status }}
|
||||
job_name: '*Freqtrade CI Deploy*'
|
||||
mention: 'here'
|
||||
mention_if: 'failure'
|
||||
channel: '#notifications'
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
severity: info
|
||||
details: Deploy Succeeded!
|
||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
|
||||
deploy_arm:
|
||||
@@ -420,7 +405,7 @@ jobs:
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
|
4
.github/workflows/docker_update_readme.yml
vendored
4
.github/workflows/docker_update_readme.yml
vendored
@@ -8,9 +8,9 @@ jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v2.1.0
|
||||
uses: peter-evans/dockerhub-description@v2.4.3
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Freqtrade rules
|
||||
config*.json
|
||||
*.sqlite
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
logfile.txt
|
||||
user_data/*
|
||||
!user_data/strategy/sample_strategy.py
|
||||
@@ -10,6 +12,9 @@ freqtrade-plot.html
|
||||
freqtrade-profit-plot.html
|
||||
freqtrade/rpc/api_server/ui/*
|
||||
|
||||
# Macos related
|
||||
.DS_Store
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
55
.travis.yml
55
.travis.yml
@@ -1,55 +0,0 @@
|
||||
os:
|
||||
- linux
|
||||
dist: bionic
|
||||
language: python
|
||||
python:
|
||||
- 3.8
|
||||
services:
|
||||
- docker
|
||||
env:
|
||||
global:
|
||||
- IMAGE_NAME=freqtradeorg/freqtrade
|
||||
install:
|
||||
- cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies; cd ..
|
||||
- export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
||||
- export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
- export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
- pip install -r requirements-dev.txt
|
||||
- pip install -e .
|
||||
jobs:
|
||||
|
||||
include:
|
||||
- stage: tests
|
||||
script:
|
||||
- pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||
# Allow failure for coveralls
|
||||
# - coveralls || true
|
||||
name: pytest
|
||||
- script:
|
||||
- cp config_examples/config_bittrex.example.json config.json
|
||||
- freqtrade create-userdir --userdir user_data
|
||||
- freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||
name: backtest
|
||||
- script:
|
||||
- cp config_examples/config_bittrex.example.json config.json
|
||||
- freqtrade create-userdir --userdir user_data
|
||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily
|
||||
name: hyperopt
|
||||
- script: flake8
|
||||
name: flake8
|
||||
- script:
|
||||
# Test Documentation boxes -
|
||||
# !!! <TYPE>: is not allowed!
|
||||
# !!! <TYPE> "title" - Title needs to be quoted!
|
||||
- grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*; test $? -ne 0
|
||||
name: doc syntax
|
||||
- script: mypy freqtrade scripts
|
||||
name: mypy
|
||||
|
||||
notifications:
|
||||
slack:
|
||||
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
||||
cache:
|
||||
pip: True
|
||||
directories:
|
||||
- $HOME/dependencies
|
@@ -56,6 +56,13 @@ To help with that, we encourage you to install the git pre-commit
|
||||
hook that will warn you when you try to commit code that fails these checks.
|
||||
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
||||
|
||||
##### Additional styles applied
|
||||
|
||||
* Have docstrings on all public methods
|
||||
* Use double-quotes for docstrings
|
||||
* Multiline docstrings should be indented to the level of the first quote
|
||||
* Doc-strings should follow the reST format (`:param xxx: ...`, `:return: ...`, `:raises KeyError: ... `)
|
||||
|
||||
### 3. Test if all type-hints are correct
|
||||
|
||||
#### Run mypy
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM python:3.9.7-slim-buster as base
|
||||
FROM python:3.9.9-slim-bullseye as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
|
48
README.md
48
README.md
@@ -5,10 +5,14 @@
|
||||
[](https://www.freqtrade.io)
|
||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||
|
||||
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||
|
||||

|
||||
|
||||
## Sponsored promotion
|
||||
|
||||
[](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This software is for educational purposes only. Do not risk money which
|
||||
@@ -26,11 +30,13 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||
|
||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||
|
||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bittrex](https://bittrex.com/)
|
||||
- [X] [Kraken](https://kraken.com/)
|
||||
- [X] [FTX](https://ftx.com)
|
||||
- [X] [FTX](https://ftx.com/#a=2258149)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [Huobi](http://huobi.com/)
|
||||
- [X] [Kraken](https://kraken.com/)
|
||||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Community tested
|
||||
@@ -44,34 +50,28 @@ Exchanges confirmed working by the community:
|
||||
|
||||
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
||||
|
||||
Please find the complete documentation on our [website](https://www.freqtrade.io).
|
||||
Please find the complete documentation on the [freqtrade website](https://www.freqtrade.io).
|
||||
|
||||
## Features
|
||||
|
||||
- [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux.
|
||||
- [x] **Based on Python 3.8+**: For botting on any operating system - Windows, macOS and Linux.
|
||||
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||
- [x] **Dry-run**: Run the bot without paying 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] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/).
|
||||
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/).
|
||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||
- [x] **Builtin WebUI**: Builtin web UI to manage your bot.
|
||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram.
|
||||
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
|
||||
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
|
||||
- [x] **Display profit/loss in fiat**: Display your profit/loss in fiat currency.
|
||||
- [x] **Performance status report**: Provide a performance status of your current trades.
|
||||
|
||||
## Quick start
|
||||
|
||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
||||
Please refer to the [Docker Quickstart documentation](https://www.freqtrade.io/en/stable/docker_quickstart/) on how to get started quickly.
|
||||
|
||||
```bash
|
||||
git clone -b develop https://github.com/freqtrade/freqtrade.git
|
||||
cd freqtrade
|
||||
./setup.sh --install
|
||||
```
|
||||
|
||||
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/).
|
||||
For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
@@ -121,7 +121,7 @@ optional arguments:
|
||||
|
||||
### Telegram RPC commands
|
||||
|
||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on the [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
||||
|
||||
- `/start`: Starts the trader.
|
||||
- `/stop`: Stops the trader.
|
||||
@@ -152,10 +152,10 @@ For any questions not covered by the documentation or for further information ab
|
||||
### [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)
|
||||
[search the issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||
first. If it hasn't been reported, please
|
||||
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and
|
||||
ensure you follow the template guide so that our team can assist you as
|
||||
ensure you follow the template guide so that the team can assist you as
|
||||
quickly as possible.
|
||||
|
||||
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
||||
@@ -169,13 +169,13 @@ 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!
|
||||
Feel like the bot is missing a feature? We welcome your pull requests!
|
||||
|
||||
Please read our
|
||||
Please read the
|
||||
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
to understand the requirements before sending your pull-requests.
|
||||
|
||||
Coding is not a necessity to contribute - maybe start with improving our documentation?
|
||||
Coding is not a necessity to contribute - maybe start with improving the documentation?
|
||||
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
||||
|
||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) (please use the #dev channel for this). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||
@@ -196,7 +196,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||
|
||||
### Software requirements
|
||||
|
||||
- [Python 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [Python >= 3.8](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)
|
||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.24-cp310-cp310-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.24-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.24-cp38-cp38-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.24-cp38-cp38-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.24-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.24-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
@@ -11,8 +11,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
|
||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||
&& make -j$(nproc) \
|
||||
&& which sudo && sudo make install || make install
|
||||
&& make
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed building ta-lib."
|
||||
cd .. && rm -rf ./ta-lib/
|
||||
exit 1
|
||||
fi
|
||||
which sudo && sudo make install || make install
|
||||
if [ -x "$(command -v apt-get)" ]; then
|
||||
echo "Updating library path using ldconfig"
|
||||
sudo ldconfig
|
||||
|
@@ -1,19 +1,18 @@
|
||||
# Downloads don't work automatically, since the URL is regenerated via javascript.
|
||||
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
|
||||
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade pip wheel
|
||||
|
||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
if ($pyv -eq '3.7') {
|
||||
pip install build_helpers\TA_Lib-0.4.21-cp37-cp37m-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.8') {
|
||||
pip install build_helpers\TA_Lib-0.4.21-cp38-cp38-win_amd64.whl
|
||||
pip install build_helpers\TA_Lib-0.4.24-cp38-cp38-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.9') {
|
||||
pip install build_helpers\TA_Lib-0.4.21-cp39-cp39-win_amd64.whl
|
||||
pip install build_helpers\TA_Lib-0.4.24-cp39-cp39-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.10') {
|
||||
pip install build_helpers\TA_Lib-0.4.24-cp310-cp310-win_amd64.whl
|
||||
}
|
||||
|
||||
pip install -r requirements-dev.txt
|
||||
pip install -e .
|
||||
|
@@ -9,7 +9,9 @@
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
"sell": 10,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0,
|
||||
@@ -28,10 +30,8 @@
|
||||
"name": "binance",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 200
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"ALGO/BTC",
|
||||
|
@@ -9,7 +9,9 @@
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
"sell": 10,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"bid_strategy": {
|
||||
"use_order_book": true,
|
||||
|
@@ -9,7 +9,9 @@
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
"sell": 10,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0,
|
||||
@@ -28,11 +30,8 @@
|
||||
"name": "ftx",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 50
|
||||
},
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"BTC/USD",
|
||||
"ETH/USD",
|
||||
|
@@ -8,6 +8,7 @@
|
||||
"amend_last_stake_amount": false,
|
||||
"last_stake_amount_min_ratio": 0.5,
|
||||
"dry_run": true,
|
||||
"dry_run_wallet": 1000,
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"timeframe": "5m",
|
||||
"trailing_stop": false,
|
||||
@@ -18,6 +19,7 @@
|
||||
"sell_profit_only": false,
|
||||
"sell_profit_offset": 0.0,
|
||||
"ignore_roi_if_buy_signal": false,
|
||||
"ignore_buying_expired_candle_after": 300,
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
@@ -27,7 +29,8 @@
|
||||
"stoploss": -0.10,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30,
|
||||
"sell": 10,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"bid_strategy": {
|
||||
@@ -53,7 +56,8 @@
|
||||
"forcebuy": "market",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": false,
|
||||
"stoploss_on_exchange_interval": 60
|
||||
"stoploss_on_exchange_interval": 60,
|
||||
"stoploss_on_exchange_limit_ratio": 0.99
|
||||
},
|
||||
"order_time_in_force": {
|
||||
"buy": "gtc",
|
||||
@@ -84,12 +88,9 @@
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"password": "",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 500,
|
||||
"aiohttp_trust_env": false
|
||||
},
|
||||
"log_responses": false,
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"ALGO/BTC",
|
||||
"ATOM/BTC",
|
||||
|
@@ -9,7 +9,9 @@
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
"sell": 10,
|
||||
"exit_timeout_count": 0,
|
||||
"unit": "minutes"
|
||||
},
|
||||
"bid_strategy": {
|
||||
"use_order_book": true,
|
||||
@@ -28,10 +30,8 @@
|
||||
"name": "kraken",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_key",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 1000
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"ADA/EUR",
|
||||
|
@@ -15,10 +15,10 @@ services:
|
||||
volumes:
|
||||
- "./user_data:/freqtrade/user_data"
|
||||
# Expose api on port 8080 (localhost only)
|
||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||
# Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation
|
||||
# before enabling this.
|
||||
# ports:
|
||||
# - "127.0.0.1:8080:8080"
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
# Default command used when running `docker compose up`
|
||||
command: >
|
||||
trade
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM python:3.7.10-slim-buster as base
|
||||
FROM python:3.9.9-slim-bullseye as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
|
@@ -13,7 +13,7 @@ A sample of this can be found below, which is identical to the Default Hyperopt
|
||||
|
||||
``` python
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
@@ -105,7 +105,7 @@ You can define your own estimator for Hyperopt by implementing `generate_estimat
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator():
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
return "RF"
|
||||
|
||||
```
|
||||
@@ -119,13 +119,34 @@ Example for `ExtraTreesRegressor` ("ET") with additional parameters:
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator():
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
from skopt.learning import ExtraTreesRegressor
|
||||
# Corresponds to "ET" - but allows additional parameters.
|
||||
return ExtraTreesRegressor(n_estimators=100)
|
||||
|
||||
```
|
||||
|
||||
The `dimensions` parameter is the list of `skopt.space.Dimension` objects corresponding to the parameters to be optimized. It can be used to create isotropic kernels for the `skopt.learning.GaussianProcessRegressor` estimator. Here's an example:
|
||||
|
||||
```python
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
class HyperOpt:
|
||||
def generate_estimator(dimensions: List['Dimension'], **kwargs):
|
||||
from skopt.utils import cook_estimator
|
||||
from skopt.learning.gaussian_process.kernels import (Matern, ConstantKernel)
|
||||
kernel_bounds = (0.0001, 10000)
|
||||
kernel = (
|
||||
ConstantKernel(1.0, kernel_bounds) *
|
||||
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=2.5)
|
||||
)
|
||||
kernel += (
|
||||
ConstantKernel(1.0, kernel_bounds) *
|
||||
Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=1.5)
|
||||
)
|
||||
|
||||
return cook_estimator("GP", space=dimensions, kernel=kernel, n_restarts_optimizer=2)
|
||||
```
|
||||
|
||||
!!! Note
|
||||
While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used.
|
||||
If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters.
|
||||
|
@@ -52,6 +52,71 @@ freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user
|
||||
|
||||
For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md).
|
||||
|
||||
### Multiple instances using docker
|
||||
|
||||
To run multiple instances of freqtrade using docker you will need to edit the docker-compose.yml file and add all the instances you want as separate services. Remember, you can separate your configuration into multiple files, so it's a good idea to think about making them modular, then if you need to edit something common to all bots, you can do that in a single config file.
|
||||
``` yml
|
||||
---
|
||||
version: '3'
|
||||
services:
|
||||
freqtrade1:
|
||||
image: freqtradeorg/freqtrade:stable
|
||||
# image: freqtradeorg/freqtrade:develop
|
||||
# Use plotting image
|
||||
# image: freqtradeorg/freqtrade:develop_plot
|
||||
# Build step - only needed when additional dependencies are needed
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: "./docker/Dockerfile.custom"
|
||||
restart: always
|
||||
container_name: freqtrade1
|
||||
volumes:
|
||||
- "./user_data:/freqtrade/user_data"
|
||||
# Expose api on port 8080 (localhost only)
|
||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||
# before enabling this.
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
# Default command used when running `docker compose up`
|
||||
command: >
|
||||
trade
|
||||
--logfile /freqtrade/user_data/logs/freqtrade1.log
|
||||
--db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade1.sqlite
|
||||
--config /freqtrade/user_data/config.json
|
||||
--config /freqtrade/user_data/config.freqtrade1.json
|
||||
--strategy SampleStrategy
|
||||
|
||||
freqtrade2:
|
||||
image: freqtradeorg/freqtrade:stable
|
||||
# image: freqtradeorg/freqtrade:develop
|
||||
# Use plotting image
|
||||
# image: freqtradeorg/freqtrade:develop_plot
|
||||
# Build step - only needed when additional dependencies are needed
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: "./docker/Dockerfile.custom"
|
||||
restart: always
|
||||
container_name: freqtrade2
|
||||
volumes:
|
||||
- "./user_data:/freqtrade/user_data"
|
||||
# Expose api on port 8080 (localhost only)
|
||||
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||
# before enabling this.
|
||||
ports:
|
||||
- "127.0.0.1:8081:8080"
|
||||
# Default command used when running `docker compose up`
|
||||
command: >
|
||||
trade
|
||||
--logfile /freqtrade/user_data/logs/freqtrade2.log
|
||||
--db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade2.sqlite
|
||||
--config /freqtrade/user_data/config.json
|
||||
--config /freqtrade/user_data/config.freqtrade2.json
|
||||
--strategy SampleStrategy
|
||||
|
||||
```
|
||||
You can use whatever naming convention you want, freqtrade1 and 2 are arbitrary. Note, that you will need to use different database files, port mappings and telegram configurations for each instance, as mentioned above.
|
||||
|
||||
|
||||
## Configure the bot running as a systemd service
|
||||
|
||||
Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||
@@ -111,12 +176,15 @@ Log messages are send to `syslog` with the `user` facility. So you can see them
|
||||
On many systems `syslog` (`rsyslog`) fetches data from `journald` (and vice versa), so both `--logfile syslog` or `--logfile journald` can be used and the messages be viewed with both `journalctl` and a syslog viewer utility. You can combine this in any way which suites you better.
|
||||
|
||||
For `rsyslog` the messages from the bot can be redirected into a separate dedicated log file. To achieve this, add
|
||||
|
||||
```
|
||||
if $programname startswith "freqtrade" then -/var/log/freqtrade.log
|
||||
```
|
||||
|
||||
to one of the rsyslog configuration files, for example at the end of the `/etc/rsyslog.d/50-default.conf`.
|
||||
|
||||
For `syslog` (`rsyslog`), the reduction mode can be switched on. This will reduce the number of repeating messages. For instance, multiple bot Heartbeat messages will be reduced to a single message when nothing else happens with the bot. To achieve this, set in `/etc/rsyslog.conf`:
|
||||
|
||||
```
|
||||
# Filter duplicated messages
|
||||
$RepeatedMsgReduction on
|
||||
|
BIN
docs/assets/TokenBot-Freqtrade-banner.png
Normal file
BIN
docs/assets/TokenBot-Freqtrade-banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
docs/assets/frequi_url.png
Normal file
BIN
docs/assets/frequi_url.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 143 KiB |
BIN
docs/assets/windows_install.png
Normal file
BIN
docs/assets/windows_install.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
@@ -21,16 +21,18 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export {none,trades}] [--export-filename PATH]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--cache {none,day,week,month}]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
--data-format-ohlcv {json,jsongz,hdf5}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `None`).
|
||||
(default: `json`).
|
||||
--max-open-trades INT
|
||||
Override the value of the `max_open_trades`
|
||||
configuration setting.
|
||||
@@ -61,12 +63,11 @@ optional arguments:
|
||||
`30m`, `1h`, `1d`).
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
Provide a space-separated list of strategies to
|
||||
backtest. Please note that ticker-interval needs to be
|
||||
backtest. Please note that timeframe needs to be
|
||||
set either in config or via command line. When using
|
||||
this together with `--export trades`, the strategy-
|
||||
name is injected into the filename (so `backtest-
|
||||
data.json` becomes `backtest-data-
|
||||
SampleStrategy.json`
|
||||
data.json` becomes `backtest-data-SampleStrategy.json`
|
||||
--export {none,trades}
|
||||
Export backtest results (default: trades).
|
||||
--export-filename PATH
|
||||
@@ -74,6 +75,11 @@ optional arguments:
|
||||
Requires `--export` to be set as well. Example:
|
||||
`--export-filename=user_data/backtest_results/backtest
|
||||
_today.json`
|
||||
--breakdown {day,week,month} [{day,week,month} ...]
|
||||
Show backtesting breakdown per [day, week, month].
|
||||
--cache {none,day,week,month}
|
||||
Load a cached backtest result no older than specified
|
||||
age (default: day).
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
@@ -113,7 +119,7 @@ The result of backtesting will confirm if your bot has better odds of making a p
|
||||
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
|
||||
|
||||
!!! Warning "Using dynamic pairlists for backtesting"
|
||||
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
||||
Using dynamic pairlists is possible (not all of the handlers are allowed to be used in backtest mode), however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
||||
Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed.
|
||||
Please read the [pairlists documentation](plugins.md#pairlists) for more information.
|
||||
|
||||
@@ -307,10 +313,11 @@ A backtesting result will look like that:
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Rejected Buy signals | 3089 |
|
||||
| Entry/Exit Timeouts | 0 / 0 |
|
||||
| | |
|
||||
| Min balance | 0.00945123 BTC |
|
||||
| Max balance | 0.01846651 BTC |
|
||||
| Drawdown | 50.63% |
|
||||
| Drawdown (Account) | 13.33% |
|
||||
| Drawdown | 0.0015 BTC |
|
||||
| Drawdown high | 0.0013 BTC |
|
||||
| Drawdown low | -0.0002 BTC |
|
||||
@@ -394,10 +401,11 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Rejected Buy signals | 3089 |
|
||||
| Entry/Exit Timeouts | 0 / 0 |
|
||||
| | |
|
||||
| Min balance | 0.00945123 BTC |
|
||||
| Max balance | 0.01846651 BTC |
|
||||
| Drawdown | 50.63% |
|
||||
| Drawdown (Account) | 13.33% |
|
||||
| Drawdown | 0.0015 BTC |
|
||||
| Drawdown high | 0.0013 BTC |
|
||||
| Drawdown low | -0.0002 BTC |
|
||||
@@ -423,16 +431,51 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
|
||||
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
|
||||
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
|
||||
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.
|
||||
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
|
||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
||||
|
||||
### Daily / Weekly / Monthly breakdown
|
||||
|
||||
You can get an overview over daily / weekly or monthly results by using the `--breakdown <>` switch.
|
||||
|
||||
To visualize daily and weekly breakdowns, you can use the following:
|
||||
|
||||
``` bash
|
||||
freqtrade backtesting --strategy MyAwesomeStrategy --breakdown day month
|
||||
```
|
||||
|
||||
``` output
|
||||
======================== DAY BREAKDOWN =========================
|
||||
| Day | Tot Profit USDT | Wins | Draws | Losses |
|
||||
|------------+-------------------+--------+---------+----------|
|
||||
| 03/07/2021 | 200.0 | 2 | 0 | 0 |
|
||||
| 04/07/2021 | -50.31 | 0 | 0 | 2 |
|
||||
| 05/07/2021 | 220.611 | 3 | 2 | 0 |
|
||||
| 06/07/2021 | 150.974 | 3 | 0 | 2 |
|
||||
| 07/07/2021 | -70.193 | 1 | 0 | 2 |
|
||||
| 08/07/2021 | 212.413 | 2 | 0 | 3 |
|
||||
|
||||
```
|
||||
|
||||
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day.
|
||||
|
||||
### Backtest result caching
|
||||
|
||||
To save time, by default backtest will reuse a cached result from within the last day when the backtested strategy and config match that of a previous backtest. To force a new backtest despite existing result for an identical run specify `--cache none` parameter.
|
||||
|
||||
!!! Warning
|
||||
Caching is automatically disabled for open-ended timeranges (`--timerange 20210101-`), as freqtrade cannot ensure reliably that the underlying data didn't change. It can also use cached results where it shouldn't if the original backtest had missing data at the end, which was fixed by downloading more data.
|
||||
In this instance, please use `--cache none` once to force a fresh backtest.
|
||||
|
||||
### Further backtest-result analysis
|
||||
|
||||
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
||||
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
|
||||
You can then load the trades to perform further analysis as shown in the [data analysis](data-analysis.md#backtesting) backtesting section.
|
||||
|
||||
## Assumptions made by backtesting
|
||||
|
||||
@@ -451,13 +494,14 @@ Since backtesting lacks some detailed information about what happens within a ca
|
||||
- Low happens before high for stoploss, protecting capital first
|
||||
- Trailing stoploss
|
||||
- Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
|
||||
- On trade entry candles that trigger trailing stoploss, the "minimum offset" (`stop_positive_offset`) is assumed (instead of high) - and the stop is calculated from this point
|
||||
- High happens first - adjusting stoploss
|
||||
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
|
||||
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
|
||||
- Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used)
|
||||
- Evaluation sequence (if multiple signals happen on the same candle)
|
||||
- ROI (if not stoploss)
|
||||
- Sell-signal
|
||||
- ROI (if not stoploss)
|
||||
- Stoploss
|
||||
|
||||
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
|
||||
|
@@ -24,7 +24,7 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
|
||||
|
||||
* Fetch open trades from persistence.
|
||||
* Calculate current list of tradable pairs.
|
||||
* Download ohlcv data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs)
|
||||
* Download OHLCV data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs)
|
||||
This step is only executed once per Candle to avoid unnecessary network traffic.
|
||||
* Call `bot_loop_start()` strategy callback.
|
||||
* Analyze strategy per pair.
|
||||
@@ -38,6 +38,7 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
|
||||
* Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
|
||||
* Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
|
||||
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||
* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required.
|
||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||
* Verifies buy signal trying to enter new positions.
|
||||
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
|
||||
@@ -56,7 +57,12 @@ This loop will be repeated again and again until the bot is stopped.
|
||||
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
|
||||
* Loops per candle simulating entry and exit points.
|
||||
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
||||
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
||||
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
||||
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks.
|
||||
* Generate backtest report output
|
||||
|
||||
!!! Note
|
||||
|
@@ -37,6 +37,15 @@ Using this scheme, all configuration settings will also be available as environm
|
||||
|
||||
Please note that Environment variables will overwrite corresponding settings in your configuration, but command line Arguments will always win.
|
||||
|
||||
Common example:
|
||||
|
||||
```
|
||||
FREQTRADE__TELEGRAM__CHAT_ID=<telegramchatid>
|
||||
FREQTRADE__TELEGRAM__TOKEN=<telegramToken>
|
||||
FREQTRADE__EXCHANGE__KEY=<yourExchangeKey>
|
||||
FREQTRADE__EXCHANGE__SECRET=<yourExchangeSecret>
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable.
|
||||
|
||||
@@ -77,7 +86,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
||||
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
||||
| `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
||||
| `timeframe` | The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
|
||||
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
|
||||
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
|
||||
@@ -93,6 +102,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
||||
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
||||
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
|
||||
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
|
||||
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
||||
@@ -116,14 +126,16 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `exchange.uid` | API uid to use for the exchange. Only required when you are in production mode and for exchanges that use uid for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
||||
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
||||
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs. <br> **Datatype:** Dict
|
||||
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
@@ -160,6 +172,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
|
||||
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
|
||||
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
|
||||
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
|
||||
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
|
||||
|
||||
### Parameters in the strategy
|
||||
|
||||
@@ -184,6 +198,8 @@ Values set in the configuration file always overwrite values set in the strategy
|
||||
* `sell_profit_offset`
|
||||
* `ignore_roi_if_buy_signal`
|
||||
* `ignore_buying_expired_candle_after`
|
||||
* `position_adjustment_enable`
|
||||
* `max_entry_position_adjustment`
|
||||
|
||||
### Configuring amount per trade
|
||||
|
||||
@@ -192,9 +208,8 @@ There are several methods to configure how much of the stake currency the bot wi
|
||||
#### Minimum trade stake
|
||||
|
||||
The minimum stake amount will depend on exchange and pair and is usually listed in the exchange support pages.
|
||||
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$.
|
||||
|
||||
The minimum stake amount to buy this pair is, therefore, `20 * 0.6 ~= 12`.
|
||||
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$, the minimum stake amount to buy this pair is `20 * 0.6 ~= 12`.
|
||||
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
|
||||
|
||||
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
|
||||
@@ -204,7 +219,7 @@ With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)
|
||||
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
|
||||
|
||||
!!! Warning
|
||||
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange.
|
||||
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange. Freqtrade adjusts the stake-amount to this value, unless it's > 30% more than the calculated/desired stake-amount - in which case the trade is rejected.
|
||||
|
||||
#### Tradable balance
|
||||
|
||||
@@ -291,6 +306,15 @@ To allow the bot to trade all the available `stake_currency` in your account (mi
|
||||
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve.
|
||||
It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise, it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
||||
|
||||
#### Dynamic stake amount with position adjustment
|
||||
|
||||
When you want to use position adjustment with unlimited stakes, you must also implement `custom_stake_amount` to a return a value depending on your strategy.
|
||||
Typical value would be in the range of 25% - 50% of the proposed stakes, but depends highly on your strategy and how much you wish to leave into the wallet as position adjustment buffer.
|
||||
|
||||
For example if your position adjustment assumes it can do 2 additional buys with the same stake amounts then your buffer should be 66.6667% of the initially proposed unlimited stake amount.
|
||||
|
||||
Or another example if your position adjustment assumes it can do 1 additional buy with 3x the original stake amount then `custom_stake_amount` should return 25% of proposed stake amount and leave 75% for possible later position adjustments.
|
||||
|
||||
--8<-- "includes/pricing.md"
|
||||
|
||||
### Understand minimal_roi
|
||||
@@ -447,45 +471,6 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
|
||||
This is ongoing work. For now, it is supported only for binance and kucoin.
|
||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
||||
|
||||
### Exchange configuration
|
||||
|
||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 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 by the development team with only Bittrex, Binance and Kraken,
|
||||
so these are the only officially supported exchanges:
|
||||
|
||||
- [Bittrex](https://bittrex.com/): "bittrex"
|
||||
- [Binance](https://www.binance.com/): "binance"
|
||||
- [Kraken](https://kraken.com/): "kraken"
|
||||
|
||||
Feel free to test other exchanges and submit your PR to improve the bot.
|
||||
|
||||
Some exchanges require special configuration, which can be found on the [Exchange-specific Notes](exchanges.md) documentation page.
|
||||
|
||||
#### Sample exchange configuration
|
||||
|
||||
A exchange configuration for "binance" would look as follows:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 200
|
||||
},
|
||||
```
|
||||
|
||||
This configuration enables binance, as well as rate-limiting to avoid bans from the exchange.
|
||||
`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
|
||||
|
||||
!!! Note
|
||||
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
||||
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
|
||||
|
||||
### What values can be used for fiat_display_currency?
|
||||
|
||||
The `fiat_display_currency` configuration parameter sets the base currency to use for the
|
||||
|
@@ -50,19 +50,22 @@ Repetitive tasks | Shell scripts
|
||||
Data analysis & visualization | Notebook
|
||||
|
||||
1. Use the CLI to
|
||||
|
||||
* download historical data
|
||||
* run a backtest
|
||||
* run with real-time data
|
||||
* export results
|
||||
|
||||
1. Collect these actions in shell scripts
|
||||
|
||||
* save complicated commands with arguments
|
||||
* execute multi-step operations
|
||||
* automate testing strategies and preparing data for analysis
|
||||
|
||||
1. Use a notebook to
|
||||
|
||||
* visualize data
|
||||
* munge and plot to generate insights
|
||||
* mangle and plot to generate insights
|
||||
|
||||
## Example utility snippets
|
||||
|
||||
|
@@ -11,7 +11,7 @@ Otherwise `--exchange` becomes mandatory.
|
||||
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
|
||||
|
||||
!!! Tip "Tip: Updating existing data"
|
||||
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data.
|
||||
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, freqtrade will automatically calculate the data missing for the existing pairs and the download will occur from the latest available point until "now", neither --days or --timerange parameters are required. Freqtrade will keep the available data and only download the missing data.
|
||||
If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only.
|
||||
If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data.
|
||||
|
||||
@@ -22,6 +22,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-d PATH] [--userdir PATH]
|
||||
[-p PAIRS [PAIRS ...]] [--pairs-file FILE]
|
||||
[--days INT] [--new-pairs-days INT]
|
||||
[--include-inactive-pairs]
|
||||
[--timerange TIMERANGE] [--dl-trades]
|
||||
[--exchange EXCHANGE]
|
||||
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
|
||||
@@ -38,6 +39,8 @@ optional arguments:
|
||||
--days INT Download data for given number of days.
|
||||
--new-pairs-days INT Download data of new pairs for given number of days.
|
||||
Default: `None`.
|
||||
--include-inactive-pairs
|
||||
Also download data from inactive pairs.
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
--dl-trades Download trades instead of OHLCV data. The bot will
|
||||
@@ -52,10 +55,10 @@ optional arguments:
|
||||
exchange/pairs/timeframes.
|
||||
--data-format-ohlcv {json,jsongz,hdf5}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `None`).
|
||||
(default: `json`).
|
||||
--data-format-trades {json,jsongz,hdf5}
|
||||
Storage format for downloaded trades data. (default:
|
||||
`None`).
|
||||
`jsongz`).
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
@@ -80,6 +83,82 @@ Common arguments:
|
||||
|
||||
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
|
||||
|
||||
### Pairs file
|
||||
|
||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||
If you are using Binance for example:
|
||||
|
||||
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
||||
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
||||
|
||||
```bash
|
||||
mkdir -p user_data/data/binance
|
||||
touch user_data/data/binance/pairs.json
|
||||
```
|
||||
|
||||
The format of the `pairs.json` file is a simple json list.
|
||||
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
||||
|
||||
``` json
|
||||
[
|
||||
"ETH/BTC",
|
||||
"ETH/USDT",
|
||||
"BTC/USDT",
|
||||
"XRP/ETH"
|
||||
]
|
||||
```
|
||||
|
||||
!!! Tip "Downloading all data for one quote currency"
|
||||
Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
|
||||
`freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
|
||||
To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
|
||||
|
||||
??? Note "Permission denied errors"
|
||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||
|
||||
```
|
||||
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
|
||||
```
|
||||
|
||||
You can fix the permissions of your user-data directory as follows:
|
||||
|
||||
```
|
||||
sudo chown -R $UID:$GID user_data
|
||||
```
|
||||
|
||||
### Start download
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance
|
||||
```
|
||||
|
||||
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
|
||||
|
||||
Alternatively, specify the pairs directly
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
|
||||
```
|
||||
|
||||
or as regex (to download all active USDT pairs)
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance --pairs .*/USDT
|
||||
```
|
||||
|
||||
### Other Notes
|
||||
|
||||
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||
- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
||||
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
|
||||
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||
|
||||
|
||||
### Data format
|
||||
|
||||
Freqtrade currently supports 3 data-formats for both OHLCV and trades data:
|
||||
@@ -204,6 +283,61 @@ It'll also remove original jsongz data files (`--erase` parameter).
|
||||
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
|
||||
```
|
||||
|
||||
### Sub-command trades to ohlcv
|
||||
|
||||
When you need to use `--dl-trades` (kraken only) to download data, conversion of trades data to ohlcv data is the last step.
|
||||
This command will allow you to repeat this last step for additional timeframes without re-downloading the data.
|
||||
|
||||
```
|
||||
usage: freqtrade trades-to-ohlcv [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-d PATH] [--userdir PATH]
|
||||
[-p PAIRS [PAIRS ...]]
|
||||
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
|
||||
[--exchange EXCHANGE]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||
[--data-format-trades {json,jsongz,hdf5}]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
separated.
|
||||
-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]
|
||||
Specify which tickers to download. Space-separated
|
||||
list. Default: `1m 5m`.
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
--data-format-ohlcv {json,jsongz,hdf5}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `json`).
|
||||
--data-format-trades {json,jsongz,hdf5}
|
||||
Storage format for downloaded trades data. (default:
|
||||
`jsongz`).
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
-c PATH, --config PATH
|
||||
Specify configuration file (default:
|
||||
`userdir/config.json` or `config.json` whichever
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
```
|
||||
|
||||
#### Example trade-to-ohlcv conversion
|
||||
|
||||
``` bash
|
||||
freqtrade trades-to-ohlcv --exchange kraken -t 5m 1h 1d --pairs BTC/EUR ETH/EUR
|
||||
```
|
||||
|
||||
### Sub-command list-data
|
||||
|
||||
You can get a list of downloaded data using the `list-data` sub-command.
|
||||
@@ -257,64 +391,6 @@ ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
||||
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
||||
```
|
||||
|
||||
### Pairs file
|
||||
|
||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||
|
||||
If you are using Binance for example:
|
||||
|
||||
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
||||
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
||||
|
||||
```bash
|
||||
mkdir -p user_data/data/binance
|
||||
cp tests/testdata/pairs.json user_data/data/binance
|
||||
```
|
||||
|
||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||
|
||||
```
|
||||
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
|
||||
```
|
||||
|
||||
You can fix the permissions of your user-data directory as follows:
|
||||
|
||||
```
|
||||
sudo chown -R $UID:$GID user_data
|
||||
```
|
||||
|
||||
The format of the `pairs.json` file is a simple json list.
|
||||
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
||||
|
||||
``` json
|
||||
[
|
||||
"ETH/BTC",
|
||||
"ETH/USDT",
|
||||
"BTC/USDT",
|
||||
"XRP/ETH"
|
||||
]
|
||||
```
|
||||
|
||||
### Start download
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance
|
||||
```
|
||||
|
||||
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
|
||||
|
||||
### Other Notes
|
||||
|
||||
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||
- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
||||
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
|
||||
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||
|
||||
### Trades (tick) data
|
||||
|
||||
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
|
||||
|
@@ -15,8 +15,8 @@ This command line option was deprecated in 2019.7-dev (develop branch) and remov
|
||||
|
||||
### The **--dynamic-whitelist** command line option
|
||||
|
||||
This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch)
|
||||
and in freqtrade 2019.7.
|
||||
This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) and in freqtrade 2019.7.
|
||||
Please refer to [pairlists](plugins.md#pairlists-and-pairlist-handlers) instead.
|
||||
|
||||
### the `--live` command line option
|
||||
|
||||
@@ -24,6 +24,10 @@ and in freqtrade 2019.7.
|
||||
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
|
||||
Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8.
|
||||
|
||||
### `ticker_interval` (now `timeframe`)
|
||||
|
||||
Support for `ticker_interval` terminology was deprecated in 2020.6 in favor of `timeframe` - and compatibility code was removed in 2022.3.
|
||||
|
||||
### Allow running multiple pairlists in sequence
|
||||
|
||||
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
|
||||
|
@@ -8,7 +8,7 @@ All contributions, bug reports, bug fixes, documentation improvements, enhanceme
|
||||
|
||||
Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
|
||||
|
||||
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/).
|
||||
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/reference/admonitions/).
|
||||
|
||||
To test the documentation locally use the following commands.
|
||||
|
||||
@@ -26,6 +26,8 @@ Alternatively (e.g. if your system is not supported by the setup.sh script), fol
|
||||
|
||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
||||
|
||||
Before opening a pull request, please familiarize yourself with our [Contributing Guidelines](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md).
|
||||
|
||||
### Devcontainer setup
|
||||
|
||||
The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension.
|
||||
@@ -250,7 +252,23 @@ Most exchanges supported by CCXT should work out of the box.
|
||||
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
|
||||
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
||||
|
||||
Also try to use `freqtrade download-data` for an extended timerange and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
|
||||
Also try to use `freqtrade download-data` for an extended timerange (multiple months) and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
|
||||
|
||||
These are prerequisites to have an exchange listed as either Supported or Community tested (listed on the homepage).
|
||||
The below are "extras", which will make an exchange better (feature-complete) - but are not absolutely necessary for either of the 2 categories.
|
||||
|
||||
Additional tests / steps to complete:
|
||||
|
||||
* Verify data provided by `fetch_ohlcv()` - and eventually adjust `ohlcv_candle_limit` for this exchange
|
||||
* Check L2 orderbook limit range (API documentation) - and eventually set as necessary
|
||||
* Check if balance shows correctly (*)
|
||||
* Create market order (*)
|
||||
* Create limit order (*)
|
||||
* Complete trade (buy + sell) (*)
|
||||
* Compare result calculation between exchange and bot
|
||||
* Ensure fees are applied correctly (check the database against the exchange)
|
||||
|
||||
(*) Requires API keys and Balance on the exchange.
|
||||
|
||||
### Stoploss On Exchange
|
||||
|
||||
@@ -306,9 +324,8 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade
|
||||
This documents some decisions taken for the CI Pipeline.
|
||||
|
||||
* CI runs on all OS variants, Linux (ubuntu), macOS and Windows.
|
||||
* Docker images are build for the branches `stable` and `develop`.
|
||||
* Docker images are build for the branches `stable` and `develop`, and are built as multiarch builds, supporting multiple platforms via the same tag.
|
||||
* Docker images containing Plot dependencies are also available as `stable_plot` and `develop_plot`.
|
||||
* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:stable_pi` and `develop_pi`.
|
||||
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
|
||||
* Full docker image rebuilds are run once a week via schedule.
|
||||
* Deployments run on ubuntu.
|
||||
|
@@ -70,6 +70,18 @@ docker-compose up -d
|
||||
!!! Warning "Default configuration"
|
||||
While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot.
|
||||
|
||||
#### Accessing the UI
|
||||
|
||||
If you've selected to enable FreqUI in the `new-config` step, you will have freqUI available at port `localhost:8080`.
|
||||
|
||||
You can now access the UI by typing localhost:8080 in your browser.
|
||||
|
||||
??? Note "UI Access on a remote servers"
|
||||
If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot.
|
||||
This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box).
|
||||
Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet.
|
||||
Please also read the [API configuration with docker](rest-api.md#configuration-with-docker) section to learn more about this configuration.
|
||||
|
||||
#### Monitoring the bot
|
||||
|
||||
You can check for running instances with `docker-compose ps`.
|
||||
@@ -109,10 +121,17 @@ All freqtrade arguments will be available by running `docker-compose run --rm fr
|
||||
!!! Warning "`docker-compose` for trade commands"
|
||||
Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead.
|
||||
This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot.
|
||||
If you intend to use freqUI, please also ensure to adjust the [configuration accordingly](rest-api.md#configuration-with-docker), otherwise the UI will not be available.
|
||||
|
||||
!!! Note "`docker-compose run --rm`"
|
||||
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
|
||||
|
||||
??? Note "Using docker without docker-compose"
|
||||
"`docker-compose run --rm`" will require a compose file to be provided.
|
||||
Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead.
|
||||
For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`.
|
||||
This can be useful for fetching exchange information to add to your `config.json` without affecting your running containers.
|
||||
|
||||
#### Example: Download data with docker-compose
|
||||
|
||||
Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host.
|
||||
@@ -147,27 +166,9 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the
|
||||
dockerfile: "./Dockerfile.<yourextension>"
|
||||
```
|
||||
|
||||
You can then run `docker-compose build` to build the docker image, and run it using the commands described above.
|
||||
You can then run `docker-compose build --pull` to build the docker image, and run it using the commands described above.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Docker on Windows
|
||||
|
||||
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
||||
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
|
||||
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
|
||||
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
|
||||
```
|
||||
taskkill /IM "Docker Desktop.exe" /F
|
||||
wsl --shutdown
|
||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
||||
Best use a linux-VPS for running freqtrade reliably.
|
||||
|
||||
## Plotting with docker-compose
|
||||
### Plotting with docker-compose
|
||||
|
||||
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
|
||||
You can then use these commands as follows:
|
||||
@@ -178,7 +179,7 @@ docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p B
|
||||
|
||||
The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser.
|
||||
|
||||
## Data analysis using docker compose
|
||||
### Data analysis using docker compose
|
||||
|
||||
Freqtrade provides a docker-compose file which starts up a jupyter lab server.
|
||||
You can run this server using the following command:
|
||||
@@ -195,3 +196,22 @@ Since part of this image is built on your machine, it is recommended to rebuild
|
||||
``` bash
|
||||
docker-compose -f docker/docker-compose-jupyter.yml build --no-cache
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Docker on Windows
|
||||
|
||||
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
||||
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
|
||||
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
|
||||
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
|
||||
|
||||
``` bash
|
||||
taskkill /IM "Docker Desktop.exe" /F
|
||||
wsl --shutdown
|
||||
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
||||
Best use a linux-VPS for running freqtrade reliably.
|
||||
|
@@ -222,7 +222,7 @@ usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
|
@@ -2,12 +2,62 @@
|
||||
|
||||
This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges.
|
||||
|
||||
## Exchange configuration
|
||||
|
||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 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 by the development team with only a few exchanges.
|
||||
A current list of these can be found in the "Home" section of this documentation.
|
||||
|
||||
Feel free to test other exchanges and submit your feedback or PR to improve the bot or confirm exchanges that work flawlessly..
|
||||
|
||||
Some exchanges require special configuration, which can be found below.
|
||||
|
||||
### Sample exchange configuration
|
||||
|
||||
A exchange configuration for "binance" would look as follows:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "binance",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
// ...
|
||||
```
|
||||
|
||||
### Setting rate limits
|
||||
|
||||
Usually, rate limits set by CCXT are reliable and work well.
|
||||
In case of problems related to rate-limits (usually DDOS Exceptions in your logs), it's easy to change rateLimit settings to other values.
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "kraken",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
"rateLimit": 3100
|
||||
},
|
||||
```
|
||||
|
||||
This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange.
|
||||
`"rateLimit": 3100` defines a wait-event of 3.1s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
|
||||
|
||||
!!! Note
|
||||
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
||||
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
|
||||
|
||||
## Binance
|
||||
|
||||
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
Binance supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||
|
||||
### Binance Blacklist
|
||||
|
||||
@@ -127,11 +177,45 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
||||
|
||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||
|
||||
### Kucoin Blacklists
|
||||
|
||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
||||
|
||||
## Huobi
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||
|
||||
## OKX (former OKEX)
|
||||
|
||||
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "okx",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"password": "your_exchange_api_key_password",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
|
||||
|
||||
## Gate.io
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||
|
||||
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
||||
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
||||
|
||||
## All exchanges
|
||||
|
||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||
|
53
docs/faq.md
53
docs/faq.md
@@ -42,7 +42,7 @@ position for a trade. Be patient!
|
||||
### I have made 12 trades already, why is my total profit negative?
|
||||
|
||||
I understand your disappointment but unfortunately 12 trades is just
|
||||
not enough to say anything. If you run backtesting, you can see that our
|
||||
not enough to say anything. If you run backtesting, you can see that the
|
||||
current algorithm does leave you on the plus side, but that is after
|
||||
thousands of trades and even there, you will be left with losses on
|
||||
specific coins that you have traded tens if not hundreds of times. We
|
||||
@@ -54,9 +54,26 @@ you can't say much from few trades.
|
||||
|
||||
Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy.
|
||||
|
||||
### I want to improve the bot with a new strategy
|
||||
### Why does my bot not sell everything it bought?
|
||||
|
||||
That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
|
||||
This is called "coin dust" and can happen on all exchanges.
|
||||
It happens because many exchanges subtract fees from the "receiving currency" - so you buy 100 COIN - but you only get 99.9 COIN.
|
||||
As COIN is trading in full lot sizes (1COIN steps), you cannot sell 0.9 COIN (or 99.9 COIN) - but you need to round down to 99 COIN.
|
||||
|
||||
This is not a bot-problem, but will also happen while manual trading.
|
||||
|
||||
While freqtrade can handle this (it'll sell 99 COIN), fees are often below the minimum tradable lot-size (you can only trade full COIN, not 0.9 COIN).
|
||||
Leaving the dust (0.9 COIN) on the exchange makes usually sense, as the next time freqtrade buys COIN, it'll eat into the remaining small balance, this time selling everything it bought, and therefore slowly declining the dust balance (although it most likely will never reach exactly 0).
|
||||
|
||||
Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this.
|
||||
On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations).
|
||||
Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange.
|
||||
|
||||
### I want to use incomplete candles
|
||||
|
||||
Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened.
|
||||
|
||||
You can use "current" market data by using the [dataprovider](strategy-customization.md#orderbookpair-maximum)'s orderbook or ticker methods - which however cannot be used during backtesting.
|
||||
|
||||
### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
|
||||
|
||||
@@ -76,17 +93,29 @@ If this happens for all pairs in the pairlist, this might indicate a recent exch
|
||||
|
||||
Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles.
|
||||
|
||||
### I'm getting "Outdated history for pair xxx" in the log
|
||||
|
||||
The bot is trying to tell you that it got an outdated last candle (not the last complete candle).
|
||||
As a consequence, Freqtrade will not enter a trade for this pair - as trading on old information is usually not what is desired.
|
||||
|
||||
This warning can point to one of the below problems:
|
||||
|
||||
* Exchange downtime -> Check your exchange status page / blog / twitter feed for details.
|
||||
* Wrong system time -> Ensure your system-time is correct.
|
||||
* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling.
|
||||
* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges).
|
||||
|
||||
### I'm getting the "RESTRICTED_MARKET" message in the log
|
||||
|
||||
Currently known to happen for US Bittrex users.
|
||||
|
||||
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
|
||||
|
||||
### I'm getting the "Exchange Bittrex does not support market orders." message and cannot run my strategy
|
||||
### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy
|
||||
|
||||
As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex).
|
||||
As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex and Gate.io).
|
||||
|
||||
To fix it for Bittrex, redefine order types in the strategy to use "limit" instead of "market":
|
||||
To fix this, redefine order types in the strategy to use "limit" instead of "market":
|
||||
|
||||
```
|
||||
order_types = {
|
||||
@@ -136,6 +165,8 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us
|
||||
> type \path\to\mylogfile.log | findstr "something"
|
||||
```
|
||||
|
||||
## Hyperopt module
|
||||
|
||||
### Why does freqtrade not have GPU support?
|
||||
|
||||
First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations.
|
||||
@@ -152,19 +183,17 @@ The benefit of using GPU would therefore be pretty slim - and will not justify t
|
||||
|
||||
There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity).
|
||||
|
||||
## Hyperopt module
|
||||
|
||||
### How many epochs do I need to get a good Hyperopt result?
|
||||
|
||||
Per default Hyperopt called without the `-e`/`--epochs` command line option will only
|
||||
run 100 epochs, means 100 evaluations of your triggers, guards, ... Too few
|
||||
to find a great result (unless if you are very lucky), so you probably
|
||||
have to run it for 10.000 or more. But it will take an eternity to
|
||||
have to run it for 10000 or more. But it will take an eternity to
|
||||
compute.
|
||||
|
||||
Since hyperopt uses Bayesian search, running for too many epochs may not produce greater results.
|
||||
|
||||
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
||||
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
||||
|
||||
```bash
|
||||
freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
|
||||
@@ -188,9 +217,9 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evaluations.
|
||||
Did you run 100 000 evaluations? Congrats, you've done roughly 1 / 100 000 th
|
||||
of the search space, assuming that the bot never tests the same parameters more than once.
|
||||
|
||||
* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades.
|
||||
* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 100000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades.
|
||||
|
||||
Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days.
|
||||
Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we assume you set the --timerange to 365 days.
|
||||
|
||||
Example:
|
||||
`freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601`
|
||||
|
@@ -51,16 +51,17 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||
[--random-state INT] [--min-trades INT]
|
||||
[--hyperopt-loss NAME] [--disable-param-export]
|
||||
[--ignore-missing-spaces]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
--data-format-ohlcv {json,jsongz,hdf5}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `None`).
|
||||
(default: `json`).
|
||||
--max-open-trades INT
|
||||
Override the value of the `max_open_trades`
|
||||
configuration setting.
|
||||
@@ -114,9 +115,13 @@ optional arguments:
|
||||
Hyperopt-loss-functions are:
|
||||
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
||||
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily
|
||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily,
|
||||
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, ProfitDrawDownHyperOptLoss
|
||||
--disable-param-export
|
||||
Disable automatic hyperopt parameter export.
|
||||
--ignore-missing-spaces, --ignore-unparameterized-spaces
|
||||
Suppress errors for any requested Hyperopt spaces that
|
||||
do not contain any parameters.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
@@ -503,6 +508,46 @@ class MyAwesomeStrategy(IStrategy):
|
||||
|
||||
You will then obviously also change potential interesting entries to parameters to allow hyper-optimization.
|
||||
|
||||
### Optimizing `max_entry_position_adjustment`
|
||||
|
||||
While `max_entry_position_adjustment` is not a separate space, it can still be used in hyperopt by using the property approach shown above.
|
||||
|
||||
``` python
|
||||
from pandas import DataFrame
|
||||
from functools import reduce
|
||||
|
||||
import talib.abstract as ta
|
||||
|
||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IStrategy, IntParameter)
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
stoploss = -0.05
|
||||
timeframe = '15m'
|
||||
|
||||
# Define the parameter spaces
|
||||
max_epa = CategoricalParameter([-1, 0, 1, 3, 5, 10], default=1, space="buy", optimize=True)
|
||||
|
||||
@property
|
||||
def max_entry_position_adjustment(self):
|
||||
return self.max_epa.value
|
||||
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# ...
|
||||
```
|
||||
|
||||
??? Tip "Using `IntParameter`"
|
||||
You can also use the `IntParameter` for this optimization, but you must explicitly return an integer:
|
||||
``` python
|
||||
max_epa = IntParameter(-1, 10, default=1, space="buy", optimize=True)
|
||||
|
||||
@property
|
||||
def max_entry_position_adjustment(self):
|
||||
return int(self.max_epa.value)
|
||||
```
|
||||
|
||||
## Loss-functions
|
||||
|
||||
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
||||
@@ -512,12 +557,15 @@ This class should be in its own file within the `user_data/hyperopts/` directory
|
||||
|
||||
Currently, the following loss functions are builtin:
|
||||
|
||||
* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
|
||||
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
|
||||
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation)
|
||||
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation)
|
||||
* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation)
|
||||
* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation)
|
||||
* `ShortTradeDurHyperOptLoss` - (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
|
||||
* `OnlyProfitHyperOptLoss` - takes only amount of profit into consideration.
|
||||
* `SharpeHyperOptLoss` - optimizes Sharpe Ratio calculated on trade returns relative to standard deviation.
|
||||
* `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation.
|
||||
* `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation.
|
||||
* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
|
||||
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown.
|
||||
* `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown.
|
||||
* `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes.
|
||||
|
||||
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
||||
|
||||
|
@@ -52,6 +52,8 @@ To skip pair validation against active markets, set `"allow_inactive": true` wit
|
||||
This can be useful for backtesting expired pairs (like quarterly spot-markets).
|
||||
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
|
||||
|
||||
When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist.
|
||||
|
||||
#### Volume Pair List
|
||||
|
||||
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
||||
@@ -194,23 +196,33 @@ Trade count is used as a tie breaker.
|
||||
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
||||
Not defining this parameter (or setting it to 0) will use all-time performance.
|
||||
|
||||
The optional `min_profit` (as ratio -> a setting of `0.01` corresponds to 1%) parameter defines the minimum profit a pair must have to be considered.
|
||||
Pairs below this level will be filtered out.
|
||||
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without a way to recover.
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
// ...
|
||||
{
|
||||
"method": "PerformanceFilter",
|
||||
"minutes": 1440 // rolling 24h
|
||||
"minutes": 1440, // rolling 24h
|
||||
"min_profit": 0.01 // minimal profit 1%
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
!!! Note
|
||||
As this Filter uses past performance of the bot, it'll have some startup-period - and should only be used after the bot has a few 100 trades in the database.
|
||||
|
||||
!!! Warning "Backtesting"
|
||||
`PerformanceFilter` does not support backtesting mode.
|
||||
|
||||
#### PrecisionFilter
|
||||
|
||||
Filters low-value coins which would not allow setting stoplosses.
|
||||
|
||||
!!! Warning "Backtesting"
|
||||
`PrecisionFilter` does not support backtesting mode using multiple strategies.
|
||||
|
||||
#### PriceFilter
|
||||
|
||||
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
||||
@@ -234,7 +246,7 @@ On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can
|
||||
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
|
||||
This option is disabled by default, and will only apply if set to > 0.
|
||||
|
||||
For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
|
||||
For `PriceFilter` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
|
||||
|
||||
Calculation example:
|
||||
|
||||
@@ -248,7 +260,7 @@ Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 -
|
||||
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
|
||||
|
||||
!!! Tip
|
||||
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order.
|
||||
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. ShuffleFilter will automatically detect runmodes and apply the `seed` only for backtesting modes - if a `seed` value is set.
|
||||
|
||||
#### SpreadFilter
|
||||
|
||||
@@ -283,7 +295,7 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from
|
||||
|
||||
#### VolatilityFilter
|
||||
|
||||
Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)).
|
||||
Volatility is the degree of historical variation of a pairs over time, it is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)).
|
||||
|
||||
This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
|
||||
|
||||
@@ -337,5 +349,5 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
|
||||
"refresh_period": 86400
|
||||
},
|
||||
{"method": "ShuffleFilter", "seed": 42}
|
||||
],
|
||||
],
|
||||
```
|
||||
|
@@ -51,9 +51,9 @@ When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Fre
|
||||
|
||||
#### Buy price without Orderbook enabled
|
||||
|
||||
The following section uses `side` as the configured `bid_strategy.price_side`.
|
||||
The following section uses `side` as the configured `bid_strategy.price_side` (defaults to `"bid"`).
|
||||
|
||||
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
|
||||
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `bid_strategy.ask_last_balance`..
|
||||
|
||||
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
|
||||
|
||||
@@ -88,9 +88,9 @@ When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Fr
|
||||
|
||||
#### Sell price without Orderbook enabled
|
||||
|
||||
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
|
||||
The following section uses `side` as the configured `ask_strategy.price_side` (defaults to `"ask"`).
|
||||
|
||||
When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
|
||||
When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `ask_strategy.bid_last_balance`.
|
||||
|
||||
The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
Freqtrade is a crypto-currency algorithmic trading software developed in python (3.7+) and supported on Windows, macOS and Linux.
|
||||
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||
|
||||
!!! Danger "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.
|
||||
@@ -20,6 +20,12 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
|
||||
|
||||
We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it.
|
||||
|
||||

|
||||
|
||||
## Sponsored promotion
|
||||
|
||||
[](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs)
|
||||
|
||||
## Features
|
||||
|
||||
- Develop your Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
||||
@@ -29,18 +35,20 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
|
||||
- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade.
|
||||
- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode).
|
||||
- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital.
|
||||
- Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
|
||||
- Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
|
||||
- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
|
||||
|
||||
## Supported exchange marketplaces
|
||||
|
||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||
|
||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bittrex](https://bittrex.com/)
|
||||
- [X] [FTX](https://ftx.com)
|
||||
- [X] [Kraken](https://kraken.com/)
|
||||
- [X] [FTX](https://ftx.com/#a=2258149)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [Huobi](http://huobi.com/)
|
||||
- [X] [Kraken](https://kraken.com/)
|
||||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Community tested
|
||||
@@ -66,7 +74,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
||||
|
||||
Alternatively
|
||||
|
||||
- Python 3.7+
|
||||
- Python 3.8+
|
||||
- pip (pip3)
|
||||
- git
|
||||
- TA-Lib
|
||||
@@ -80,4 +88,4 @@ For any questions not covered by the documentation or for further information ab
|
||||
|
||||
## Ready to try?
|
||||
|
||||
Begin by reading our installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md).
|
||||
Begin by reading the installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md).
|
||||
|
@@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
||||
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
|
||||
|
||||
!!! Note
|
||||
Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
||||
Python3.8 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
|
||||
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
|
||||
|
||||
!!! Warning "Up-to-date clock"
|
||||
@@ -36,9 +36,13 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
||||
|
||||
These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation).
|
||||
|
||||
!!! Note "ARM64 systems"
|
||||
If you are running an ARM64 system (like a MacOS M1 or an Oracle VM), please use [docker](docker_quickstart.md) to run freqtrade.
|
||||
While native installation is possible with some manual effort, this is not supported at the moment.
|
||||
|
||||
### Install guide
|
||||
|
||||
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [Python >= 3.8.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.html) (Recommended)
|
||||
@@ -50,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
|
||||
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
|
||||
|
||||
!!! Note
|
||||
Python3.7 or higher and the corresponding pip are assumed to be available.
|
||||
Python3.8 or higher and the corresponding pip are assumed to be available.
|
||||
|
||||
=== "Debian/Ubuntu"
|
||||
#### Install necessary dependencies
|
||||
@@ -60,18 +64,18 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
|
||||
sudo apt-get update
|
||||
|
||||
# install packages
|
||||
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git
|
||||
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git curl
|
||||
```
|
||||
|
||||
=== "RaspberryPi/Raspbian"
|
||||
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
|
||||
This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running.
|
||||
This image comes with python3.9 preinstalled, making it easy to get freqtrade up and running.
|
||||
|
||||
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
|
||||
|
||||
|
||||
```bash
|
||||
sudo apt-get install python3-venv libatlas-base-dev cmake
|
||||
sudo apt-get install python3-venv libatlas-base-dev cmake curl
|
||||
# Use pywheels.org to speed up installation
|
||||
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
||||
|
||||
@@ -113,6 +117,13 @@ git checkout develop
|
||||
|
||||
You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands.
|
||||
|
||||
??? Note "Install from pypi"
|
||||
An alternative way to install Freqtrade is from [pypi](https://pypi.org/project/freqtrade/). The downside is that this method requires ta-lib to be correctly installed beforehand, and is therefore currently not the recommended way to install Freqtrade.
|
||||
|
||||
``` bash
|
||||
pip install freqtrade
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
## Script Installation
|
||||
@@ -158,7 +169,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr
|
||||
** --install **
|
||||
|
||||
With this option, the script will install the bot and most dependencies:
|
||||
You will need to have git and python3.7+ installed beforehand for this to work.
|
||||
You will need to have git and python3.8+ installed beforehand for this to work.
|
||||
|
||||
* Mandatory software as: `ta-lib`
|
||||
* Setup your virtualenv under `.env/`
|
||||
@@ -409,16 +420,3 @@ open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10
|
||||
```
|
||||
|
||||
If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
|
||||
|
||||
### MacOS installation error with python 3.9
|
||||
|
||||
When using python 3.9 on macOS, it's currently necessary to install some os-level modules to allow dependencies to compile.
|
||||
The errors you'll see happen during installation and are related to the installation of `tables` or `blosc`.
|
||||
|
||||
You can install the necessary libraries with the following command:
|
||||
|
||||
```bash
|
||||
brew install hdf5 c-blosc
|
||||
```
|
||||
|
||||
After this, please run the installation (script) again.
|
||||
|
123
docs/plotting.md
123
docs/plotting.md
@@ -65,7 +65,7 @@ optional arguments:
|
||||
_today.json`
|
||||
--timerange TIMERANGE
|
||||
Specify what timerange of data to use.
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--no-trades Skip using trades from backtesting file and DB.
|
||||
|
||||
@@ -164,7 +164,7 @@ The resulting plot will have the following elements:
|
||||
|
||||
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
||||
|
||||
Additional features when using plot_config include:
|
||||
Additional features when using `plot_config` include:
|
||||
|
||||
* Specify colors per indicator
|
||||
* Specify additional subplots
|
||||
@@ -174,6 +174,7 @@ The sample plot configuration below specifies fixed colors for the indicators. O
|
||||
It also allows multiple subplots to display both MACD and RSI at the same time.
|
||||
|
||||
Plot type can be configured using `type` key. Possible types are:
|
||||
|
||||
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
||||
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
||||
|
||||
@@ -182,40 +183,89 @@ Extra parameters to `plotly.graph_objects.*` constructor can be specified in `pl
|
||||
Sample configuration with inline comments explaining the process:
|
||||
|
||||
``` python
|
||||
plot_config = {
|
||||
'main_plot': {
|
||||
# Configuration for main plot indicators.
|
||||
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
||||
'ema10': {'color': 'red'},
|
||||
'ema50': {'color': '#CCCCCC'},
|
||||
# By omitting color, a random color is selected.
|
||||
'sar': {},
|
||||
# fill area between senkou_a and senkou_b
|
||||
'senkou_a': {
|
||||
'color': 'green', #optional
|
||||
'fill_to': 'senkou_b',
|
||||
'fill_label': 'Ichimoku Cloud', #optional
|
||||
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||
},
|
||||
# plot senkou_b, too. Not only the area to it.
|
||||
'senkou_b': {}
|
||||
@property
|
||||
def plot_config(self):
|
||||
"""
|
||||
There are a lot of solutions how to build the return dictionary.
|
||||
The only important point is the return value.
|
||||
Example:
|
||||
plot_config = {'main_plot': {}, 'subplots': {}}
|
||||
|
||||
"""
|
||||
plot_config = {}
|
||||
plot_config['main_plot'] = {
|
||||
# Configuration for main plot indicators.
|
||||
# Assumes 2 parameters, emashort and emalong to be specified.
|
||||
f'ema_{self.emashort.value}': {'color': 'red'},
|
||||
f'ema_{self.emalong.value}': {'color': '#CCCCCC'},
|
||||
# By omitting color, a random color is selected.
|
||||
'sar': {},
|
||||
# fill area between senkou_a and senkou_b
|
||||
'senkou_a': {
|
||||
'color': 'green', #optional
|
||||
'fill_to': 'senkou_b',
|
||||
'fill_label': 'Ichimoku Cloud', #optional
|
||||
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||
},
|
||||
'subplots': {
|
||||
# Create subplot MACD
|
||||
"MACD": {
|
||||
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||
'macdsignal': {'color': 'orange'},
|
||||
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||
},
|
||||
# Additional subplot RSI
|
||||
"RSI": {
|
||||
'rsi': {'color': 'red'}
|
||||
}
|
||||
# plot senkou_b, too. Not only the area to it.
|
||||
'senkou_b': {}
|
||||
}
|
||||
plot_config['subplots'] = {
|
||||
# Create subplot MACD
|
||||
"MACD": {
|
||||
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||
'macdsignal': {'color': 'orange'},
|
||||
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||
},
|
||||
# Additional subplot RSI
|
||||
"RSI": {
|
||||
'rsi': {'color': 'red'}
|
||||
}
|
||||
}
|
||||
|
||||
return plot_config
|
||||
```
|
||||
|
||||
??? Note "As attribute (former method)"
|
||||
Assigning plot_config is also possible as Attribute (this used to be the default way).
|
||||
This has the disadvantage that strategy parameters are not available, preventing certain configurations from working.
|
||||
|
||||
``` python
|
||||
plot_config = {
|
||||
'main_plot': {
|
||||
# Configuration for main plot indicators.
|
||||
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
||||
'ema10': {'color': 'red'},
|
||||
'ema50': {'color': '#CCCCCC'},
|
||||
# By omitting color, a random color is selected.
|
||||
'sar': {},
|
||||
# fill area between senkou_a and senkou_b
|
||||
'senkou_a': {
|
||||
'color': 'green', #optional
|
||||
'fill_to': 'senkou_b',
|
||||
'fill_label': 'Ichimoku Cloud', #optional
|
||||
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||
},
|
||||
# plot senkou_b, too. Not only the area to it.
|
||||
'senkou_b': {}
|
||||
},
|
||||
'subplots': {
|
||||
# Create subplot MACD
|
||||
"MACD": {
|
||||
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||
'macdsignal': {'color': 'orange'},
|
||||
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||
},
|
||||
# Additional subplot RSI
|
||||
"RSI": {
|
||||
'rsi': {'color': 'red'}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
!!! Note
|
||||
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
||||
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
||||
@@ -223,6 +273,9 @@ Sample configuration with inline comments explaining the process:
|
||||
!!! Warning
|
||||
`plotly` arguments are only supported with plotly library and will not work with freq-ui.
|
||||
|
||||
!!! Note "Trade position adjustments"
|
||||
If `position_adjustment_enable` / `adjust_trade_position()` is used, the trade initial buy price is averaged over multiple orders and the trade start price will most likely appear outside the candle range.
|
||||
|
||||
## Plot profit
|
||||
|
||||

|
||||
@@ -233,6 +286,8 @@ The `plot-profit` subcommand shows an interactive graph with three plots:
|
||||
* The summarized profit made by backtesting.
|
||||
Note that this is not the real-world profit, but more of an estimate.
|
||||
* Profit for each individual pair.
|
||||
* Parallelism of trades.
|
||||
* Underwater (Periods of drawdown).
|
||||
|
||||
The first graph is good to get a grip of how the overall market progresses.
|
||||
|
||||
@@ -242,6 +297,8 @@ This graph will also highlight the start (and end) of the Max drawdown period.
|
||||
|
||||
The third graph can be useful to spot outliers, events in pairs that cause profit spikes.
|
||||
|
||||
The forth graph can help you analyze trade parallelism, showing how often max_open_trades have been maxed out.
|
||||
|
||||
Possible options for the `freqtrade plot-profit` subcommand:
|
||||
|
||||
```
|
||||
@@ -261,8 +318,8 @@ optional arguments:
|
||||
Specify what timerange of data to use.
|
||||
--export EXPORT Export backtest results, argument are: trades.
|
||||
Example: `--export=trades`
|
||||
--export-filename PATH
|
||||
Save backtest results to the file with this filename.
|
||||
--export-filename PATH, --backtest-filename PATH
|
||||
Use backtest results from this filename.
|
||||
Requires `--export` to be set as well. Example:
|
||||
`--export-filename=user_data/backtest_results/backtest
|
||||
_today.json`
|
||||
@@ -273,7 +330,7 @@ optional arguments:
|
||||
--trade-source {DB,file}
|
||||
Specify the source for trades (Can be DB or file
|
||||
(backtest file)) Default: file
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--auto-open Automatically open generated plot.
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
mkdocs==1.2.2
|
||||
mkdocs-material==7.3.0
|
||||
mkdocs==1.2.3
|
||||
mkdocs-material==8.2.5
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==8.2
|
||||
pymdown-extensions==9.3
|
||||
jinja2==3.0.3
|
||||
|
@@ -38,6 +38,11 @@ Sample configuration:
|
||||
!!! Danger "Security warning"
|
||||
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
||||
|
||||
??? Note "API/UI Access on a remote servers"
|
||||
If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot.
|
||||
This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box).
|
||||
Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet.
|
||||
|
||||
You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly.
|
||||
This should return the response:
|
||||
|
||||
@@ -78,7 +83,7 @@ If you run your bot using docker, you'll need to have the bot listen to incoming
|
||||
},
|
||||
```
|
||||
|
||||
Uncomment the following from your docker-compose file:
|
||||
Make sure that the following 2 lines are available in your docker-compose file:
|
||||
|
||||
```yml
|
||||
ports:
|
||||
@@ -330,12 +335,15 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques
|
||||
|
||||
### CORS
|
||||
|
||||
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
|
||||
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
|
||||
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
|
||||
This whole section is only necessary in cross-origin cases (where you multiple bot API's running on `localhost:8081`, `localhost:8082`, ...), and want to combine them into one FreqUI instance.
|
||||
|
||||
Users can configure this themselves via the `CORS_origins` configuration setting.
|
||||
It consists of a list of allowed sites that are allowed to consume resources from the bot's API.
|
||||
??? info "Technical explanation"
|
||||
All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
|
||||
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
|
||||
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.
|
||||
|
||||
Users can allow access from different origin URL's to the bot API via the `CORS_origins` configuration setting.
|
||||
It consists of a list of allowed URL's that are allowed to consume resources from the bot's API.
|
||||
|
||||
Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary:
|
||||
|
||||
@@ -348,5 +356,19 @@ Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - t
|
||||
}
|
||||
```
|
||||
|
||||
In the following (pretty common) case, FreqUI is accessible on `http://localhost:8080/trade` (this is what you see in your navbar when navigating to freqUI).
|
||||

|
||||
|
||||
The correct configuration for this case is `http://localhost:8080` - the main part of the URL including the port.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
//...
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"CORS_origins": ["http://localhost:8080"],
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot.
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
The `stoploss` configuration parameter is loss as ratio 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.
|
||||
Stoploss calculations do include fees, so a stoploss of -10% is placed exactly 10% below the entry point.
|
||||
|
||||
Most of the strategy files already include the optimal `stoploss` value.
|
||||
|
||||
@@ -23,14 +24,14 @@ These modes can be configured with these values:
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now.
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now.
|
||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
||||
|
||||
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
||||
|
||||
Enable or Disable stop loss on exchange.
|
||||
If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled.
|
||||
If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order fills. This will protect you against sudden crashes in market, as the order execution happens purely within the exchange, and has no potential network overhead.
|
||||
|
||||
If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
||||
`stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this.
|
||||
@@ -182,7 +183,7 @@ For example, simplified math:
|
||||
* the bot buys an asset at a price of 100$
|
||||
* the stop loss is defined at -10%
|
||||
* the stop loss would get triggered once the asset drops below 90$
|
||||
* stoploss will remain at 90$ unless asset increases to or above our configured offset
|
||||
* stoploss will remain at 90$ unless asset increases to or above the configured offset
|
||||
* assuming the asset now increases to 103$ (where we have the offset configured)
|
||||
* the stop loss will now be -2% of 103$ = 100.94$
|
||||
* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$
|
||||
|
@@ -77,43 +77,6 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
***
|
||||
|
||||
## Custom sell signal
|
||||
|
||||
It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision.
|
||||
|
||||
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
|
||||
|
||||
Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
||||
|
||||
!!! Note
|
||||
Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
||||
|
||||
An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day:
|
||||
|
||||
``` python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||
current_profit: float, **kwargs):
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
# Above 20% profit, sell when rsi < 80
|
||||
if current_profit > 0.2:
|
||||
if last_candle['rsi'] < 80:
|
||||
return 'rsi_below_80'
|
||||
|
||||
# Between 2% and 10%, sell if EMA-long above EMA-short
|
||||
if 0.02 < current_profit < 0.1:
|
||||
if last_candle['emalong'] > last_candle['emashort']:
|
||||
return 'ema_long_below_80'
|
||||
|
||||
# Sell any positions at a loss if they are held for more than one day.
|
||||
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
|
||||
return 'unclog'
|
||||
```
|
||||
|
||||
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||
|
||||
## Buy Tag
|
||||
|
||||
When your strategy has multiple buy signals, you can name the signal that triggered.
|
||||
@@ -143,512 +106,47 @@ def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_r
|
||||
!!! Note
|
||||
`buy_tag` is limited to 100 characters, remaining data will be truncated.
|
||||
|
||||
## Exit tag
|
||||
|
||||
## Custom stoploss
|
||||
|
||||
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
|
||||
|
||||
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
||||
The method must return a stoploss value (float / number) as a percentage of the current price.
|
||||
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
|
||||
|
||||
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
|
||||
|
||||
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
||||
Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag.
|
||||
|
||||
``` python
|
||||
# additional imports required
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['rsi'] > 70) &
|
||||
(dataframe['volume'] > 0)
|
||||
),
|
||||
['sell', 'exit_tag']] = (1, 'exit_rsi')
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
"""
|
||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns the initial stoploss value
|
||||
Only called when use_custom_stoploss is set to True.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: trade object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New stoploss value, relative to the current rate
|
||||
"""
|
||||
return -0.04
|
||||
return dataframe
|
||||
```
|
||||
|
||||
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
|
||||
|
||||
!!! Note "Use of dates"
|
||||
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
||||
|
||||
!!! Tip "Trailing stoploss"
|
||||
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
||||
|
||||
### Custom stoploss examples
|
||||
|
||||
The next section will show some examples on what's possible with the custom stoploss function.
|
||||
Of course, many more things are possible, and all examples can be combined at will.
|
||||
|
||||
#### Time based trailing stop
|
||||
|
||||
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||
return -0.05
|
||||
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
||||
return -0.10
|
||||
return 1
|
||||
```
|
||||
|
||||
#### Different stoploss per pair
|
||||
|
||||
Use a different stoploss depending on the pair.
|
||||
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
|
||||
|
||||
``` python
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||
return -0.10
|
||||
elif pair in ('LTC/BTC'):
|
||||
return -0.05
|
||||
return -0.15
|
||||
```
|
||||
|
||||
#### Trailing stoploss with positive offset
|
||||
|
||||
Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%.
|
||||
|
||||
Please note that the stoploss can only increase, values lower than the current stoploss are ignored.
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
if current_profit < 0.04:
|
||||
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
||||
|
||||
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
||||
desired_stoploss = current_profit / 2
|
||||
|
||||
# Use a minimum of 2.5% and a maximum of 5%
|
||||
return max(min(desired_stoploss, 0.05), 0.025)
|
||||
```
|
||||
|
||||
#### Calculating stoploss relative to open price
|
||||
|
||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||
|
||||
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||
|
||||
### Calculating stoploss percentage from absolute price
|
||||
|
||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||
|
||||
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||
|
||||
#### Stepped stoploss
|
||||
|
||||
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
||||
|
||||
* Use the regular stoploss until 20% profit is reached
|
||||
* Once profit is > 20% - set stoploss to 7% above open price.
|
||||
* Once profit is > 25% - set stoploss to 15% above open price.
|
||||
* Once profit is > 40% - set stoploss to 25% above open price.
|
||||
|
||||
``` python
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import stoploss_from_open
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
# evaluate highest to lowest, so that highest possible stop is used
|
||||
if current_profit > 0.40:
|
||||
return stoploss_from_open(0.25, current_profit)
|
||||
elif current_profit > 0.25:
|
||||
return stoploss_from_open(0.15, current_profit)
|
||||
elif current_profit > 0.20:
|
||||
return stoploss_from_open(0.07, current_profit)
|
||||
|
||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||
return 1
|
||||
```
|
||||
|
||||
#### Custom stoploss using an indicator from dataframe example
|
||||
|
||||
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
|
||||
|
||||
``` python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# <...>
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
# Use parabolic sar as absolute stoploss price
|
||||
stoploss_price = last_candle['sar']
|
||||
|
||||
# Convert absolute price to percentage relative to current_rate
|
||||
if stoploss_price < current_rate:
|
||||
return (stoploss_price / current_rate) - 1
|
||||
|
||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||
return 1
|
||||
```
|
||||
|
||||
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||
|
||||
---
|
||||
|
||||
## Custom order price rules
|
||||
|
||||
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||
|
||||
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
||||
The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
|
||||
|
||||
!!! Note
|
||||
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||
`sell_reason` is limited to 100 characters, remaining data will be truncated.
|
||||
|
||||
### Custom order entry and exit price example
|
||||
## Strategy version
|
||||
|
||||
You can implement custom strategy versioning by using the "version" method, and returning the version you would like this strategy to have.
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: datetime,
|
||||
proposed_rate, **kwargs) -> float:
|
||||
|
||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||
timeframe=self.timeframe)
|
||||
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||
|
||||
return new_entryprice
|
||||
|
||||
def custom_exit_price(self, pair: str, trade: Trade,
|
||||
current_time: datetime, proposed_rate: float,
|
||||
current_profit: float, **kwargs) -> float:
|
||||
|
||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||
timeframe=self.timeframe)
|
||||
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
||||
|
||||
return new_exitprice
|
||||
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
||||
|
||||
!!! Example
|
||||
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98.
|
||||
|
||||
!!! Warning "No backtesting support"
|
||||
Custom entry-prices are currently not supported during backtesting.
|
||||
|
||||
## Custom order timeout rules
|
||||
|
||||
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
||||
|
||||
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
|
||||
|
||||
!!! Note
|
||||
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
||||
|
||||
### Custom order timeout example
|
||||
|
||||
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
|
||||
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
|
||||
|
||||
The function must return either `True` (cancel order) or `False` (keep order alive).
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
# Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
|
||||
unfilledtimeout = {
|
||||
'buy': 60 * 25,
|
||||
'sell': 60 * 25
|
||||
}
|
||||
|
||||
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
||||
return True
|
||||
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
||||
return True
|
||||
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
|
||||
return True
|
||||
elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
|
||||
return True
|
||||
elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
|
||||
return True
|
||||
return False
|
||||
def version(self) -> str:
|
||||
"""
|
||||
Returns version of the strategy.
|
||||
"""
|
||||
return "1.1"
|
||||
```
|
||||
|
||||
!!! Note
|
||||
For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
|
||||
|
||||
### Custom order timeout example (using additional data)
|
||||
|
||||
``` python
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
# Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
|
||||
unfilledtimeout = {
|
||||
'buy': 60 * 25,
|
||||
'sell': 60 * 25
|
||||
}
|
||||
|
||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
||||
ob = self.dp.orderbook(pair, 1)
|
||||
current_price = ob['bids'][0][0]
|
||||
# Cancel buy order if price is more than 2% above the order.
|
||||
if current_price > order['price'] * 1.02:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
||||
ob = self.dp.orderbook(pair, 1)
|
||||
current_price = ob['asks'][0][0]
|
||||
# Cancel sell order if price is more than 2% below the order.
|
||||
if current_price < order['price'] * 0.98:
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bot loop start callback
|
||||
|
||||
A simple callback which is called once at the start of every bot throttling iteration.
|
||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||
|
||||
``` python
|
||||
import requests
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def bot_loop_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Called at the start of the bot iteration (one loop).
|
||||
Might be used to perform pair-independent tasks
|
||||
(e.g. gather some remote resource for comparison)
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||
# Assign this to the class by using self.*
|
||||
# can then be used by populate_* methods
|
||||
self.remote_data = requests.get('https://some_remote_source.example.com')
|
||||
|
||||
```
|
||||
|
||||
## Bot order confirmation
|
||||
|
||||
### Trade entry (buy order) confirmation
|
||||
|
||||
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
|
||||
|
||||
``` python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a buy order.
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
network requests in this method.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns True (always confirming).
|
||||
|
||||
:param pair: Pair that's about to be bought.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in target (quote) currency that's going to be traded.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
return True
|
||||
|
||||
```
|
||||
|
||||
### Trade exit (sell order) confirmation
|
||||
|
||||
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
||||
|
||||
``` python
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||||
rate: float, time_in_force: str, sell_reason: str,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a regular sell order.
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
network requests in this method.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns True (always confirming).
|
||||
|
||||
:param pair: Pair that's about to be sold.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in quote currency.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param sell_reason: Sell reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
'sell_signal', 'force_sell', 'emergency_sell']
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the sell-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
|
||||
# Reject force-sells with negative profit
|
||||
# This is just a sample, please adjust to your needs
|
||||
# (this does not necessarily make sense, assuming you know when you're force-selling)
|
||||
return False
|
||||
return True
|
||||
|
||||
```
|
||||
|
||||
### Stake size management
|
||||
|
||||
It is possible to manage your risk by reducing or increasing stake amount when placing a new trade.
|
||||
|
||||
```python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
**kwargs) -> float:
|
||||
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
|
||||
current_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
|
||||
if self.config['stake_amount'] == 'unlimited':
|
||||
# Use entire available wallet during favorable conditions when in compounding mode.
|
||||
return max_stake
|
||||
else:
|
||||
# Compound profits during favorable conditions instead of using a static stake.
|
||||
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']
|
||||
|
||||
# Use default stake amount.
|
||||
return proposed_stake
|
||||
```
|
||||
|
||||
Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.
|
||||
|
||||
!!! Tip
|
||||
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged.
|
||||
|
||||
!!! Tip
|
||||
Returning `0` or `None` will prevent trades from being placed.
|
||||
|
||||
---
|
||||
You should make sure to implement proper version control (like a git repository) alongside this, as freqtrade will not keep historic versions of your strategy, so it's up to the user to be able to eventually roll back to a prior version of the strategy.
|
||||
|
||||
## Derived strategies
|
||||
|
||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||
|
||||
``` python
|
||||
``` python title="user_data/strategies/myawesomestrategy.py"
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
...
|
||||
stoploss = 0.13
|
||||
@@ -657,6 +155,10 @@ class MyAwesomeStrategy(IStrategy):
|
||||
# should be in any custom strategy...
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
``` python title="user_data/strategies/MyAwesomeStrategy2.py"
|
||||
from myawesomestrategy import MyAwesomeStrategy
|
||||
class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||
# Override something
|
||||
stoploss = 0.08
|
||||
@@ -665,16 +167,7 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
|
||||
|
||||
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
||||
|
||||
!!! Note "Parent-strategy in different files"
|
||||
If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly.
|
||||
|
||||
``` python
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.append(str(Path(__file__).parent))
|
||||
|
||||
from myawesomestrategy import MyAwesomeStrategy
|
||||
```
|
||||
While keeping the subclass in the same file is technically possible, it can lead to some problems with hyperopt parameter files, we therefore recommend to use separate strategy files, and import the parent strategy as shown above.
|
||||
|
||||
## Embedding Strategies
|
||||
|
||||
@@ -724,9 +217,9 @@ should be rewritten to
|
||||
```python
|
||||
frames = [dataframe]
|
||||
for val in self.buy_ema_short.range:
|
||||
frames.append({
|
||||
frames.append(DataFrame({
|
||||
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
||||
})
|
||||
}))
|
||||
|
||||
# Append columns to existing dataframe
|
||||
merged_frame = pd.concat(frames, axis=1)
|
||||
|
684
docs/strategy-callbacks.md
Normal file
684
docs/strategy-callbacks.md
Normal file
@@ -0,0 +1,684 @@
|
||||
# Strategy Callbacks
|
||||
|
||||
While the main strategy functions (`populate_indicators()`, `populate_buy_trend()`, `populate_sell_trend()`) should be used in a vectorized way, and are only called [once during backtesting](bot-basics.md#backtesting-hyperopt-execution-logic), callbacks are called "whenever needed".
|
||||
|
||||
As such, you should avoid doing heavy calculations in callbacks to avoid delays during operations.
|
||||
Depending on the callback used, they may be called when entering / exiting a trade, or throughout the duration of a trade.
|
||||
|
||||
Currently available callbacks:
|
||||
|
||||
* [`bot_loop_start()`](#bot-loop-start)
|
||||
* [`custom_stake_amount()`](#custom-stake-size)
|
||||
* [`custom_sell()`](#custom-sell-signal)
|
||||
* [`custom_stoploss()`](#custom-stoploss)
|
||||
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
|
||||
* [`check_buy_timeout()` and `check_sell_timeout()](#custom-order-timeout-rules)
|
||||
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
|
||||
* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation)
|
||||
* [`adjust_trade_position()`](#adjust-trade-position)
|
||||
|
||||
!!! Tip "Callback calling sequence"
|
||||
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
||||
|
||||
## Bot loop start
|
||||
|
||||
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||
|
||||
``` python
|
||||
import requests
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def bot_loop_start(self, **kwargs) -> None:
|
||||
"""
|
||||
Called at the start of the bot iteration (one loop).
|
||||
Might be used to perform pair-independent tasks
|
||||
(e.g. gather some remote resource for comparison)
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||
# Assign this to the class by using self.*
|
||||
# can then be used by populate_* methods
|
||||
self.remote_data = requests.get('https://some_remote_source.example.com')
|
||||
|
||||
```
|
||||
|
||||
## Custom Stake size
|
||||
|
||||
Called before entering a trade, makes it possible to manage your position size when placing a new trade.
|
||||
|
||||
```python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
|
||||
current_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
|
||||
if self.config['stake_amount'] == 'unlimited':
|
||||
# Use entire available wallet during favorable conditions when in compounding mode.
|
||||
return max_stake
|
||||
else:
|
||||
# Compound profits during favorable conditions instead of using a static stake.
|
||||
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']
|
||||
|
||||
# Use default stake amount.
|
||||
return proposed_stake
|
||||
```
|
||||
|
||||
Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.
|
||||
|
||||
!!! Tip
|
||||
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this action will be logged.
|
||||
|
||||
!!! Tip
|
||||
Returning `0` or `None` will prevent trades from being placed.
|
||||
|
||||
## Custom sell signal
|
||||
|
||||
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
|
||||
|
||||
Allows to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need trade data to make an exit decision.
|
||||
|
||||
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
|
||||
|
||||
Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
||||
|
||||
!!! Note
|
||||
Returning a (none-empty) `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
||||
|
||||
An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day:
|
||||
|
||||
``` python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||
current_profit: float, **kwargs):
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
# Above 20% profit, sell when rsi < 80
|
||||
if current_profit > 0.2:
|
||||
if last_candle['rsi'] < 80:
|
||||
return 'rsi_below_80'
|
||||
|
||||
# Between 2% and 10%, sell if EMA-long above EMA-short
|
||||
if 0.02 < current_profit < 0.1:
|
||||
if last_candle['emalong'] > last_candle['emashort']:
|
||||
return 'ema_long_below_80'
|
||||
|
||||
# Sell any positions at a loss if they are held for more than one day.
|
||||
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
|
||||
return 'unclog'
|
||||
```
|
||||
|
||||
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||
|
||||
## Custom stoploss
|
||||
|
||||
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
|
||||
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
||||
|
||||
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade).
|
||||
|
||||
The method must return a stoploss value (float / number) as a percentage of the current price.
|
||||
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
|
||||
|
||||
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
|
||||
|
||||
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
||||
|
||||
``` python
|
||||
# additional imports required
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
"""
|
||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns the initial stoploss value
|
||||
Only called when use_custom_stoploss is set to True.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: trade object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New stoploss value, relative to the current rate
|
||||
"""
|
||||
return -0.04
|
||||
```
|
||||
|
||||
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
|
||||
|
||||
!!! Note "Use of dates"
|
||||
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
||||
|
||||
!!! Tip "Trailing stoploss"
|
||||
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
||||
|
||||
### Custom stoploss examples
|
||||
|
||||
The next section will show some examples on what's possible with the custom stoploss function.
|
||||
Of course, many more things are possible, and all examples can be combined at will.
|
||||
|
||||
#### Time based trailing stop
|
||||
|
||||
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||
return -0.05
|
||||
elif current_time - timedelta(minutes=60) > trade.open_date_utc:
|
||||
return -0.10
|
||||
return 1
|
||||
```
|
||||
|
||||
#### Different stoploss per pair
|
||||
|
||||
Use a different stoploss depending on the pair.
|
||||
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
|
||||
|
||||
``` python
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||
return -0.10
|
||||
elif pair in ('LTC/BTC'):
|
||||
return -0.05
|
||||
return -0.15
|
||||
```
|
||||
|
||||
#### Trailing stoploss with positive offset
|
||||
|
||||
Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%.
|
||||
|
||||
Please note that the stoploss can only increase, values lower than the current stoploss are ignored.
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
if current_profit < 0.04:
|
||||
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
||||
|
||||
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
||||
desired_stoploss = current_profit / 2
|
||||
|
||||
# Use a minimum of 2.5% and a maximum of 5%
|
||||
return max(min(desired_stoploss, 0.05), 0.025)
|
||||
```
|
||||
|
||||
#### Stepped stoploss
|
||||
|
||||
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
||||
|
||||
* Use the regular stoploss until 20% profit is reached
|
||||
* Once profit is > 20% - set stoploss to 7% above open price.
|
||||
* Once profit is > 25% - set stoploss to 15% above open price.
|
||||
* Once profit is > 40% - set stoploss to 25% above open price.
|
||||
|
||||
``` python
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import stoploss_from_open
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
# evaluate highest to lowest, so that highest possible stop is used
|
||||
if current_profit > 0.40:
|
||||
return stoploss_from_open(0.25, current_profit)
|
||||
elif current_profit > 0.25:
|
||||
return stoploss_from_open(0.15, current_profit)
|
||||
elif current_profit > 0.20:
|
||||
return stoploss_from_open(0.07, current_profit)
|
||||
|
||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||
return 1
|
||||
```
|
||||
|
||||
#### Custom stoploss using an indicator from dataframe example
|
||||
|
||||
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
|
||||
|
||||
``` python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# <...>
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
# Use parabolic sar as absolute stoploss price
|
||||
stoploss_price = last_candle['sar']
|
||||
|
||||
# Convert absolute price to percentage relative to current_rate
|
||||
if stoploss_price < current_rate:
|
||||
return (stoploss_price / current_rate) - 1
|
||||
|
||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||
return 1
|
||||
```
|
||||
|
||||
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||
|
||||
### Common helpers for stoploss calculations
|
||||
|
||||
#### Stoploss relative to open price
|
||||
|
||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||
|
||||
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||
|
||||
#### Stoploss percentage from absolute price
|
||||
|
||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||
|
||||
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||
|
||||
---
|
||||
|
||||
## Custom order price rules
|
||||
|
||||
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||
|
||||
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
||||
|
||||
Each of these methods are called right before placing an order on the exchange.
|
||||
|
||||
!!! Note
|
||||
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||
|
||||
### Custom order entry and exit price example
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
|
||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||
timeframe=self.timeframe)
|
||||
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||
|
||||
return new_entryprice
|
||||
|
||||
def custom_exit_price(self, pair: str, trade: Trade,
|
||||
current_time: datetime, proposed_rate: float,
|
||||
current_profit: float, **kwargs) -> float:
|
||||
|
||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||
timeframe=self.timeframe)
|
||||
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
||||
|
||||
return new_exitprice
|
||||
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
||||
**Example**:
|
||||
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
||||
|
||||
!!! Warning "Backtesting"
|
||||
Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range.
|
||||
Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle.
|
||||
`custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices.
|
||||
|
||||
## Custom order timeout rules
|
||||
|
||||
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
||||
|
||||
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
|
||||
|
||||
!!! Note
|
||||
Backtesting fills orders if their price falls within the candle's low/high range.
|
||||
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
|
||||
|
||||
### Custom order timeout example
|
||||
|
||||
Called for every open order until that order is either filled or cancelled.
|
||||
`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders.
|
||||
|
||||
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
|
||||
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
|
||||
|
||||
The function must return either `True` (cancel order) or `False` (keep order alive).
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
|
||||
unfilledtimeout = {
|
||||
'buy': 60 * 25,
|
||||
'sell': 60 * 25
|
||||
}
|
||||
|
||||
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||
return True
|
||||
elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
|
||||
return True
|
||||
elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||
return True
|
||||
elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
|
||||
return True
|
||||
elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
!!! Note
|
||||
For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
|
||||
|
||||
### Custom order timeout example (using additional data)
|
||||
|
||||
``` python
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
|
||||
unfilledtimeout = {
|
||||
'buy': 60 * 25,
|
||||
'sell': 60 * 25
|
||||
}
|
||||
|
||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
ob = self.dp.orderbook(pair, 1)
|
||||
current_price = ob['bids'][0][0]
|
||||
# Cancel buy order if price is more than 2% above the order.
|
||||
if current_price > order['price'] * 1.02:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
ob = self.dp.orderbook(pair, 1)
|
||||
current_price = ob['asks'][0][0]
|
||||
# Cancel sell order if price is more than 2% below the order.
|
||||
if current_price < order['price'] * 0.98:
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bot order confirmation
|
||||
|
||||
Confirm trade entry / exits.
|
||||
This are the last methods that will be called before an order is placed.
|
||||
|
||||
### Trade entry (buy order) confirmation
|
||||
|
||||
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
|
||||
|
||||
``` python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||
**kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a buy order.
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
network requests in this method.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns True (always confirming).
|
||||
|
||||
:param pair: Pair that's about to be bought.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in target (quote) currency that's going to be traded.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
return True
|
||||
|
||||
```
|
||||
|
||||
### Trade exit (sell order) confirmation
|
||||
|
||||
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
|
||||
|
||||
``` python
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||||
rate: float, time_in_force: str, sell_reason: str,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a regular sell order.
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
network requests in this method.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||
|
||||
When not implemented by a strategy, returns True (always confirming).
|
||||
|
||||
:param pair: Pair that's about to be sold.
|
||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||
:param amount: Amount in quote currency.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param sell_reason: Sell reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
'sell_signal', 'force_sell', 'emergency_sell']
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the sell-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
|
||||
# Reject force-sells with negative profit
|
||||
# This is just a sample, please adjust to your needs
|
||||
# (this does not necessarily make sense, assuming you know when you're force-selling)
|
||||
return False
|
||||
return True
|
||||
|
||||
```
|
||||
|
||||
## Adjust trade position
|
||||
|
||||
The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy.
|
||||
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
||||
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging).
|
||||
|
||||
`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys.
|
||||
|
||||
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased).
|
||||
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
||||
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||
|
||||
This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`.
|
||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||
|
||||
!!! Note "About stake size"
|
||||
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
||||
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
|
||||
Using 'unlimited' stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order.
|
||||
|
||||
!!! Warning
|
||||
Stoploss is still calculated from the initial opening price, not averaged price.
|
||||
|
||||
!!! Warning "/stopbuy"
|
||||
While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades.
|
||||
|
||||
!!! Warning "Backtesting"
|
||||
During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so performance will be affected.
|
||||
|
||||
``` python
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
class DigDeeperStrategy(IStrategy):
|
||||
|
||||
position_adjustment_enable = True
|
||||
|
||||
# Attempts to handle large drops with DCA. High stoploss is required.
|
||||
stoploss = -0.30
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
# Example specific variables
|
||||
max_entry_position_adjustment = 3
|
||||
# This number is explained a bit further down
|
||||
max_dca_multiplier = 5.5
|
||||
|
||||
# This is called when placing the initial order (opening trade)
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
entry_tag: Optional[str], **kwargs) -> float:
|
||||
|
||||
# We need to leave most of the funds for possible further DCA orders
|
||||
# This also applies to fixed stakes
|
||||
return proposed_stake / self.max_dca_multiplier
|
||||
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, min_stake: float,
|
||||
max_stake: float, **kwargs):
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||
This means extra buy orders with additional fees.
|
||||
|
||||
:param trade: trade object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param current_rate: Current buy rate.
|
||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||
:param min_stake: Minimal stake size allowed by exchange.
|
||||
:param max_stake: Balance available for trading.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: Stake amount to adjust your trade
|
||||
"""
|
||||
|
||||
if current_profit > -0.05:
|
||||
return None
|
||||
|
||||
# Obtain pair dataframe (just to show how to access it)
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
|
||||
# Only buy when not actively falling price.
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
previous_candle = dataframe.iloc[-2].squeeze()
|
||||
if last_candle['close'] < previous_candle['close']:
|
||||
return None
|
||||
|
||||
filled_buys = trade.select_filled_orders('buy')
|
||||
count_of_buys = trade.nr_of_successful_buys
|
||||
# Allow up to 3 additional increasingly larger buys (4 in total)
|
||||
# Initial buy is 1x
|
||||
# If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2%
|
||||
# If that falls down to -5% again, we buy 1.5x more
|
||||
# If that falls once again down to -5%, we buy 1.75x more
|
||||
# Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake.
|
||||
# That is why max_dca_multiplier is 5.5
|
||||
# Hope you have a deep wallet!
|
||||
try:
|
||||
# This returns first order stake size
|
||||
stake_amount = filled_buys[0].cost
|
||||
# This then calculates current safety order size
|
||||
stake_amount = stake_amount * (1 + (count_of_buys * 0.25))
|
||||
return stake_amount
|
||||
except Exception as exception:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
```
|
@@ -4,33 +4,23 @@ This page explains how to customize your strategies, add new indicators and set
|
||||
|
||||
Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates.
|
||||
|
||||
## Install a custom strategy file
|
||||
|
||||
This is very simple. Copy paste your strategy file into the directory `user_data/strategies`.
|
||||
|
||||
Let assume you have a class called `AwesomeStrategy` in the file `AwesomeStrategy.py`:
|
||||
|
||||
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/AwesomeStrategy.py`
|
||||
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
||||
|
||||
```bash
|
||||
freqtrade trade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
## Develop your own strategy
|
||||
|
||||
The bot includes a default strategy file.
|
||||
Also, several other strategies are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies).
|
||||
|
||||
You will however most likely have your own idea for a strategy.
|
||||
This document intends to help you develop one for yourself.
|
||||
This document intends to help you convert your strategy idea into your own strategy.
|
||||
|
||||
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy`.
|
||||
To get started, use `freqtrade new-strategy --strategy AwesomeStrategy` (you can obviously use your own naming for your strategy).
|
||||
This will create a new strategy file from a template, which will be located under `user_data/strategies/AwesomeStrategy.py`.
|
||||
|
||||
!!! Note
|
||||
This is just a template file, which will most likely not be profitable out of the box.
|
||||
|
||||
??? Hint "Different template levels"
|
||||
`freqtrade new-strategy` has an additional parameter, `--template`, which controls the amount of pre-build information you get in the created strategy. Use `--template minimal` to get an empty strategy without any indicator examples, or `--template advanced` to get a template with most callbacks defined.
|
||||
|
||||
### Anatomy of a strategy
|
||||
|
||||
A strategy file contains all the information needed to build a good strategy:
|
||||
@@ -67,6 +57,46 @@ file as reference.**
|
||||
needs to take care to avoid having the strategy utilize data from the future.
|
||||
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
|
||||
|
||||
### Dataframe
|
||||
|
||||
Freqtrade uses [pandas](https://pandas.pydata.org/) to store/provide the candlestick (OHLCV) data.
|
||||
Pandas is a great library developed for processing large amounts of data.
|
||||
|
||||
Each row in a dataframe corresponds to one candle on a chart, with the latest candle always being the last in the dataframe (sorted by date).
|
||||
|
||||
``` output
|
||||
> dataframe.head()
|
||||
date open high low close volume
|
||||
0 2021-11-09 23:25:00+00:00 67279.67 67321.84 67255.01 67300.97 44.62253
|
||||
1 2021-11-09 23:30:00+00:00 67300.97 67301.34 67183.03 67187.01 61.38076
|
||||
2 2021-11-09 23:35:00+00:00 67187.02 67187.02 67031.93 67123.81 113.42728
|
||||
3 2021-11-09 23:40:00+00:00 67123.80 67222.40 67080.33 67160.48 78.96008
|
||||
4 2021-11-09 23:45:00+00:00 67160.48 67160.48 66901.26 66943.37 111.39292
|
||||
```
|
||||
|
||||
Pandas provides fast ways to calculate metrics. To benefit from this speed, it's advised to not use loops, but use vectorized methods instead.
|
||||
|
||||
Vectorized operations perform calculations across the whole range of data and are therefore, compared to looping through each row, a lot faster when calculating indicators.
|
||||
|
||||
As a dataframe is a table, simple python comparisons like the following will not work
|
||||
|
||||
``` python
|
||||
if dataframe['rsi'] > 30:
|
||||
dataframe['buy'] = 1
|
||||
```
|
||||
|
||||
The above section will fail with `The truth value of a Series is ambiguous. [...]`.
|
||||
|
||||
This must instead be written in a pandas-compatible way, so the operation is performed across the whole dataframe.
|
||||
|
||||
``` python
|
||||
dataframe.loc[
|
||||
(dataframe['rsi'] > 30)
|
||||
, 'buy'] = 1
|
||||
```
|
||||
|
||||
With this section, you have a new column in your dataframe, which has `1` assigned whenever RSI is above 30.
|
||||
|
||||
### Customize Indicators
|
||||
|
||||
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.
|
||||
@@ -122,9 +152,19 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||
Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py).
|
||||
Then uncomment indicators you need.
|
||||
|
||||
#### Indicator libraries
|
||||
|
||||
Out of the box, freqtrade installs the following technical libraries:
|
||||
|
||||
* [ta-lib](http://mrjbq7.github.io/ta-lib/)
|
||||
* [pandas-ta](https://twopirllc.github.io/pandas-ta/)
|
||||
* [technical](https://github.com/freqtrade/technical/)
|
||||
|
||||
Additional technical libraries can be installed as necessary, or custom indicators may be written / invented by the strategy author.
|
||||
|
||||
### Strategy startup period
|
||||
|
||||
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
||||
Most indicators have an instable startup period, in which they are either not available (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
||||
To account for this, the strategy can be assigned the `startup_candle_count` attribute.
|
||||
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
|
||||
|
||||
@@ -136,8 +176,14 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100
|
||||
|
||||
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
|
||||
|
||||
!!! Warning "Using x calls to get OHLCV"
|
||||
If you receive a warning like `WARNING - Using 3 calls to get OHLCV. This can result in slower operations for the bot. Please check if you really need 1500 candles for your strategy` - you should consider if you really need this much historic data for your signals.
|
||||
Having this will cause Freqtrade to make multiple calls for the same pair, which will obviously be slower than one network request.
|
||||
As a consequence, Freqtrade will take longer to refresh candles - and should therefore be avoided if possible.
|
||||
This is capped to 5 total calls to avoid overloading the exchange, or make freqtrade too slow.
|
||||
|
||||
!!! Warning
|
||||
`startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
|
||||
`startup_candle_count` should be below `ohlcv_candle_limit * 5` (which is 500 * 5 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -271,21 +317,15 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
||||
|
||||
Sample:
|
||||
Sample of setting a 10% stoploss:
|
||||
|
||||
``` python
|
||||
stoploss = -0.10
|
||||
```
|
||||
|
||||
This would signify a stoploss of -10%.
|
||||
|
||||
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
|
||||
|
||||
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order_types dictionary, so your stoploss is on the exchange and cannot be missed due to network problems, high load or other reasons.
|
||||
|
||||
For more information on order_types please look [here](configuration.md#understand-order_types).
|
||||
|
||||
### Timeframe (formerly ticker interval)
|
||||
### Timeframe
|
||||
|
||||
This is the set of candles the bot should download and use for the analysis.
|
||||
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
||||
@@ -300,9 +340,22 @@ The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `p
|
||||
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
||||
|
||||
The Metadata-dict should not be modified and does not persist information across multiple calls.
|
||||
Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information)
|
||||
Instead, have a look at the [Storing information](strategy-advanced.md#Storing-information) section.
|
||||
|
||||
## Additional data (informative_pairs)
|
||||
## Strategy file loading
|
||||
|
||||
By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`.
|
||||
|
||||
Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade with `freqtrade trade --strategy AwesomeStrategy`.
|
||||
Note that we're using the class-name, not the file name.
|
||||
|
||||
You can use `freqtrade list-strategies` to see a list of all strategies Freqtrade is able to load (all strategies in the correct folder).
|
||||
It will also include a "status" field, highlighting potential problems.
|
||||
|
||||
??? Hint "Customize strategy directory"
|
||||
You can use a different directory by using `--strategy-path user_data/otherPath`. This parameter is available to all commands that require a strategy.
|
||||
|
||||
## Informative Pairs
|
||||
|
||||
### Get data for non-tradeable pairs
|
||||
|
||||
@@ -331,6 +384,133 @@ A full sample can be found [in the DataProvider section](#complete-data-provider
|
||||
|
||||
***
|
||||
|
||||
### Informative pairs decorator (`@informative()`)
|
||||
|
||||
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
||||
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
||||
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
||||
for more information.
|
||||
|
||||
??? info "Full documentation"
|
||||
``` python
|
||||
def informative(timeframe: str, asset: str = '',
|
||||
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
||||
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||
"""
|
||||
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||
define informative indicators.
|
||||
|
||||
Example usage:
|
||||
|
||||
@informative('1h')
|
||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||
current pair.
|
||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||
specified, defaults to:
|
||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||
* {column}_{timeframe} if asset is not specified.
|
||||
Format string supports these format variables:
|
||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||
* {base} - base currency in lower case, for example 'eth'.
|
||||
* {BASE} - same as {base}, except in upper case.
|
||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||
* {QUOTE} - same as {quote}, except in upper case.
|
||||
* {column} - name of dataframe column.
|
||||
* {timeframe} - timeframe of informative dataframe.
|
||||
:param ffill: ffill dataframe after merging informative pair.
|
||||
"""
|
||||
```
|
||||
|
||||
??? Example "Fast and easy way to define informative pairs"
|
||||
|
||||
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
||||
|
||||
``` python
|
||||
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import IStrategy, informative
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# This method is not required.
|
||||
# def informative_pairs(self): ...
|
||||
|
||||
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
||||
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
||||
@informative('30m')
|
||||
@informative('1h')
|
||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
||||
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
||||
# instead of hardcoding actual stake currency. Available in populate_indicators and other
|
||||
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
||||
@informative('1h', 'BTC/{stake}')
|
||||
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
||||
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
||||
@informative('1h', 'ETH/BTC')
|
||||
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
||||
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
||||
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
||||
@informative('1h', 'BTC/{stake}', '{column}')
|
||||
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# Strategy timeframe indicators for current pair.
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
# Informative pairs are available in this method.
|
||||
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||
return dataframe
|
||||
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
||||
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
||||
|
||||
!!! Note
|
||||
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
||||
|
||||
``` python
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
stake = self.config['stake_currency']
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
||||
&
|
||||
(dataframe['volume'] > 0)
|
||||
),
|
||||
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
||||
|
||||
return dataframe
|
||||
```
|
||||
|
||||
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
||||
|
||||
!!! Warning "Duplicate method names"
|
||||
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
||||
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
||||
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||
|
||||
|
||||
## Additional data (DataProvider)
|
||||
|
||||
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
||||
@@ -374,9 +554,9 @@ The strategy might look something like this:
|
||||
|
||||
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.*
|
||||
|
||||
Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
||||
Due to the limited available data, it's very difficult to resample `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
||||
|
||||
Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use.
|
||||
Since we can't resample the data we will have to use an informative pair; and since the whitelist will be dynamic we don't know which pair(s) to use.
|
||||
|
||||
This is where calling `self.dp.current_whitelist()` comes in handy.
|
||||
|
||||
@@ -658,7 +838,7 @@ In some situations it may be confusing to deal with stops relative to current ra
|
||||
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import IStrategy, stoploss_from_open
|
||||
from freqtrade.strategy import IStrategy, stoploss_from_absolute
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -676,131 +856,6 @@ In some situations it may be confusing to deal with stops relative to current ra
|
||||
|
||||
```
|
||||
|
||||
### *@informative()*
|
||||
|
||||
``` python
|
||||
def informative(timeframe: str, asset: str = '',
|
||||
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
||||
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||
"""
|
||||
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||
define informative indicators.
|
||||
|
||||
Example usage:
|
||||
|
||||
@informative('1h')
|
||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||
current pair.
|
||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||
specified, defaults to:
|
||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||
* {column}_{timeframe} if asset is not specified.
|
||||
Format string supports these format variables:
|
||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||
* {base} - base currency in lower case, for example 'eth'.
|
||||
* {BASE} - same as {base}, except in upper case.
|
||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||
* {QUOTE} - same as {quote}, except in upper case.
|
||||
* {column} - name of dataframe column.
|
||||
* {timeframe} - timeframe of informative dataframe.
|
||||
:param ffill: ffill dataframe after merging informative pair.
|
||||
"""
|
||||
```
|
||||
|
||||
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
||||
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
||||
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
||||
for more information.
|
||||
|
||||
??? Example "Fast and easy way to define informative pairs"
|
||||
|
||||
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
||||
|
||||
``` python
|
||||
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import IStrategy, informative
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# This method is not required.
|
||||
# def informative_pairs(self): ...
|
||||
|
||||
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
||||
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
||||
@informative('30m')
|
||||
@informative('1h')
|
||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
||||
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
||||
# instead of hardcoding actual stake currency. Available in populate_indicators and other
|
||||
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
||||
@informative('1h', 'BTC/{stake}')
|
||||
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
||||
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
||||
@informative('1h', 'ETH/BTC')
|
||||
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
||||
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
||||
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
||||
@informative('1h', 'BTC/{stake}', '{column}')
|
||||
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# Strategy timeframe indicators for current pair.
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
# Informative pairs are available in this method.
|
||||
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||
return dataframe
|
||||
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
||||
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
||||
|
||||
!!! Note
|
||||
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
||||
|
||||
``` python
|
||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
stake = self.config['stake_currency']
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
||||
&
|
||||
(dataframe['volume'] > 0)
|
||||
),
|
||||
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
||||
|
||||
return dataframe
|
||||
```
|
||||
|
||||
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
||||
|
||||
!!! Warning "Duplicate method names"
|
||||
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
||||
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
||||
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||
|
||||
## Additional data (Wallets)
|
||||
|
||||
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
||||
@@ -884,7 +939,8 @@ Sometimes it may be desired to lock a pair after certain events happen (e.g. mul
|
||||
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`.
|
||||
`until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked.
|
||||
|
||||
Locks can also be lifted manually, by calling `self.unlock_pair(pair)`.
|
||||
Locks can also be lifted manually, by calling `self.unlock_pair(pair)` or `self.unlock_reason(<reason>)` - providing reason the pair was locked with.
|
||||
`self.unlock_reason(<reason>)` will unlock all pairs currently locked with the provided reason.
|
||||
|
||||
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||
|
||||
@@ -954,9 +1010,13 @@ The following lists some common patterns which should be avoided to prevent frus
|
||||
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
|
||||
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||
|
||||
### Colliding signals
|
||||
|
||||
When buy and sell signals collide (both `'buy'` and `'sell'` are 1), freqtrade will do nothing and ignore the entry (buy) signal. This will avoid trades that buy, and sell immediately. Obviously, this can potentially lead to missed entries.
|
||||
|
||||
## Further strategy ideas
|
||||
|
||||
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
||||
To get additional Ideas for strategies, head over to the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
||||
Feel free to use any of them as inspiration for your own strategies.
|
||||
We're happy to accept Pull Requests containing new Strategies to that repo.
|
||||
|
||||
|
@@ -50,7 +50,9 @@ candles.head()
|
||||
```python
|
||||
# Load strategy using values set above
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
strategy = StrategyResolver.load_strategy(config)
|
||||
strategy.dp = DataProvider(config, None, None)
|
||||
|
||||
# Generate buy/sell signals using strategy
|
||||
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||
@@ -228,7 +230,7 @@ graph = generate_candlestick_graph(pair=pair,
|
||||
# Show graph inline
|
||||
# graph.show()
|
||||
|
||||
# Render graph in a separate window
|
||||
# Render graph in a seperate window
|
||||
graph.show(renderer="browser")
|
||||
|
||||
```
|
||||
|
@@ -58,6 +58,8 @@ For the Freqtrade configuration, you can then use the the full value (including
|
||||
```json
|
||||
"chat_id": "-1001332619709"
|
||||
```
|
||||
!!! Warning "Using telegram groups"
|
||||
When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasent surprises.
|
||||
|
||||
## Control telegram noise
|
||||
|
||||
@@ -171,10 +173,12 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
||||
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True)
|
||||
| `/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 (n defaults to 7)
|
||||
| `/weekly <n>` | Shows profit or loss per week, over the last n weeks (n defaults to 8)
|
||||
| `/monthly <n>` | Shows profit or loss per month, over the last n months (n defaults to 6)
|
||||
| `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells
|
||||
| `/whitelist` | Show the current whitelist
|
||||
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
@@ -273,6 +277,7 @@ Starting capital is either taken from the `available_capital` setting, or calcul
|
||||
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||
|
||||
Omitting the pair will open a query asking for the pair to buy (based on the current whitelist).
|
||||
Trades crated through `/forcebuy` will have the buy-tag of `forceentry`.
|
||||
|
||||

|
||||
|
||||
@@ -307,8 +312,7 @@ Return the balance of all crypto-currency your have on the exchange.
|
||||
|
||||
### /daily <n>
|
||||
|
||||
Per default `/daily` will return the 7 last days.
|
||||
The example below if for `/daily 3`:
|
||||
Per default `/daily` will return the 7 last days. The example below if for `/daily 3`:
|
||||
|
||||
> **Daily Profit over the last 3 days:**
|
||||
```
|
||||
@@ -319,6 +323,34 @@ Day Profit BTC Profit USD
|
||||
2018-01-01 0.00269130 BTC 34.986 USD
|
||||
```
|
||||
|
||||
### /weekly <n>
|
||||
|
||||
Per default `/weekly` will return the 8 last weeks, including the current week. Each week starts
|
||||
from Monday. The example below if for `/weekly 3`:
|
||||
|
||||
> **Weekly Profit over the last 3 weeks (starting from Monday):**
|
||||
```
|
||||
Monday Profit BTC Profit USD
|
||||
---------- -------------- ------------
|
||||
2018-01-03 0.00224175 BTC 29,142 USD
|
||||
2017-12-27 0.00033131 BTC 4,307 USD
|
||||
2017-12-20 0.00269130 BTC 34.986 USD
|
||||
```
|
||||
|
||||
### /monthly <n>
|
||||
|
||||
Per default `/monthly` will return the 6 last months, including the current month. The example below
|
||||
if for `/monthly 3`:
|
||||
|
||||
> **Monthly Profit over the last 3 months:**
|
||||
```
|
||||
Month Profit BTC Profit USD
|
||||
---------- -------------- ------------
|
||||
2018-01 0.00224175 BTC 29,142 USD
|
||||
2017-12 0.00033131 BTC 4,307 USD
|
||||
2017-11 0.00269130 BTC 34.986 USD
|
||||
```
|
||||
|
||||
### /whitelist
|
||||
|
||||
Shows the current whitelist
|
||||
|
@@ -59,7 +59,7 @@ $ freqtrade new-config --config config_binance.json
|
||||
? Do you want to enable Dry-run (simulated trades)? Yes
|
||||
? Please insert your stake currency: BTC
|
||||
? Please insert your stake amount: 0.05
|
||||
? Please insert max_open_trades (Integer or 'unlimited'): 3
|
||||
? Please insert max_open_trades (Integer or -1 for unlimited open trades): 3
|
||||
? Please insert your desired timeframe (e.g. 5m): 5m
|
||||
? Please insert your display Currency (for reporting): USD
|
||||
? Select exchange binance
|
||||
@@ -281,7 +281,7 @@ bitmax True missing opt: fetchMyTrades
|
||||
bitmex False Various reasons.
|
||||
bitpanda True
|
||||
bitso False missing: fetchOHLCV
|
||||
bitstamp False Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983
|
||||
bitstamp True missing opt: fetchTickers
|
||||
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
||||
bittrex True
|
||||
bitvavo True
|
||||
@@ -517,20 +517,25 @@ Requires a configuration with specified `pairlists` attribute.
|
||||
Can be used to generate static pairlists to be used during backtesting / hyperopt.
|
||||
|
||||
```
|
||||
usage: freqtrade test-pairlist [-h] [-c PATH]
|
||||
usage: freqtrade test-pairlist [-h] [-v] [-c PATH]
|
||||
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
|
||||
[-1] [--print-json]
|
||||
[-1] [--print-json] [--exchange EXCHANGE]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
-c PATH, --config PATH
|
||||
Specify configuration file (default: `config.json`).
|
||||
Multiple --config options may be used. Can be set to
|
||||
`-` to read config from stdin.
|
||||
Specify configuration file (default:
|
||||
`userdir/config.json` or `config.json` whichever
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]
|
||||
Specify quote currency(-ies). Space-separated list.
|
||||
-1, --one-column Print output in one column.
|
||||
--print-json Print list of pairs or market symbols in JSON format.
|
||||
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
|
||||
config is provided.
|
||||
|
||||
```
|
||||
|
||||
### Examples
|
||||
@@ -577,6 +582,46 @@ Common arguments:
|
||||
|
||||
```
|
||||
|
||||
## Show previous Backtest results
|
||||
|
||||
Allows you to show previous backtest results.
|
||||
Adding `--show-pair-list` outputs a sorted pair list you can easily copy/paste into your configuration (omitting bad pairs).
|
||||
|
||||
??? Warning "Strategy overfitting"
|
||||
Only using winning pairs can lead to an overfitted strategy, which will not work well on future data. Make sure to extensively test your strategy in dry-run before risking real money.
|
||||
|
||||
```
|
||||
usage: freqtrade backtesting-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-d PATH] [--userdir PATH]
|
||||
[--export-filename PATH] [--show-pair-list]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--export-filename PATH
|
||||
Save backtest results to the file with this filename.
|
||||
Requires `--export` to be set as well. Example:
|
||||
`--export-filename=user_data/backtest_results/backtest
|
||||
_today.json`
|
||||
--show-pair-list Show backtesting pairlist sorted by profit.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
-c PATH, --config PATH
|
||||
Specify configuration file (default:
|
||||
`userdir/config.json` or `config.json` whichever
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
```
|
||||
|
||||
## List Hyperopt results
|
||||
|
||||
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
||||
@@ -667,6 +712,7 @@ usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[--profitable] [-n INT] [--print-json]
|
||||
[--hyperopt-filename FILENAME] [--no-header]
|
||||
[--disable-param-export]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -680,6 +726,8 @@ optional arguments:
|
||||
--no-header Do not print epoch details header.
|
||||
--disable-param-export
|
||||
Disable automatic hyperopt parameter export.
|
||||
--breakdown {day,week,month} [{day,week,month} ...]
|
||||
Show backtesting breakdown per [day, week, month].
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
@@ -48,9 +48,9 @@ Sample configuration (tested using IFTTT).
|
||||
},
|
||||
```
|
||||
|
||||
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.
|
||||
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 your event and key to the url.
|
||||
|
||||
You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use `"format": "form"` or `"format": "json"` respectively. Example configuration for Mattermost Cloud integration:
|
||||
You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw data. Use `"format": "form"`, `"format": "json"`, or `"format": "raw"` respectively. Example configuration for Mattermost Cloud integration:
|
||||
|
||||
```json
|
||||
"webhook": {
|
||||
@@ -63,7 +63,36 @@ You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use
|
||||
},
|
||||
```
|
||||
|
||||
The result would be POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
|
||||
The result would be a POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
|
||||
|
||||
When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be ouput in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example:
|
||||
|
||||
```json
|
||||
"webhook": {
|
||||
"enabled": true,
|
||||
"url": "https://<YOURHOOKURL>",
|
||||
"format": "raw",
|
||||
"webhookstatus": {
|
||||
"data": "Status: {status}"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header.
|
||||
|
||||
Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries:
|
||||
|
||||
```json
|
||||
"webhook": {
|
||||
"enabled": true,
|
||||
"url": "https://<YOURHOOKURL>",
|
||||
"retries": 3,
|
||||
"retry_delay": 0.2,
|
||||
"webhookstatus": {
|
||||
"status": "Status: {status}"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -75,11 +104,13 @@ Possible parameters are:
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `limit`
|
||||
* ~~`limit` # Deprecated - should no longer be used.~~
|
||||
* `open_rate`
|
||||
* `amount`
|
||||
* `open_date`
|
||||
* `stake_amount`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
@@ -98,6 +129,7 @@ Possible parameters are:
|
||||
* `open_date`
|
||||
* `stake_amount`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
@@ -116,7 +148,10 @@ Possible parameters are:
|
||||
* `open_date`
|
||||
* `stake_amount`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
* `buy_tag`
|
||||
|
||||
### Webhooksell
|
||||
@@ -134,6 +169,7 @@ Possible parameters are:
|
||||
* `profit_amount`
|
||||
* `profit_ratio`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `fiat_currency`
|
||||
* `sell_reason`
|
||||
* `order_type`
|
||||
@@ -156,6 +192,7 @@ Possible parameters are:
|
||||
* `profit_amount`
|
||||
* `profit_ratio`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `fiat_currency`
|
||||
* `sell_reason`
|
||||
* `order_type`
|
||||
@@ -178,6 +215,7 @@ Possible parameters are:
|
||||
* `profit_amount`
|
||||
* `profit_ratio`
|
||||
* `stake_currency`
|
||||
* `base_currency`
|
||||
* `fiat_currency`
|
||||
* `sell_reason`
|
||||
* `order_type`
|
||||
|
@@ -23,9 +23,9 @@ git clone https://github.com/freqtrade/freqtrade.git
|
||||
|
||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.21-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.24-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
||||
|
||||
Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows.
|
||||
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows.
|
||||
Other versions must be downloaded from the above link.
|
||||
|
||||
``` powershell
|
||||
@@ -54,6 +54,8 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
|
||||
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-built 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 compose](docker_quickstart.md) first.
|
||||
You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
@@ -4,7 +4,7 @@ channels:
|
||||
# - defaults
|
||||
dependencies:
|
||||
# 1/4 req main
|
||||
- python>=3.7,<3.9
|
||||
- python>=3.8,<=3.10
|
||||
- numpy
|
||||
- pandas
|
||||
- pip
|
||||
@@ -16,7 +16,6 @@ dependencies:
|
||||
- cachetools
|
||||
- requests
|
||||
- urllib3
|
||||
- wrapt
|
||||
- jsonschema
|
||||
- TA-Lib
|
||||
- tabulate
|
||||
@@ -26,9 +25,12 @@ dependencies:
|
||||
- fastapi
|
||||
- uvicorn
|
||||
- pyjwt
|
||||
- aiofiles
|
||||
- psutil
|
||||
- colorama
|
||||
- questionary
|
||||
- prompt-toolkit
|
||||
- python-dateutil
|
||||
|
||||
|
||||
# ============================
|
||||
@@ -64,7 +66,6 @@ dependencies:
|
||||
- py_find_1st
|
||||
- tables
|
||||
- pytest-random-order
|
||||
- flake8-type-annotations
|
||||
- ccxt
|
||||
- flake8-tidy-imports
|
||||
- -e .
|
||||
|
@@ -1,27 +1,14 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2021.9'
|
||||
|
||||
if __version__ == 'develop':
|
||||
__version__ = '2022.3'
|
||||
|
||||
if 'dev' in __version__:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
__version__ = 'develop-' + subprocess.check_output(
|
||||
__version__ = __version__ + '-' + subprocess.check_output(
|
||||
['git', 'log', '--format="%h"', '-n 1'],
|
||||
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||
|
||||
# from datetime import datetime
|
||||
# last_release = subprocess.check_output(
|
||||
# ['git', 'tag']
|
||||
# ).decode('utf-8').split()[-1].split(".")
|
||||
# # Releases are in the format "2020.1" - we increment the latest version for dev.
|
||||
# prefix = f"{last_release[0]}.{int(last_release[1]) + 1}"
|
||||
# dev_version = int(datetime.now().timestamp() // 1000)
|
||||
# __version__ = f"{prefix}.dev{dev_version}"
|
||||
|
||||
# subprocess.check_output(
|
||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||
except Exception: # pragma: no cover
|
||||
# git not available, ignore
|
||||
try:
|
||||
|
@@ -3,7 +3,7 @@
|
||||
__main__.py for Freqtrade
|
||||
To launch Freqtrade as a module
|
||||
|
||||
> python -m freqtrade (with Python >= 3.7)
|
||||
> python -m freqtrade (with Python >= 3.8)
|
||||
"""
|
||||
|
||||
from freqtrade import main
|
||||
|
@@ -8,15 +8,16 @@ Note: Be careful with file-scoped imports in these subfiles.
|
||||
"""
|
||||
from freqtrade.commands.arguments import Arguments
|
||||
from freqtrade.commands.build_config_commands import start_new_config
|
||||
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
||||
start_list_data)
|
||||
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
|
||||
start_download_data, start_list_data)
|
||||
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
||||
start_new_strategy)
|
||||
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_show_trades)
|
||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
||||
from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show,
|
||||
start_edge, start_hyperopt)
|
||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.commands.trade_commands import start_trading
|
||||
|
@@ -23,7 +23,8 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
||||
|
||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
||||
"strategy_list", "export", "exportfilename"]
|
||||
"strategy_list", "export", "exportfilename",
|
||||
"backtest_breakdown", "backtest_cache"]
|
||||
|
||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||
"position_stacking", "use_max_market_positions",
|
||||
@@ -31,7 +32,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||
"epochs", "spaces", "print_all",
|
||||
"print_colorized", "print_json", "hyperopt_jobs",
|
||||
"hyperopt_random_state", "hyperopt_min_trades",
|
||||
"hyperopt_loss", "disableparamexport"]
|
||||
"hyperopt_loss", "disableparamexport",
|
||||
"hyperopt_ignore_missing_space"]
|
||||
|
||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||
|
||||
@@ -39,6 +41,8 @@ ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"]
|
||||
|
||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
||||
|
||||
ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"]
|
||||
|
||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
|
||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||
@@ -47,7 +51,7 @@ ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one
|
||||
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
|
||||
|
||||
ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
|
||||
"list_pairs_print_json"]
|
||||
"list_pairs_print_json", "exchange"]
|
||||
|
||||
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
||||
|
||||
@@ -58,20 +62,22 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
||||
|
||||
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
|
||||
|
||||
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
|
||||
|
||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
|
||||
"download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv",
|
||||
"dataformat_trades"]
|
||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
|
||||
"timerange", "download_trades", "exchange", "timeframes",
|
||||
"erase", "dataformat_ohlcv", "dataformat_trades"]
|
||||
|
||||
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
||||
"db_url", "trade_source", "export", "exportfilename",
|
||||
"timerange", "timeframe", "no_trades"]
|
||||
|
||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||
"trade_source", "timeframe", "plot_auto_open"]
|
||||
"trade_source", "timeframe", "plot_auto_open", ]
|
||||
|
||||
ARGS_INSTALL_UI = ["erase_ui_only"]
|
||||
ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version']
|
||||
|
||||
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
|
||||
|
||||
@@ -86,12 +92,12 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
||||
|
||||
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
||||
"disableparamexport"]
|
||||
"disableparamexport", "backtest_breakdown"]
|
||||
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||
"hyperopt-list", "hyperopt-show",
|
||||
"plot-dataframe", "plot-profit", "show-trades"]
|
||||
"hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
|
||||
@@ -169,14 +175,15 @@ class Arguments:
|
||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||
self._build_args(optionlist=['version'], parser=self.parser)
|
||||
|
||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
||||
start_download_data, start_edge, start_hyperopt,
|
||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
||||
start_list_data, start_list_exchanges, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
||||
start_plot_profit, start_show_trades, start_test_pairlist,
|
||||
start_trading, start_webserver)
|
||||
from freqtrade.commands import (start_backtesting, start_backtesting_show,
|
||||
start_convert_data, start_convert_trades,
|
||||
start_create_userdir, start_download_data, start_edge,
|
||||
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
||||
start_install_ui, start_list_data, start_list_exchanges,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_new_config, start_new_strategy,
|
||||
start_plot_dataframe, start_plot_profit, start_show_trades,
|
||||
start_test_pairlist, start_trading, start_webserver)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
# Use custom message when no subhandler is added
|
||||
@@ -236,6 +243,15 @@ class Arguments:
|
||||
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
|
||||
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
|
||||
|
||||
# Add trades-to-ohlcv subcommand
|
||||
convert_trade_data_cmd = subparsers.add_parser(
|
||||
'trades-to-ohlcv',
|
||||
help='Convert trade data to OHLCV data.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
convert_trade_data_cmd.set_defaults(func=start_convert_trades)
|
||||
self._build_args(optionlist=ARGS_CONVERT_TRADES, parser=convert_trade_data_cmd)
|
||||
|
||||
# Add list-data subcommand
|
||||
list_data_cmd = subparsers.add_parser(
|
||||
'list-data',
|
||||
@@ -251,6 +267,15 @@ class Arguments:
|
||||
backtesting_cmd.set_defaults(func=start_backtesting)
|
||||
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
||||
|
||||
# Add backtesting-show subcommand
|
||||
backtesting_show_cmd = subparsers.add_parser(
|
||||
'backtesting-show',
|
||||
help='Show past Backtest results',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
||||
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
||||
parents=[_common_parser, _strategy_parser])
|
||||
|
@@ -76,18 +76,23 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
{
|
||||
"type": "text",
|
||||
"name": "max_open_trades",
|
||||
"message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):",
|
||||
"default": "3",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val),
|
||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||
if val == UNLIMITED_STAKE_AMOUNT
|
||||
else val
|
||||
"validate": lambda val: validate_is_int(val)
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"name": "timeframe_in_config",
|
||||
"message": "Time",
|
||||
"choices": ["Have the strategy define timeframe.", "Override in configuration."]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "timeframe",
|
||||
"message": "Please insert your desired timeframe (e.g. 5m):",
|
||||
"default": "5m",
|
||||
"when": lambda x: x["timeframe_in_config"] == 'Override in configuration.'
|
||||
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
@@ -103,10 +108,12 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"binance",
|
||||
"binanceus",
|
||||
"bittrex",
|
||||
"kraken",
|
||||
"ftx",
|
||||
"kucoin",
|
||||
"gateio",
|
||||
"huobi",
|
||||
"kraken",
|
||||
"kucoin",
|
||||
"okx",
|
||||
Separator(),
|
||||
"other",
|
||||
],
|
||||
@@ -134,7 +141,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"type": "password",
|
||||
"name": "exchange_key_password",
|
||||
"message": "Insert Exchange API Key password",
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin'
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okx')
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
@@ -163,7 +170,8 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
{
|
||||
"type": "text",
|
||||
"name": "api_server_listen_addr",
|
||||
"message": "Insert Api server Listen Address (best left untouched default!)",
|
||||
"message": ("Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||
"otherwise best left untouched)"),
|
||||
"default": "127.0.0.1",
|
||||
"when": lambda x: x['api_server']
|
||||
},
|
||||
|
@@ -117,7 +117,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
# Optimize common
|
||||
"timeframe": Arg(
|
||||
'-i', '--timeframe', '--ticker-interval',
|
||||
'-i', '--timeframe',
|
||||
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||
),
|
||||
"timerange": Arg(
|
||||
@@ -152,6 +152,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
action='store_false',
|
||||
default=True,
|
||||
),
|
||||
"backtest_show_pair_list": Arg(
|
||||
'--show-pair-list',
|
||||
help='Show backtesting pairlist sorted by profit.',
|
||||
action='store_true',
|
||||
default=False,
|
||||
),
|
||||
"enable_protections": Arg(
|
||||
'--enable-protections', '--enableprotections',
|
||||
help='Enable protections for backtesting.'
|
||||
@@ -163,7 +169,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"strategy_list": Arg(
|
||||
'--strategy-list',
|
||||
help='Provide a space-separated list of strategies to backtest. '
|
||||
'Please note that ticker-interval needs to be set either in config '
|
||||
'Please note that timeframe needs to be set either in config '
|
||||
'or via command line. When using this together with `--export trades`, '
|
||||
'the strategy-name is injected into the filename '
|
||||
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
||||
@@ -176,11 +182,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
|
||||
),
|
||||
"exportfilename": Arg(
|
||||
'--export-filename',
|
||||
help='Save backtest results to the file with this filename. '
|
||||
'Requires `--export` to be set as well. '
|
||||
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
|
||||
metavar='PATH',
|
||||
"--export-filename",
|
||||
"--backtest-filename",
|
||||
help="Use this filename for backtest results."
|
||||
"Requires `--export` to be set as well. "
|
||||
"Example: `--export-filename=user_data/backtest_results/backtest_today.json`",
|
||||
metavar="PATH",
|
||||
),
|
||||
"disableparamexport": Arg(
|
||||
'--disable-param-export',
|
||||
@@ -193,6 +200,18 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
type=float,
|
||||
metavar='FLOAT',
|
||||
),
|
||||
"backtest_breakdown": Arg(
|
||||
'--breakdown',
|
||||
help='Show backtesting breakdown per [day, week, month].',
|
||||
nargs='+',
|
||||
choices=constants.BACKTEST_BREAKDOWNS
|
||||
),
|
||||
"backtest_cache": Arg(
|
||||
'--cache',
|
||||
help='Load a cached backtest result no older than specified age (default: %(default)s).',
|
||||
default=constants.BACKTEST_CACHE_DEFAULT,
|
||||
choices=constants.BACKTEST_CACHE_AGE,
|
||||
),
|
||||
# Edge
|
||||
"stoploss_range": Arg(
|
||||
'--stoplosses',
|
||||
@@ -355,6 +374,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
type=check_int_positive,
|
||||
metavar='INT',
|
||||
),
|
||||
"include_inactive": Arg(
|
||||
'--include-inactive-pairs',
|
||||
help='Also download data from inactive pairs.',
|
||||
action='store_true',
|
||||
),
|
||||
"new_pairs_days": Arg(
|
||||
'--new-pairs-days',
|
||||
help='Download data of new pairs for given number of days. Default: `%(default)s`.',
|
||||
@@ -381,12 +405,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"dataformat_ohlcv": Arg(
|
||||
'--data-format-ohlcv',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `json`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
),
|
||||
"dataformat_trades": Arg(
|
||||
'--data-format-trades',
|
||||
help='Storage format for downloaded trades data. (default: `%(default)s`).',
|
||||
help='Storage format for downloaded trades data. (default: `jsongz`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
),
|
||||
"exchange": Arg(
|
||||
@@ -414,6 +438,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
action='store_true',
|
||||
default=False,
|
||||
),
|
||||
"ui_version": Arg(
|
||||
'--ui-version',
|
||||
help=('Specify a specific version of FreqUI to install. '
|
||||
'Not specifying this installs the latest version.'),
|
||||
type=str,
|
||||
),
|
||||
# Templating options
|
||||
"template": Arg(
|
||||
'--template',
|
||||
@@ -552,4 +582,10 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help='Do not print epoch details header.',
|
||||
action='store_true',
|
||||
),
|
||||
"hyperopt_ignore_missing_space": Arg(
|
||||
"--ignore-missing-spaces", "--ignore-unparameterized-spaces",
|
||||
help=("Suppress errors for any requested Hyperopt spaces "
|
||||
"that do not contain any parameters."),
|
||||
action="store_true",
|
||||
),
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.exchange.exchange import market_is_active
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
|
||||
@@ -47,11 +48,13 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
|
||||
or config.get('include_inactive')]
|
||||
expanded_pairs = expand_pairlist(config['pairs'], markets)
|
||||
|
||||
# Manual validations of relevant settings
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
exchange.validate_pairs(config['pairs'])
|
||||
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
||||
|
||||
exchange.validate_pairs(expanded_pairs)
|
||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||
|
||||
@@ -89,6 +92,41 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
f"on exchange {exchange.name}.")
|
||||
|
||||
|
||||
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
timerange = TimeRange()
|
||||
|
||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||
config['stake_currency'] = ''
|
||||
|
||||
if 'pairs' not in config:
|
||||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
"Please check the documentation on how to configure this.")
|
||||
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
# Manual validations of relevant settings
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
exchange.validate_pairs(config['pairs'])
|
||||
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
||||
|
||||
logger.info(f"About to Convert pairs: {expanded_pairs}, "
|
||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||
|
||||
for timeframe in config['timeframes']:
|
||||
exchange.validate_timeframes(timeframe)
|
||||
# Convert downloaded trade data to different timeframes
|
||||
convert_trades_to_ohlcv(
|
||||
pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
|
||||
data_format_ohlcv=config['dataformat_ohlcv'],
|
||||
data_format_trades=config['dataformat_trades'],
|
||||
)
|
||||
|
||||
|
||||
def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||
"""
|
||||
Convert data from one format to another
|
||||
|
@@ -128,7 +128,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
|
||||
f.write(version)
|
||||
|
||||
|
||||
def get_ui_download_url() -> Tuple[str, str]:
|
||||
def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]:
|
||||
base_url = 'https://api.github.com/repos/freqtrade/frequi/'
|
||||
# Get base UI Repo path
|
||||
|
||||
@@ -136,8 +136,16 @@ def get_ui_download_url() -> Tuple[str, str]:
|
||||
resp.raise_for_status()
|
||||
r = resp.json()
|
||||
|
||||
latest_version = r[0]['name']
|
||||
assets = r[0].get('assets', [])
|
||||
if version:
|
||||
tmp = [x for x in r if x['name'] == version]
|
||||
if tmp:
|
||||
latest_version = tmp[0]['name']
|
||||
assets = tmp[0].get('assets', [])
|
||||
else:
|
||||
raise ValueError("UI-Version not found.")
|
||||
else:
|
||||
latest_version = r[0]['name']
|
||||
assets = r[0].get('assets', [])
|
||||
dl_url = ''
|
||||
if assets and len(assets) > 0:
|
||||
dl_url = assets[0]['browser_download_url']
|
||||
@@ -156,7 +164,7 @@ def start_install_ui(args: Dict[str, Any]) -> None:
|
||||
|
||||
dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/'
|
||||
# First make sure the assets are removed.
|
||||
dl_url, latest_version = get_ui_download_url()
|
||||
dl_url, latest_version = get_ui_download_url(args.get('ui_version'))
|
||||
|
||||
curr_version = read_ui_version(dest_folder)
|
||||
if curr_version == latest_version and not args.get('erase_ui_only'):
|
||||
|
@@ -96,7 +96,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
if 'strategy_name' in metrics:
|
||||
strategy_name = metrics['strategy_name']
|
||||
show_backtest_result(strategy_name, metrics,
|
||||
metrics['stake_currency'])
|
||||
metrics['stake_currency'], config.get('backtest_breakdown', []))
|
||||
|
||||
HyperoptTools.try_export_params(config, strategy_name, val)
|
||||
|
||||
|
@@ -25,12 +25,16 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
||||
RunMode.HYPEROPT: 'hyperoptimization',
|
||||
}
|
||||
if method in no_unlimited_runmodes.keys():
|
||||
wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio']
|
||||
# tradable_balance_ratio
|
||||
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||
and config['stake_amount'] > config['dry_run_wallet']):
|
||||
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
|
||||
and config['stake_amount'] > wallet_size):
|
||||
wallet = round_coin_value(wallet_size, config['stake_currency'])
|
||||
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
||||
raise OperationalException(f"Starting balance ({wallet}) "
|
||||
f"is smaller than stake_amount {stake}.")
|
||||
raise OperationalException(
|
||||
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
||||
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
@@ -54,6 +58,22 @@ def start_backtesting(args: Dict[str, Any]) -> None:
|
||||
backtesting.start()
|
||||
|
||||
|
||||
def start_backtesting_show(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Show previous backtest result
|
||||
"""
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
from freqtrade.data.btanalysis import load_backtest_stats
|
||||
from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist
|
||||
|
||||
results = load_backtest_stats(config['exportfilename'])
|
||||
|
||||
show_backtest_results(config, results)
|
||||
show_sorted_pairlist(config, results)
|
||||
|
||||
|
||||
def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Start hyperopt script
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from cachetools.ttl import TTLCache
|
||||
from cachetools import TTLCache
|
||||
|
||||
|
||||
class PeriodicCache(TTLCache):
|
||||
|
@@ -245,6 +245,10 @@ class Configuration:
|
||||
self._args_to_config(config, argname='timeframe_detail',
|
||||
logstring='Parameter --timeframe-detail detected, '
|
||||
'using {} for intra-candle backtesting ...')
|
||||
|
||||
self._args_to_config(config, argname='backtest_show_pair_list',
|
||||
logstring='Parameter --show-pair-list detected.')
|
||||
|
||||
self._args_to_config(config, argname='stake_amount',
|
||||
logstring='Parameter --stake-amount detected, '
|
||||
'overriding stake_amount to: {} ...')
|
||||
@@ -269,8 +273,15 @@ class Configuration:
|
||||
self._args_to_config(config, argname='export',
|
||||
logstring='Parameter --export detected: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='backtest_breakdown',
|
||||
logstring='Parameter --breakdown detected ...')
|
||||
|
||||
self._args_to_config(config, argname='backtest_cache',
|
||||
logstring='Parameter --cache={} detected ...')
|
||||
|
||||
self._args_to_config(config, argname='disableparamexport',
|
||||
logstring='Parameter --disableparamexport detected: {} ...')
|
||||
|
||||
# Edge section:
|
||||
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
|
||||
txt_range = eval(self.args["stoploss_range"])
|
||||
@@ -369,6 +380,9 @@ class Configuration:
|
||||
self._args_to_config(config, argname='hyperopt_show_no_header',
|
||||
logstring='Parameter --no-header detected: {}')
|
||||
|
||||
self._args_to_config(config, argname="hyperopt_ignore_missing_space",
|
||||
logstring="Paramter --ignore-missing-space detected: {}")
|
||||
|
||||
def _process_plot_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
self._args_to_config(config, argname='pairs',
|
||||
@@ -404,6 +418,9 @@ class Configuration:
|
||||
self._args_to_config(config, argname='days',
|
||||
logstring='Detected --days: {}')
|
||||
|
||||
self._args_to_config(config, argname='include_inactive',
|
||||
logstring='Detected --include-inactive-pairs: {}')
|
||||
|
||||
self._args_to_config(config, argname='download_trades',
|
||||
logstring='Detected --dl-trades: {}')
|
||||
|
||||
@@ -414,7 +431,6 @@ class Configuration:
|
||||
logstring='Using "{}" to store trades data.')
|
||||
|
||||
def _process_data_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
self._args_to_config(config, argname='new_pairs_days',
|
||||
logstring='Detected --new-pairs-days: {}')
|
||||
|
||||
|
@@ -100,16 +100,11 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
"from the edge configuration."
|
||||
)
|
||||
if 'ticker_interval' in config:
|
||||
logger.warning(
|
||||
"DEPRECATED: "
|
||||
|
||||
raise OperationalException(
|
||||
"DEPRECATED: 'ticker_interval' detected. "
|
||||
"Please use 'timeframe' instead of 'ticker_interval."
|
||||
)
|
||||
if 'timeframe' in config:
|
||||
raise OperationalException(
|
||||
"Both 'timeframe' and 'ticker_interval' detected."
|
||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
||||
)
|
||||
config['timeframe'] = config['ticker_interval']
|
||||
|
||||
if 'protections' in config:
|
||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||
|
@@ -32,6 +32,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
||||
:param prefix: Prefix to consider (usually FREQTRADE__)
|
||||
:return: Nested dict based on available and relevant variables.
|
||||
"""
|
||||
no_convert = ['CHAT_ID']
|
||||
relevant_vars: Dict[str, Any] = {}
|
||||
|
||||
for env_var, val in sorted(env_dict.items()):
|
||||
@@ -39,9 +40,9 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
||||
logger.info(f"Loading variable '{env_var}'")
|
||||
key = env_var.replace(prefix, '')
|
||||
for k in reversed(key.split('__')):
|
||||
val = {k.lower(): get_var_typed(val) if type(val) != dict else val}
|
||||
val = {k.lower(): get_var_typed(val)
|
||||
if type(val) != dict and k not in no_convert else val}
|
||||
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||
|
||||
return relevant_vars
|
||||
|
||||
|
||||
|
@@ -24,13 +24,18 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
|
||||
'CalmarHyperOptLoss',
|
||||
'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
|
||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||
BACKTEST_CACHE_DEFAULT = 'day'
|
||||
DRY_RUN_WALLET = 1000
|
||||
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||
@@ -47,11 +52,12 @@ USERPATH_STRATEGIES = 'strategies'
|
||||
USERPATH_NOTEBOOKS = 'notebooks'
|
||||
|
||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||
|
||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||
|
||||
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
||||
|
||||
|
||||
# Define decimals per coin for outputs
|
||||
# Only used for outputs.
|
||||
DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's
|
||||
@@ -65,7 +71,6 @@ DUST_PER_COIN = {
|
||||
'ETH': 0.01
|
||||
}
|
||||
|
||||
|
||||
# Source files with destination directories within user-directory
|
||||
USER_DATA_FILES = {
|
||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||
@@ -135,7 +140,7 @@ CONF_SCHEMA = {
|
||||
'minProperties': 1
|
||||
},
|
||||
'amount_reserve_percent': {'type': 'number', 'minimum': 0.0, 'maximum': 0.5},
|
||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True, 'minimum': -1},
|
||||
'trailing_stop': {'type': 'boolean'},
|
||||
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
@@ -145,12 +150,17 @@ CONF_SCHEMA = {
|
||||
'sell_profit_offset': {'type': 'number'},
|
||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
||||
'ignore_buying_expired_candle_after': {'type': 'number'},
|
||||
'backtest_breakdown': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
|
||||
},
|
||||
'bot_name': {'type': 'string'},
|
||||
'unfilledtimeout': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'buy': {'type': 'number', 'minimum': 1},
|
||||
'sell': {'type': 'number', 'minimum': 1},
|
||||
'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0},
|
||||
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
|
||||
}
|
||||
},
|
||||
@@ -192,7 +202,7 @@ CONF_SCHEMA = {
|
||||
'required': ['price_side']
|
||||
},
|
||||
'custom_price_max_distance_ratio': {
|
||||
'type': 'number', 'minimum': 0.0
|
||||
'type': 'number', 'minimum': 0.0
|
||||
},
|
||||
'order_types': {
|
||||
'type': 'object',
|
||||
@@ -201,7 +211,10 @@ CONF_SCHEMA = {
|
||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'emergencysell': {
|
||||
'type': 'string',
|
||||
'enum': ORDERTYPE_POSSIBILITIES,
|
||||
'default': 'market'},
|
||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss_on_exchange': {'type': 'boolean'},
|
||||
'stoploss_on_exchange_interval': {'type': 'number'},
|
||||
@@ -303,10 +316,16 @@ CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'enabled': {'type': 'boolean'},
|
||||
'url': {'type': 'string'},
|
||||
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||
'retries': {'type': 'integer', 'minimum': 0},
|
||||
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||
'webhookbuy': {'type': 'object'},
|
||||
'webhookbuycancel': {'type': 'object'},
|
||||
'webhookbuyfill': {'type': 'object'},
|
||||
'webhooksell': {'type': 'object'},
|
||||
'webhooksellcancel': {'type': 'object'},
|
||||
'webhooksellfill': {'type': 'object'},
|
||||
'webhookstatus': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
@@ -345,14 +364,16 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'dataformat_ohlcv': {
|
||||
'type': 'string',
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'json'
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'json'
|
||||
},
|
||||
'dataformat_trades': {
|
||||
'type': 'string',
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'jsongz'
|
||||
}
|
||||
'enum': AVAILABLE_DATAHANDLERS,
|
||||
'default': 'jsongz'
|
||||
},
|
||||
'position_adjustment_enable': {'type': 'boolean'},
|
||||
'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1},
|
||||
},
|
||||
'definitions': {
|
||||
'exchange': {
|
||||
@@ -378,6 +399,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'uniqueItems': True
|
||||
},
|
||||
'unknown_fee_rate': {'type': 'number'},
|
||||
'outdated_offset': {'type': 'integer', 'minimum': 1},
|
||||
'markets_refresh_interval': {'type': 'integer'},
|
||||
'ccxt_config': {'type': 'object'},
|
||||
@@ -418,7 +440,6 @@ SCHEMA_TRADE_REQUIRED = [
|
||||
'dry_run_wallet',
|
||||
'ask_strategy',
|
||||
'bid_strategy',
|
||||
'unfilledtimeout',
|
||||
'stoploss',
|
||||
'minimal_roi',
|
||||
'internals',
|
||||
|
@@ -2,6 +2,8 @@
|
||||
Helpers when analyzing backtest data
|
||||
"""
|
||||
import logging
|
||||
from copy import copy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
@@ -9,21 +11,13 @@ import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
from freqtrade.misc import json_load
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import get_backtest_metadata_filename, json_load
|
||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Old format - maybe remove?
|
||||
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
|
||||
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||
|
||||
# Mid-term format, created by BacktestResult Named Tuple
|
||||
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
|
||||
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
|
||||
'fee_close', 'amount', 'profit_abs', 'profit_ratio']
|
||||
|
||||
# Newest format
|
||||
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
||||
'open_rate', 'close_rate',
|
||||
@@ -106,10 +100,30 @@ def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str =
|
||||
if isinstance(directory, str):
|
||||
directory = Path(directory)
|
||||
if predef_filename:
|
||||
if Path(predef_filename).is_absolute():
|
||||
raise OperationalException(
|
||||
"--hyperopt-filename expects only the filename, not an absolute path.")
|
||||
return directory / predef_filename
|
||||
return directory / get_latest_hyperopt_filename(directory)
|
||||
|
||||
|
||||
def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Read metadata dictionary from backtest results file without reading and deserializing entire
|
||||
file.
|
||||
:param filename: path to backtest results file.
|
||||
:return: metadata dict or None if metadata is not present.
|
||||
"""
|
||||
filename = get_backtest_metadata_filename(filename)
|
||||
try:
|
||||
with filename.open() as fp:
|
||||
return json_load(fp)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
except Exception as e:
|
||||
raise OperationalException('Unexpected error while loading backtest metadata.') from e
|
||||
|
||||
|
||||
def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Load backtest statistics file.
|
||||
@@ -126,9 +140,80 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
with filename.open() as file:
|
||||
data = json_load(file)
|
||||
|
||||
# Legacy list format does not contain metadata.
|
||||
if isinstance(data, dict):
|
||||
data['metadata'] = load_backtest_metadata(filename)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
|
||||
bt_data = load_backtest_stats(filename)
|
||||
for k in ('metadata', 'strategy'):
|
||||
results[k][strategy_name] = bt_data[k][strategy_name]
|
||||
comparison = bt_data['strategy_comparison']
|
||||
for i in range(len(comparison)):
|
||||
if comparison[i]['key'] == strategy_name:
|
||||
results['strategy_comparison'].append(comparison[i])
|
||||
break
|
||||
|
||||
|
||||
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
||||
min_backtest_date: datetime = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Find existing backtest stats that match specified run IDs and load them.
|
||||
:param dirname: pathlib.Path object, or string pointing to the file.
|
||||
:param run_ids: {strategy_name: id_string} dictionary.
|
||||
:param min_backtest_date: do not load a backtest older than specified date.
|
||||
:return: results dict.
|
||||
"""
|
||||
# Copy so we can modify this dict without affecting parent scope.
|
||||
run_ids = copy(run_ids)
|
||||
dirname = Path(dirname)
|
||||
results: Dict[str, Any] = {
|
||||
'metadata': {},
|
||||
'strategy': {},
|
||||
'strategy_comparison': [],
|
||||
}
|
||||
|
||||
# Weird glob expression here avoids including .meta.json files.
|
||||
for filename in reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))):
|
||||
metadata = load_backtest_metadata(filename)
|
||||
if not metadata:
|
||||
# Files are sorted from newest to oldest. When file without metadata is encountered it
|
||||
# is safe to assume older files will also not have any metadata.
|
||||
break
|
||||
|
||||
for strategy_name, run_id in list(run_ids.items()):
|
||||
strategy_metadata = metadata.get(strategy_name, None)
|
||||
if not strategy_metadata:
|
||||
# This strategy is not present in analyzed backtest.
|
||||
continue
|
||||
|
||||
if min_backtest_date is not None:
|
||||
try:
|
||||
backtest_date = strategy_metadata['backtest_start_time']
|
||||
except KeyError:
|
||||
# TODO: this can be removed starting from feb 2022
|
||||
# The metadata-file without start_time was only available in develop
|
||||
# and was never included in an official release.
|
||||
# Older metadata format without backtest time, too old to consider.
|
||||
return results
|
||||
backtest_date = datetime.fromtimestamp(backtest_date, tz=timezone.utc)
|
||||
if backtest_date < min_backtest_date:
|
||||
# Do not use a cached result for this strategy as first result is too old.
|
||||
del run_ids[strategy_name]
|
||||
continue
|
||||
|
||||
if strategy_metadata['run_id'] == run_id:
|
||||
del run_ids[strategy_name]
|
||||
_load_and_merge_backtest_result(strategy_name, filename, results)
|
||||
|
||||
if len(run_ids) == 0:
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
||||
"""
|
||||
Load backtest data file.
|
||||
@@ -167,23 +252,9 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
||||
)
|
||||
else:
|
||||
# old format - only with lists.
|
||||
df = pd.DataFrame(data, columns=BT_DATA_COLUMNS_OLD)
|
||||
if not df.empty:
|
||||
df['open_date'] = pd.to_datetime(df['open_date'],
|
||||
unit='s',
|
||||
utc=True,
|
||||
infer_datetime_format=True
|
||||
)
|
||||
df['close_date'] = pd.to_datetime(df['close_date'],
|
||||
unit='s',
|
||||
utc=True,
|
||||
infer_datetime_format=True
|
||||
)
|
||||
# Create compatibility with new format
|
||||
df['profit_abs'] = df['close_rate'] - df['open_rate']
|
||||
raise OperationalException(
|
||||
"Backtest-results with only trades data are no longer supported.")
|
||||
if not df.empty:
|
||||
if 'profit_ratio' not in df.columns:
|
||||
df['profit_ratio'] = df['profit_percent']
|
||||
df = df.sort_values("open_date").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
@@ -325,6 +396,7 @@ def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame],
|
||||
:param column: Column in the original dataframes to use
|
||||
:return: DataFrame with the column renamed to the dict key, and a column
|
||||
named mean, containing the mean of all pairs.
|
||||
:raise: ValueError if no data is provided.
|
||||
"""
|
||||
df_comb = pd.concat([data[pair].set_index('date').rename(
|
||||
{column: pair}, axis=1)[pair] for pair in data], axis=1)
|
||||
@@ -360,9 +432,19 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
|
||||
return df
|
||||
|
||||
|
||||
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||
value_col: str = 'profit_ratio'
|
||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]:
|
||||
def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str
|
||||
) -> pd.DataFrame:
|
||||
max_drawdown_df = pd.DataFrame()
|
||||
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
|
||||
max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
|
||||
max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value']
|
||||
max_drawdown_df['date'] = profit_results.loc[:, date_col]
|
||||
return max_drawdown_df
|
||||
|
||||
|
||||
def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||
value_col: str = 'profit_ratio'
|
||||
):
|
||||
"""
|
||||
Calculate max drawdown and the corresponding close dates
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||
@@ -375,10 +457,29 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
||||
if len(trades) == 0:
|
||||
raise ValueError("Trade dataframe empty.")
|
||||
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||
max_drawdown_df = pd.DataFrame()
|
||||
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
|
||||
max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
|
||||
max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value']
|
||||
max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col)
|
||||
|
||||
return max_drawdown_df
|
||||
|
||||
|
||||
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
|
||||
value_col: str = 'profit_abs', starting_balance: float = 0
|
||||
) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]:
|
||||
"""
|
||||
Calculate max drawdown and the corresponding close dates
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
|
||||
:param value_col: Column in DataFrame to use for values (defaults to 'profit_abs')
|
||||
:param starting_balance: Portfolio starting balance - properly calculate relative drawdown.
|
||||
:return: Tuple (float, highdate, lowdate, highvalue, lowvalue, relative_drawdown)
|
||||
with absolute max drawdown, high and low time and high and low value,
|
||||
and the relative account drawdown
|
||||
:raise: ValueError if trade-dataframe was found empty.
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
raise ValueError("Trade dataframe empty.")
|
||||
profit_results = trades.sort_values(date_col).reset_index(drop=True)
|
||||
max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col)
|
||||
|
||||
idxmin = max_drawdown_df['drawdown'].idxmin()
|
||||
if idxmin == 0:
|
||||
@@ -388,7 +489,18 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
|
||||
high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
|
||||
['high_value'].idxmax(), 'cumulative']
|
||||
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
|
||||
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val
|
||||
max_drawdown_rel = 0.0
|
||||
if high_val + starting_balance != 0:
|
||||
max_drawdown_rel = (high_val - low_val) / (high_val + starting_balance)
|
||||
|
||||
return (
|
||||
abs(min(max_drawdown_df['drawdown'])),
|
||||
high_date,
|
||||
low_date,
|
||||
high_val,
|
||||
low_val,
|
||||
max_drawdown_rel
|
||||
)
|
||||
|
||||
|
||||
def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]:
|
||||
|
@@ -113,7 +113,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
||||
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
||||
if len_before != len_after:
|
||||
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
|
||||
f" - {round(pct_missing * 100, 2)}%")
|
||||
f" - {pct_missing:.2%}")
|
||||
if pct_missing > 0.01:
|
||||
logger.info(message)
|
||||
else:
|
||||
|
@@ -6,7 +6,6 @@ from typing import List, Optional
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade import misc
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
|
||||
ListPairsWithTimeframes, TradeList)
|
||||
@@ -61,10 +60,10 @@ class HDF5DataHandler(IDataHandler):
|
||||
|
||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||
|
||||
ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc')
|
||||
ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date'])
|
||||
|
||||
ds.close()
|
||||
_data.loc[:, self._columns].to_hdf(
|
||||
filename, key, mode='a', complevel=9, complib='blosc',
|
||||
format='table', data_columns=['date']
|
||||
)
|
||||
|
||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
||||
timerange: Optional[TimeRange] = None) -> pd.DataFrame:
|
||||
@@ -99,19 +98,6 @@ class HDF5DataHandler(IDataHandler):
|
||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||
return pairdata
|
||||
|
||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
||||
"""
|
||||
Remove data for this pair
|
||||
:param pair: Delete data for this pair.
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:return: True when deleted, false if file did not exist.
|
||||
"""
|
||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||
if filename.exists():
|
||||
filename.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
|
||||
"""
|
||||
Append data to existing data structures
|
||||
@@ -142,11 +128,11 @@ class HDF5DataHandler(IDataHandler):
|
||||
"""
|
||||
key = self._pair_trades_key(pair)
|
||||
|
||||
ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair),
|
||||
mode='a', complevel=9, complib='blosc')
|
||||
ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
|
||||
format='table', data_columns=['timestamp'])
|
||||
ds.close()
|
||||
pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf(
|
||||
self._pair_trades_filename(self._datadir, pair), key,
|
||||
mode='a', complevel=9, complib='blosc',
|
||||
format='table', data_columns=['timestamp']
|
||||
)
|
||||
|
||||
def trades_append(self, pair: str, data: TradeList):
|
||||
"""
|
||||
@@ -180,17 +166,9 @@ class HDF5DataHandler(IDataHandler):
|
||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||
return trades.values.tolist()
|
||||
|
||||
def trades_purge(self, pair: str) -> bool:
|
||||
"""
|
||||
Remove data for this pair
|
||||
:param pair: Delete data for this pair.
|
||||
:return: True when deleted, false if file did not exist.
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
if filename.exists():
|
||||
filename.unlink()
|
||||
return True
|
||||
return False
|
||||
@classmethod
|
||||
def _get_file_extension(cls):
|
||||
return "h5"
|
||||
|
||||
@classmethod
|
||||
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
||||
@@ -199,15 +177,3 @@ class HDF5DataHandler(IDataHandler):
|
||||
@classmethod
|
||||
def _pair_trades_key(cls, pair: str) -> str:
|
||||
return f"{pair}/trades"
|
||||
|
||||
@classmethod
|
||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
||||
pair_s = misc.pair_to_filename(pair)
|
||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.h5')
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
||||
pair_s = misc.pair_to_filename(pair)
|
||||
filename = datadir.joinpath(f'{pair_s}-trades.h5')
|
||||
return filename
|
||||
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
@@ -208,7 +208,7 @@ def _download_pair_history(pair: str, *,
|
||||
else:
|
||||
# Run cleaning again to ensure there were no duplicate candles
|
||||
# Especially between existing and new data.
|
||||
data = clean_ohlcv_dataframe(data.append(new_dataframe), timeframe, pair,
|
||||
data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair,
|
||||
fill_missing=False, drop_incomplete=False)
|
||||
|
||||
logger.debug("New Start: %s",
|
||||
|
@@ -12,6 +12,7 @@ from typing import List, Optional, Type
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import misc
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
||||
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
||||
@@ -26,6 +27,13 @@ class IDataHandler(ABC):
|
||||
def __init__(self, datadir: Path) -> None:
|
||||
self._datadir = datadir
|
||||
|
||||
@classmethod
|
||||
def _get_file_extension(cls) -> str:
|
||||
"""
|
||||
Get file extension for this particular datahandler
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractclassmethod
|
||||
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
|
||||
"""
|
||||
@@ -70,7 +78,6 @@ class IDataHandler(ABC):
|
||||
:return: DataFrame with ohlcv data, or empty DataFrame
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
||||
"""
|
||||
Remove data for this pair
|
||||
@@ -78,6 +85,11 @@ class IDataHandler(ABC):
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:return: True when deleted, false if file did not exist.
|
||||
"""
|
||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||
if filename.exists():
|
||||
filename.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
||||
@@ -123,13 +135,17 @@ class IDataHandler(ABC):
|
||||
:return: List of trades
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def trades_purge(self, pair: str) -> bool:
|
||||
"""
|
||||
Remove data for this pair
|
||||
:param pair: Delete data for this pair.
|
||||
:return: True when deleted, false if file did not exist.
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
if filename.exists():
|
||||
filename.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||
"""
|
||||
@@ -141,6 +157,18 @@ class IDataHandler(ABC):
|
||||
"""
|
||||
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
||||
|
||||
@classmethod
|
||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
||||
pair_s = misc.pair_to_filename(pair)
|
||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}')
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
||||
pair_s = misc.pair_to_filename(pair)
|
||||
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
||||
return filename
|
||||
|
||||
def ohlcv_load(self, pair, timeframe: str,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
fill_missing: bool = True,
|
||||
@@ -173,7 +201,7 @@ class IDataHandler(ABC):
|
||||
enddate = pairdf.iloc[-1]['date']
|
||||
|
||||
if timerange_startup:
|
||||
self._validate_pairdata(pair, pairdf, timerange_startup)
|
||||
self._validate_pairdata(pair, pairdf, timeframe, timerange_startup)
|
||||
pairdf = trim_dataframe(pairdf, timerange_startup)
|
||||
if self._check_empty_df(pairdf, pair, timeframe, warn_no_data):
|
||||
return pairdf
|
||||
@@ -200,7 +228,7 @@ class IDataHandler(ABC):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _validate_pairdata(self, pair, pairdata: DataFrame, timerange: TimeRange):
|
||||
def _validate_pairdata(self, pair, pairdata: DataFrame, timeframe: str, timerange: TimeRange):
|
||||
"""
|
||||
Validates pairdata for missing data at start end end and logs warnings.
|
||||
:param pairdata: Dataframe to validate
|
||||
@@ -210,12 +238,12 @@ class IDataHandler(ABC):
|
||||
if timerange.starttype == 'date':
|
||||
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
||||
if pairdata.iloc[0]['date'] > start:
|
||||
logger.warning(f"Missing data at start for pair {pair}, "
|
||||
logger.warning(f"Missing data at start for pair {pair} at {timeframe}, "
|
||||
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
|
||||
if timerange.stoptype == 'date':
|
||||
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
||||
if pairdata.iloc[-1]['date'] < stop:
|
||||
logger.warning(f"Missing data at end for pair {pair}, "
|
||||
logger.warning(f"Missing data at end for pair {pair} at {timeframe}, "
|
||||
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
|
||||
|
||||
|
||||
|
@@ -174,34 +174,10 @@ class JsonDataHandler(IDataHandler):
|
||||
pass
|
||||
return tradesdata
|
||||
|
||||
def trades_purge(self, pair: str) -> bool:
|
||||
"""
|
||||
Remove data for this pair
|
||||
:param pair: Delete data for this pair.
|
||||
:return: True when deleted, false if file did not exist.
|
||||
"""
|
||||
filename = self._pair_trades_filename(self._datadir, pair)
|
||||
if filename.exists():
|
||||
filename.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
||||
pair_s = misc.pair_to_filename(pair)
|
||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}')
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def _get_file_extension(cls):
|
||||
return "json.gz" if cls._use_zip else "json"
|
||||
|
||||
@classmethod
|
||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
||||
pair_s = misc.pair_to_filename(pair)
|
||||
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
||||
return filename
|
||||
|
||||
|
||||
class JsonGzDataHandler(JsonDataHandler):
|
||||
|
||||
|
@@ -219,9 +219,11 @@ class Edge:
|
||||
"""
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \
|
||||
pair in pairs:
|
||||
if (
|
||||
info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2))
|
||||
and info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))
|
||||
and pair in pairs
|
||||
):
|
||||
final.append(pair)
|
||||
|
||||
if self._final_pairs != final:
|
||||
@@ -246,8 +248,8 @@ class Edge:
|
||||
"""
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
|
||||
if (info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and
|
||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))):
|
||||
final.append({
|
||||
'Pair': pair,
|
||||
'Winrate': info.winrate,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.enums.backteststate import BacktestState
|
||||
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||
from freqtrade.enums.selltype import SellType
|
||||
|
6
freqtrade/enums/ordertypevalue.py
Normal file
6
freqtrade/enums/ordertypevalue.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class OrderTypeValues(str, Enum):
|
||||
limit = 'limit'
|
||||
market = 'market'
|
@@ -14,3 +14,4 @@ class SignalTagType(Enum):
|
||||
Enum for signal columns
|
||||
"""
|
||||
BUY_TAG = "buy_tag"
|
||||
EXIT_TAG = "exit_tag"
|
||||
|
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
class FreqtradeException(Exception):
|
||||
"""
|
||||
Freqtrade base exception. Handled at the outermost level.
|
||||
|
@@ -5,6 +5,7 @@ from freqtrade.exchange.exchange import Exchange
|
||||
# isort: on
|
||||
from freqtrade.exchange.bibox import Bibox
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.bitpanda import Bitpanda
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.bybit import Bybit
|
||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||
@@ -17,5 +18,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
||||
from freqtrade.exchange.ftx import Ftx
|
||||
from freqtrade.exchange.gateio import Gateio
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.huobi import Huobi
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.okx import Okx
|
||||
|
@@ -1,14 +1,10 @@
|
||||
""" Binance exchange subclass """
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -18,6 +14,7 @@ class Binance(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_candle_limit": 1000,
|
||||
@@ -33,68 +30,10 @@ class Binance(Exchange):
|
||||
"""
|
||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
this stoploss-limit is binance-specific.
|
||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
rate = stop_price * limit_price_pct
|
||||
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price <= rate:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=rate, params=params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
# Errors:
|
||||
# `binance Order would trigger immediately.`
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool
|
||||
) -> List:
|
||||
since_ms: int, is_new_pair: bool = False,
|
||||
raise_: bool = False
|
||||
) -> Tuple[str, str, List]:
|
||||
"""
|
||||
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||
@@ -107,4 +46,5 @@ class Binance(Exchange):
|
||||
logger.info(f"Candle-data for {pair} available starting with "
|
||||
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
||||
return await super()._async_get_historic_ohlcv(
|
||||
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
|
||||
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair,
|
||||
raise_=raise_)
|
||||
|
37
freqtrade/exchange/bitpanda.py
Normal file
37
freqtrade/exchange/bitpanda.py
Normal file
@@ -0,0 +1,37 @@
|
||||
""" Bitpanda exchange subclass """
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Bitpanda(Exchange):
|
||||
"""
|
||||
Bitpanda exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||
params: Optional[Dict] = None) -> List:
|
||||
"""
|
||||
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
|
||||
The "since" argument passed in is coming from the database and is in UTC,
|
||||
as timezone-native datetime object.
|
||||
From the python documentation:
|
||||
> Naive datetime instances are assumed to represent local time
|
||||
Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
|
||||
transformation from local timezone to UTC.
|
||||
This works for timezones UTC+ since then the result will contain trades from a few hours
|
||||
instead of from the last 5 seconds, however fails for UTC- timezones,
|
||||
since we're then asking for trades with a "since" argument in the future.
|
||||
|
||||
:param order_id order_id: Order-id as given when creating the order
|
||||
:param pair: Pair the order is for
|
||||
:param since: datetime object of the order creation time. Assumes object is in UTC.
|
||||
"""
|
||||
params = {'to': int(datetime.now(timezone.utc).timestamp() * 1000)}
|
||||
return super().get_trades_for_order(order_id, pair, since, params)
|
@@ -4,9 +4,20 @@ import time
|
||||
from functools import wraps
|
||||
|
||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
__logging_mixin = None
|
||||
|
||||
|
||||
def _get_logging_mixin():
|
||||
# Logging-mixin to cache kucoin responses
|
||||
# Only to be used in retrier
|
||||
global __logging_mixin
|
||||
if not __logging_mixin:
|
||||
__logging_mixin = LoggingMixin(logger)
|
||||
return __logging_mixin
|
||||
|
||||
|
||||
# Maximum default retry count.
|
||||
@@ -16,15 +27,15 @@ API_FETCH_ORDER_RETRY_COUNT = 5
|
||||
|
||||
BAD_EXCHANGES = {
|
||||
"bitmex": "Various reasons.",
|
||||
"bitstamp": "Does not provide history. "
|
||||
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
|
||||
"phemex": "Does not provide history. ",
|
||||
"phemex": "Does not provide history.",
|
||||
"probit": "Requires additional, regular calls to `signIn()`.",
|
||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
|
||||
}
|
||||
|
||||
MAP_EXCHANGE_CHILDCLASS = {
|
||||
'binanceus': 'binance',
|
||||
'binanceje': 'binance',
|
||||
'okex': 'okx',
|
||||
}
|
||||
|
||||
|
||||
@@ -74,21 +85,33 @@ def calculate_backoff(retrycount, max_retries):
|
||||
def retrier_async(f):
|
||||
async def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||
kucoin = args[0].name == "Kucoin" # Check if the exchange is KuCoin.
|
||||
try:
|
||||
return await f(*args, **kwargs)
|
||||
except TemporaryError as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
msg = f'{f.__name__}() returned exception: "{ex}". '
|
||||
if count > 0:
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
msg += f'Retrying still for {count} times.'
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
kwargs['count'] = count
|
||||
if isinstance(ex, DDosProtection):
|
||||
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
||||
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
await asyncio.sleep(backoff_delay)
|
||||
if kucoin and "429000" in str(ex):
|
||||
# Temporary fix for 429000 error on kucoin
|
||||
# see https://github.com/freqtrade/freqtrade/issues/5700 for details.
|
||||
_get_logging_mixin().log_once(
|
||||
f"Kucoin 429 error, avoid triggering DDosProtection backoff delay. "
|
||||
f"{count} tries left before giving up", logmethod=logger.warning)
|
||||
# Reset msg to avoid logging too many times.
|
||||
msg = ''
|
||||
else:
|
||||
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
||||
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
await asyncio.sleep(backoff_delay)
|
||||
if msg:
|
||||
logger.warning(msg)
|
||||
return await wrapper(*args, **kwargs)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
logger.warning(msg + 'Giving up.')
|
||||
raise ex
|
||||
return wrapper
|
||||
|
||||
@@ -101,9 +124,9 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except (TemporaryError, RetryableOrderError) as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
msg = f'{f.__name__}() returned exception: "{ex}". '
|
||||
if count > 0:
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
logger.warning(msg + f'Retrying still for {count} times.')
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
if isinstance(ex, (DDosProtection, RetryableOrderError)):
|
||||
@@ -113,7 +136,7 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
|
||||
time.sleep(backoff_delay)
|
||||
return wrapper(*args, **kwargs)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
logger.warning(msg + 'Giving up.')
|
||||
raise ex
|
||||
return wrapper
|
||||
# Support both @retrier and @retrier(retries=2) syntax
|
||||
|
@@ -7,9 +7,9 @@ import http
|
||||
import inspect
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Coroutine, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
@@ -67,6 +67,8 @@ class Exchange:
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
"ohlcv_partial_candle": True,
|
||||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||
"trades_pagination": "time", # Possible are "time" or "id"
|
||||
"trades_pagination_arg": "since",
|
||||
"l2_limit_range": None,
|
||||
@@ -83,6 +85,8 @@ class Exchange:
|
||||
self._api: ccxt.Exchange = None
|
||||
self._api_async: ccxt_async.Exchange = None
|
||||
self._markets: Dict = {}
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
self._config.update(config)
|
||||
|
||||
@@ -155,8 +159,8 @@ class Exchange:
|
||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||
self.validate_ordertypes(config.get('order_types', {}))
|
||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||
self.validate_required_startup_candles(config.get('startup_candle_count', 0),
|
||||
config.get('timeframe', ''))
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
config.get('startup_candle_count', 0), config.get('timeframe', ''))
|
||||
|
||||
# Converts the interval provided in minutes in config to seconds
|
||||
self.markets_refresh_interval: int = exchange_config.get(
|
||||
@@ -170,8 +174,10 @@ class Exchange:
|
||||
|
||||
def close(self):
|
||||
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())
|
||||
if (self._api_async and inspect.iscoroutinefunction(self._api_async.close)
|
||||
and self._api_async.session):
|
||||
logger.info("Closing async ccxt session.")
|
||||
self.loop.run_until_complete(self._api_async.close())
|
||||
|
||||
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
|
||||
@@ -326,7 +332,7 @@ class Exchange:
|
||||
def _load_async_markets(self, reload: bool = False) -> None:
|
||||
try:
|
||||
if self._api_async:
|
||||
asyncio.get_event_loop().run_until_complete(
|
||||
self.loop.run_until_complete(
|
||||
self._api_async.load_markets(reload=reload))
|
||||
|
||||
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
||||
@@ -370,7 +376,7 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
'Could not load markets, therefore cannot start. '
|
||||
'Please investigate the above error for more details.'
|
||||
)
|
||||
)
|
||||
quote_currencies = self.get_quote_currencies()
|
||||
if stake_currency not in quote_currencies:
|
||||
raise OperationalException(
|
||||
@@ -471,16 +477,29 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Time in force policies are not supported for {self.name} yet.')
|
||||
|
||||
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None:
|
||||
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
|
||||
"""
|
||||
Checks if required startup_candles is more than ohlcv_candle_limit().
|
||||
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
|
||||
"""
|
||||
candle_limit = self.ohlcv_candle_limit(timeframe)
|
||||
if startup_candles + 5 > candle_limit:
|
||||
# Require one more candle - to account for the still open candle.
|
||||
candle_count = startup_candles + 1
|
||||
# Allow 5 calls to the exchange per pair
|
||||
required_candle_call_count = int(
|
||||
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
|
||||
|
||||
if required_candle_call_count > 5:
|
||||
# Only allow 5 calls per pair to somewhat limit the impact
|
||||
raise OperationalException(
|
||||
f"This strategy requires {startup_candles} candles to start. "
|
||||
f"{self.name} only provides {candle_limit} for {timeframe}.")
|
||||
f"This strategy requires {startup_candles} candles to start, which is more than 5x "
|
||||
f"the amount of candles {self.name} provides for {timeframe}.")
|
||||
|
||||
if required_candle_call_count > 1:
|
||||
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
|
||||
f"This can result in slower operations for the bot. Please check "
|
||||
f"if you really need {startup_candles} candles for your strategy")
|
||||
return required_candle_call_count
|
||||
|
||||
def exchange_has(self, endpoint: str) -> bool:
|
||||
"""
|
||||
@@ -523,7 +542,7 @@ class Exchange:
|
||||
precision = self.markets[pair]['precision']['price']
|
||||
missing = price % precision
|
||||
if missing != 0:
|
||||
price = price - missing + precision
|
||||
price = round(price - missing + precision, 10)
|
||||
else:
|
||||
symbol_prec = self.markets[pair]['precision']['price']
|
||||
big_price = price * pow(10, symbol_prec)
|
||||
@@ -581,7 +600,8 @@ class Exchange:
|
||||
# Dry-run methods
|
||||
|
||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
rate: float, params: Dict = {},
|
||||
stop_loss: bool = False) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||
_amount = self.amount_to_precision(pair, amount)
|
||||
dry_order: Dict[str, Any] = {
|
||||
@@ -593,21 +613,26 @@ class Exchange:
|
||||
'cost': _amount * rate,
|
||||
'type': ordertype,
|
||||
'side': side,
|
||||
'filled': 0,
|
||||
'remaining': _amount,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||
'status': "closed" if ordertype == "market" else "open",
|
||||
'status': "closed" if ordertype == "market" and not stop_loss else "open",
|
||||
'fee': None,
|
||||
'info': {}
|
||||
}
|
||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
if stop_loss:
|
||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||
dry_order["stopPrice"] = dry_order["price"]
|
||||
# Workaround to avoid filling stoploss orders immediately
|
||||
dry_order["ft_order_type"] = "stoploss"
|
||||
|
||||
if dry_order["type"] == "market":
|
||||
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||
# Update market order pricing
|
||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||
dry_order.update({
|
||||
'average': average,
|
||||
'filled': _amount,
|
||||
'cost': dry_order['amount'] * average,
|
||||
})
|
||||
dry_order = self.add_dry_order_fee(pair, dry_order)
|
||||
@@ -639,7 +664,8 @@ class Exchange:
|
||||
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
|
||||
|
||||
remaining_amount = amount
|
||||
filled_amount = 0
|
||||
filled_amount = 0.0
|
||||
book_entry_price = 0.0
|
||||
for book_entry in ob[ob_type]:
|
||||
book_entry_price = book_entry[0]
|
||||
book_entry_coin_volume = book_entry[1]
|
||||
@@ -672,23 +698,29 @@ class Exchange:
|
||||
if not self.exchange_has('fetchL2OrderBook'):
|
||||
return True
|
||||
ob = self.fetch_l2_order_book(pair, 1)
|
||||
if side == 'buy':
|
||||
price = ob['asks'][0][0]
|
||||
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
||||
if limit >= price:
|
||||
return True
|
||||
else:
|
||||
price = ob['bids'][0][0]
|
||||
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
||||
if limit <= price:
|
||||
return True
|
||||
try:
|
||||
if side == 'buy':
|
||||
price = ob['asks'][0][0]
|
||||
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
||||
if limit >= price:
|
||||
return True
|
||||
else:
|
||||
price = ob['bids'][0][0]
|
||||
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
||||
if limit <= price:
|
||||
return True
|
||||
except IndexError:
|
||||
# Ignore empty orderbooks when filling - can be filled with the next iteration.
|
||||
pass
|
||||
return False
|
||||
|
||||
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Check dry-run limit order fill and update fee (if it filled).
|
||||
"""
|
||||
if order['status'] != "closed" and order['type'] in ["limit"]:
|
||||
if (order['status'] != "closed"
|
||||
and order['type'] in ["limit"]
|
||||
and not order.get('ft_order_type')):
|
||||
pair = order['symbol']
|
||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
||||
order.update({
|
||||
@@ -765,25 +797,96 @@ class Exchange:
|
||||
"""
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
params = self._params.copy()
|
||||
# Verify if stopPrice works for your exchange!
|
||||
params.update({'stopPrice': stop_price})
|
||||
return params
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss order.
|
||||
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
|
||||
to the corresponding exchange type.
|
||||
|
||||
The precise ordertype is determined by the order_types dict or exchange default.
|
||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
||||
exchange's subclass.
|
||||
|
||||
The exception below should never raise, since we disallow
|
||||
starting the bot in validate_ordertypes()
|
||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
||||
"""
|
||||
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
This may work with a limited number of other exchanges, but correct working
|
||||
needs to be tested individually.
|
||||
WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
|
||||
`stoploss_adjust` must still be implemented for this to work.
|
||||
"""
|
||||
if not self._ft_has['stoploss_on_exchange']:
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
user_order_type = order_types.get('stoploss', 'market')
|
||||
if user_order_type in self._ft_has["stoploss_order_types"].keys():
|
||||
ordertype = self._ft_has["stoploss_order_types"][user_order_type]
|
||||
else:
|
||||
# Otherwise pick only one available
|
||||
ordertype = list(self._ft_has["stoploss_order_types"].values())[0]
|
||||
user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0]
|
||||
|
||||
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||
rate = None
|
||||
if user_order_type == 'limit':
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
rate = stop_price * limit_price_pct
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price_norm <= rate:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price_norm, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=rate, params=params)
|
||||
logger.info(f"stoploss {user_order_type} order added for {pair}. "
|
||||
f"stop price: {stop_price}. limit: {rate}")
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
# Errors:
|
||||
# `Order would trigger immediately.`
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not place stoploss order due to {e.__class__.__name__}. "
|
||||
f"Message: {e}") from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||
def fetch_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
try:
|
||||
order = self._api.fetch_order(order_id, pair)
|
||||
order = self._api.fetch_order(order_id, pair, params=params)
|
||||
self._log_exchange_response('fetch_order', order)
|
||||
return order
|
||||
except ccxt.OrderNotFound as e:
|
||||
@@ -826,7 +929,7 @@ class Exchange:
|
||||
and order.get('filled') == 0.0)
|
||||
|
||||
@retrier
|
||||
def cancel_order(self, order_id: str, pair: str) -> Dict:
|
||||
def cancel_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self.fetch_dry_run_order(order_id)
|
||||
@@ -837,7 +940,7 @@ class Exchange:
|
||||
return {}
|
||||
|
||||
try:
|
||||
order = self._api.cancel_order(order_id, pair)
|
||||
order = self._api.cancel_order(order_id, pair, params=params)
|
||||
self._log_exchange_response('cancel_order', order)
|
||||
return order
|
||||
except ccxt.InvalidOrder as e:
|
||||
@@ -927,7 +1030,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_tickers(self, cached: bool = False) -> Dict:
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
"""
|
||||
:param cached: Allow cached result
|
||||
:return: fetch_tickers result
|
||||
@@ -937,7 +1040,7 @@ class Exchange:
|
||||
if tickers:
|
||||
return tickers
|
||||
try:
|
||||
tickers = self._api.fetch_tickers()
|
||||
tickers = self._api.fetch_tickers(symbols)
|
||||
self._fetch_tickers_cache['fetch_tickers'] = tickers
|
||||
return tickers
|
||||
except ccxt.NotSupported as e:
|
||||
@@ -1058,7 +1161,7 @@ class Exchange:
|
||||
ticker_rate = ticker[conf_strategy['price_side']]
|
||||
if ticker['last'] and ticker_rate:
|
||||
if side == 'buy' and ticker_rate > ticker['last']:
|
||||
balance = conf_strategy['ask_last_balance']
|
||||
balance = conf_strategy.get('ask_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||
elif side == 'sell' and ticker_rate < ticker['last']:
|
||||
balance = conf_strategy.get('bid_last_balance', 0.0)
|
||||
@@ -1074,7 +1177,8 @@ class Exchange:
|
||||
# Fee handling
|
||||
|
||||
@retrier
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||
params: Optional[Dict] = None) -> List:
|
||||
"""
|
||||
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
|
||||
The "since" argument passed in is coming from the database and is in UTC,
|
||||
@@ -1098,8 +1202,10 @@ class Exchange:
|
||||
try:
|
||||
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||
# since needs to be int in milliseconds
|
||||
_params = params if params else {}
|
||||
my_trades = self._api.fetch_my_trades(
|
||||
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
|
||||
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
|
||||
params=_params)
|
||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||
|
||||
self._log_exchange_response('get_trades_for_order', matched_trades)
|
||||
@@ -1177,9 +1283,11 @@ class Exchange:
|
||||
tick = self.fetch_ticker(comb)
|
||||
|
||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
||||
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
|
||||
except ExchangeError:
|
||||
return None
|
||||
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
|
||||
if not fee_to_quote_rate:
|
||||
return None
|
||||
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
|
||||
|
||||
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
|
||||
"""
|
||||
@@ -1205,9 +1313,11 @@ class Exchange:
|
||||
:param since_ms: Timestamp in milliseconds to get history from
|
||||
:return: List with candle (OHLCV) data
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
pair, timeframe, data = self.loop.run_until_complete(
|
||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||
since_ms=since_ms, is_new_pair=is_new_pair))
|
||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||
return data
|
||||
|
||||
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
||||
since_ms: int) -> DataFrame:
|
||||
@@ -1223,8 +1333,9 @@ class Exchange:
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool
|
||||
) -> List:
|
||||
since_ms: int, is_new_pair: bool = False,
|
||||
raise_: bool = False
|
||||
) -> Tuple[str, str, List]:
|
||||
"""
|
||||
Download historic ohlcv
|
||||
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||
@@ -1247,16 +1358,34 @@ class Exchange:
|
||||
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||
if raise_:
|
||||
raise
|
||||
continue
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(new_data)
|
||||
else:
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(new_data)
|
||||
# Sort data again after extending the result - above calls return in "async order"
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||
return data
|
||||
return pair, timeframe, data
|
||||
|
||||
def _build_coroutine(self, pair: str, timeframe: str, since_ms: Optional[int]) -> Coroutine:
|
||||
if not since_ms and self.required_candle_call_count > 1:
|
||||
# Multiple calls for one pair - to get more history
|
||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
||||
move_to = one_call * self.required_candle_call_count
|
||||
now = timeframe_to_next_date(timeframe)
|
||||
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
|
||||
|
||||
if since_ms:
|
||||
return self._async_get_historic_ohlcv(
|
||||
pair, timeframe, since_ms=since_ms, raise_=True)
|
||||
else:
|
||||
# One call ... "regular" refresh
|
||||
return self._async_get_candle_history(
|
||||
pair, timeframe, since_ms=since_ms)
|
||||
|
||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||
since_ms: Optional[int] = None, cache: bool = True
|
||||
@@ -1276,10 +1405,15 @@ class Exchange:
|
||||
cached_pairs = []
|
||||
# Gather coroutines to run
|
||||
for pair, timeframe in set(pair_list):
|
||||
if (((pair, timeframe) not in self._klines)
|
||||
if timeframe not in self.timeframes:
|
||||
logger.warning(
|
||||
f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
|
||||
f"not available on {self.name}. Available timeframes are "
|
||||
f"{', '.join(self.timeframes)}.")
|
||||
continue
|
||||
if ((pair, timeframe) not in self._klines or not cache
|
||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||
input_coroutines.append(self._async_get_candle_history(pair, timeframe,
|
||||
since_ms=since_ms))
|
||||
input_coroutines.append(self._build_coroutine(pair, timeframe, since_ms))
|
||||
else:
|
||||
logger.debug(
|
||||
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
||||
@@ -1287,27 +1421,32 @@ class Exchange:
|
||||
)
|
||||
cached_pairs.append((pair, timeframe))
|
||||
|
||||
results = asyncio.get_event_loop().run_until_complete(
|
||||
asyncio.gather(*input_coroutines, return_exceptions=True))
|
||||
|
||||
results_df = {}
|
||||
# handle caching
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
# Deconstruct tuple (has 3 elements)
|
||||
pair, timeframe, ticks = res
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if ticks:
|
||||
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
ohlcv_df = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
results_df[(pair, timeframe)] = ohlcv_df
|
||||
if cache:
|
||||
self._klines[(pair, timeframe)] = ohlcv_df
|
||||
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||
for input_coro in chunks(input_coroutines, 100):
|
||||
async def gather_stuff():
|
||||
return await asyncio.gather(*input_coro, return_exceptions=True)
|
||||
|
||||
results = self.loop.run_until_complete(gather_stuff())
|
||||
|
||||
# handle caching
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||
continue
|
||||
# Deconstruct tuple (has 3 elements)
|
||||
pair, timeframe, ticks = res
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if ticks:
|
||||
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
ohlcv_df = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
results_df[(pair, timeframe)] = ohlcv_df
|
||||
if cache:
|
||||
self._klines[(pair, timeframe)] = ohlcv_df
|
||||
|
||||
# Return cached klines
|
||||
for pair, timeframe in cached_pairs:
|
||||
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)
|
||||
@@ -1524,7 +1663,7 @@ class Exchange:
|
||||
if not self.exchange_has("fetchTrades"):
|
||||
raise OperationalException("This exchange does not support downloading Trades.")
|
||||
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
return self.loop.run_until_complete(
|
||||
self._async_get_trade_history(pair=pair, since=since,
|
||||
until=until, from_id=from_id))
|
||||
|
||||
@@ -1534,7 +1673,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
||||
|
||||
|
||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
return exchange_name in ['bittrex', 'binance', 'kraken']
|
||||
return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx']
|
||||
|
||||
|
||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
|
@@ -19,6 +19,7 @@ class Ftx(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"ohlcv_candle_limit": 1500,
|
||||
"ohlcv_volume_currency": "quote",
|
||||
}
|
||||
|
||||
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
|
||||
@@ -55,7 +56,7 @@ class Ftx(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
@@ -105,15 +106,18 @@ class Ftx(Exchange):
|
||||
if order[0].get('status') == 'closed':
|
||||
# Trigger order was triggered ...
|
||||
real_order_id = order[0].get('info', {}).get('orderId')
|
||||
# OrderId may be None for stoploss-market orders
|
||||
# But contains "average" in these cases.
|
||||
if real_order_id:
|
||||
order1 = self._api.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order1)
|
||||
# Fake type to stop - as this was really a stop order.
|
||||
order1['id_stop'] = order1['id']
|
||||
order1['id'] = order_id
|
||||
order1['type'] = 'stop'
|
||||
order1['status_stop'] = 'triggered'
|
||||
return order1
|
||||
|
||||
order1 = self._api.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order1)
|
||||
# Fake type to stop - as this was really a stop order.
|
||||
order1['id_stop'] = order1['id']
|
||||
order1['id'] = order_id
|
||||
order1['type'] = 'stop'
|
||||
order1['status_stop'] = 'triggered'
|
||||
return order1
|
||||
return order[0]
|
||||
else:
|
||||
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
@@ -20,6 +21,35 @@ class Gateio(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"ohlcv_volume_currency": "quote",
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
}
|
||||
|
||||
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
|
||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||
super().validate_ordertypes(order_types)
|
||||
|
||||
if any(v == 'market' for k, v in order_types.items()):
|
||||
raise OperationalException(
|
||||
f'Exchange {self.name} does not support market orders.')
|
||||
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
return self.fetch_order(
|
||||
order_id=order_id,
|
||||
pair=pair,
|
||||
params={'stop': True}
|
||||
)
|
||||
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
|
||||
return self.cancel_order(
|
||||
order_id=order_id,
|
||||
pair=pair,
|
||||
params={'stop': True}
|
||||
)
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return stop_loss > float(order['stopPrice'])
|
||||
|
39
freqtrade/exchange/huobi.py
Normal file
39
freqtrade/exchange/huobi.py
Normal file
@@ -0,0 +1,39 @@
|
||||
""" Huobi exchange subclass """
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Huobi(Exchange):
|
||||
"""
|
||||
Huobi exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "stop-limit"},
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"l2_limit_range": [5, 10, 20],
|
||||
"l2_limit_range_required": False,
|
||||
}
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
|
||||
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
|
||||
params = self._params.copy()
|
||||
params.update({
|
||||
"stopPrice": stop_price,
|
||||
"operator": "lte",
|
||||
})
|
||||
return params
|
@@ -1,6 +1,6 @@
|
||||
""" Kraken exchange subclass """
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import ccxt
|
||||
|
||||
@@ -33,6 +33,12 @@ class Kraken(Exchange):
|
||||
return (parent_check and
|
||||
market.get('darkpool', False) is False)
|
||||
|
||||
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
|
||||
# Only fetch tickers for current stake currency
|
||||
# Otherwise the request for kraken becomes too large.
|
||||
symbols = list(self.get_markets(quote_currencies=[self._config['stake_currency']]))
|
||||
return super().get_tickers(symbols=symbols, cached=cached)
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> dict:
|
||||
if self._config['dry_run']:
|
||||
@@ -80,6 +86,8 @@ class Kraken(Exchange):
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
Stoploss market orders is the only stoploss type supported by kraken.
|
||||
TODO: investigate if this can be combined with generic implementation
|
||||
(careful, prices are reversed)
|
||||
"""
|
||||
params = self._params.copy()
|
||||
|
||||
@@ -95,7 +103,7 @@ class Kraken(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
""" Kucoin exchange subclass """
|
||||
"""Kucoin exchange subclass."""
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
@@ -9,9 +9,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Kucoin(Exchange):
|
||||
"""
|
||||
Kucoin exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
"""Kucoin exchange class.
|
||||
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
@@ -19,8 +19,26 @@ class Kucoin(Exchange):
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"l2_limit_range": [20, 100],
|
||||
"l2_limit_range_required": False,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
}
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
|
||||
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
|
||||
params = self._params.copy()
|
||||
params.update({
|
||||
'stopPrice': stop_price,
|
||||
'stop': 'loss'
|
||||
})
|
||||
return params
|
||||
|
18
freqtrade/exchange/okx.py
Normal file
18
freqtrade/exchange/okx.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Okx(Exchange):
|
||||
"""Okx exchange class.
|
||||
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 300,
|
||||
}
|
@@ -7,16 +7,14 @@ import traceback
|
||||
from datetime import datetime, timezone
|
||||
from math import isclose
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.data.converter import order_book_to_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.enums import RPCMessageType, SellType, State
|
||||
from freqtrade.enums import RPCMessageType, RunMode, SellType, State
|
||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, PricingError)
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
@@ -102,6 +100,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
self._exit_lock = Lock()
|
||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||
|
||||
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
def notify_status(self, msg: str) -> None:
|
||||
"""
|
||||
Public method for users of this class (worker, etc.) to send notifications
|
||||
@@ -126,6 +126,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
self.rpc.cleanup()
|
||||
cleanup_db()
|
||||
self.exchange.close()
|
||||
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
@@ -139,7 +140,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# Only update open orders on startup
|
||||
# This will update the database after the initial migration
|
||||
self.update_open_orders()
|
||||
self.startup_update_open_orders()
|
||||
|
||||
def process(self) -> None:
|
||||
"""
|
||||
@@ -178,11 +179,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
# First process current opened trades (positions)
|
||||
self.exit_positions(trades)
|
||||
|
||||
# Check if we need to adjust our current positions before attempting to buy new trades.
|
||||
if self.strategy.position_adjustment_enable:
|
||||
with self._exit_lock:
|
||||
self.process_open_trade_positions()
|
||||
|
||||
# Then looking for buy opportunities
|
||||
if self.get_free_open_trades():
|
||||
self.enter_positions()
|
||||
|
||||
Trade.commit()
|
||||
self.last_process = datetime.now(timezone.utc)
|
||||
|
||||
def process_stopped(self) -> None:
|
||||
"""
|
||||
@@ -193,19 +200,20 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def check_for_open_trades(self):
|
||||
"""
|
||||
Notify the user when the bot is stopped
|
||||
Notify the user when the bot is stopped (not reloaded)
|
||||
and there are still open trades active.
|
||||
"""
|
||||
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
|
||||
|
||||
if len(open_trades) != 0:
|
||||
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
|
||||
msg = {
|
||||
'type': RPCMessageType.WARNING,
|
||||
'status': f"{len(open_trades)} open trades active.\n\n"
|
||||
f"Handle these trades manually on {self.exchange.name}, "
|
||||
f"or '/start' the bot again and use '/stopbuy' "
|
||||
f"to handle open trades gracefully. \n"
|
||||
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
|
||||
'status':
|
||||
f"{len(open_trades)} open trades active.\n\n"
|
||||
f"Handle these trades manually on {self.exchange.name}, "
|
||||
f"or '/start' the bot again and use '/stopbuy' "
|
||||
f"to handle open trades gracefully. \n"
|
||||
f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}",
|
||||
}
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
@@ -237,7 +245,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
open_trades = len(Trade.get_open_trades())
|
||||
return max(0, self.config['max_open_trades'] - open_trades)
|
||||
|
||||
def update_open_orders(self):
|
||||
def startup_update_open_orders(self):
|
||||
"""
|
||||
Updates open orders based on order list kept in the database.
|
||||
Mainly updates the state of orders - but may also close trades
|
||||
@@ -277,39 +285,19 @@ class FreqtradeBot(LoggingMixin):
|
||||
if order:
|
||||
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.")
|
||||
self.update_trade_state(trade, order.order_id,
|
||||
stoploss_order=order.ft_order_side == 'stoploss')
|
||||
stoploss_order=order.ft_order_side == 'stoploss',
|
||||
send_msg=False)
|
||||
|
||||
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
||||
for trade in trades:
|
||||
if trade.is_open and not trade.fee_updated('buy'):
|
||||
order = trade.select_order('buy', False)
|
||||
if order:
|
||||
open_order = trade.select_order('buy', True)
|
||||
if order and open_order is None:
|
||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
||||
self.update_trade_state(trade, order.order_id)
|
||||
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||
|
||||
def handle_insufficient_funds(self, trade: Trade):
|
||||
"""
|
||||
Determine if we ever opened a sell order for this trade.
|
||||
If not, try update buy fees - otherwise "refind" the open order we obviously lost.
|
||||
"""
|
||||
sell_order = trade.select_order('sell', None)
|
||||
if sell_order:
|
||||
self.refind_lost_order(trade)
|
||||
else:
|
||||
self.reupdate_enter_order_fees(trade)
|
||||
|
||||
def reupdate_enter_order_fees(self, trade: Trade):
|
||||
"""
|
||||
Get buy order from database, and try to reupdate.
|
||||
Handles trades where the initial fee-update did not work.
|
||||
"""
|
||||
logger.info(f"Trying to reupdate buy fees for {trade}")
|
||||
order = trade.select_order('buy', False)
|
||||
if order:
|
||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
||||
self.update_trade_state(trade, order.order_id)
|
||||
|
||||
def refind_lost_order(self, trade):
|
||||
"""
|
||||
Try refinding a lost trade.
|
||||
Only used when InsufficientFunds appears on sell orders (stoploss or sell).
|
||||
@@ -322,9 +310,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
if not order.ft_is_open:
|
||||
logger.debug(f"Order {order} is no longer open.")
|
||||
continue
|
||||
if order.ft_order_side == 'buy':
|
||||
# Skip buy side - this is handled by reupdate_buy_order_fees
|
||||
continue
|
||||
try:
|
||||
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
|
||||
order.ft_order_side == 'stoploss')
|
||||
@@ -336,6 +321,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
if fo and fo['status'] == 'open':
|
||||
# Assume this as the open order
|
||||
trade.open_order_id = order.order_id
|
||||
elif order.ft_order_side == 'buy':
|
||||
if fo and fo['status'] == 'open':
|
||||
trade.open_order_id = order.order_id
|
||||
if fo:
|
||||
logger.info(f"Found {order} for trade {trade}.")
|
||||
self.update_trade_state(trade, order.order_id, fo,
|
||||
@@ -420,7 +408,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
return False
|
||||
|
||||
# running get_signal on historical data fetched
|
||||
(buy, sell, buy_tag) = self.strategy.get_signal(
|
||||
(buy, sell, buy_tag, _) = self.strategy.get_signal(
|
||||
pair,
|
||||
self.strategy.timeframe,
|
||||
analyzed_df
|
||||
@@ -441,6 +429,59 @@ class FreqtradeBot(LoggingMixin):
|
||||
else:
|
||||
return False
|
||||
|
||||
#
|
||||
# BUY / increase positions / DCA logic and methods
|
||||
#
|
||||
def process_open_trade_positions(self):
|
||||
"""
|
||||
Tries to execute additional buy or sell orders for open trades (positions)
|
||||
"""
|
||||
# Walk through each pair and check if it needs changes
|
||||
for trade in Trade.get_open_trades():
|
||||
# If there is any open orders, wait for them to finish.
|
||||
if trade.open_order_id is None:
|
||||
try:
|
||||
self.check_and_call_adjust_trade_position(trade)
|
||||
except DependencyException as exception:
|
||||
logger.warning(
|
||||
f"Unable to adjust position of trade for {trade.pair}: {exception}")
|
||||
|
||||
def check_and_call_adjust_trade_position(self, trade: Trade):
|
||||
"""
|
||||
Check the implemented trading strategy for adjustment command.
|
||||
If the strategy triggers the adjustment, a new order gets issued.
|
||||
Once that completes, the existing trade is modified to match new data.
|
||||
"""
|
||||
if self.strategy.max_entry_position_adjustment > -1:
|
||||
count_of_buys = trade.nr_of_successful_buys
|
||||
if count_of_buys > self.strategy.max_entry_position_adjustment:
|
||||
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
|
||||
return
|
||||
else:
|
||||
logger.debug("Max adjustment entries is set to unlimited.")
|
||||
current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy")
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair,
|
||||
current_rate,
|
||||
self.strategy.stoploss)
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
|
||||
default_retval=None)(
|
||||
trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_rate,
|
||||
current_profit=current_profit, min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||
|
||||
if stake_amount is not None and stake_amount > 0.0:
|
||||
# We should increase our position
|
||||
self.execute_entry(trade.pair, stake_amount, trade=trade)
|
||||
|
||||
if stake_amount is not None and stake_amount < 0.0:
|
||||
# We should decrease our position
|
||||
# TODO: Selling part of the trade not implemented yet.
|
||||
logger.error(f"Unable to decrease trade position / sell partially"
|
||||
f" for pair {trade.pair}, feature not implemented.")
|
||||
|
||||
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
|
||||
"""
|
||||
Checks depth of market before executing a buy
|
||||
@@ -465,67 +506,49 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||
return False
|
||||
|
||||
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
||||
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
|
||||
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *,
|
||||
ordertype: Optional[str] = None, buy_tag: Optional[str] = None,
|
||||
trade: Optional[Trade] = None) -> bool:
|
||||
"""
|
||||
Executes a limit buy for the given pair
|
||||
:param pair: pair for which we want to create a LIMIT_BUY
|
||||
:param stake_amount: amount of stake-currency for the pair
|
||||
:return: True if a buy order is created, false if it fails.
|
||||
"""
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
|
||||
if price:
|
||||
enter_limit_requested = price
|
||||
else:
|
||||
# Calculate price
|
||||
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=proposed_enter_rate)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
proposed_rate=proposed_enter_rate)
|
||||
pos_adjust = trade is not None
|
||||
|
||||
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||
|
||||
if not enter_limit_requested:
|
||||
raise PricingError('Could not determine buy price.')
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested,
|
||||
self.strategy.stoploss)
|
||||
|
||||
if not self.edge:
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake(
|
||||
pair, price, stake_amount, buy_tag, trade)
|
||||
|
||||
if not stake_amount:
|
||||
return False
|
||||
|
||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||
f"{stake_amount} ...")
|
||||
if pos_adjust:
|
||||
logger.info(f"Position adjust: about to create a new order for {pair} with stake: "
|
||||
f"{stake_amount} for {trade}")
|
||||
else:
|
||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||
f"{stake_amount} ...")
|
||||
|
||||
amount = stake_amount / enter_limit_requested
|
||||
order_type = self.strategy.order_types['buy']
|
||||
if forcebuy:
|
||||
# Forcebuy can define a different ordertype
|
||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
||||
order_type = ordertype or self.strategy.order_types['buy']
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
if not pos_adjust and not strategy_safe_wrapper(
|
||||
self.strategy.confirm_trade_entry, default_retval=True)(
|
||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
|
||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
|
||||
entry_tag=buy_tag):
|
||||
logger.info(f"User requested abortion of buying {pair}")
|
||||
return False
|
||||
amount = self.exchange.amount_to_precision(pair, amount)
|
||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||
amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force)
|
||||
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
||||
order_id = order['id']
|
||||
order_status = order.get('status', None)
|
||||
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
|
||||
|
||||
# we assume the order is executed at the price requested
|
||||
enter_limit_filled_price = enter_limit_requested
|
||||
@@ -561,57 +584,125 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# 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(
|
||||
pair=pair,
|
||||
stake_amount=stake_amount,
|
||||
amount=amount,
|
||||
is_open=True,
|
||||
amount_requested=amount_requested,
|
||||
fee_open=fee,
|
||||
fee_close=fee,
|
||||
open_rate=enter_limit_filled_price,
|
||||
open_rate_requested=enter_limit_requested,
|
||||
open_date=datetime.utcnow(),
|
||||
exchange=self.exchange.id,
|
||||
open_order_id=order_id,
|
||||
strategy=self.strategy.get_strategy_name(),
|
||||
buy_tag=buy_tag,
|
||||
timeframe=timeframe_to_minutes(self.config['timeframe'])
|
||||
)
|
||||
# This is a new trade
|
||||
if trade is None:
|
||||
trade = Trade(
|
||||
pair=pair,
|
||||
stake_amount=stake_amount,
|
||||
amount=amount,
|
||||
is_open=True,
|
||||
amount_requested=amount_requested,
|
||||
fee_open=fee,
|
||||
fee_close=fee,
|
||||
open_rate=enter_limit_filled_price,
|
||||
open_rate_requested=enter_limit_requested,
|
||||
open_date=datetime.utcnow(),
|
||||
exchange=self.exchange.id,
|
||||
open_order_id=order_id,
|
||||
fee_open_currency=None,
|
||||
strategy=self.strategy.get_strategy_name(),
|
||||
buy_tag=buy_tag,
|
||||
timeframe=timeframe_to_minutes(self.config['timeframe'])
|
||||
)
|
||||
else:
|
||||
# This is additional buy, we reset fee_open_currency so timeout checking can work
|
||||
trade.is_open = True
|
||||
trade.fee_open_currency = None
|
||||
trade.open_rate_requested = enter_limit_requested
|
||||
trade.open_order_id = order_id
|
||||
|
||||
trade.orders.append(order_obj)
|
||||
|
||||
# Update fees if order is closed
|
||||
if order_status == 'closed':
|
||||
self.update_trade_state(trade, order_id, order)
|
||||
|
||||
trade.recalc_trade_from_orders()
|
||||
Trade.query.session.add(trade)
|
||||
Trade.commit()
|
||||
|
||||
# Updating wallets
|
||||
self.wallets.update()
|
||||
|
||||
self._notify_enter(trade, order_type)
|
||||
self._notify_enter(trade, order, order_type)
|
||||
|
||||
if pos_adjust:
|
||||
if order_status == 'closed':
|
||||
logger.info(f"DCA order closed, trade should be up to date: {trade}")
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
else:
|
||||
logger.info(f"DCA order {order_status}, will wait for resolution: {trade}")
|
||||
|
||||
# Update fees if order is closed
|
||||
if order_status == 'closed':
|
||||
self.update_trade_state(trade, order_id, order)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_enter(self, trade: Trade, order_type: str) -> None:
|
||||
def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
|
||||
# First cancelling stoploss on exchange ...
|
||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||
try:
|
||||
logger.info(f"Canceling stoploss on exchange for {trade}")
|
||||
co = self.exchange.cancel_stoploss_order_with_result(
|
||||
trade.stoploss_order_id, trade.pair, trade.amount)
|
||||
trade.update_order(co)
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||
return trade
|
||||
|
||||
def get_valid_enter_price_and_stake(
|
||||
self, pair: str, price: Optional[float], stake_amount: float,
|
||||
entry_tag: Optional[str],
|
||||
trade: Optional[Trade]) -> Tuple[float, float]:
|
||||
if price:
|
||||
enter_limit_requested = price
|
||||
else:
|
||||
# Calculate price
|
||||
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||
default_retval=proposed_enter_rate)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
proposed_rate=proposed_enter_rate, entry_tag=entry_tag)
|
||||
|
||||
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||
if not enter_limit_requested:
|
||||
raise PricingError('Could not determine buy price.')
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested,
|
||||
self.strategy.stoploss)
|
||||
if not self.edge and trade is None:
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||
min_stake=min_stake_amount, max_stake=max_stake_amount, entry_tag=entry_tag)
|
||||
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
return enter_limit_requested, stake_amount
|
||||
|
||||
def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None,
|
||||
fill: bool = False) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy occurred.
|
||||
"""
|
||||
open_rate = safe_value_fallback(order, 'average', 'price')
|
||||
if open_rate is None:
|
||||
open_rate = trade.open_rate
|
||||
|
||||
current_rate = trade.open_rate_requested
|
||||
if self.dataprovider.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy")
|
||||
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY,
|
||||
'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'limit': trade.open_rate,
|
||||
'limit': open_rate, # Deprecated (?)
|
||||
'open_rate': open_rate,
|
||||
'order_type': order_type,
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': trade.amount,
|
||||
'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount,
|
||||
'open_date': trade.open_date or datetime.utcnow(),
|
||||
'current_rate': trade.open_rate_requested,
|
||||
'current_rate': current_rate,
|
||||
}
|
||||
|
||||
# Send the message
|
||||
@@ -643,22 +734,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_FILL,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'open_rate': trade.open_rate,
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': trade.amount,
|
||||
'open_date': trade.open_date,
|
||||
}
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
#
|
||||
# SELL / exit positions / close trades logic and methods
|
||||
#
|
||||
@@ -681,7 +756,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trades_closed += 1
|
||||
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to sell trade %s: %s', trade.pair, exception)
|
||||
logger.warning(f'Unable to sell trade {trade.pair}: {exception}')
|
||||
|
||||
# Updating wallets if any trade occurred
|
||||
if trades_closed:
|
||||
@@ -700,21 +775,22 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.debug('Handling %s ...', trade)
|
||||
|
||||
(buy, sell) = (False, False)
|
||||
exit_tag = None
|
||||
|
||||
if (self.config.get('use_sell_signal', True) or
|
||||
self.config.get('ignore_roi_if_buy_signal', False)):
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||
self.strategy.timeframe)
|
||||
|
||||
(buy, sell, _) = self.strategy.get_signal(
|
||||
(buy, sell, _, exit_tag) = self.strategy.get_signal(
|
||||
trade.pair,
|
||||
self.strategy.timeframe,
|
||||
analyzed_df
|
||||
)
|
||||
|
||||
logger.debug('checking sell')
|
||||
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||
if self._check_and_execute_exit(trade, exit_rate, buy, sell):
|
||||
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||
if self._check_and_execute_exit(trade, sell_rate, buy, sell, exit_tag):
|
||||
return True
|
||||
|
||||
logger.debug('Found no sell signal for %s.', trade)
|
||||
@@ -797,11 +873,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
|
||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
|
||||
# The above will return False if the placement failed and the trade was force-sold.
|
||||
# in which case the trade will be closed - which we must check below.
|
||||
trade.stoploss_last_update = datetime.utcnow()
|
||||
return False
|
||||
|
||||
# If stoploss order is canceled for some reason we add it
|
||||
if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'):
|
||||
if (trade.is_open
|
||||
and stoploss_order
|
||||
and stoploss_order['status'] in ('canceled', 'cancelled')):
|
||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
|
||||
return False
|
||||
else:
|
||||
@@ -811,7 +891,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||
# Triggered Orders are now real orders - so don't replace stoploss anymore
|
||||
if (
|
||||
stoploss_order
|
||||
trade.is_open and stoploss_order
|
||||
and stoploss_order.get('status_stop') != 'triggered'
|
||||
and (self.config.get('trailing_stop', False)
|
||||
or self.config.get('use_custom_stoploss', False))
|
||||
@@ -823,7 +903,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return False
|
||||
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
|
||||
"""
|
||||
Check to see if stoploss on exchange should be updated
|
||||
in case of trailing stoploss on exchange
|
||||
@@ -831,7 +911,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
:param order: Current on exchange stoploss order
|
||||
:return: None
|
||||
"""
|
||||
if self.exchange.stoploss_adjust(trade.stop_loss, order):
|
||||
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss)
|
||||
|
||||
if self.exchange.stoploss_adjust(stoploss_norm, order):
|
||||
# we check if the update is necessary
|
||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||
@@ -852,35 +934,24 @@ class FreqtradeBot(LoggingMixin):
|
||||
f"for pair {trade.pair}.")
|
||||
|
||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||
buy: bool, sell: bool) -> bool:
|
||||
buy: bool, sell: bool, exit_tag: Optional[str]) -> bool:
|
||||
"""
|
||||
Check and execute exit
|
||||
"""
|
||||
|
||||
should_sell = self.strategy.should_sell(
|
||||
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
|
||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||
)
|
||||
|
||||
if should_sell.sell_flag:
|
||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||
self.execute_trade_exit(trade, exit_rate, should_sell)
|
||||
logger.info(
|
||||
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
|
||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||
self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag=exit_tag)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _check_timed_out(self, side: str, order: dict) -> bool:
|
||||
"""
|
||||
Check if timeout is active, and if the order is still open and timed out
|
||||
"""
|
||||
timeout = self.config.get('unfilledtimeout', {}).get(side)
|
||||
ordertime = arrow.get(order['datetime']).datetime
|
||||
if timeout is not None:
|
||||
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
|
||||
timeout_kwargs = {timeout_unit: -timeout}
|
||||
timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime
|
||||
return (order['status'] == 'open' and order['side'] == side
|
||||
and ordertime < timeout_threshold)
|
||||
return False
|
||||
|
||||
def check_handle_timedout(self) -> None:
|
||||
"""
|
||||
Check if any orders are timed out and cancel if necessary
|
||||
@@ -899,23 +970,32 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
||||
|
||||
order_obj = trade.select_order_by_order_id(trade.open_order_id)
|
||||
|
||||
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
or self._check_timed_out('buy', order)
|
||||
or strategy_safe_wrapper(self.strategy.check_buy_timeout,
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order))):
|
||||
or (order_obj and self.strategy.ft_check_timed_out(
|
||||
'buy', trade, order_obj, datetime.now(timezone.utc))
|
||||
))):
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
|
||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||
fully_cancelled
|
||||
or self._check_timed_out('sell', order)
|
||||
or strategy_safe_wrapper(self.strategy.check_sell_timeout,
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
order=order))):
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
or (order_obj and self.strategy.ft_check_timed_out(
|
||||
'sell', trade, order_obj, datetime.now(timezone.utc))
|
||||
))):
|
||||
canceled = self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
canceled_count = trade.get_exit_order_count()
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
||||
f'timed out {max_timeouts} times.')
|
||||
try:
|
||||
self.execute_trade_exit(
|
||||
trade, order.get('price'),
|
||||
sell_reason=SellCheckTuple(sell_type=SellType.EMERGENCY_SELL))
|
||||
except DependencyException as exception:
|
||||
logger.warning(f'Unable to emergency sell trade {trade.pair}: {exception}')
|
||||
|
||||
def cancel_all_open_orders(self) -> None:
|
||||
"""
|
||||
@@ -946,12 +1026,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
filled_val = order.get('filled', 0.0) or 0.0
|
||||
filled_val: float = order.get('filled', 0.0) or 0.0
|
||||
filled_stake = filled_val * trade.open_rate
|
||||
minstake = self.exchange.get_min_pair_stake_amount(
|
||||
trade.pair, trade.open_rate, self.strategy.stoploss)
|
||||
|
||||
if filled_val > 0 and filled_stake < minstake:
|
||||
if filled_val > 0 and minstake and filled_stake < minstake:
|
||||
logger.warning(
|
||||
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
|
||||
f"as the filled amount of {filled_val} would result in an unsellable trade.")
|
||||
@@ -975,10 +1055,16 @@ class FreqtradeBot(LoggingMixin):
|
||||
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
|
||||
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
|
||||
logger.info('Buy order fully cancelled. Removing %s from database.', trade)
|
||||
# if trade is not partially completed, just delete the trade
|
||||
trade.delete()
|
||||
was_trade_fully_canceled = True
|
||||
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
||||
# if trade is not partially completed and it's the only order, just delete the trade
|
||||
if len(trade.orders) <= 1:
|
||||
trade.delete()
|
||||
was_trade_fully_canceled = True
|
||||
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
||||
else:
|
||||
# FIXME TODO: This could possibly reworked to not duplicate the code 15 lines below.
|
||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||
trade.open_order_id = None
|
||||
logger.info('Partial buy order timeout for %s.', trade)
|
||||
else:
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
# and close the order
|
||||
@@ -998,11 +1084,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason=reason)
|
||||
return was_trade_fully_canceled
|
||||
|
||||
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
|
||||
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||
"""
|
||||
Sell cancel - cancel order and update trade
|
||||
:return: Reason for cancel
|
||||
:return: True if exit order was cancelled, false otherwise
|
||||
"""
|
||||
cancelled = False
|
||||
# if trade is not partially completed, just cancel the order
|
||||
if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
|
||||
if not self.exchange.check_order_canceled_empty(order):
|
||||
@@ -1013,7 +1100,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.update_order(co)
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel sell order {trade.open_order_id}")
|
||||
return 'error cancelling order'
|
||||
return False
|
||||
logger.info('Sell order %s for %s.', reason, trade)
|
||||
else:
|
||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
@@ -1027,9 +1114,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.sell_reason = None
|
||||
cancelled = True
|
||||
else:
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
cancelled = False
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_exit_cancel(
|
||||
@@ -1037,7 +1127,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
order_type=self.strategy.order_types['sell'],
|
||||
reason=reason
|
||||
)
|
||||
return reason
|
||||
return cancelled
|
||||
|
||||
def _safe_exit_amount(self, pair: str, amount: float) -> float:
|
||||
"""
|
||||
@@ -1064,7 +1154,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
raise DependencyException(
|
||||
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||
|
||||
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
||||
def execute_trade_exit(
|
||||
self,
|
||||
trade: Trade,
|
||||
limit: float,
|
||||
sell_reason: SellCheckTuple,
|
||||
*,
|
||||
exit_tag: Optional[str] = None,
|
||||
ordertype: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Executes a trade exit for the given trade and limit
|
||||
:param trade: Trade instance
|
||||
@@ -1078,8 +1176,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# if stoploss is on exchange and we are on dry_run mode,
|
||||
# we consider the sell price stop price
|
||||
if self.config['dry_run'] and sell_type == 'stoploss' \
|
||||
and self.strategy.order_types['stoploss_on_exchange']:
|
||||
if (self.config['dry_run'] and sell_type == 'stoploss'
|
||||
and self.strategy.order_types['stoploss_on_exchange']):
|
||||
limit = trade.stop_loss
|
||||
|
||||
# set custom_exit_price if available
|
||||
@@ -1094,22 +1192,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
||||
|
||||
# First cancelling stoploss on exchange ...
|
||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||
try:
|
||||
co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id,
|
||||
trade.pair, trade.amount)
|
||||
trade.update_order(co)
|
||||
except InvalidOrderException:
|
||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
|
||||
order_type = self.strategy.order_types[sell_type]
|
||||
order_type = ordertype or self.strategy.order_types[sell_type]
|
||||
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
||||
# Emergency sells (default to market!)
|
||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||
if sell_reason.sell_type == SellType.FORCE_SELL:
|
||||
# Force sells (default to the sell_type defined in the strategy,
|
||||
# but we allow this value to be changed)
|
||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||
|
||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
@@ -1140,17 +1228,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.open_order_id = order['id']
|
||||
trade.sell_order_status = ''
|
||||
trade.close_rate_requested = limit
|
||||
trade.sell_reason = sell_reason.sell_reason
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
Trade.commit()
|
||||
trade.sell_reason = exit_tag or sell_reason.sell_reason
|
||||
|
||||
# Lock pair for one candle to prevent immediate re-buys
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
|
||||
self._notify_exit(trade, order_type)
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
Trade.commit()
|
||||
|
||||
return True
|
||||
|
||||
@@ -1181,6 +1269,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
@@ -1224,6 +1313,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_ratio': profit_ratio,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||
@@ -1245,13 +1335,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
#
|
||||
|
||||
def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
|
||||
stoploss_order: bool = False) -> bool:
|
||||
stoploss_order: bool = False, send_msg: bool = True) -> bool:
|
||||
"""
|
||||
Checks trades with open orders and updates the amount if necessary
|
||||
Handles closing both buy and sell orders.
|
||||
:param trade: Trade object of the trade we're analyzing
|
||||
:param order_id: Order-id of the order we're analyzing
|
||||
:param action_order: Already acquired order object
|
||||
:param send_msg: Send notification - should always be True except in "recovery" methods
|
||||
:return: True if order has been cancelled without being filled partially, False otherwise
|
||||
"""
|
||||
if not order_id:
|
||||
@@ -1259,7 +1350,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
return False
|
||||
|
||||
# Update trade with order values
|
||||
logger.info('Found open order for %s', trade)
|
||||
logger.info(f'Found open order for {trade}')
|
||||
try:
|
||||
order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id,
|
||||
trade.pair,
|
||||
@@ -1270,33 +1361,36 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
trade.update_order(order)
|
||||
|
||||
# Try update amount (binance-fix)
|
||||
try:
|
||||
new_amount = self.get_real_amount(trade, order)
|
||||
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
|
||||
abs_tol=constants.MATH_CLOSE_PREC):
|
||||
order['amount'] = new_amount
|
||||
order.pop('filled', None)
|
||||
trade.recalc_open_trade_value()
|
||||
except DependencyException as exception:
|
||||
logger.warning("Could not update trade amount: %s", exception)
|
||||
|
||||
if self.exchange.check_order_canceled_empty(order):
|
||||
# Trade has been cancelled on exchange
|
||||
# Handling of this will happen in check_handle_timeout.
|
||||
# Handling of this will happen in check_handle_timedout.
|
||||
return True
|
||||
trade.update(order)
|
||||
|
||||
order_obj = trade.select_order_by_order_id(order_id)
|
||||
if not order_obj:
|
||||
raise DependencyException(
|
||||
f"Order_obj not found for {order_id}. This should not have happened.")
|
||||
self.handle_order_fee(trade, order_obj, order)
|
||||
|
||||
trade.update_trade(order_obj)
|
||||
# TODO: is the below necessary? it's already done in update_trade for filled buys
|
||||
trade.recalc_trade_from_orders()
|
||||
Trade.commit()
|
||||
|
||||
# Updating wallets when order is closed
|
||||
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
# If a buy order was closed, force update on stoploss on exchange
|
||||
if order.get('side', None) == 'buy':
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
# Updating wallets when order is closed
|
||||
self.wallets.update()
|
||||
|
||||
if not trade.is_open:
|
||||
if not stoploss_order and not trade.open_order_id:
|
||||
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||
self._notify_exit(trade, '', True)
|
||||
self.handle_protections(trade.pair)
|
||||
self.wallets.update()
|
||||
elif not trade.open_order_id:
|
||||
elif send_msg and not trade.open_order_id:
|
||||
# Buy fill
|
||||
self._notify_enter_fill(trade)
|
||||
self._notify_enter(trade, order, fill=True)
|
||||
|
||||
return False
|
||||
|
||||
@@ -1331,7 +1425,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
return real_amount
|
||||
return amount
|
||||
|
||||
def get_real_amount(self, trade: Trade, order: Dict) -> float:
|
||||
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
|
||||
# Try update amount (binance-fix)
|
||||
try:
|
||||
new_amount = self.get_real_amount(trade, order, order_obj)
|
||||
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
|
||||
abs_tol=constants.MATH_CLOSE_PREC):
|
||||
order_obj.ft_fee_base = trade.amount - new_amount
|
||||
except DependencyException as exception:
|
||||
logger.warning("Could not update trade amount: %s", exception)
|
||||
|
||||
def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> float:
|
||||
"""
|
||||
Detect and update trade fee.
|
||||
Calls trade.update_fee() upon correct detection.
|
||||
@@ -1349,7 +1453,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# use fee from order-dict if possible
|
||||
if self.exchange.order_has_fee(order):
|
||||
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
|
||||
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
|
||||
logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
|
||||
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
|
||||
if fee_rate is None or fee_rate < 0.02:
|
||||
# Reject all fees that report as > 2%.
|
||||
@@ -1361,14 +1465,18 @@ class FreqtradeBot(LoggingMixin):
|
||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
||||
amount=order_amount, fee_abs=fee_cost)
|
||||
return order_amount
|
||||
return self.fee_detection_from_trades(trade, order, order_amount)
|
||||
return self.fee_detection_from_trades(
|
||||
trade, order, order_obj, order_amount, order.get('trades', []))
|
||||
|
||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
|
||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order,
|
||||
order_amount: float, trades: List) -> float:
|
||||
"""
|
||||
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
|
||||
fee-detection fallback to Trades.
|
||||
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
|
||||
"""
|
||||
trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),
|
||||
trade.pair, trade.open_date)
|
||||
if not trades:
|
||||
trades = self.exchange.get_trades_for_order(
|
||||
self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date)
|
||||
|
||||
if len(trades) == 0:
|
||||
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
||||
|
@@ -7,11 +7,25 @@ from typing import Any, Dict
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
class FTBufferingHandler(BufferingHandler):
|
||||
def flush(self):
|
||||
"""
|
||||
Override Flush behaviour - we keep half of the configured capacity
|
||||
otherwise, we have moments with "empty" logs.
|
||||
"""
|
||||
self.acquire()
|
||||
try:
|
||||
# Keep half of the records in buffer.
|
||||
self.buffer = self.buffer[-int(self.capacity / 2):]
|
||||
finally:
|
||||
self.release()
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
|
||||
# Initialize bufferhandler - will be used for /log endpoints
|
||||
bufferHandler = BufferingHandler(1000)
|
||||
bufferHandler = FTBufferingHandler(1000)
|
||||
bufferHandler.setFormatter(Formatter(LOGFORMAT))
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user