mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-12-03 18:43:04 +00:00
Compare commits
1188 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab42f84daa | ||
|
|
f83a57ae5c | ||
|
|
56d747818d | ||
|
|
64b50d13fa | ||
|
|
0ada6fbd46 | ||
|
|
2c8840b7e0 | ||
|
|
55247b4e12 | ||
|
|
f31b26b1ab | ||
|
|
2b350eb872 | ||
|
|
88d77658f8 | ||
|
|
cbbc386170 | ||
|
|
7f9226861a | ||
|
|
9ff4298475 | ||
|
|
5793216d5c | ||
|
|
cf9c9fe278 | ||
|
|
7571ae19a0 | ||
|
|
58cde3253d | ||
|
|
47262b9350 | ||
|
|
183909f474 | ||
|
|
06bb43b13e | ||
|
|
2ed8e1639f | ||
|
|
e78d5b7787 | ||
|
|
e0c6cc1563 | ||
|
|
472132c236 | ||
|
|
1262aa19ec | ||
|
|
e9e3f7ef16 | ||
|
|
d9d4c478cc | ||
|
|
db56fc158f | ||
|
|
af8d2490e2 | ||
|
|
fdc77bfd5b | ||
|
|
5572d37c7f | ||
|
|
57f2ab2f67 | ||
|
|
1cab52f555 | ||
|
|
748233ca70 | ||
|
|
f76108c5b5 | ||
|
|
09f94f7b56 | ||
|
|
a85b8d6edc | ||
|
|
52c9761e5b | ||
|
|
c2dbb9cb43 | ||
|
|
59c3743f77 | ||
|
|
98422f8476 | ||
|
|
8c6283a687 | ||
|
|
1fd492d838 | ||
|
|
fc63a713fd | ||
|
|
e97b9820a4 | ||
|
|
52f24af701 | ||
|
|
b30c1523e3 | ||
|
|
6e1c4c2efd | ||
|
|
3b542ea008 | ||
|
|
5841b56f2d | ||
|
|
85ea29c93c | ||
|
|
f7e979141b | ||
|
|
e0bdc68a9e | ||
|
|
e6cfde211b | ||
|
|
978aaa5c4b | ||
|
|
b67beaa35a | ||
|
|
19f23106e7 | ||
|
|
d8a2c59b04 | ||
|
|
dea785f562 | ||
|
|
ca42f86900 | ||
|
|
de9294d1f4 | ||
|
|
91b83519f5 | ||
|
|
3eb7f296aa | ||
|
|
f3b0e404f0 | ||
|
|
3ba3d7ffcf | ||
|
|
832fa88f08 | ||
|
|
6e9d349ceb | ||
|
|
8bd1524abc | ||
|
|
72f5633061 | ||
|
|
3b25bdc1cb | ||
|
|
3690609519 | ||
|
|
43c9d14ac2 | ||
|
|
83122fa58b | ||
|
|
f23da7ea75 | ||
|
|
7ef93e92e6 | ||
|
|
945f9f8c47 | ||
|
|
29b40d27e7 | ||
|
|
cd83ed699c | ||
|
|
02fcded368 | ||
|
|
18b0dfe28c | ||
|
|
69be62f1e6 | ||
|
|
7136c5b414 | ||
|
|
b637e7dde4 | ||
|
|
f545274a0e | ||
|
|
e8c122d5ed | ||
|
|
9457f63e01 | ||
|
|
df004a2c7a | ||
|
|
a698ee8bc8 | ||
|
|
1228ffe109 | ||
|
|
076a10887b | ||
|
|
fc3db2d29c | ||
|
|
971cdbb918 | ||
|
|
73adbe9e0c | ||
|
|
8363609637 | ||
|
|
705c3a408a | ||
|
|
228fc5ce73 | ||
|
|
09885535ac | ||
|
|
de139af5a5 | ||
|
|
d9c7790e82 | ||
|
|
aa03b7d9a0 | ||
|
|
8f9d0019e6 | ||
|
|
9e3fae1991 | ||
|
|
5dcb239253 | ||
|
|
9fcdea14d7 | ||
|
|
85dbe83154 | ||
|
|
ee48da2a0a | ||
|
|
f477389159 | ||
|
|
3bb7f78255 | ||
|
|
25aecfa9b2 | ||
|
|
f821e504c3 | ||
|
|
d66381863e | ||
|
|
68712c884e | ||
|
|
db9b31cb85 | ||
|
|
7090d4e5ba | ||
|
|
b2423fa390 | ||
|
|
83b87cb812 | ||
|
|
44dcb4cc78 | ||
|
|
64803a735d | ||
|
|
20e77bb5d9 | ||
|
|
f11e5128ad | ||
|
|
bbe13cafcb | ||
|
|
1acca87fbf | ||
|
|
2012bb7c53 | ||
|
|
14d384a30a | ||
|
|
927f5e2c8b | ||
|
|
d59bedf977 | ||
|
|
c61a71a36e | ||
|
|
46f2e693bc | ||
|
|
cbc4bb5420 | ||
|
|
49e85e0680 | ||
|
|
e8d724680a | ||
|
|
6d9ab5e9ff | ||
|
|
a6d513a0e6 | ||
|
|
d565c40f4b | ||
|
|
52b4ae5eae | ||
|
|
5c4b9e59d1 | ||
|
|
a02f63dcc3 | ||
|
|
a3a94e6ea4 | ||
|
|
c039a45def | ||
|
|
3276dd6b94 | ||
|
|
de306de78b | ||
|
|
9360c109a9 | ||
|
|
5d9af5a7da | ||
|
|
88e0fed4c4 | ||
|
|
98dcc08c6d | ||
|
|
702b50e7e9 | ||
|
|
8f946ea1b2 | ||
|
|
4e5ae0af84 | ||
|
|
ca00871362 | ||
|
|
a1d8658297 | ||
|
|
31814e8bf9 | ||
|
|
0028df2564 | ||
|
|
9b3764212f | ||
|
|
8128b85706 | ||
|
|
d7c88a38fc | ||
|
|
0b0b221c02 | ||
|
|
d9b8f46282 | ||
|
|
fbb64db3ae | ||
|
|
33d8e67a87 | ||
|
|
f05f173d23 | ||
|
|
81a622a9fa | ||
|
|
67415dd7e2 | ||
|
|
e8cd6b3be3 | ||
|
|
7e96e7af83 | ||
|
|
119b73ead2 | ||
|
|
365c454da1 | ||
|
|
b6d1f9f754 | ||
|
|
6719d9670d | ||
|
|
62234878a1 | ||
|
|
851a9a7620 | ||
|
|
61d9002cb1 | ||
|
|
84fc5dfcf7 | ||
|
|
e1b3ae208d | ||
|
|
60ec9739c2 | ||
|
|
c9fb3dfb1d | ||
|
|
8230d1b515 | ||
|
|
838b4f4e52 | ||
|
|
eae5b154c9 | ||
|
|
ebc870d2d4 | ||
|
|
986b1aadbf | ||
|
|
9ced73bf19 | ||
|
|
f0428d5c99 | ||
|
|
486a0d2c6e | ||
|
|
13cf6a1007 | ||
|
|
4c59bcc1bb | ||
|
|
057bb31d94 | ||
|
|
6258c0671b | ||
|
|
b3b45458f6 | ||
|
|
54047a98b3 | ||
|
|
6411dcbf72 | ||
|
|
1818215fee | ||
|
|
1ffceef9f0 | ||
|
|
be1426a5f6 | ||
|
|
3ebb819ba3 | ||
|
|
5f16e534ee | ||
|
|
8f8e6f16b4 | ||
|
|
b5592b88fa | ||
|
|
ba06269ed7 | ||
|
|
e450baeac4 | ||
|
|
3a81b6a08f | ||
|
|
b7a2efcd6a | ||
|
|
a605d50d69 | ||
|
|
27d894b8e9 | ||
|
|
14f1ef8e30 | ||
|
|
7b471d59c5 | ||
|
|
5ca102ffee | ||
|
|
0e04f79b6d | ||
|
|
c141b873c3 | ||
|
|
3cc97690a4 | ||
|
|
5b7f08137f | ||
|
|
00318be59f | ||
|
|
37c00d4eeb | ||
|
|
3300d25e57 | ||
|
|
f443b1426f | ||
|
|
96354ee1d8 | ||
|
|
41b725714f | ||
|
|
12560e983c | ||
|
|
44ff6d50ec | ||
|
|
29c1cca990 | ||
|
|
979f7ba78c | ||
|
|
b6dfe01628 | ||
|
|
d4cd5ba231 | ||
|
|
e7cf1e34cf | ||
|
|
c9718de24c | ||
|
|
c58b1f0fd2 | ||
|
|
b720ae5d75 | ||
|
|
3cea1a99b7 | ||
|
|
03eb079a5e | ||
|
|
899db2de78 | ||
|
|
22338d7058 | ||
|
|
db6d7714a4 | ||
|
|
2de4f425ff | ||
|
|
a71ab6e2ae | ||
|
|
0b6bf701ef | ||
|
|
38eb4eed97 | ||
|
|
bdb54f56d9 | ||
|
|
3eb2981fb4 | ||
|
|
835269825d | ||
|
|
12ab1cb2c4 | ||
|
|
07f0e3803a | ||
|
|
3d307fe8ff | ||
|
|
5cb4a65cbd | ||
|
|
84ce065f76 | ||
|
|
6398d7ff23 | ||
|
|
7e3f8ad012 | ||
|
|
8703c87880 | ||
|
|
7e010c88b9 | ||
|
|
dad921bf5b | ||
|
|
e020bafc38 | ||
|
|
ecf5e1ce6b | ||
|
|
31bcee1a13 | ||
|
|
6245127ce1 | ||
|
|
d1a4b9ad85 | ||
|
|
167e615f67 | ||
|
|
9c1b0471ca | ||
|
|
96a8a94309 | ||
|
|
d2960aae3c | ||
|
|
3f1a23615a | ||
|
|
78de0f3167 | ||
|
|
718e900a39 | ||
|
|
eff2a857c0 | ||
|
|
cf2b5cc24d | ||
|
|
7859fadf6b | ||
|
|
34588b7dc0 | ||
|
|
f6999898d5 | ||
|
|
821243ce7c | ||
|
|
44d0abb111 | ||
|
|
10fd2b291e | ||
|
|
5034c7c312 | ||
|
|
ff36ba7742 | ||
|
|
9b2fabfb3e | ||
|
|
4008eb20a3 | ||
|
|
e224ee8277 | ||
|
|
d236f97605 | ||
|
|
0efb966b68 | ||
|
|
63dfa52ea8 | ||
|
|
74507e9981 | ||
|
|
d24bef7883 | ||
|
|
59bf0c080a | ||
|
|
e8553cfac0 | ||
|
|
baadf62fc3 | ||
|
|
c4312fbb31 | ||
|
|
1bf71b47df | ||
|
|
67f26fa1ac | ||
|
|
dafabd5b99 | ||
|
|
878adfb0c0 | ||
|
|
3da9f6d0e3 | ||
|
|
f5fdd8a6ab | ||
|
|
47e4599fe2 | ||
|
|
81b432dc0e | ||
|
|
87c5668b14 | ||
|
|
84da133e95 | ||
|
|
80e254d46b | ||
|
|
7bce16f811 | ||
|
|
0d430be1fa | ||
|
|
a8516ff0f5 | ||
|
|
e99adc339e | ||
|
|
1f237db2b5 | ||
|
|
04e8c3ceea | ||
|
|
592848ad03 | ||
|
|
22318eb80e | ||
|
|
c03f8afca1 | ||
|
|
88f8e0b8b1 | ||
|
|
8f6b831512 | ||
|
|
85a2c12768 | ||
|
|
1b8fd63853 | ||
|
|
ae325ff124 | ||
|
|
1698d96883 | ||
|
|
e35f5c9fde | ||
|
|
26a59e2cc5 | ||
|
|
56df7ad9fc | ||
|
|
52ae7c3615 | ||
|
|
c9e56ab04b | ||
|
|
cfa527e9af | ||
|
|
6cb17caf91 | ||
|
|
67a0040732 | ||
|
|
3913b450b6 | ||
|
|
13d61e15f4 | ||
|
|
7d46236900 | ||
|
|
a7e54967c3 | ||
|
|
a440e4ef79 | ||
|
|
b5a52129fb | ||
|
|
8e72e0ec07 | ||
|
|
8204ecc131 | ||
|
|
beaf5215b7 | ||
|
|
b3fa997bea | ||
|
|
eab5bfb475 | ||
|
|
9f5d4dd67f | ||
|
|
fc757f6cf7 | ||
|
|
fd619de1d3 | ||
|
|
4c0341b232 | ||
|
|
a3bcc9d91e | ||
|
|
d215e3ca68 | ||
|
|
493fa8541b | ||
|
|
930b5e07f8 | ||
|
|
5174717ca9 | ||
|
|
a7681fc712 | ||
|
|
7d1976ad05 | ||
|
|
d317f33f4e | ||
|
|
9e53b0742c | ||
|
|
4c78b8c6d1 | ||
|
|
b88db55db3 | ||
|
|
b8f8d1d4b1 | ||
|
|
c4cbf6de3b | ||
|
|
e7b0e3293d | ||
|
|
87c8e85068 | ||
|
|
c9ae5e1e9f | ||
|
|
3d9514d266 | ||
|
|
3d9cbf52b9 | ||
|
|
5b4043db85 | ||
|
|
661d36614f | ||
|
|
11cef55c03 | ||
|
|
667d08d003 | ||
|
|
ba780276a2 | ||
|
|
46db0bc08c | ||
|
|
0de3c6945b | ||
|
|
0bf30aaa6b | ||
|
|
faac205464 | ||
|
|
1d5d7048d6 | ||
|
|
033643c4ca | ||
|
|
8b8b5cfac4 | ||
|
|
9ad32fd846 | ||
|
|
d9766bfd7e | ||
|
|
186f2a8392 | ||
|
|
c6a43b0d0c | ||
|
|
a1513b7c7c | ||
|
|
6571ef4e45 | ||
|
|
a3f8a4f45e | ||
|
|
336111fbd4 | ||
|
|
f100ad12e0 | ||
|
|
7adc505a00 | ||
|
|
d781ee0596 | ||
|
|
0ea9240226 | ||
|
|
dbbdb7cd7e | ||
|
|
2f98d2a781 | ||
|
|
d9ea89857c | ||
|
|
a29a697012 | ||
|
|
430706529a | ||
|
|
9b1999bc02 | ||
|
|
196e82d3ab | ||
|
|
1761d0e65c | ||
|
|
9465723026 | ||
|
|
c9246bf946 | ||
|
|
4f0380bb2c | ||
|
|
41a4c7c39f | ||
|
|
e3bdf89cab | ||
|
|
0843f09c86 | ||
|
|
93afe00205 | ||
|
|
0f978bbcc7 | ||
|
|
88c49a9f7c | ||
|
|
83f285c913 | ||
|
|
24499c5ea9 | ||
|
|
d02b1f04e2 | ||
|
|
c818003399 | ||
|
|
306db6d026 | ||
|
|
b75ff4bdf5 | ||
|
|
60439c7d8e | ||
|
|
3518a4167c | ||
|
|
51dd7fa58e | ||
|
|
0a5549ebc9 | ||
|
|
607167ebe7 | ||
|
|
1183517c3a | ||
|
|
6d169784ed | ||
|
|
9b16cc6266 | ||
|
|
d56f46e1b1 | ||
|
|
6a5feacd38 | ||
|
|
0a91189e6a | ||
|
|
e9d9ebf156 | ||
|
|
1a85dc9b99 | ||
|
|
660af7ec70 | ||
|
|
102d44a7b8 | ||
|
|
549b0f8ef4 | ||
|
|
c6fe8ae70c | ||
|
|
9e1af0db63 | ||
|
|
43b6cb9bff | ||
|
|
9b7acf33c6 | ||
|
|
3914abda20 | ||
|
|
e47042d1cb | ||
|
|
a40a2f96b5 | ||
|
|
efc689a937 | ||
|
|
78bf40eaa9 | ||
|
|
578604619a | ||
|
|
6eac798d91 | ||
|
|
121bdbad84 | ||
|
|
aaeaa02406 | ||
|
|
5b2b2d1a5c | ||
|
|
9209d11084 | ||
|
|
60f54f1bc9 | ||
|
|
8b1de5d9b5 | ||
|
|
b2095481b3 | ||
|
|
a8e8517364 | ||
|
|
c9b31c62cd | ||
|
|
c72cf64c8a | ||
|
|
8c2f841972 | ||
|
|
0c1c750005 | ||
|
|
33d98dfe8c | ||
|
|
256815e148 | ||
|
|
cdbfbb5504 | ||
|
|
753a88c409 | ||
|
|
eef6966a85 | ||
|
|
d1d6a30884 | ||
|
|
3a190a1c37 | ||
|
|
ae70902921 | ||
|
|
5418237919 | ||
|
|
42fcdb33a1 | ||
|
|
2098e69fb4 | ||
|
|
2415275e90 | ||
|
|
83fcc92f75 | ||
|
|
64a7ab6e2e | ||
|
|
a772e1f9ca | ||
|
|
7bbed1705c | ||
|
|
4cd4660357 | ||
|
|
d503724bda | ||
|
|
2756a1f1a0 | ||
|
|
883122a98b | ||
|
|
bd7f0383d1 | ||
|
|
aa8cefc110 | ||
|
|
48740f5032 | ||
|
|
35b81a2f38 | ||
|
|
4b582b9d7e | ||
|
|
d72f45a94b | ||
|
|
a71ed3ec5a | ||
|
|
92b2a6fa24 | ||
|
|
ddeb64964d | ||
|
|
b2d6c5b6a7 | ||
|
|
271375d171 | ||
|
|
5693039f0d | ||
|
|
52304b37a2 | ||
|
|
fe8eabda37 | ||
|
|
a00032de53 | ||
|
|
b320358dc4 | ||
|
|
f81ab8e4d4 | ||
|
|
9a537248a4 | ||
|
|
eeed65b354 | ||
|
|
3c6711c590 | ||
|
|
90d8dfcf12 | ||
|
|
03b76f3c8a | ||
|
|
b9caed4392 | ||
|
|
6993a650b0 | ||
|
|
4000794af6 | ||
|
|
7c9e6779c6 | ||
|
|
ec6d9ec7de | ||
|
|
58fccadf08 | ||
|
|
34e7362132 | ||
|
|
fca39a7809 | ||
|
|
278404f47c | ||
|
|
64612712a5 | ||
|
|
9ffd0ad982 | ||
|
|
e703fec3af | ||
|
|
78e9eac64a | ||
|
|
2b1fc8725e | ||
|
|
b885c3dda3 | ||
|
|
aa2c1501da | ||
|
|
ed5c929b23 | ||
|
|
672a23dbcd | ||
|
|
24c09d0319 | ||
|
|
092275a981 | ||
|
|
3fe97e1709 | ||
|
|
1ad4f0c9b5 | ||
|
|
27a327402b | ||
|
|
e2adfe9eab | ||
|
|
67690c9e8e | ||
|
|
4c3c2eaa3b | ||
|
|
cf6c41f9db | ||
|
|
2e0a597ee4 | ||
|
|
628983d123 | ||
|
|
8ec5dd6def | ||
|
|
acc40c73f3 | ||
|
|
1d4658e978 | ||
|
|
b8bbf3b69e | ||
|
|
d8e41fa8b1 | ||
|
|
e9a6ba03f9 | ||
|
|
2e69e38adb | ||
|
|
c5ed876c09 | ||
|
|
96f737f13e | ||
|
|
f369151e8e | ||
|
|
d1b9990e4e | ||
|
|
ed7eb01d1b | ||
|
|
6601127693 | ||
|
|
43236c1cc4 | ||
|
|
65bbf7b2a2 | ||
|
|
74b8dca63e | ||
|
|
7db0e3ac3f | ||
|
|
f889c24497 | ||
|
|
852a1900b4 | ||
|
|
30a273c946 | ||
|
|
903614140a | ||
|
|
0fe772efa8 | ||
|
|
e6dedee56f | ||
|
|
66605e91aa | ||
|
|
137db2c86c | ||
|
|
9c28a6ff4a | ||
|
|
48a8d7de1c | ||
|
|
5fa96c944d | ||
|
|
fe270bd9ae | ||
|
|
a0912ad6b4 | ||
|
|
9ba0c54295 | ||
|
|
86721b88ce | ||
|
|
9bdee1b82d | ||
|
|
abe01f8f48 | ||
|
|
c5525d356e | ||
|
|
36ae564d26 | ||
|
|
cba6bd6ef5 | ||
|
|
4d40ffedff | ||
|
|
319e8d746f | ||
|
|
fe7a88362b | ||
|
|
8bf314202f | ||
|
|
ac8bc7dec2 | ||
|
|
0d5919392e | ||
|
|
1473abf19a | ||
|
|
45e75f3d09 | ||
|
|
5358f2fb9e | ||
|
|
c316d27444 | ||
|
|
0c0bb29f83 | ||
|
|
82bc3270e7 | ||
|
|
ec79b0b17b | ||
|
|
0560567058 | ||
|
|
3de740b35f | ||
|
|
b69f598e51 | ||
|
|
1ad177fca7 | ||
|
|
c3ad3ff348 | ||
|
|
ad578bb171 | ||
|
|
9538c49ac8 | ||
|
|
84d43db500 | ||
|
|
bf2d0468f9 | ||
|
|
d2bc47cfe8 | ||
|
|
d8c6c766b9 | ||
|
|
98f4bf2f65 | ||
|
|
f9c8b7f4ab | ||
|
|
a92532c0c9 | ||
|
|
bede81632d | ||
|
|
e975062e0e | ||
|
|
0cd051cee2 | ||
|
|
682980be49 | ||
|
|
878043ea3d | ||
|
|
757ae65189 | ||
|
|
cd63abba5b | ||
|
|
a0f00d0c83 | ||
|
|
915591c6d6 | ||
|
|
157cb7d982 | ||
|
|
cdcb21b841 | ||
|
|
2bf79a5298 | ||
|
|
ebefefa0eb | ||
|
|
a740883c8d | ||
|
|
da760075d2 | ||
|
|
b8833772a3 | ||
|
|
131c6e761e | ||
|
|
1fb0f2f048 | ||
|
|
2a25ac9d1f | ||
|
|
773bf765ad | ||
|
|
b0b866eec5 | ||
|
|
8cc928c841 | ||
|
|
4180420765 | ||
|
|
30945dc240 | ||
|
|
4ef87e3419 | ||
|
|
4785194527 | ||
|
|
8a742ce3ec | ||
|
|
27af9455f5 | ||
|
|
98ff572afe | ||
|
|
f77fedbea4 | ||
|
|
39c582dac2 | ||
|
|
8736728478 | ||
|
|
23cf9f47b0 | ||
|
|
b8feefc541 | ||
|
|
e3a6c71087 | ||
|
|
d6cc88fa99 | ||
|
|
428d451e55 | ||
|
|
aa67abad94 | ||
|
|
415b8354f4 | ||
|
|
51c596a21f | ||
|
|
5816a594fd | ||
|
|
f4d76aa360 | ||
|
|
56835f5f09 | ||
|
|
255ad7cac5 | ||
|
|
8c097a81ea | ||
|
|
3dc92b42fe | ||
|
|
1ed5a37280 | ||
|
|
cb36f2844e | ||
|
|
7b93b55b78 | ||
|
|
6837196e44 | ||
|
|
31680f3b59 | ||
|
|
91d9c9b4d5 | ||
|
|
d18d8cf0ea | ||
|
|
123909cdac | ||
|
|
f0eaccc6ac | ||
|
|
1d66ef2f2d | ||
|
|
8aefae3aff | ||
|
|
514558796b | ||
|
|
b00ca54707 | ||
|
|
0dbe507b26 | ||
|
|
4b70bea21f | ||
|
|
096a051b99 | ||
|
|
a3ca1ff1e9 | ||
|
|
28eabfe477 | ||
|
|
0a68b0515c | ||
|
|
566c0c8f72 | ||
|
|
333f2cb472 | ||
|
|
3d1acc65af | ||
|
|
5907de90c1 | ||
|
|
25d8a9d1f8 | ||
|
|
b44e8199b5 | ||
|
|
91449d0c8b | ||
|
|
b228f177f3 | ||
|
|
9a40a2d4f2 | ||
|
|
cdc3dabba1 | ||
|
|
40ba2cbe79 | ||
|
|
7ede8af193 | ||
|
|
a835b8cc8f | ||
|
|
e54b47b857 | ||
|
|
c93c25829b | ||
|
|
1cdf8b29a5 | ||
|
|
b6eacf0771 | ||
|
|
4ea23c1bd8 | ||
|
|
70398820c0 | ||
|
|
04abc4d12f | ||
|
|
8491f46045 | ||
|
|
4aa909e587 | ||
|
|
9f5e4b5812 | ||
|
|
06eb5abf11 | ||
|
|
29e6e3b374 | ||
|
|
2fc97f83f4 | ||
|
|
1e761b4c7d | ||
|
|
a2ca136f1f | ||
|
|
94322664f2 | ||
|
|
cbd5c6d3e9 | ||
|
|
0428dc8381 | ||
|
|
01e7b0da46 | ||
|
|
b37dadcc05 | ||
|
|
8e6151fe65 | ||
|
|
9b346c0937 | ||
|
|
b09f80ca30 | ||
|
|
0e0af82290 | ||
|
|
c69b09cbff | ||
|
|
b8ba6cd970 | ||
|
|
004e30d6be | ||
|
|
b084efdd06 | ||
|
|
d377d8462f | ||
|
|
2bbec9f9b1 | ||
|
|
b326908487 | ||
|
|
b21156a886 | ||
|
|
85138b0bc8 | ||
|
|
e0df0257d1 | ||
|
|
d23c1e8f92 | ||
|
|
670a40e67b | ||
|
|
3bbc6cbab1 | ||
|
|
15de53a22d | ||
|
|
59a44d6973 | ||
|
|
7561692352 | ||
|
|
f50a633f87 | ||
|
|
50f07e7b11 | ||
|
|
660623181a | ||
|
|
03ee3aaf40 | ||
|
|
ad295946c0 | ||
|
|
c28446dad0 | ||
|
|
ff9d1f2728 | ||
|
|
0b7cb2a1a8 | ||
|
|
92af01b0cb | ||
|
|
dc26d0d7ba | ||
|
|
2fe67edab3 | ||
|
|
167e43cbef | ||
|
|
d8cb407c25 | ||
|
|
9452afe3f7 | ||
|
|
8dc6d9ce7d | ||
|
|
bf4b8a318d | ||
|
|
20f6022050 | ||
|
|
65e6c737cd | ||
|
|
6d2572e347 | ||
|
|
52a35197c7 | ||
|
|
2b3a41db3e | ||
|
|
09c1459411 | ||
|
|
98f18b89da | ||
|
|
4249db4330 | ||
|
|
a7f46500ed | ||
|
|
c73fa2b0eb | ||
|
|
db4c4b971a | ||
|
|
e9ccc98ada | ||
|
|
11d6ec33b3 | ||
|
|
c3b6f4ca85 | ||
|
|
d37405a307 | ||
|
|
d7a9841328 | ||
|
|
cf3af42477 | ||
|
|
ad8e6e7d67 | ||
|
|
ae41ab101a | ||
|
|
f4881e7c6f | ||
|
|
94ef4380d4 | ||
|
|
7ebe1b8c14 | ||
|
|
79020bba28 | ||
|
|
95c250ebcc | ||
|
|
bfb14614cc | ||
|
|
12299d4810 | ||
|
|
c67a9d4e84 | ||
|
|
af422c7cd4 | ||
|
|
51bdecea53 | ||
|
|
0f505c6d7b | ||
|
|
ae72f10448 | ||
|
|
9f34153c84 | ||
|
|
c04cf6c5cb | ||
|
|
5112736385 | ||
|
|
11eaa6d77c | ||
|
|
6024903bde | ||
|
|
e96928588e | ||
|
|
94e38d4cdd | ||
|
|
d15921b3f2 | ||
|
|
3c6e2b89a4 | ||
|
|
439658fcf1 | ||
|
|
c9acb1466c | ||
|
|
addd27faf8 | ||
|
|
5605bdc7a3 | ||
|
|
f1df7e9bdc | ||
|
|
4765656f87 | ||
|
|
c3a00b93c2 | ||
|
|
e5d2ba7835 | ||
|
|
8c1b119e84 | ||
|
|
593a54e6cb | ||
|
|
95fa7083a9 | ||
|
|
98e08df807 | ||
|
|
01da36f984 | ||
|
|
ae155c78c2 | ||
|
|
9742216479 | ||
|
|
f720183281 | ||
|
|
4bc84acac6 | ||
|
|
14a8086677 | ||
|
|
686b96222e | ||
|
|
0962f37f55 | ||
|
|
2c17551b27 | ||
|
|
3f5a5e35c2 | ||
|
|
07c6d37ff0 | ||
|
|
916ea7acc0 | ||
|
|
eae7e865a5 | ||
|
|
776e5054aa | ||
|
|
bf2b8b280e | ||
|
|
7aa7027a34 | ||
|
|
d4713b2091 | ||
|
|
9b97be4aa4 | ||
|
|
621be11395 | ||
|
|
05af6df536 | ||
|
|
b7bda2355d | ||
|
|
b0976031ae | ||
|
|
d099f30a34 | ||
|
|
699be03bb7 | ||
|
|
ccf93cfdcd | ||
|
|
47358a8229 | ||
|
|
9856c2cfc4 | ||
|
|
df9669ba2c | ||
|
|
f970454cb4 | ||
|
|
69678574d4 | ||
|
|
53cab5074b | ||
|
|
c6c65b1799 | ||
|
|
bb9f64027a | ||
|
|
5f52fc4338 | ||
|
|
82e30c8519 | ||
|
|
6e2aa6b4b8 | ||
|
|
a1681cdd63 | ||
|
|
611a3ce138 | ||
|
|
396d933e34 | ||
|
|
0858e0a21e | ||
|
|
704e32b0dc | ||
|
|
f95cc960e1 | ||
|
|
1b00f512c1 | ||
|
|
d9ec66695c | ||
|
|
1a2578a4b7 | ||
|
|
f714e306da | ||
|
|
6a4b641250 | ||
|
|
8d96844312 | ||
|
|
b7145debfb | ||
|
|
990dbb6c06 | ||
|
|
c6a66a8fac | ||
|
|
65ba67dedc | ||
|
|
824db78234 | ||
|
|
2fdf108198 | ||
|
|
63092d7d1a | ||
|
|
964d437c7a | ||
|
|
d49c556291 | ||
|
|
d6b2748293 | ||
|
|
e3a5831d64 | ||
|
|
08d5174d02 | ||
|
|
dacb926db5 | ||
|
|
c0e9173c9b | ||
|
|
f46308bbdb | ||
|
|
331db99a4e | ||
|
|
d84f32f27d | ||
|
|
ac28a44b92 | ||
|
|
003a41b920 | ||
|
|
bc4c693525 | ||
|
|
d4ba837641 | ||
|
|
4a1592dd92 | ||
|
|
ac145a0b65 | ||
|
|
50c00dcae6 | ||
|
|
95d964140b | ||
|
|
0a73a7eb52 | ||
|
|
0aecb24930 | ||
|
|
904f5303a6 | ||
|
|
585761e931 | ||
|
|
4d53797cba | ||
|
|
803677e884 | ||
|
|
17617c58d7 | ||
|
|
96d03ec13d | ||
|
|
4726afbebf | ||
|
|
97c937e554 | ||
|
|
b25520cf18 | ||
|
|
c6b46d75cb | ||
|
|
a554352ae0 | ||
|
|
a7fd03f1b7 | ||
|
|
ef96116c3f | ||
|
|
01d10aebca | ||
|
|
7edc50865f | ||
|
|
a881d3fd81 | ||
|
|
5e9d2323e3 | ||
|
|
a98b5dd86e | ||
|
|
a250cf7ebe | ||
|
|
1c5ca0f022 | ||
|
|
ca3dee7b37 | ||
|
|
59d47955a0 | ||
|
|
d05ca3db0b | ||
|
|
87678eff98 | ||
|
|
c1f54b14d0 | ||
|
|
4c487d666f | ||
|
|
655a300acb | ||
|
|
8d61d66d79 | ||
|
|
13f391fe4a | ||
|
|
660a5d910a | ||
|
|
a58c5b372c | ||
|
|
ec55fdb8d8 | ||
|
|
19d670826d | ||
|
|
7033bd19fe | ||
|
|
aea75b9e52 | ||
|
|
eaf68fe105 | ||
|
|
a27237286c | ||
|
|
502ca6b612 | ||
|
|
a9451a5413 | ||
|
|
ee54047b94 | ||
|
|
a2e2c0a41a | ||
|
|
8e7bfba0ab | ||
|
|
6d280be081 | ||
|
|
877c6635e4 | ||
|
|
ca0be181bc | ||
|
|
ba2cf8015b | ||
|
|
f1f4ed97ca | ||
|
|
24785d28e6 | ||
|
|
0076205da6 | ||
|
|
6235b50c9d | ||
|
|
5cca19bb83 | ||
|
|
1b7056853b | ||
|
|
d1bc519599 | ||
|
|
bcae1dce7b | ||
|
|
e87927564b | ||
|
|
01b7ad4a3f | ||
|
|
235d38752c | ||
|
|
fd30edf2bb | ||
|
|
0a2be142ff | ||
|
|
33614d8ff0 | ||
|
|
3dce1d32f9 | ||
|
|
4a62199682 | ||
|
|
68be56240d | ||
|
|
19ccb27dbd | ||
|
|
a7e2bf071b | ||
|
|
e05a6e976e | ||
|
|
c7485e3fd4 | ||
|
|
80ad1a68e7 | ||
|
|
f4440d43de | ||
|
|
201a5c06fe | ||
|
|
6bd21b8995 | ||
|
|
ce66fbb595 | ||
|
|
226ebdd935 | ||
|
|
fcc400b20d | ||
|
|
d2c908b1ab | ||
|
|
976f9b2590 | ||
|
|
4d175a466e | ||
|
|
986ff7d1b1 | ||
|
|
bc719feb5d | ||
|
|
fe41612738 | ||
|
|
91f36ae42a | ||
|
|
2750981b64 | ||
|
|
268683f8ea | ||
|
|
5737165f37 | ||
|
|
a70116ed4d | ||
|
|
31d2296777 | ||
|
|
b859d7f3a5 | ||
|
|
75714ae84a | ||
|
|
be221c5a3e | ||
|
|
064ff34866 | ||
|
|
9807d6bb2c | ||
|
|
8321425e62 | ||
|
|
ba3223a9a3 | ||
|
|
a266997b69 | ||
|
|
314983b139 | ||
|
|
8896b0ae7c | ||
|
|
b6aa922c09 | ||
|
|
95732f4170 | ||
|
|
b6702d1d32 | ||
|
|
c3679910a4 | ||
|
|
624dfdf6ac | ||
|
|
83e0cf75c5 | ||
|
|
19a2e06c0b | ||
|
|
7fe23ad8c9 | ||
|
|
fd9ec438dc | ||
|
|
7cab973cbf | ||
|
|
9e3e5038f7 | ||
|
|
7952712c5e | ||
|
|
d754a2e295 | ||
|
|
768b4e5e2b | ||
|
|
b1ae09c003 | ||
|
|
9408e858cd | ||
|
|
0995164110 | ||
|
|
b3a042a63b | ||
|
|
c2ac70ff10 | ||
|
|
e7b57d8dee | ||
|
|
5bc8b02b0f | ||
|
|
d6f96b2c53 | ||
|
|
6c131b5648 | ||
|
|
27a4a502d7 | ||
|
|
f0a25ea485 | ||
|
|
4ca6e61726 | ||
|
|
e26ac6ed00 | ||
|
|
f341edb975 | ||
|
|
fdad24aaac | ||
|
|
3a676f98db | ||
|
|
8498cb17e7 | ||
|
|
36098f6b78 | ||
|
|
34667c69d3 | ||
|
|
756fef53f9 | ||
|
|
2ffe938206 | ||
|
|
d521699305 | ||
|
|
5ad23405b7 | ||
|
|
04cdd807ba | ||
|
|
646ed50f37 | ||
|
|
1b0ba0fa68 | ||
|
|
21c5c919ea | ||
|
|
d9f6f0847d | ||
|
|
59c897b53e | ||
|
|
77b32e94f1 | ||
|
|
331159a3d8 | ||
|
|
23510c80be | ||
|
|
ef497beaea | ||
|
|
6ea450a4e1 | ||
|
|
f64786543d | ||
|
|
c60e00c77f | ||
|
|
7e502beafc | ||
|
|
aa6c30ade6 | ||
|
|
0b8dfa6878 | ||
|
|
d7bee0c9e0 | ||
|
|
350c2241c4 | ||
|
|
cfa591838f | ||
|
|
ac1ac0debe | ||
|
|
54bc60b08f | ||
|
|
f8de46cea9 | ||
|
|
6fc2a604b4 | ||
|
|
1e410feed1 | ||
|
|
948e67a2b7 | ||
|
|
0f820e4498 | ||
|
|
c7bc1b10e2 | ||
|
|
ef04324f9d | ||
|
|
2b86865b9b | ||
|
|
f009625c1a | ||
|
|
a991c76842 | ||
|
|
d02ea3244a | ||
|
|
5a9f87ac63 | ||
|
|
9bfd0cb63c | ||
|
|
3f4c19abbc | ||
|
|
cf26635e3c | ||
|
|
e540862401 | ||
|
|
263be72c11 | ||
|
|
b63c04df4f | ||
|
|
784208dd87 | ||
|
|
5cb6c234c4 | ||
|
|
7972a023ed | ||
|
|
6cf92c2a90 | ||
|
|
50835c878e | ||
|
|
b727e5ca1c | ||
|
|
5773d1fd8d | ||
|
|
530226dbe8 | ||
|
|
4882a18bf9 | ||
|
|
70f3018e67 | ||
|
|
08c10c1f9b | ||
|
|
7945eba386 | ||
|
|
b6f4e124ce | ||
|
|
f01e101447 | ||
|
|
980b81f009 | ||
|
|
10f0522a6b | ||
|
|
2bc9cdafb2 | ||
|
|
e643a2ea32 | ||
|
|
b456afa2ac | ||
|
|
50b55c3f31 | ||
|
|
88b754e38c | ||
|
|
b3868a77f1 | ||
|
|
16d5d7b318 | ||
|
|
dbef33fe00 | ||
|
|
8c11ea69a0 | ||
|
|
fa0ee035e9 | ||
|
|
010dbf82f3 | ||
|
|
9a9d27b862 | ||
|
|
0afd3fc5e1 | ||
|
|
f5ebfcca5a | ||
|
|
6f33115187 | ||
|
|
58c65ab48c | ||
|
|
42294ff695 | ||
|
|
ed59f74cb8 | ||
|
|
d91dee141d | ||
|
|
2b4438720c | ||
|
|
758e532a6a | ||
|
|
d4ca6617de | ||
|
|
67fe7f8d3b | ||
|
|
f6040c5f06 | ||
|
|
324c384fdf | ||
|
|
de70ee117c | ||
|
|
85844c8ed4 | ||
|
|
101dc850a2 | ||
|
|
d88c7c76c6 | ||
|
|
1fea5f53bd | ||
|
|
ca13109a20 | ||
|
|
5650de0627 | ||
|
|
999ee981f7 | ||
|
|
ee6e78927f | ||
|
|
9dd9ae7a2f | ||
|
|
cbd178dab2 | ||
|
|
e34a28ee53 | ||
|
|
ffcc55b42b | ||
|
|
72028a9a2e | ||
|
|
d453aa849a | ||
|
|
9d0cd961b4 | ||
|
|
900922760a | ||
|
|
af462fa086 | ||
|
|
bc728d3a31 | ||
|
|
8085e24dcd | ||
|
|
d351ed0173 | ||
|
|
3ebc5b136c | ||
|
|
a1d02ca689 | ||
|
|
95546e0a7b | ||
|
|
103991746b | ||
|
|
7f0e5dd335 | ||
|
|
fa2fc63b7c | ||
|
|
4ad915761b | ||
|
|
d6a29c1ad5 | ||
|
|
3d439c8c01 | ||
|
|
af00374cb0 | ||
|
|
0bb46aaef4 | ||
|
|
00377e91b4 | ||
|
|
477448114a | ||
|
|
91da1c3f8b | ||
|
|
eddf99ddd0 | ||
|
|
f27580b5ae | ||
|
|
b2d35751b4 | ||
|
|
18df06a7ce | ||
|
|
0bee3c9db0 | ||
|
|
e3ba28d767 | ||
|
|
1f9c2cd181 | ||
|
|
ea2b12a548 | ||
|
|
c9f4db2a4f | ||
|
|
1e5154c901 | ||
|
|
17dc41279c | ||
|
|
cb4747aed2 | ||
|
|
ce8d03ddce | ||
|
|
366c7e2b91 | ||
|
|
6c5fb5e22b | ||
|
|
4854bdd02f | ||
|
|
d7ecdc9b07 | ||
|
|
c8d30ae801 | ||
|
|
9eebe82b34 | ||
|
|
b3915ff8fd | ||
|
|
805c946b33 | ||
|
|
8bc1949466 | ||
|
|
a6689b1035 | ||
|
|
f63910d355 | ||
|
|
98c8521057 | ||
|
|
57139295b5 | ||
|
|
79d4dc1646 | ||
|
|
1760624954 | ||
|
|
ecf9c173c4 | ||
|
|
b0e863dbbb | ||
|
|
8f8859a5f5 | ||
|
|
9429657a2b | ||
|
|
2b0b1e23eb | ||
|
|
dd55baf148 | ||
|
|
4542157192 | ||
|
|
9e47172d69 | ||
|
|
2ad921f99e | ||
|
|
a840969512 | ||
|
|
67fdfdf584 | ||
|
|
abef8e376c | ||
|
|
8a85077e70 | ||
|
|
b3ac296cac | ||
|
|
b2db733c83 | ||
|
|
92dfcf3b6d | ||
|
|
af554fc3f7 | ||
|
|
02621eee74 | ||
|
|
8105f51603 | ||
|
|
c40ac27d71 | ||
|
|
d33c930f26 | ||
|
|
eb0fc0fc80 | ||
|
|
a6563543a3 | ||
|
|
40b20c5595 | ||
|
|
1ebbfffd2a | ||
|
|
24d3e09618 | ||
|
|
d92ddc4c7a | ||
|
|
c8b7580830 | ||
|
|
a9d0c052bc | ||
|
|
ad0b349a3f | ||
|
|
9da51a8d85 | ||
|
|
814f21a50e | ||
|
|
092669fb9d | ||
|
|
3789e1339b | ||
|
|
a1490d07b4 | ||
|
|
08144382b8 | ||
|
|
cb90e1388f | ||
|
|
5e1038dc67 | ||
|
|
5e852ebb5d | ||
|
|
097786c62d | ||
|
|
baeced32c3 | ||
|
|
2f4e4343c2 | ||
|
|
9fd6d7318e | ||
|
|
fd2be958ba | ||
|
|
719889b27a | ||
|
|
faaa1050da | ||
|
|
27aed5cd7e | ||
|
|
aa327643f5 | ||
|
|
3a481df45d | ||
|
|
ac1e405c34 | ||
|
|
4932473b3f | ||
|
|
206baf7d80 | ||
|
|
4ac7a4fdab | ||
|
|
283e8045d8 | ||
|
|
8637f4a70d | ||
|
|
4a768682ea | ||
|
|
dad4f30597 | ||
|
|
1b81de01b4 | ||
|
|
b09f9e8c12 | ||
|
|
4b1177e07e | ||
|
|
f714d1ab28 | ||
|
|
dcc9d20cca | ||
|
|
d590ab003f | ||
|
|
a3c52445ee | ||
|
|
be3fcd90e2 | ||
|
|
26aa336450 | ||
|
|
65972d9c0c | ||
|
|
d13f47ec0b | ||
|
|
1e36bc98b9 | ||
|
|
16dd86e732 | ||
|
|
be894664ef | ||
|
|
f126120421 | ||
|
|
af505b346c | ||
|
|
77b4689ac8 | ||
|
|
57118691d8 | ||
|
|
2b456cbdeb | ||
|
|
1d3ca5743b | ||
|
|
5f0a27d355 | ||
|
|
42d0f342b2 | ||
|
|
c12adea655 | ||
|
|
2a82e00857 |
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -1,9 +1,14 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: docker
|
||||
directory: "/"
|
||||
directories:
|
||||
- "/"
|
||||
- "/docker"
|
||||
schedule:
|
||||
interval: daily
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
- package-ecosystem: pip
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
run: python build_helpers/binance_update_lev_tiers.py
|
||||
|
||||
|
||||
- uses: peter-evans/create-pull-request@v6
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
add-paths: freqtrade/exchange/binance_leverage_tiers.json
|
||||
|
||||
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||
key: pip-${{ matrix.python-version }}-ubuntu
|
||||
|
||||
- name: TA binary *nix
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Installation - *nix
|
||||
run: |
|
||||
python -m pip install --upgrade "pip<=24.0" wheel
|
||||
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
|
||||
@@ -68,11 +68,17 @@ jobs:
|
||||
python build_helpers/freqtrade_client_version_align.py
|
||||
|
||||
- name: Tests
|
||||
if: (!(runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04'))
|
||||
run: |
|
||||
pytest --random-order
|
||||
|
||||
- name: Tests with Coveralls
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04')
|
||||
run: |
|
||||
pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc
|
||||
|
||||
- name: Coveralls
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04')
|
||||
if: (runner.os == 'Linux' && matrix.python-version == '3.12' && matrix.os == 'ubuntu-22.04')
|
||||
env:
|
||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
@@ -138,11 +144,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ "macos-12", "macos-13", "macos-14" ]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
exclude:
|
||||
- os: "macos-14"
|
||||
python-version: "3.9"
|
||||
os: [ "macos-13", "macos-14", "macos-15" ]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -164,7 +167,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/Library/Caches/pip
|
||||
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||
key: pip-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
|
||||
- name: TA binary *nix
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
@@ -193,11 +196,11 @@ jobs:
|
||||
rm /usr/local/bin/python3.11-config || true
|
||||
rm /usr/local/bin/python3.12-config || true
|
||||
|
||||
brew install hdf5 c-blosc libomp
|
||||
brew install libomp
|
||||
|
||||
- name: Installation (python)
|
||||
run: |
|
||||
python -m pip install --upgrade "pip<=24.0" wheel
|
||||
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
|
||||
@@ -263,7 +266,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest ]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -277,7 +280,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||
key: pip-${{ matrix.os }}-${{ matrix.python-version }}
|
||||
|
||||
- name: Installation
|
||||
run: |
|
||||
@@ -384,7 +387,6 @@ jobs:
|
||||
- name: Documentation build
|
||||
run: |
|
||||
pip install -r docs/requirements-docs.txt
|
||||
pip install mkdocs
|
||||
mkdocs build
|
||||
|
||||
- name: Discord notification
|
||||
@@ -418,7 +420,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
|
||||
key: pip-3.12-ubuntu
|
||||
|
||||
- name: TA binary *nix
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
@@ -427,7 +429,7 @@ jobs:
|
||||
|
||||
- name: Installation - *nix
|
||||
run: |
|
||||
python -m pip install --upgrade "pip<=24.0" wheel
|
||||
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
|
||||
@@ -538,12 +540,12 @@ jobs:
|
||||
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.2
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.2
|
||||
|
||||
|
||||
deploy-docker:
|
||||
|
||||
55
.github/workflows/deploy-docs.yml
vendored
Normal file
55
.github/workflows/deploy-docs.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Build Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
|
||||
# disable permissions for all of the available permissions
|
||||
permissions: {}
|
||||
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
permissions:
|
||||
contents: write # for mike to push
|
||||
name: Deploy Docs through mike
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r docs/requirements-docs.txt
|
||||
|
||||
- name: Fetch gh-pages branch
|
||||
run: |
|
||||
git fetch origin gh-pages --depth=1
|
||||
|
||||
- name: Configure Git user
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
|
||||
- name: Build and push Mike
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
mike deploy ${{ github.ref_name }} latest --push --update-aliases
|
||||
|
||||
- name: Build and push Mike - Release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
run: |
|
||||
mike deploy ${{ github.ref_name }} stable --push --update-aliases
|
||||
|
||||
- name: Show mike versions
|
||||
run: |
|
||||
mike list
|
||||
2
.github/workflows/pre-commit-update.yml
vendored
2
.github/workflows/pre-commit-update.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Run auto-update
|
||||
run: pre-commit autoupdate
|
||||
|
||||
- uses: peter-evans/create-pull-request@v6
|
||||
- uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.REPO_SCOPED_TOKEN }}
|
||||
add-paths: .pre-commit-config.yaml
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -114,3 +114,5 @@ target/
|
||||
!config_examples/config_full.example.json
|
||||
!config_examples/config_kraken.example.json
|
||||
!config_examples/config_freqai.example.json
|
||||
|
||||
docker-compose-*.yml
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: "7.1.0"
|
||||
rev: "7.1.1"
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [Flake8-pyproject]
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.11.0"
|
||||
rev: "v1.13.0"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.4.0.20240717
|
||||
- types-cachetools==5.5.0.20240820
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.32.0.20240712
|
||||
- types-requests==2.32.0.20241016
|
||||
- types-tabulate==0.9.0.20240106
|
||||
- types-python-dateutil==2.9.0.20240316
|
||||
- SQLAlchemy==2.0.31
|
||||
- types-python-dateutil==2.9.0.20241003
|
||||
- SQLAlchemy==2.0.36
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -31,12 +31,13 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.5.4'
|
||||
rev: 'v0.8.0'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
exclude: |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12.4-slim-bookworm as base
|
||||
FROM python:3.12.7-slim-bookworm as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
@@ -25,7 +25,7 @@ FROM base as python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade "pip<=24.0" wheel
|
||||
&& pip install --upgrade pip wheel
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
|
||||
24
README.md
24
README.md
@@ -30,8 +30,10 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bitmart](https://bitmart.com/)
|
||||
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
||||
- [X] [Bybit](https://bybit.com/)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [HTX](https://www.htx.com/) (Former Huobi)
|
||||
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
||||
- [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)_
|
||||
@@ -40,6 +42,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
||||
- [X] [OKX](https://okx.com/)
|
||||
- [X] [Bybit](https://bybit.com/)
|
||||
|
||||
@@ -60,7 +63,7 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||
|
||||
## Features
|
||||
|
||||
- [x] **Based on Python 3.9+**: For botting on any operating system - Windows, macOS and Linux.
|
||||
- [x] **Based on Python 3.10+**: 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.
|
||||
@@ -86,41 +89,50 @@ For further (native) installation methods, please refer to the [Installation doc
|
||||
|
||||
```
|
||||
usage: freqtrade [-h] [-V]
|
||||
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver}
|
||||
{trade,create-userdir,new-config,show-config,new-strategy,download-data,convert-data,convert-trade-data,trades-to-ohlcv,list-data,backtesting,backtesting-show,backtesting-analysis,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-markets,list-pairs,list-strategies,list-freqaimodels,list-timeframes,show-trades,test-pairlist,convert-db,install-ui,plot-dataframe,plot-profit,webserver,strategy-updater,lookahead-analysis,recursive-analysis}
|
||||
...
|
||||
|
||||
Free, open source crypto trading bot
|
||||
|
||||
positional arguments:
|
||||
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver}
|
||||
{trade,create-userdir,new-config,show-config,new-strategy,download-data,convert-data,convert-trade-data,trades-to-ohlcv,list-data,backtesting,backtesting-show,backtesting-analysis,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-markets,list-pairs,list-strategies,list-freqaimodels,list-timeframes,show-trades,test-pairlist,convert-db,install-ui,plot-dataframe,plot-profit,webserver,strategy-updater,lookahead-analysis,recursive-analysis}
|
||||
trade Trade module.
|
||||
create-userdir Create user-data directory.
|
||||
new-config Create new config
|
||||
show-config Show resolved config
|
||||
new-strategy Create new strategy
|
||||
download-data Download backtesting data.
|
||||
convert-data Convert candle (OHLCV) data from one format to
|
||||
another.
|
||||
convert-trade-data Convert trade data from one format to another.
|
||||
trades-to-ohlcv Convert trade data to OHLCV data.
|
||||
list-data List downloaded data.
|
||||
backtesting Backtesting module.
|
||||
backtesting-show Show past Backtest results
|
||||
backtesting-analysis
|
||||
Backtest Analysis module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
hyperopt-list List Hyperopt results
|
||||
hyperopt-show Show details of Hyperopt results
|
||||
list-exchanges Print available exchanges.
|
||||
list-hyperopts Print available hyperopt classes.
|
||||
list-markets Print markets on exchange.
|
||||
list-pairs Print pairs on exchange.
|
||||
list-strategies Print available strategies.
|
||||
list-freqaimodels Print available freqAI models.
|
||||
list-timeframes Print available timeframes for the exchange.
|
||||
show-trades Show trades.
|
||||
test-pairlist Test your pairlist configuration.
|
||||
convert-db Migrate database to different system
|
||||
install-ui Install FreqUI
|
||||
plot-dataframe Plot candles with indicators.
|
||||
plot-profit Generate plot showing profits.
|
||||
webserver Webserver module.
|
||||
strategy-updater updates outdated strategy files to the current version
|
||||
lookahead-analysis Check for potential look ahead bias.
|
||||
recursive-analysis Check for potential recursive formula issue.
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-V, --version show program's version number and exit
|
||||
|
||||
@@ -208,7 +220,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||
|
||||
### Software requirements
|
||||
|
||||
- [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [Python >= 3.10](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://ta-lib.github.io/ta-lib-python/)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
|
||||
|
||||
python -m pip install --upgrade "pip<=24.0" wheel
|
||||
python -m pip install --upgrade pip wheel
|
||||
|
||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -9,11 +9,6 @@
|
||||
],
|
||||
"minimum": -1
|
||||
},
|
||||
"new_pairs_days": {
|
||||
"description": "Download data of new pairs for given number of days",
|
||||
"type": "integer",
|
||||
"default": 30
|
||||
},
|
||||
"timeframe": {
|
||||
"description": "The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). \nUsually specified in the strategy and missing in the configuration.",
|
||||
"type": "string"
|
||||
@@ -562,6 +557,7 @@
|
||||
"enum": [
|
||||
"StaticPairList",
|
||||
"VolumePairList",
|
||||
"PercentChangePairList",
|
||||
"ProducerPairList",
|
||||
"RemotePairList",
|
||||
"MarketCapPairList",
|
||||
@@ -583,53 +579,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"protections": {
|
||||
"description": "Configuration for various protections.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"description": "Method used for the protection.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"CooldownPeriod",
|
||||
"LowProfitPairs",
|
||||
"MaxDrawdown",
|
||||
"StoplossGuard"
|
||||
]
|
||||
},
|
||||
"stop_duration": {
|
||||
"description": "Duration to lock the pair after a protection is triggered, in minutes.",
|
||||
"type": "number",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"stop_duration_candles": {
|
||||
"description": "Duration to lock the pair after a protection is triggered, in number of candles.",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"trade_limit": {
|
||||
"description": "Minimum number of trades required during lookback period.",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"lookback_period": {
|
||||
"description": "Period to look back for protection checks, in minutes.",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"lookback_period_candles": {
|
||||
"description": "Period to look back for protection checks, in number of candles.",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
]
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"description": "Telegram settings.",
|
||||
"type": "object",
|
||||
@@ -733,12 +682,18 @@
|
||||
},
|
||||
"exit_fill": {
|
||||
"description": "Telegram setting for exit fill signals.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"on",
|
||||
"off",
|
||||
"silent"
|
||||
"type": [
|
||||
"string",
|
||||
"object"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"on",
|
||||
"off",
|
||||
"silent"
|
||||
]
|
||||
},
|
||||
"default": "on"
|
||||
},
|
||||
"exit_cancel": {
|
||||
@@ -1064,7 +1019,7 @@
|
||||
"default": {},
|
||||
"properties": {
|
||||
"process_throttle_secs": {
|
||||
"description": "Throttle time in seconds for processing.",
|
||||
"description": "Minimum loop duration for one bot iteration in seconds.",
|
||||
"type": "integer"
|
||||
},
|
||||
"interval": {
|
||||
@@ -1105,6 +1060,15 @@
|
||||
"description": "Enable position adjustment. \nUsually specified in the strategy and missing in the configuration.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"new_pairs_days": {
|
||||
"description": "Download data of new pairs for given number of days",
|
||||
"type": "integer",
|
||||
"default": 30
|
||||
},
|
||||
"download_trades": {
|
||||
"description": "Download trades data by default (instead of ohlcv data).",
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_entry_position_adjustment": {
|
||||
"description": "Maximum entry position adjustment allowed. \nUsually specified in the strategy and missing in the configuration.",
|
||||
"type": [
|
||||
@@ -1113,6 +1077,13 @@
|
||||
],
|
||||
"minimum": -1
|
||||
},
|
||||
"add_config_files": {
|
||||
"description": "Additional configuration files to load.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"orderflow": {
|
||||
"description": "Settings related to order flow.",
|
||||
"type": "object",
|
||||
@@ -1208,6 +1179,11 @@
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"log_responses": {
|
||||
"description": "Log responses from the exchange.Useful/required to debug issues with order processing.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"unknown_fee_rate": {
|
||||
"description": "Fee rate for unknown markets.",
|
||||
"type": "number"
|
||||
@@ -1413,6 +1389,11 @@
|
||||
"type": "string",
|
||||
"default": "example"
|
||||
},
|
||||
"wait_for_training_iteration_on_reload": {
|
||||
"description": "Wait for the next training iteration to complete after /reload or ctrl+c.",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"feature_parameters": {
|
||||
"description": "The parameters used to engineer the feature set",
|
||||
"type": "object",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "USDT",
|
||||
"stake_amount": 0.05,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated",
|
||||
"max_open_trades": 5,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.05,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
"max_open_trades": 5,
|
||||
"stake_currency": "EUR",
|
||||
"stake_amount": 10,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.8-slim-bookworm as base
|
||||
FROM python:3.11.10-slim-bookworm as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
@@ -17,7 +17,7 @@ RUN mkdir /freqtrade \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
# Allow sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
|
||||
&& pip install --upgrade "pip<=24.0"
|
||||
&& pip install --upgrade pip
|
||||
|
||||
WORKDIR /freqtrade
|
||||
|
||||
|
||||
@@ -18,15 +18,13 @@ freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_nam
|
||||
```
|
||||
|
||||
This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding
|
||||
DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy
|
||||
makes, this file may get quite large, so periodically check your `user_data/backtest_results`
|
||||
folder to delete old exports.
|
||||
DataFrame of the candles that resulted in entry and exit signals.
|
||||
Depending on how many entries your strategy makes, this file may get quite large, so periodically check your `user_data/backtest_results` folder to delete old exports.
|
||||
|
||||
Before running your next backtest, make sure you either delete your old backtest results or run
|
||||
backtesting with the `--cache none` option to make sure no cached results are used.
|
||||
|
||||
If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the
|
||||
`user_data/backtest_results` folder.
|
||||
If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` and `backtest-result-{timestamp}_exited.pkl` files in the `user_data/backtest_results` folder.
|
||||
|
||||
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
|
||||
with `--analysis-groups` option provided with space-separated arguments:
|
||||
@@ -103,6 +101,10 @@ The indicators have to be present in your strategy's main DataFrame (either for
|
||||
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
||||
output.
|
||||
|
||||
!!! Note "Indicator List"
|
||||
The indicator values will be displayed for both entry and exit points. If `--indicator-list all` is specified,
|
||||
only the indicators at the entry point will be shown to avoid excessively large lists, which could occur depending on the strategy.
|
||||
|
||||
There are a range of candle and trade-related fields that are included in the analysis so are
|
||||
automatically accessible by including them on the indicator-list, and these include:
|
||||
|
||||
@@ -118,6 +120,53 @@ automatically accessible by including them on the indicator-list, and these incl
|
||||
- **profit_ratio :** trade profit ratio
|
||||
- **profit_abs :** absolute profit return of the trade
|
||||
|
||||
#### Sample Output for Indicator Values
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen
|
||||
```
|
||||
|
||||
In this example,
|
||||
we aim to display the `chikou_span` and `tenkan_sen` indicator values at both the entry and exit points of trades.
|
||||
|
||||
A sample output for indicators might look like this:
|
||||
|
||||
| pair | open_date | enter_reason | exit_reason | chikou_span (entry) | tenkan_sen (entry) | chikou_span (exit) | tenkan_sen (exit) |
|
||||
|-----------|---------------------------|--------------|-------------|---------------------|--------------------|--------------------|-------------------|
|
||||
| DOGE/USDT | 2024-07-06 00:35:00+00:00 | | exit_signal | 0.105 | 0.106 | 0.105 | 0.107 |
|
||||
| BTC/USDT | 2024-08-05 14:20:00+00:00 | | roi | 54643.440 | 51696.400 | 54386.000 | 52072.010 |
|
||||
|
||||
As shown in the table, `chikou_span (entry)` represents the indicator value at the time of trade entry,
|
||||
while `chikou_span (exit)` reflects its value at the time of exit.
|
||||
This detailed view of indicator values enhances the analysis.
|
||||
|
||||
The `(entry)` and `(exit)` suffixes are added to indicators
|
||||
to distinguish the values at the entry and exit points of the trade.
|
||||
|
||||
!!! Note "Trade-wide Indicators"
|
||||
Certain trade-wide indicators do not have the `(entry)` or `(exit)` suffix. These indicators include: `pair`, `stake_amount`,
|
||||
`max_stake_amount`, `amount`, `open_date`, `close_date`, `open_rate`, `close_rate`, `fee_open`, `fee_close`, `trade_duration`,
|
||||
`profit_ratio`, `profit_abs`, `exit_reason`,`initial_stop_loss_abs`, `initial_stop_loss_ratio`, `stop_loss_abs`, `stop_loss_ratio`,
|
||||
`min_rate`, `max_rate`, `is_open`, `enter_tag`, `leverage`, `is_short`, `open_timestamp`, `close_timestamp` and `orders`
|
||||
|
||||
#### Filtering Indicators Based on Entry or Exit Signals
|
||||
|
||||
The `--indicator-list` option, by default, displays indicator values for both entry and exit signals. To filter the indicator values exclusively for entry signals, you can use the `--entry-only` argument. Similarly, to display indicator values only at exit signals, use the `--exit-only` argument.
|
||||
|
||||
Example: Display indicator values at entry signals:
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --entry-only
|
||||
```
|
||||
|
||||
Example: Display indicator values at exit signals:
|
||||
|
||||
```bash
|
||||
freqtrade backtesting-analysis -c user_data/config.json --analysis-groups 0 --indicator-list chikou_span tenkan_sen --exit-only
|
||||
```
|
||||
|
||||
!!! note
|
||||
When using these filters, the indicator names will not be suffixed with `(entry)` or `(exit)`.
|
||||
|
||||
### Filtering the trade output by date
|
||||
|
||||
|
||||
@@ -30,11 +30,17 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
config: Config, processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args, **kwargs) -> float:
|
||||
def hyperopt_loss_function(
|
||||
*,
|
||||
results: DataFrame,
|
||||
trade_count: int,
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
config: Config,
|
||||
processed: dict[str, DataFrame],
|
||||
backtest_stats: dict[str, Any],
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
This is the legacy algorithm (used until now in freqtrade).
|
||||
@@ -97,7 +103,7 @@ class MyAwesomeStrategy(IStrategy):
|
||||
SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'),
|
||||
]
|
||||
|
||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||
def generate_roi_table(params: Dict) -> dict[int, float]:
|
||||
|
||||
roi_table = {}
|
||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||
|
||||
@@ -10,12 +10,14 @@ To learn how to get data for the pairs and exchange you're interested in, head o
|
||||
```
|
||||
usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-d PATH] [--userdir PATH] [-s NAME]
|
||||
[--strategy-path PATH] [-i TIMEFRAME]
|
||||
[--timerange TIMERANGE]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||
[--strategy-path PATH]
|
||||
[--recursive-strategy-search]
|
||||
[--freqaimodel NAME] [--freqaimodel-path PATH]
|
||||
[-i TIMEFRAME] [--timerange TIMERANGE]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
||||
[--max-open-trades INT]
|
||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||
[-p PAIRS [PAIRS ...]] [--eps] [--dmmp]
|
||||
[-p PAIRS [PAIRS ...]] [--eps]
|
||||
[--enable-protections]
|
||||
[--dry-run-wallet DRY_RUN_WALLET]
|
||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||
@@ -24,8 +26,9 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[--export-filename PATH]
|
||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||
[--cache {none,day,week,month}]
|
||||
[--freqai-backtest-live-models]
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
@@ -48,10 +51,6 @@ optional arguments:
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
--dmmp, --disable-max-market-positions
|
||||
Disable applying `max_open_trades` during backtest
|
||||
(same as setting `max_open_trades` to a very high
|
||||
number).
|
||||
--enable-protections, --enableprotections
|
||||
Enable protections for backtesting.Will slow
|
||||
backtesting down by a considerable amount, but will
|
||||
@@ -80,10 +79,13 @@ optional arguments:
|
||||
--cache {none,day,week,month}
|
||||
Load a cached backtest result no older than specified
|
||||
age (default: day).
|
||||
--freqai-backtest-live-models
|
||||
Run backtest with ready models.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
--logfile FILE, --log-file FILE
|
||||
Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
@@ -92,7 +94,7 @@ Common arguments:
|
||||
`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
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
@@ -102,6 +104,12 @@ Strategy arguments:
|
||||
Specify strategy class name which will be used by the
|
||||
bot.
|
||||
--strategy-path PATH Specify additional strategy lookup path.
|
||||
--recursive-strategy-search
|
||||
Recursively search for a strategy in the strategies
|
||||
folder.
|
||||
--freqaimodel NAME Specify a custom freqaimodels.
|
||||
--freqaimodel-path PATH
|
||||
Specify additional lookup path for freqaimodels.
|
||||
|
||||
```
|
||||
|
||||
@@ -293,6 +301,7 @@ A backtesting result will look like that:
|
||||
|-----------------------------+---------------------|
|
||||
| Backtesting from | 2019-01-01 00:00:00 |
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Trading Mode | Spot |
|
||||
| Max open trades | 3 |
|
||||
| | |
|
||||
| Total/Daily Avg Trades | 429 / 3.575 |
|
||||
@@ -398,6 +407,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
|-----------------------------+---------------------|
|
||||
| Backtesting from | 2019-01-01 00:00:00 |
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Trading Mode | Spot |
|
||||
| Max open trades | 3 |
|
||||
| | |
|
||||
| Total/Daily Avg Trades | 429 / 3.575 |
|
||||
@@ -452,6 +462,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
|
||||
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
||||
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
|
||||
- `Trading Mode`: Spot or Futures trading.
|
||||
- `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
|
||||
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
|
||||
- `Final balance`: Final balance - starting balance + absolute profit.
|
||||
@@ -530,10 +541,10 @@ You can then load the trades to perform further analysis as shown in the [data a
|
||||
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
|
||||
|
||||
- Exchange [trading limits](#trading-limits-in-backtesting) are respected
|
||||
- Entries happen at open-price
|
||||
- Entries happen at open-price unless a custom price logic has been specified
|
||||
- All orders are filled at the requested price (no slippage) as long as the price is within the candle's high/low range
|
||||
- Exit-signal exits happen at open-price of the consecutive candle
|
||||
- Exits don't free their trade slot for a new trade until the next candle
|
||||
- Exits free their trade slot for a new trade with a different pair
|
||||
- Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open
|
||||
- ROI
|
||||
- Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%)
|
||||
@@ -555,6 +566,7 @@ Since backtesting lacks some detailed information about what happens within a ca
|
||||
- Stoploss
|
||||
- ROI
|
||||
- Trailing stoploss
|
||||
- Position reversals (futures only) happen if an entry signal in the other direction than the closing trade triggers at the candle the existing trade closes.
|
||||
|
||||
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
|
||||
Also, keep in mind that past results don't guarantee future success.
|
||||
@@ -569,7 +581,7 @@ These limits are usually listed in the exchange documentation as "trading rules"
|
||||
Backtesting (as well as live and dry-run) does honor these limits, and will ensure that a stoploss can be placed below this value - so the value will be slightly higher than what the exchange specifies.
|
||||
Freqtrade has however no information about historic limits.
|
||||
|
||||
This can lead to situations where trading-limits are inflated by using a historic price, resulting in minimum amounts > 50$.
|
||||
This can lead to situations where trading-limits are inflated by using a historic price, resulting in minimum amounts > 50\$.
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
@@ -12,41 +12,50 @@ This page explains the different parameters of the bot and how to run it.
|
||||
|
||||
```
|
||||
usage: freqtrade [-h] [-V]
|
||||
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver}
|
||||
{trade,create-userdir,new-config,show-config,new-strategy,download-data,convert-data,convert-trade-data,trades-to-ohlcv,list-data,backtesting,backtesting-show,backtesting-analysis,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-markets,list-pairs,list-strategies,list-freqaimodels,list-timeframes,show-trades,test-pairlist,convert-db,install-ui,plot-dataframe,plot-profit,webserver,strategy-updater,lookahead-analysis,recursive-analysis}
|
||||
...
|
||||
|
||||
Free, open source crypto trading bot
|
||||
|
||||
positional arguments:
|
||||
{trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver}
|
||||
{trade,create-userdir,new-config,show-config,new-strategy,download-data,convert-data,convert-trade-data,trades-to-ohlcv,list-data,backtesting,backtesting-show,backtesting-analysis,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-markets,list-pairs,list-strategies,list-freqaimodels,list-timeframes,show-trades,test-pairlist,convert-db,install-ui,plot-dataframe,plot-profit,webserver,strategy-updater,lookahead-analysis,recursive-analysis}
|
||||
trade Trade module.
|
||||
create-userdir Create user-data directory.
|
||||
new-config Create new config
|
||||
show-config Show resolved config
|
||||
new-strategy Create new strategy
|
||||
download-data Download backtesting data.
|
||||
convert-data Convert candle (OHLCV) data from one format to
|
||||
another.
|
||||
convert-trade-data Convert trade data from one format to another.
|
||||
trades-to-ohlcv Convert trade data to OHLCV data.
|
||||
list-data List downloaded data.
|
||||
backtesting Backtesting module.
|
||||
backtesting-show Show past Backtest results
|
||||
backtesting-analysis
|
||||
Backtest Analysis module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
hyperopt-list List Hyperopt results
|
||||
hyperopt-show Show details of Hyperopt results
|
||||
list-exchanges Print available exchanges.
|
||||
list-hyperopts Print available hyperopt classes.
|
||||
list-markets Print markets on exchange.
|
||||
list-pairs Print pairs on exchange.
|
||||
list-strategies Print available strategies.
|
||||
list-freqaimodels Print available freqAI models.
|
||||
list-timeframes Print available timeframes for the exchange.
|
||||
show-trades Show trades.
|
||||
test-pairlist Test your pairlist configuration.
|
||||
convert-db Migrate database to different system
|
||||
install-ui Install FreqUI
|
||||
plot-dataframe Plot candles with indicators.
|
||||
plot-profit Generate plot showing profits.
|
||||
webserver Webserver module.
|
||||
strategy-updater updates outdated strategy files to the current version
|
||||
lookahead-analysis Check for potential look ahead bias.
|
||||
recursive-analysis Check for potential recursive formula issue.
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-V, --version show program's version number and exit
|
||||
|
||||
|
||||
@@ -123,6 +123,19 @@ This is similar to using multiple `--config` parameters, but simpler in usage as
|
||||
|
||||
If multiple files are in the `add_config_files` section, then they will be assumed to be at identical levels, having the last occurrence override the earlier config (unless a parent already defined such a key).
|
||||
|
||||
## Editor autocomplete and validation
|
||||
|
||||
If you are using an editor that supports JSON schema, you can use the schema provided by Freqtrade to get autocompletion and validation of your configuration file by adding the following line to the top of your configuration file:
|
||||
|
||||
``` json
|
||||
{
|
||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||
}
|
||||
```
|
||||
|
||||
??? Note "Develop version"
|
||||
The develop schema is available as `https://schema.freqtrade.io/schema_dev.json` - though we recommend to stick to the stable version for the best experience.
|
||||
|
||||
## Configuration parameters
|
||||
|
||||
The table below will list all configuration parameters available.
|
||||
@@ -133,10 +146,10 @@ Freqtrade can also load many options via command line (CLI) arguments (check out
|
||||
|
||||
The prevalence for all Options is as follows:
|
||||
|
||||
- CLI arguments override any other option
|
||||
- [Environment Variables](#environment-variables)
|
||||
- Configuration files are used in sequence (the last file wins) and override Strategy configurations.
|
||||
- Strategy configurations are only used if they are not set via configuration or command-line arguments. These options are marked with [Strategy Override](#parameters-in-the-strategy) in the below table.
|
||||
* CLI arguments override any other option
|
||||
* [Environment Variables](#environment-variables)
|
||||
* Configuration files are used in sequence (the last file wins) and override Strategy configurations.
|
||||
* Strategy configurations are only used if they are not set via configuration or command-line arguments. These options are marked with [Strategy Override](#parameters-in-the-strategy) in the below table.
|
||||
|
||||
### Parameters table
|
||||
|
||||
@@ -170,7 +183,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
|
||||
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
|
||||
| | **Unfilled timeout**
|
||||
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry 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.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit 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 exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
|
||||
@@ -209,7 +222,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `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://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation) <br> **Datatype:** Dict
|
||||
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `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
|
||||
@@ -217,7 +229,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| | **Plugins**
|
||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options.
|
||||
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). <br> **Datatype:** List of Dicts
|
||||
| | **Telegram**
|
||||
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
||||
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
@@ -284,10 +295,10 @@ Values set in the configuration file always overwrite values set in the strategy
|
||||
* `order_time_in_force`
|
||||
* `unfilledtimeout`
|
||||
* `disable_dataframe_checks`
|
||||
- `use_exit_signal`
|
||||
* `use_exit_signal`
|
||||
* `exit_profit_only`
|
||||
- `exit_profit_offset`
|
||||
- `ignore_roi_if_entry_signal`
|
||||
* `exit_profit_offset`
|
||||
* `ignore_roi_if_entry_signal`
|
||||
* `ignore_buying_expired_candle_after`
|
||||
* `position_adjustment_enable`
|
||||
* `max_entry_position_adjustment`
|
||||
@@ -300,12 +311,12 @@ There are several methods to configure how much of the stake currency the bot wi
|
||||
|
||||
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 `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.
|
||||
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%).
|
||||
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%).
|
||||
|
||||
With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)`). If we take into account a stoploss of 10% on top of that - we'd end up with a value of ~14$ (`12.6 / (1 - 0.1)`).
|
||||
With a reserve of 5%, the minimum stake amount would be ~12.6\$ (`12 * (1 + 0.05)`). If we take into account a stoploss of 10% on top of that - we'd end up with a value of ~14\$ (`12.6 / (1 - 0.1)`).
|
||||
|
||||
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.
|
||||
|
||||
@@ -351,9 +362,9 @@ To overcome this, the option `amend_last_stake_amount` can be set to `True`, whi
|
||||
|
||||
In the example above this would mean:
|
||||
|
||||
- Trade1: 400 USDT
|
||||
- Trade2: 400 USDT
|
||||
- Trade3: 200 USDT
|
||||
* Trade1: 400 USDT
|
||||
* Trade2: 400 USDT
|
||||
* Trade3: 200 USDT
|
||||
|
||||
!!! Note
|
||||
This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly.
|
||||
|
||||
@@ -11,9 +11,8 @@ Without provided configuration, `--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, 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.
|
||||
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 missing timerange 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 the `--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.
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -90,7 +89,7 @@ Common arguments:
|
||||
|
||||
!!! 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.
|
||||
`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 "Startup period"
|
||||
@@ -117,16 +116,17 @@ freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
|
||||
or as regex (in this case, to download all active USDT pairs)
|
||||
|
||||
```bash
|
||||
freqtrade download-data --exchange binance --pairs .*/USDT
|
||||
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 change the exchange used to download the historical data from, either use `--exchange <exchange>` - or specify a different configuration file.
|
||||
* 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.
|
||||
* Given starting points are ignored if data is already available, downloading only missing data up to today.
|
||||
* 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.
|
||||
|
||||
@@ -423,7 +423,8 @@ You can get a list of downloaded data using the `list-data` sub-command.
|
||||
usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
[--userdir PATH] [--exchange EXCHANGE]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
||||
[-p PAIRS [PAIRS ...]]
|
||||
[--data-format-trades {json,jsongz,hdf5,feather,parquet}]
|
||||
[--trades] [-p PAIRS [PAIRS ...]]
|
||||
[--trading-mode {spot,margin,futures}]
|
||||
[--show-timerange]
|
||||
|
||||
@@ -433,6 +434,10 @@ options:
|
||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `feather`).
|
||||
--data-format-trades {json,jsongz,hdf5,feather,parquet}
|
||||
Storage format for downloaded trades data. (default:
|
||||
`feather`).
|
||||
--trades Work on trades data instead of OHLCV data.
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
separated.
|
||||
@@ -465,13 +470,29 @@ Common arguments:
|
||||
```bash
|
||||
> freqtrade list-data --userdir ~/.freqtrade/user_data/
|
||||
|
||||
Found 33 pair / timeframe combinations.
|
||||
pairs timeframe
|
||||
---------- -----------------------------------------
|
||||
ADA/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
||||
ADA/ETH 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
||||
ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
||||
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
||||
Found 33 pair / timeframe combinations.
|
||||
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓
|
||||
┃ Pair ┃ Timeframe ┃ Type ┃
|
||||
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩
|
||||
│ ADA/BTC │ 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ spot │
|
||||
│ ADA/ETH │ 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ spot │
|
||||
│ ETH/BTC │ 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d │ spot │
|
||||
│ ETH/USDT │ 5m, 15m, 30m, 1h, 2h, 4h │ spot │
|
||||
└───────────────┴───────────────────────────────────────────┴──────┘
|
||||
|
||||
```
|
||||
|
||||
Show all trades data including from/to timerange
|
||||
|
||||
``` bash
|
||||
> freqtrade list-data --show --trades
|
||||
Found trades data for 1 pair.
|
||||
┏━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
|
||||
┃ Pair ┃ Type ┃ From ┃ To ┃ Trades ┃
|
||||
┡━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
|
||||
│ XRP/ETH │ spot │ 2019-10-11 00:00:11 │ 2019-10-13 11:19:28 │ 12477 │
|
||||
└─────────┴──────┴─────────────────────┴─────────────────────┴────────┘
|
||||
|
||||
```
|
||||
|
||||
## Trades (tick) data
|
||||
|
||||
@@ -75,7 +75,10 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re
|
||||
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
|
||||
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
|
||||
|
||||
|
||||
## Removal of `populate_any_indicators`
|
||||
|
||||
version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details.
|
||||
|
||||
## Removal of `protections` from configuration
|
||||
|
||||
Setting protections from the configuration via `"protections": [],` has been removed in 2024.10, after having raised deprecation warnings for over 3 years.
|
||||
|
||||
@@ -116,7 +116,7 @@ A similar setup can also be taken for Pycharm - using `freqtrade` as module name
|
||||

|
||||
|
||||
!!! Note "Startup directory"
|
||||
This assumes that you have the repository checked out, and the editor is started at the repository root level (so setup.py is at the top level of your repository).
|
||||
This assumes that you have the repository checked out, and the editor is started at the repository root level (so pyproject.toml is at the top level of your repository).
|
||||
|
||||
## ErrorHandling
|
||||
|
||||
@@ -162,7 +162,7 @@ Hopefully you also want to contribute this back upstream.
|
||||
|
||||
Whatever your motivations are - This should get you off the ground in trying to develop a new Pairlist Handler.
|
||||
|
||||
First of all, have a look at the [VolumePairList](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/pairlist/VolumePairList.py) Handler, and best copy this file with a name of your new Pairlist Handler.
|
||||
First of all, have a look at the [VolumePairList](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/plugins/pairlist/VolumePairList.py) Handler, and best copy this file with a name of your new Pairlist Handler.
|
||||
|
||||
This is a simple Handler, which however serves as a good example on how to start developing.
|
||||
|
||||
@@ -205,7 +205,7 @@ This is called with each iteration of the bot (only if the Pairlist Handler is a
|
||||
|
||||
It must return the resulting pairlist (which may then be passed into the chain of Pairlist Handlers).
|
||||
|
||||
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the end-result is not shorter than expected.
|
||||
Validations are optional, the parent class exposes a `verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the end-result is not shorter than expected.
|
||||
|
||||
#### filter_pairlist
|
||||
|
||||
@@ -219,14 +219,14 @@ The default implementation in the base class simply calls the `_validate_pair()`
|
||||
|
||||
If overridden, it must return the resulting pairlist (which may then be passed into the next Pairlist Handler in the chain).
|
||||
|
||||
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the end result is not shorter than expected.
|
||||
Validations are optional, the parent class exposes a `verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the end result is not shorter than expected.
|
||||
|
||||
In `VolumePairList`, this implements different methods of sorting, does early validation so only the expected number of pairs is returned.
|
||||
|
||||
##### sample
|
||||
|
||||
``` python
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||
def filter_pairlist(self, pairlist: list[str], tickers: dict) -> List[str]:
|
||||
# Generate dynamic whitelist
|
||||
pairs = self._calculate_pairlist(pairlist, tickers)
|
||||
return pairs
|
||||
@@ -241,7 +241,6 @@ No protection should use datetime directly, but use the provided `date_now` vari
|
||||
|
||||
!!! Tip "Writing a new Protection"
|
||||
Best copy one of the existing Protections to have a good example.
|
||||
Don't forget to register your protection in `constants.py` under the variable `AVAILABLE_PROTECTIONS` - otherwise it will not be selectable.
|
||||
|
||||
#### Implementation of a new protection
|
||||
|
||||
@@ -481,21 +480,24 @@ Once the PR against stable is merged (best right after merging):
|
||||
|
||||
### pypi
|
||||
|
||||
!!! Note
|
||||
This process is now automated as part of Github Actions.
|
||||
!!! Warning "Manual Releases"
|
||||
This process is automated as part of Github Actions.
|
||||
Manual pypi pushes should not be necessary.
|
||||
|
||||
To create a pypi release, please run the following commands:
|
||||
??? example "Manual release"
|
||||
To manually create a pypi release, please run the following commands:
|
||||
|
||||
Additional requirement: `wheel`, `twine` (for uploading), account on pypi with proper permissions.
|
||||
Additional requirement: `wheel`, `twine` (for uploading), account on pypi with proper permissions.
|
||||
|
||||
``` bash
|
||||
python setup.py sdist bdist_wheel
|
||||
``` bash
|
||||
pip install -U build
|
||||
python -m build --sdist --wheel
|
||||
|
||||
# For pypi test (to check if some change to the installation did work)
|
||||
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
|
||||
# For pypi test (to check if some change to the installation did work)
|
||||
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
|
||||
|
||||
# For production:
|
||||
twine upload dist/*
|
||||
```
|
||||
# For production:
|
||||
twine upload dist/*
|
||||
```
|
||||
|
||||
Please don't push non-releases to the productive / real pypi instance.
|
||||
Please don't push non-releases to the productive / real pypi instance.
|
||||
|
||||
@@ -252,21 +252,36 @@ OKX requires a passphrase for each api key, you will therefore need to add this
|
||||
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
||||
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.
|
||||
|
||||
Gate API keys require the following permissions on top of the market type you want to trade:
|
||||
|
||||
* "Spot Trade" _or_ "Perpetual Futures" (Read and Write) (either select both, or the one matching the market you want to trade)
|
||||
* "Wallet" (read only)
|
||||
* "Account" (read only)
|
||||
|
||||
Without these permissions, the bot will not start correctly and show errors like "permission missing".
|
||||
|
||||
## Bybit
|
||||
|
||||
Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode.
|
||||
Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures.
|
||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors
|
||||
|
||||
On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors.
|
||||
|
||||
As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well.
|
||||
|
||||
API Keys for live futures trading (Subaccount on non-unified) must have the following permissions:
|
||||
API Keys for live futures trading must have the following permissions:
|
||||
|
||||
* Read-write
|
||||
* Contract - Orders
|
||||
* Contract - Positions
|
||||
|
||||
We do strongly recommend to limit all API keys to the IP you're going to use it from.
|
||||
|
||||
!!! Warning "Unified accounts"
|
||||
Freqtrade assumes accounts to be dedicated to the bot.
|
||||
We therefore recommend the usage of one subaccount per bot. This is especially important when using unified accounts.
|
||||
Other configurations (multiple bots on one account, manual non-bot trades on the bot account) are not supported and may lead to unexpected behavior.
|
||||
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||
On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||
@@ -289,6 +304,41 @@ It's therefore required to pass the UID as well.
|
||||
!!! Warning "Necessary Verification"
|
||||
Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification.
|
||||
|
||||
## Hyperliquid
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Hyperliquid supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it.
|
||||
|
||||
Hyperliquid is a Decentralized Exchange (DEX). Decentralized exchanges work a bit different compared to normal exchanges. Instead of authenticating private API calls using an API key, private API calls need to be signed with the private key of your wallet (We recommend using an api Wallet for this, generated either on Hyperliquid or in your wallet of choice).
|
||||
This needs to be configured like this:
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"name": "hyperliquid",
|
||||
"walletAddress": "your_eth_wallet_address",
|
||||
"privateKey": "your_api_private_key",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
* walletAddress in hex format: `0x<40 hex characters>` - Can be easily copied from your wallet - and should be your wallet address, not your API Wallet Address.
|
||||
* privateKey in hex format: `0x<64 hex characters>` - Use the key the API Wallet shows on creation.
|
||||
|
||||
Hyperliquid handles deposits and withdrawals on the Arbitrum One chain, a Layer 2 scaling solution built on top of Ethereum. Hyperliquid uses USDC as quote / collateral. The process of depositing USDC on Hyperliquid requires a couple of steps, see [how to start trading](https://hyperliquid.gitbook.io/hyperliquid-docs/onboarding/how-to-start-trading) for details on what steps are needed.
|
||||
|
||||
!!! Note "Hyperliquid general usage Notes"
|
||||
Hyperliquid does not support market orders, however ccxt will simulate market orders by placing limit orders with a maximum slippage of 5%.
|
||||
Unfortunately, hyperliquid only offers 5000 historic candles, so backtesting will either need to build candles historically (by waiting and downloading the data incrementally over time) - or will be limited to the last 5000 candles.
|
||||
|
||||
!!! Info "Some general best practices (non exhaustive)"
|
||||
* Beware of supply chain attacks, like pip package poisoning etcetera. Whenever you use your private key, make sure your environment is safe.
|
||||
* Don't use your actual wallet private key for trading. Use the Hyperliquid [API generator](https://app.hyperliquid.xyz/API) to create a separate API wallet.
|
||||
* Don't store your actual wallet private key on the server you use for freqtrade. Use the API wallet private key instead. This key won't allow withdrawals, only trading.
|
||||
* Always keep your mnemonic phrase and private key private.
|
||||
* Don't use the same mnemonic as the one you had to backup when initializing a hardware wallet, using the same mnemonic basically deletes the security of your hardware wallet.
|
||||
* Create a different software wallet, only transfer the funds you want to trade with to that wallet, and use that wallet to trade on Hyperliquid.
|
||||
* If you have funds you don't want to use for trading (after making a profit for example), transfer them back to your hardware wallet.
|
||||
|
||||
## 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.
|
||||
@@ -298,7 +348,7 @@ Should you experience constant errors with Nonce (like `InvalidNonce`), it is be
|
||||
* The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed:
|
||||
|
||||
```shell
|
||||
$ pip3 install web3
|
||||
pip3 install web3
|
||||
```
|
||||
|
||||
### Getting latest price / Incomplete candles
|
||||
|
||||
19
docs/faq.md
19
docs/faq.md
@@ -100,6 +100,19 @@ You can use the `/stopentry` command in Telegram to prevent future trade entry,
|
||||
|
||||
Please look at the [advanced setup documentation Page](advanced-setup.md#running-multiple-instances-of-freqtrade).
|
||||
|
||||
### I'm getting "Impossible to load Strategy" when starting the bot
|
||||
|
||||
This error message is shown when the bot cannot load the strategy.
|
||||
Usually, you can use `freqtrade list-strategies` to list all available strategies.
|
||||
The output of this command will also include a status column, showing if the strategy can be loaded.
|
||||
|
||||
Please check the following:
|
||||
|
||||
* Are you using the correct strategy name? The strategy name is case-sensitive and must correspond to the Strategy class name (not the filename!).
|
||||
* Is the strategy in the `user_data/strategies` directory, and has the file-ending `.py`?
|
||||
* Does the bot show other warnings before this error? Maybe you're missing some dependencies for the strategy - which would be highlighted in the log.
|
||||
* In case of docker - is the strategy directory mounted correctly (check the volumes part of the docker-compose file)?
|
||||
|
||||
### I'm getting "Missing data fillup" messages in the log
|
||||
|
||||
This message is just a warning that the latest candles had missing candles in them.
|
||||
@@ -146,9 +159,9 @@ The same fix should be applied in the configuration file, if order types are def
|
||||
|
||||
### I'm trying to start the bot live, but get an API permission error
|
||||
|
||||
Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say.
|
||||
Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console.
|
||||
Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary.
|
||||
Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say.
|
||||
Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console.
|
||||
Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary.
|
||||
Futures will usually have to be enabled specifically.
|
||||
|
||||
### How do I search the bot logs for something?
|
||||
|
||||
@@ -58,7 +58,6 @@ The plot configuration can be accessed via the "Plot Configurator" (Cog icon) bu
|
||||
|
||||
### Settings
|
||||
|
||||
|
||||
Several UI related settings can be changed by accessing the settings page.
|
||||
|
||||
Things you can change (among others):
|
||||
|
||||
@@ -293,10 +293,10 @@ class MyCoolPyTorchClassifier(BasePyTorchClassifier):
|
||||
super().__init__(**kwargs)
|
||||
config = self.freqai_info.get("model_training_parameters", {})
|
||||
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||
self.model_kwargs: dict[str, Any] = config.get("model_kwargs", {})
|
||||
self.trainer_kwargs: dict[str, Any] = config.get("trainer_kwargs", {})
|
||||
|
||||
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
"""
|
||||
User sets up the training and test data to fit their desired model here
|
||||
:param data_dictionary: the dictionary holding all data for train, test,
|
||||
@@ -359,10 +359,10 @@ class PyTorchMLPRegressor(BasePyTorchRegressor):
|
||||
super().__init__(**kwargs)
|
||||
config = self.freqai_info.get("model_training_parameters", {})
|
||||
self.learning_rate: float = config.get("learning_rate", 3e-4)
|
||||
self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {})
|
||||
self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {})
|
||||
self.model_kwargs: dict[str, Any] = config.get("model_kwargs", {})
|
||||
self.trainer_kwargs: dict[str, Any] = config.get("trainer_kwargs", {})
|
||||
|
||||
def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
def fit(self, data_dictionary: dict, dk: FreqaiDataKitchen, **kwargs) -> Any:
|
||||
n_features = data_dictionary["train_features"].shape[-1]
|
||||
model = PyTorchMLPModel(
|
||||
input_dim=n_features,
|
||||
@@ -393,7 +393,7 @@ Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. T
|
||||
|
||||
For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows:
|
||||
```python
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame:
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
self.freqai.class_names = ["down", "up"]
|
||||
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
|
||||
dataframe["close"], 'up', 'down')
|
||||
|
||||
@@ -22,6 +22,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
||||
| `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file. <br> **Datatype:** Boolean. <br> Default: `False`
|
||||
| `data_kitchen_thread_count` | <br> Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI) <br> **Datatype:** Positive integer.
|
||||
| `activate_tensorboard` | <br> Indicate whether or not to activate tensorboard for the tensorboard enabled modules (currently Reinforcment Learning, XGBoost, Catboost, and PyTorch). Tensorboard needs Torch installed, which means you will need the torch/RL docker image or you need to answer "yes" to the install question about whether or not you wish to install Torch. <br> **Datatype:** Boolean. <br> Default: `True`.
|
||||
| `wait_for_training_iteration_on_reload` | <br> When using /reload or ctrl-c, wait for the current training iteration to finish before completing graceful shutdown. If set to `False`, FreqAI will break the current training iteration, allowing you to shutdown gracefully more quickly, but you will lose your current training iteration. <br> **Datatype:** Boolean. <br> Default: `True`.
|
||||
|
||||
### Feature parameters
|
||||
|
||||
|
||||
@@ -42,11 +42,11 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
[--recursive-strategy-search] [--freqaimodel NAME]
|
||||
[--freqaimodel-path PATH] [-i TIMEFRAME]
|
||||
[--timerange TIMERANGE]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||
[--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}]
|
||||
[--max-open-trades INT]
|
||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||
[-p PAIRS [PAIRS ...]] [--hyperopt-path PATH]
|
||||
[--eps] [--dmmp] [--enable-protections]
|
||||
[--eps] [--enable-protections]
|
||||
[--dry-run-wallet DRY_RUN_WALLET]
|
||||
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
|
||||
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]]
|
||||
@@ -55,15 +55,15 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
[--hyperopt-loss NAME] [--disable-param-export]
|
||||
[--ignore-missing-spaces] [--analyze-per-epoch]
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-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}
|
||||
--data-format-ohlcv {json,jsongz,hdf5,feather,parquet}
|
||||
Storage format for downloaded candle (OHLCV) data.
|
||||
(default: `json`).
|
||||
(default: `feather`).
|
||||
--max-open-trades INT
|
||||
Override the value of the `max_open_trades`
|
||||
configuration setting.
|
||||
@@ -80,10 +80,6 @@ optional arguments:
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
--dmmp, --disable-max-market-positions
|
||||
Disable applying `max_open_trades` during backtest
|
||||
(same as setting `max_open_trades` to a very high
|
||||
number).
|
||||
--enable-protections, --enableprotections
|
||||
Enable protections for backtesting.Will slow
|
||||
backtesting down by a considerable amount, but will
|
||||
@@ -133,7 +129,8 @@ optional arguments:
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
--logfile FILE, --log-file FILE
|
||||
Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
@@ -142,7 +139,7 @@ Common arguments:
|
||||
`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
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
@@ -445,7 +442,6 @@ While this strategy is most likely too simple to provide consistent profit, it s
|
||||
|
||||
Whether you are using `.range` functionality or the alternatives above, you should try to use space ranges as small as possible since this will improve CPU/RAM usage.
|
||||
|
||||
|
||||
## Optimizing protections
|
||||
|
||||
Freqtrade can also optimize protections. How you optimize protections is up to you, and the following should be considered as example only.
|
||||
@@ -589,14 +585,15 @@ Currently, the following loss functions are builtin:
|
||||
|
||||
* `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.
|
||||
* `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 absolute drawdown.
|
||||
* `MaxDrawDownRelativeHyperOptLoss` - Optimizes both maximum absolute drawdown while also adjusting for maximum relative 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.
|
||||
* `MultiMetricHyperOptLoss` - Optimizes by several key metrics to achieve balanced performance. The primary focus is on maximizing Profit and minimizing Drawdown, while also considering additional metrics such as Profit Factor, Expectancy Ratio and Winrate. Moreover, it applies a penalty for epochs with a low number of trades, encouraging strategies with adequate trade frequency.
|
||||
|
||||
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
||||
|
||||
@@ -867,18 +864,15 @@ You can use the `--print-all` command line option if you would like to see all r
|
||||
|
||||
## Position stacking and disabling max market positions
|
||||
|
||||
In some situations, you may need to run Hyperopt (and Backtesting) with the
|
||||
`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments.
|
||||
In some situations, you may need to run Hyperopt (and Backtesting) with the `--eps`/`--enable-position-staking` argument, or you may need to set `max_open_trades` to a very high number to disable the limit on the number of open trades.
|
||||
|
||||
By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one
|
||||
open trade is allowed for every traded pair. The total number of trades open for all pairs
|
||||
open trade per pair is allowed. The total number of trades open for all pairs
|
||||
is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to
|
||||
some potential trades to be hidden (or masked) by previously open trades.
|
||||
potential trades being hidden (or masked) by already open trades.
|
||||
|
||||
The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times,
|
||||
while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades`
|
||||
during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high
|
||||
number).
|
||||
The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times.
|
||||
Using `--max-open-trades` with a very high number will disable the limit on the number of open trades.
|
||||
|
||||
!!! Note
|
||||
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
|
||||
@@ -919,13 +913,39 @@ Your epochs should therefore be aligned to the possible values - or you should b
|
||||
|
||||
After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter.
|
||||
|
||||
## Output debug messages from your strategy
|
||||
|
||||
If you want to output debug messages from your strategy, you can use the `logging` module. By default, Freqtrade will output all messages with a level of `INFO` or higher.
|
||||
|
||||
|
||||
``` python
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
...
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
logger.info("This is a debug message")
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
!!! Note "using print"
|
||||
Messages printed via `print()` will not be shown in the hyperopt output unless parallelism is disabled (`-j 1`).
|
||||
It is recommended to use the `logging` module instead.
|
||||
|
||||
## Validate backtesting results
|
||||
|
||||
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
|
||||
|
||||
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt for Backtesting.
|
||||
|
||||
### Why do my backtest results not match my hyperopt results?
|
||||
|
||||
Should results not match, check the following factors:
|
||||
|
||||
* You may have added parameters to hyperopt in `populate_indicators()` where they will be calculated only once **for all epochs**. If you are, for example, trying to optimise multiple SMA timeperiod values, the hyperoptable timeperiod parameter should be placed in `populate_entry_trend()` which is calculated every epoch. See [Optimizing an indicator parameter](https://www.freqtrade.io/en/stable/hyperopt/#optimizing-an-indicator-parameter).
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
||||
|
||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
|
||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
|
||||
|
||||
Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
||||
|
||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList` or `MarketCapPairList` as the starting Pairlist Handler.
|
||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList` or `PercentChangePairList` as the starting Pairlist Handler.
|
||||
|
||||
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
|
||||
|
||||
@@ -22,6 +22,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
||||
|
||||
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
||||
* [`VolumePairList`](#volume-pair-list)
|
||||
* [`PercentChangePairList`](#percent-change-pair-list)
|
||||
* [`ProducerPairList`](#producerpairlist)
|
||||
* [`RemotePairList`](#remotepairlist)
|
||||
* [`MarketCapPairList`](#marketcappairlist)
|
||||
@@ -54,7 +55,6 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis
|
||||
By default, only currently enabled pairs are allowed.
|
||||
To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration.
|
||||
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.
|
||||
|
||||
@@ -152,6 +152,89 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
|
||||
!!! Note
|
||||
`VolumePairList` does not support backtesting mode.
|
||||
|
||||
#### Percent Change Pair List
|
||||
|
||||
`PercentChangePairList` filters and sorts pairs based on the percentage change in their price over the last 24 hours or any defined timeframe as part of advanced options. This allows traders to focus on assets that have experienced significant price movements, either positive or negative.
|
||||
|
||||
**Configuration Options**
|
||||
|
||||
* `number_assets`: Specifies the number of top pairs to select based on the 24-hour percentage change.
|
||||
* `min_value`: Sets a minimum percentage change threshold. Pairs with a percentage change below this value will be filtered out.
|
||||
* `max_value`: Sets a maximum percentage change threshold. Pairs with a percentage change above this value will be filtered out.
|
||||
* `sort_direction`: Specifies the order in which pairs are sorted based on their percentage change. Accepts two values: `asc` for ascending order and `desc` for descending order.
|
||||
* `refresh_period`: Defines the interval (in seconds) at which the pairlist will be refreshed. The default is 1800 seconds (30 minutes).
|
||||
* `lookback_days`: Number of days to look back. When `lookback_days` is selected, the `lookback_timeframe` is defaulted to 1 day.
|
||||
* `lookback_timeframe`: Timeframe to use for the lookback period.
|
||||
* `lookback_period`: Number of periods to look back at.
|
||||
|
||||
When PercentChangePairList is used after other Pairlist Handlers, it will operate on the outputs of those handlers. If it is the leading Pairlist Handler, it will select pairs from all available markets with the specified stake currency.
|
||||
|
||||
`PercentChangePairList` uses ticker data from the exchange, provided via the ccxt library:
|
||||
The percentage change is calculated as the change in price over the last 24 hours.
|
||||
|
||||
??? Note "Unsupported exchanges"
|
||||
On some exchanges (like HTX), regular PercentChangePairList does not work as the api does not natively provide 24h percent change in price. This can be worked around by using candle data to calculate the percentage change. To roughly simulate 24h percent change, you can use the following configuration. Please note that these pairlists will only refresh once per day.
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 20,
|
||||
"min_value": 0,
|
||||
"refresh_period": 86400,
|
||||
"lookback_days": 1
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
**Example Configuration to Read from Ticker**
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 15,
|
||||
"min_value": -10,
|
||||
"max_value": 50
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
In this configuration:
|
||||
|
||||
1. The top 15 pairs are selected based on the highest percentage change in price over the last 24 hours.
|
||||
2. Only pairs with a percentage change between -10% and 50% are considered.
|
||||
|
||||
**Example Configuration to Read from Candles**
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "PercentChangePairList",
|
||||
"number_assets": 15,
|
||||
"sort_key": "percentage",
|
||||
"min_value": 0,
|
||||
"refresh_period": 3600,
|
||||
"lookback_timeframe": "1h",
|
||||
"lookback_period": 72
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
This example builds the percent change pairs based on a rolling period of 3 days of 1-hour candles by using `lookback_timeframe` for candle size and `lookback_period` which specifies the number of candles.
|
||||
|
||||
The percent change in price is calculated using the following formula, which expresses the percentage difference between the current candle's close price and the previous candle's close price, as defined by the specified timeframe and lookback period:
|
||||
|
||||
$$ Percent Change = (\frac{Current Close - Previous Close}{Previous Close}) * 100 $$
|
||||
|
||||
!!! Warning "Range look back and refresh period"
|
||||
When used in conjunction with `lookback_days` and `lookback_timeframe` the `refresh_period` can not be smaller than the candle size in seconds. As this will result in unnecessary requests to the exchanges API.
|
||||
|
||||
!!! Warning "Performance implications when using lookback range"
|
||||
If used in first position in combination with lookback, the computation of the range-based percent change can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `PercentChangePairList` to narrow the pairlist down for further percent-change calculation.
|
||||
|
||||
!!! Note "Backtesting"
|
||||
`PercentChangePairList` does not support backtesting mode.
|
||||
|
||||
#### ProducerPairList
|
||||
|
||||
With `ProducerPairList`, you can reuse the pairlist from a [Producer](producer-consumer.md) without explicitly defining the pairlist on each consumer.
|
||||
@@ -269,7 +352,7 @@ The optional `bearer_token` will be included in the requests Authorization Heade
|
||||
|
||||
#### MarketCapPairList
|
||||
|
||||
`MarketCapPairList` employs sorting/filtering of pairs by their marketcap rank based of CoinGecko. It will only recognize coins up to the coin placed at rank 250. The returned pairlist will be sorted based of their marketcap ranks.
|
||||
`MarketCapPairList` employs sorting/filtering of pairs by their marketcap rank based of CoinGecko. The returned pairlist will be sorted based of their marketcap ranks.
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
@@ -277,14 +360,22 @@ The optional `bearer_token` will be included in the requests Authorization Heade
|
||||
"method": "MarketCapPairList",
|
||||
"number_assets": 20,
|
||||
"max_rank": 50,
|
||||
"refresh_period": 86400
|
||||
"refresh_period": 86400,
|
||||
"categories": ["layer-1"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`number_assets` defines the maximum number of pairs returned by the pairlist. `max_rank` will determine the maximum rank used in creating/filtering the pairlist. It's expected that some coins within the top `max_rank` marketcap will not be included in the resulting pairlist since not all pairs will have active trading pairs in your preferred market/stake/exchange combination.
|
||||
`number_assets` defines the maximum number of pairs returned by the pairlist. `max_rank` will determine the maximum rank used in creating/filtering the pairlist. It's expected that some coins within the top `max_rank` marketcap will not be included in the resulting pairlist since not all pairs will have active trading pairs in your preferred market/stake/exchange combination.
|
||||
While using a `max_rank` bigger than 250 is supported, it's not recommended, as it'll cause multiple API calls to CoinGecko, which can lead to rate limit issues.
|
||||
|
||||
`refresh_period` setting defines the period (in seconds) at which the marketcap rank data will be refreshed. Defaults to 86,400s (1 day). The pairlist cache (`refresh_period`) is applicable on both generating pairlists (first position in the list) and filtering instances (not the first position in the list).
|
||||
The `refresh_period` setting defines the interval (in seconds) at which the marketcap rank data will be refreshed. The default is 86,400 seconds (1 day). The pairlist cache (`refresh_period`) applies to both generating pairlists (when in the first position in the list) and filtering instances (when not in the first position in the list).
|
||||
|
||||
The `categories` setting specifies the [coingecko categories](https://www.coingecko.com/en/categories) from which to select coins from. The default is an empty list `[]`, meaning no category filtering is applied.
|
||||
If an incorrect category string is chosen, the plugin will print the available categories from CoinGecko and fail. The category should be the ID of the category, for example, for `https://www.coingecko.com/en/categories/layer-1`, the category ID would be `layer-1`. You can pass multiple categories such as `["layer-1", "meme-token"]` to select from several categories.
|
||||
|
||||
!!! Warning "Many categories"
|
||||
Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues.
|
||||
|
||||
#### AgeFilter
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
## Protections
|
||||
|
||||
!!! Warning "Beta feature"
|
||||
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
|
||||
|
||||
Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs.
|
||||
All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys.
|
||||
|
||||
!!! Note
|
||||
!!! Tip "Usage tips"
|
||||
Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance.
|
||||
|
||||
!!! Tip
|
||||
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
|
||||
|
||||
!!! Note "Backtesting"
|
||||
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
|
||||
|
||||
!!! Warning "Setting protections from the configuration"
|
||||
Setting protections from the configuration via `"protections": [],` key should be considered deprecated and will be removed in a future version.
|
||||
It is also no longer guaranteed that your protections apply to the strategy in cases where the strategy defines [protections as property](hyperopt.md#optimizing-protections).
|
||||
|
||||
### Available Protections
|
||||
|
||||
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
|
||||
@@ -36,6 +28,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
||||
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles).
|
||||
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes)
|
||||
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer
|
||||
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours)
|
||||
|
||||
!!! Note "Durations"
|
||||
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
|
||||
@@ -44,7 +37,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
||||
#### Stoploss Guard
|
||||
|
||||
`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`).
|
||||
If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
|
||||
If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`).
|
||||
|
||||
This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time.
|
||||
|
||||
@@ -97,7 +90,7 @@ def protections(self):
|
||||
#### Low Profit Pairs
|
||||
|
||||
`LowProfitPairs` uses all trades for a pair within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the overall profit ratio.
|
||||
If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
|
||||
If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`).
|
||||
|
||||
For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long losses.
|
||||
|
||||
@@ -120,7 +113,7 @@ def protections(self):
|
||||
|
||||
#### Cooldown Period
|
||||
|
||||
`CooldownPeriod` locks a pair for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after selling, avoiding a re-entry for this pair for `stop_duration` minutes.
|
||||
`CooldownPeriod` locks a pair for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`) after exiting, avoiding a re-entry for this pair for `stop_duration` minutes.
|
||||
|
||||
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
|
||||
|
||||
|
||||
45
docs/includes/strategy-imports.md
Normal file
45
docs/includes/strategy-imports.md
Normal file
@@ -0,0 +1,45 @@
|
||||
## Imports necessary for a strategy
|
||||
|
||||
When creating a strategy, you will need to import the necessary modules and classes. The following imports are required for a strategy:
|
||||
|
||||
By default, we recommend the following imports as a base line for your strategy:
|
||||
This will cover all imports necessary for freqtrade functions to work.
|
||||
Obviously you can add more imports as needed for your strategy.
|
||||
|
||||
``` python
|
||||
# flake8: noqa: F401
|
||||
# isort: skip_file
|
||||
# --- Do not remove these imports ---
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pandas import DataFrame
|
||||
from typing import Dict, Optional, Union, Tuple
|
||||
|
||||
from freqtrade.strategy import (
|
||||
IStrategy,
|
||||
Trade,
|
||||
Order,
|
||||
PairLocks,
|
||||
informative, # @informative decorator
|
||||
# Hyperopt Parameters
|
||||
BooleanParameter,
|
||||
CategoricalParameter,
|
||||
DecimalParameter,
|
||||
IntParameter,
|
||||
RealParameter,
|
||||
# timeframe helpers
|
||||
timeframe_to_minutes,
|
||||
timeframe_to_next_date,
|
||||
timeframe_to_prev_date,
|
||||
# Strategy helper functions
|
||||
merge_informative_pair,
|
||||
stoploss_from_absolute,
|
||||
stoploss_from_open,
|
||||
)
|
||||
|
||||
# --------------------------------
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta
|
||||
from technical import qtpylib
|
||||
```
|
||||
@@ -28,7 +28,7 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
||||
- 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).
|
||||
- Download market data: Download historical data of the exchange and the markets your may want to trade with.
|
||||
- Backtest: Test your strategy on downloaded historical data.
|
||||
- Optimize: Find the best parameters for your strategy using hyperoptimization which employs machining learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy.
|
||||
- Optimize: Find the best parameters for your strategy using hyperoptimization which employs machine learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy.
|
||||
- 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.
|
||||
@@ -40,10 +40,12 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bitmart](https://bitmart.com/)
|
||||
- [X] [BingX](https://bingx.com/invite/0EM9RX)
|
||||
- [X] [Bitmart](https://bitmart.com/)
|
||||
- [X] [Bybit](https://bybit.com/)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [HTX](https://www.htx.com/) (Former Huobi)
|
||||
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
||||
- [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)_
|
||||
@@ -51,9 +53,10 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
||||
### Supported Futures Exchanges (experimental)
|
||||
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [OKX](https://okx.com/)
|
||||
- [X] [Bybit](https://bybit.com/)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [Hyperliquid](https://hyperliquid.xyz/) (A decentralized exchange, or DEX)
|
||||
- [X] [OKX](https://okx.com/)
|
||||
|
||||
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
||||
|
||||
@@ -84,7 +87,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of:
|
||||
|
||||
Alternatively
|
||||
|
||||
- Python 3.9+
|
||||
- Python 3.10+
|
||||
- pip (pip3)
|
||||
- git
|
||||
- TA-Lib
|
||||
|
||||
@@ -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.9 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.10 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"
|
||||
@@ -42,7 +42,7 @@ These requirements apply to both [Script Installation](#script-installation) and
|
||||
|
||||
### Install guide
|
||||
|
||||
* [Python >= 3.9](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
* [Python >= 3.10](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)
|
||||
@@ -54,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th
|
||||
OS Specific steps are listed first, the common section below is necessary for all systems.
|
||||
|
||||
!!! Note
|
||||
Python3.9 or higher and the corresponding pip are assumed to be available.
|
||||
Python3.10 or higher and the corresponding pip are assumed to be available.
|
||||
|
||||
=== "Debian/Ubuntu"
|
||||
#### Install necessary dependencies
|
||||
@@ -67,16 +67,28 @@ OS Specific steps are listed first, the common section below is necessary for al
|
||||
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git curl
|
||||
```
|
||||
|
||||
=== "MacOS"
|
||||
#### Install necessary dependencies
|
||||
|
||||
Install [Homebrew](https://brew.sh/) if you don't have it already.
|
||||
|
||||
```bash
|
||||
# install packages
|
||||
brew install gettext libomp
|
||||
```
|
||||
!!! Note
|
||||
The `setup.sh` script will install these dependencies for you - assuming brew is installed on your system.
|
||||
|
||||
=== "RaspberryPi/Raspbian"
|
||||
The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/).
|
||||
This image comes with python3.9 preinstalled, making it easy to get freqtrade up and running.
|
||||
This image comes with python3.11 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 curl
|
||||
# Use pywheels.org to speed up installation
|
||||
# Use piwheels.org to speed up installation
|
||||
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
|
||||
|
||||
git clone https://github.com/freqtrade/freqtrade.git
|
||||
@@ -150,9 +162,7 @@ Each time you open a new terminal, you must run `source .venv/bin/activate` to a
|
||||
source ./.venv/bin/activate
|
||||
```
|
||||
|
||||
### Congratulations
|
||||
|
||||
[You are ready](#you-are-ready), and run the bot
|
||||
[You are now ready](#you-are-ready) to run the bot.
|
||||
|
||||
### Other options of /setup.sh script
|
||||
|
||||
@@ -169,7 +179,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.9+ installed beforehand for this to work.
|
||||
You will need to have git and python3.10+ installed beforehand for this to work.
|
||||
|
||||
* Mandatory software as: `ta-lib`
|
||||
* Setup your virtualenv under `.venv/`
|
||||
@@ -220,7 +230,7 @@ cd ..
|
||||
rm -rf ./ta-lib*
|
||||
```
|
||||
|
||||
#### Setup Python virtual environment (virtualenv)
|
||||
### Setup Python virtual environment (virtualenv)
|
||||
|
||||
You will run freqtrade in separated `virtual environment`
|
||||
|
||||
@@ -232,19 +242,18 @@ python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
#### Install python dependencies
|
||||
### Install python dependencies
|
||||
|
||||
```bash
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install -r requirements.txt
|
||||
# install freqtrade
|
||||
python3 -m pip install -e .
|
||||
```
|
||||
|
||||
### Congratulations
|
||||
[You are now ready](#you-are-ready) to run the bot.
|
||||
|
||||
[You are ready](#you-are-ready), and run the bot
|
||||
|
||||
#### (Optional) Post-installation Tasks
|
||||
### (Optional) Post-installation Tasks
|
||||
|
||||
!!! Note
|
||||
If you run the bot on a server, you should consider using [Docker](docker_quickstart.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
|
||||
@@ -333,9 +342,7 @@ cd build_helpers
|
||||
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
|
||||
```
|
||||
|
||||
### Congratulations
|
||||
|
||||
[You are ready](#you-are-ready), and run the bot
|
||||
[You are now ready](#you-are-ready) to run the bot.
|
||||
|
||||
### Important shortcuts
|
||||
|
||||
|
||||
@@ -101,3 +101,4 @@ This could lead to a false-negative (the strategy will then be reported as non-b
|
||||
- `lookahead-analysis` has access to everything that backtesting has too.
|
||||
Please don't provoke any configs like enabling position stacking.
|
||||
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` amount and neither leftover money in your wallet.
|
||||
- In the results table, the `biased_indicators` column will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased. These are not biased and can safely be ignored.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
markdown==3.6
|
||||
mkdocs==1.6.0
|
||||
mkdocs-material==9.5.29
|
||||
markdown==3.7
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.5.45
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.8.1
|
||||
pymdown-extensions==10.12
|
||||
jinja2==3.1.4
|
||||
mike==2.1.3
|
||||
|
||||
@@ -36,6 +36,7 @@ The Order-type will be ignored if only one mode is available.
|
||||
| Gate | limit |
|
||||
| Okx | limit |
|
||||
| Kucoin | stop-limit, stop-market|
|
||||
| Hyperliquid (futures only) | limit |
|
||||
|
||||
!!! Note "Tight stoploss"
|
||||
<ins>Do not set too low/tight stoploss value when using stop loss on exchange!</ins>
|
||||
|
||||
196
docs/strategy-101.md
Normal file
196
docs/strategy-101.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Freqtrade Strategies 101: A Quick Start for Strategy Development
|
||||
|
||||
For the purposes of this quick start, we are assuming you are familiar with the basics of trading, and have read the
|
||||
[Freqtrade basics](bot-basics.md) page.
|
||||
|
||||
## Required Knowledge
|
||||
|
||||
A strategy in Freqtrade is a Python class that defines the logic for buying and selling cryptocurrency `assets`.
|
||||
|
||||
Assets are defined as `pairs`, which represent the `coin` and the `stake`. The coin is the asset you are trading using another currency as the stake.
|
||||
|
||||
Data is supplied by the exchange in the form of `candles`, which are made up of a six values: `date`, `open`, `high`, `low`, `close` and `volume`.
|
||||
|
||||
`Technical analysis` functions analyse the candle data using various computational and statistical formulae, and produce secondary values called `indicators`.
|
||||
|
||||
Indicators are analysed on the asset pair candles to generate `signals`.
|
||||
|
||||
Signals are turned into `orders` on a cryptocurrency `exchange`, i.e. `trades`.
|
||||
|
||||
We use the terms `entry` and `exit` instead of `buying` and `selling` because Freqtrade supports both `long` and `short` trades.
|
||||
|
||||
- **long**: You buy the coin based on a stake, e.g. buying the coin BTC using USDT as your stake, and you make a profit by selling the coin at a higher rate than you paid for. In long trades, profits are made by the coin value going up versus the stake.
|
||||
- **short**: You borrow capital from the exchange in the form of the coin, and you pay back the stake value of the coin later. In short trades profits are made by the coin value going down versus the stake (you pay the loan off at a lower rate).
|
||||
|
||||
Whilst Freqtrade supports spot and futures markets for certain exchanges, for simplicity we will focus on spot (long) trades only.
|
||||
|
||||
## Structure of a Basic Strategy
|
||||
|
||||
### Main dataframe
|
||||
|
||||
Freqtrade strategies use a tabular data structure with rows and columns known as a `dataframe` to generate signals to enter and exit trades.
|
||||
|
||||
Each pair in your configured pairlist has its own dataframe. Dataframes are indexed by the `date` column, e.g. `2024-06-31 12:00`.
|
||||
|
||||
The next 5 columns represent the `open`, `high`, `low`, `close` and `volume` (OHLCV) data.
|
||||
|
||||
### Populate indicator values
|
||||
|
||||
The `populate_indicators` function adds columns to the dataframe that represent the technical analysis indicator values.
|
||||
|
||||
Examples of common indicators include Relative Strength Index, Bollinger Bands, Money Flow Index, Moving Average, and Average True Range.
|
||||
|
||||
Columns are added to the dataframe by calling technical analysis functions, e.g. ta-lib's RSI function `ta.RSI()`, and assigning them to a column name, e.g. `rsi`
|
||||
|
||||
```python
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
```
|
||||
|
||||
??? Hint "Technical Analysis libraries"
|
||||
Different libraries work in different ways to generate indicator values. Please check the documentation of each library to understand
|
||||
how to integrate it into your strategy. You can also check the [Freqtrade example strategies](https://github.com/freqtrade/freqtrade-strategies) to give you ideas.
|
||||
|
||||
### Populate entry signals
|
||||
|
||||
The `populate_entry_trend` function defines conditions for an entry signal.
|
||||
|
||||
The dataframe column `enter_long` is added to the dataframe, and when a value of `1` is in this column, Freqtrade sees an entry signal.
|
||||
|
||||
??? Hint "Shorting"
|
||||
To enter short trades, use the `enter_short` column.
|
||||
|
||||
### Populate exit signals
|
||||
|
||||
The `populate_exit_trend` function defines conditions for an exit signal.
|
||||
|
||||
The dataframe column `exit_long` is added to the dataframe, and when a value of `1` is in this column, Freqtrade sees an exit signal.
|
||||
|
||||
??? Hint "Shorting"
|
||||
To exit short trades, use the `exit_short` column.
|
||||
|
||||
## A simple strategy
|
||||
|
||||
Here is a minimal example of a Freqtrade strategy:
|
||||
|
||||
```python
|
||||
from freqtrade.strategy import IStrategy
|
||||
from pandas import DataFrame
|
||||
import talib.abstract as ta
|
||||
|
||||
class MyStrategy(IStrategy):
|
||||
|
||||
# set the initial stoploss to -10%
|
||||
stoploss = -0.10
|
||||
|
||||
# exit profitable positions at any time when the profit is greater than 1%
|
||||
minimal_roi = {"0": 0.01}
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# generate values for technical analysis indicators
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# generate entry signals based on indicator values
|
||||
dataframe.loc[
|
||||
(dataframe['rsi'] < 30),
|
||||
'enter_long'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# generate exit signals based on indicator values
|
||||
dataframe.loc[
|
||||
(dataframe['rsi'] > 70),
|
||||
'exit_long'] = 1
|
||||
|
||||
return dataframe
|
||||
```
|
||||
|
||||
## Making trades
|
||||
|
||||
When a signal is found (a `1` in an entry or exit column), Freqtrade will attempt to make an order, i.e. a `trade` or `position`.
|
||||
|
||||
Each new trade position takes up a `slot`. Slots represent the maximum number of concurrent new trades that can be opened.
|
||||
|
||||
The number of slots is defined by the `max_open_trades` [configuration](configuration.md) option.
|
||||
|
||||
However, there can be a range of scenarios where generating a signal does not always create a trade order. These include:
|
||||
|
||||
- not enough remaining stake to buy an asset, or funds in your wallet to sell an asset (including any fees)
|
||||
- not enough remaining free slots for a new trade to be opened (the number of positions you have open equals the `max_open_trades` option)
|
||||
- there is already an open trade for a pair (Freqtrade cannot stack positions - however it can [adjust existing positions](strategy-callbacks.md#adjust-trade-position))
|
||||
- if an entry and exit signal is present on the same candle, they are considered as [colliding](strategy-customization.md#colliding-signals), and no order will be raised
|
||||
- the strategy actively rejects the trade order due to logic you specify by using one of the relevant [entry](strategy-callbacks.md#trade-entry-buy-order-confirmation) or [exit](strategy-callbacks.md#trade-exit-sell-order-confirmation) callbacks
|
||||
|
||||
Read through the [strategy customization](strategy-customization.md) documentation for more details.
|
||||
|
||||
## Backtesting and forward testing
|
||||
|
||||
Strategy development can be a long and frustrating process, as turning our human "gut instincts" into a working computer-controlled
|
||||
("algo") strategy is not always straightforward.
|
||||
|
||||
Therefore a strategy should be tested to verify that it is going to work as intended.
|
||||
|
||||
Freqtrade has two testing modes:
|
||||
|
||||
- **backtesting**: using historical data that you [download from an exchange](data-download.md), backtesting is a quick way to assess performance of a strategy. However, it can be very easy to distort results so a strategy will look a lot more profitable than it really is. Check the [backtesting documentation](backtesting.md) for more information.
|
||||
- **dry run**: often referred to as _forward testing_, dry runs use real time data from the exchange. However, any signals that would result in trades are tracked as normal by Freqtrade, but do not have any trades opened on the exchange itself. Forward testing runs in real time, so whilst it takes longer to get results it is a much more reliable indicator of **potential** performance than backtesting.
|
||||
|
||||
Dry runs are enabled by setting `dry_run` to true in your [configuration](configuration.md#using-dry-run-mode).
|
||||
|
||||
!!! Warning "Backtests can be very inaccurate"
|
||||
There are many reasons why backtest results may not match reality. Please check the [backtesting assumptions](backtesting.md#assumptions-made-by-backtesting) and [common strategy mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) documentation.
|
||||
Some websites that list and rank Freqtrade strategies show impressive backtest results. Do not assume these results are achieveable or realistic.
|
||||
|
||||
??? Hint "Useful commands"
|
||||
Freqtrade includes two useful commands to check for basic flaws in strategies: [lookahead-analysis](lookahead-analysis.md) and [recursive-analysis](recursive-analysis.md).
|
||||
|
||||
### Assessing backtesting and dry run results
|
||||
|
||||
Always dry run your strategy after backtesting it to see if backtesting and dry run results are sufficiently similar.
|
||||
|
||||
If there is any significant difference, verify that your entry and exit signals are consistent and appear on the same candles between the two modes. However, there will always be differences between dry runs and backtests:
|
||||
|
||||
- Backtesting assumes all orders fill. In dry runs this might not be the case if using limit orders or there is no volume on the exchange.
|
||||
- Following an entry signal on candle close, backtesting assumes trades enter at the next candle's open price (unless you have custom pricing callbacks in your strategy). In dry runs, there is often a delay between signals and trades opening.
|
||||
This is because when new candles come in on your main timeframe, e.g. every 5 minutes, it takes time for Freqtrade to analyse all pair dataframes. Therefore, Freqtrade will attempt to open trades a few seconds (ideally a small a delay as possible)
|
||||
after candle open.
|
||||
- As entry rates in dry runs might not match backtesting, this means profit calculations will also differ. Therefore, it is normal if ROI, stoploss, trailing stoploss and callback exits are not identical.
|
||||
- The more computational "lag" you have between new candles coming in and your signals being raised and trades being opened will result in greater price unpredictability. Make sure your computer is powerful enough to process the data for the number
|
||||
of pairs you have in your pairlist within a reasonable time. Freqtrade will warn you in the logs if there are significant data processing delays.
|
||||
|
||||
## Controlling or monitoring a running bot
|
||||
|
||||
Once your bot is running in dry or live mode, Freqtrade has five mechanisms to control or monitor a running bot:
|
||||
|
||||
- **[FreqUI](freq-ui.md)**: The easiest to get started with, FreqUI is a web interface to see and control current activity of your bot.
|
||||
- **[Telegram](telegram-usage.md)**: On mobile devices, Telegram integration is available to get alerts about your bot activity and to control certain aspects.
|
||||
- **[FTUI](https://github.com/freqtrade/ftui)**: FTUI is a terminal (command line) interface to Freqtrade, and allows monitoring of a running bot only.
|
||||
- **[REST API](rest-api.md)**: The REST API allows programmers to develop their own tools to interact with a Freqtrade bot.
|
||||
- **[Webhooks](webhook-config.md)**: Freqtrade can send information to other services, e.g. discord, by webhooks.
|
||||
|
||||
### Logs
|
||||
|
||||
Freqtrade generates extensive debugging logs to help you understand what's happening. Please familiarise yourself with the information and error messages you might see in your bot logs.
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
Algo trading is difficult, and most public strategies are not good performers due to the time and effort to make a strategy work profitably in multiple scenarios.
|
||||
|
||||
Therefore, taking public strategies and using backtests as a way to assess performance is often problematic. However, Freqtrade provides useful ways to help you make decisions and do your due diligence.
|
||||
|
||||
There are many different ways to achieve profitability, and there is no one single tip, trick or config option that will fix a poorly performing strategy.
|
||||
|
||||
Freqtrade is an open source platform with a large and helpful community - make sure to visit our [discord channel](https://discord.gg/p7nuUNVfP7) to discuss your strategy with others!
|
||||
|
||||
As always, only invest what you are willing to lose.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Developing a strategy in Freqtrade involves defining entry and exit signals based on technical indicators. By following the structure and methods outlined above, you can create and test your own trading strategies.
|
||||
|
||||
Common questions and answers are available on our [FAQ](faq.md).
|
||||
|
||||
To continue, refer to the more in-depth [Freqtrade strategy customization documentation](strategy-customization.md).
|
||||
@@ -38,9 +38,9 @@ class AwesomeStrategy(IStrategy):
|
||||
trade.set_custom_data(key='entry_type', value=trade_entry_type)
|
||||
return super().bot_loop_start(**kwargs)
|
||||
|
||||
def adjust_entry_price(self, trade: Trade, order: Optional[Order], pair: str,
|
||||
def adjust_entry_price(self, trade: Trade, order: Order | None, pair: str,
|
||||
current_time: datetime, proposed_rate: float, current_order_rate: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||
if (
|
||||
pair == 'BTC/USDT'
|
||||
|
||||
@@ -24,6 +24,8 @@ Currently available callbacks:
|
||||
!!! Tip "Callback calling sequence"
|
||||
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
||||
|
||||
--8<-- "includes/strategy-imports.md"
|
||||
|
||||
## Bot start
|
||||
|
||||
A simple callback which is called once when the strategy is loaded.
|
||||
@@ -41,10 +43,10 @@ class AwesomeStrategy(IStrategy):
|
||||
Called only once after bot instantiation.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||
if self.config["runmode"].value in ("live", "dry_run"):
|
||||
# Assign this to the class by using self.*
|
||||
# can then be used by populate_* methods
|
||||
self.custom_remote_data = requests.get('https://some_remote_source.example.com')
|
||||
self.custom_remote_data = requests.get("https://some_remote_source.example.com")
|
||||
|
||||
```
|
||||
|
||||
@@ -57,6 +59,7 @@ seconds, unless configured differently) or once per candle in backtest/hyperopt
|
||||
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
|
||||
|
||||
``` python
|
||||
# Default imports
|
||||
import requests
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
@@ -71,10 +74,10 @@ class AwesomeStrategy(IStrategy):
|
||||
: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.
|
||||
"""
|
||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||
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')
|
||||
self.remote_data = requests.get("https://some_remote_source.example.com")
|
||||
|
||||
```
|
||||
|
||||
@@ -83,22 +86,24 @@ class AwesomeStrategy(IStrategy):
|
||||
Called before entering a trade, makes it possible to manage your position size when placing a new trade.
|
||||
|
||||
```python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
leverage: float, entry_tag: Optional[str], side: str,
|
||||
proposed_stake: float, min_stake: float | None, max_stake: float,
|
||||
leverage: float, entry_tag: str | None, side: 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':
|
||||
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']
|
||||
return self.wallets.get_total_stake_amount() / self.config["max_open_trades"]
|
||||
|
||||
# Use default stake amount.
|
||||
return proposed_stake
|
||||
@@ -129,25 +134,27 @@ Using `custom_exit()` signals in place of stoploss though *is not recommended*.
|
||||
An example of how we can use different indicators depending on the current profit and also exit trades that were open longer than one day:
|
||||
|
||||
``` python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||
def custom_exit(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'
|
||||
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'
|
||||
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'
|
||||
return "unclog"
|
||||
```
|
||||
|
||||
See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||
@@ -158,7 +165,8 @@ Called for open trade every iteration (roughly every 5 seconds) until a trade is
|
||||
|
||||
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), and is still mandatory.
|
||||
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), and is still mandatory.
|
||||
As custom stoploss acts as regular, changing stoploss, it will behave similar to `trailing_stop` - and trades exiting due to this will have the exit_reason of `"trailing_stop_loss"`.
|
||||
|
||||
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.
|
||||
@@ -168,7 +176,6 @@ The absolute value of the return value is used (the sign is ignored), so returni
|
||||
Returning `None` will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
|
||||
`NaN` and `inf` values are considered invalid and will be ignored (identical to `None`).
|
||||
|
||||
|
||||
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-exchangefreqtrade)).
|
||||
|
||||
!!! Note "Use of dates"
|
||||
@@ -196,9 +203,7 @@ Of course, many more things are possible, and all examples can be combined at wi
|
||||
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
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -206,9 +211,9 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
"""
|
||||
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.
|
||||
@@ -236,8 +241,7 @@ class AwesomeStrategy(IStrategy):
|
||||
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
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -245,9 +249,9 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
|
||||
# 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:
|
||||
@@ -263,8 +267,7 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai
|
||||
If an additional order fills, set stoploss to -10% below the new `open_rate` ([Averaged across all entries](#position-adjust-calculations)).
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -272,9 +275,9 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
|
||||
if after_fill:
|
||||
# After an additional order, start with a stoploss of 10% below the new open rate
|
||||
@@ -293,8 +296,7 @@ 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
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -302,13 +304,13 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
|
||||
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||
if pair in ("ETH/BTC", "XRP/BTC"):
|
||||
return -0.10
|
||||
elif pair in ('LTC/BTC'):
|
||||
elif pair in ("LTC/BTC"):
|
||||
return -0.05
|
||||
return -0.15
|
||||
```
|
||||
@@ -320,8 +322,7 @@ Use the initial stoploss until the profit is above 4%, then use a trailing stopl
|
||||
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
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -329,9 +330,9 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
|
||||
if current_profit < 0.04:
|
||||
return None # return None to keep using the initial stoploss
|
||||
@@ -353,9 +354,7 @@ Instead of continuously trailing behind the current price, this example sets fix
|
||||
* 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
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -363,9 +362,9 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
|
||||
# evaluate highest to lowest, so that highest possible stop is used
|
||||
if current_profit > 0.40:
|
||||
@@ -384,23 +383,25 @@ class AwesomeStrategy(IStrategy):
|
||||
Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss.
|
||||
|
||||
``` python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# <...>
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
dataframe["sar"] = ta.SAR(dataframe)
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
|
||||
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']
|
||||
stoploss_price = last_candle["sar"]
|
||||
|
||||
# Convert absolute price to percentage relative to current_rate
|
||||
if stoploss_price < current_rate:
|
||||
@@ -429,10 +430,7 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela
|
||||
|
||||
|
||||
``` python
|
||||
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import IStrategy, stoploss_from_open
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -440,9 +438,9 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
|
||||
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||
if current_profit > 0.10:
|
||||
@@ -469,38 +467,34 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
|
||||
|
||||
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
||||
|
||||
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate=current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
|
||||
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle["atr"] * 2), current_rate=current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
|
||||
For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement.
|
||||
|
||||
``` python
|
||||
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy import IStrategy, stoploss_from_absolute, timeframe_to_prev_date
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
use_custom_stoploss = True
|
||||
|
||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
|
||||
candle = dataframe.iloc[-1].squeeze()
|
||||
side = 1 if trade.is_short else -1
|
||||
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
|
||||
return stoploss_from_absolute(current_rate + (side * candle["atr"] * 2),
|
||||
current_rate=current_rate,
|
||||
is_short=trade.is_short,
|
||||
leverage=trade.leverage)
|
||||
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Custom order price rules
|
||||
@@ -520,29 +514,28 @@ Each of these methods are called right before placing an order on the exchange.
|
||||
### Custom order entry and exit price example
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from freqtrade.persistence import Trade
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def custom_entry_price(self, pair: str, trade: Optional['Trade'], current_time: datetime, proposed_rate: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
|
||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||
timeframe=self.timeframe)
|
||||
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||
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, exit_tag: Optional[str], **kwargs) -> float:
|
||||
current_profit: float, exit_tag: str | None, **kwargs) -> float:
|
||||
|
||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||
timeframe=self.timeframe)
|
||||
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
||||
new_exitprice = dataframe["bollinger_10_upperband"].iat[-1]
|
||||
|
||||
return new_exitprice
|
||||
|
||||
@@ -579,8 +572,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to
|
||||
The function must return either `True` (cancel order) or `False` (keep order alive).
|
||||
|
||||
``` python
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade, Order
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -588,11 +580,11 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
|
||||
unfilledtimeout = {
|
||||
'entry': 60 * 25,
|
||||
'exit': 60 * 25
|
||||
"entry": 60 * 25,
|
||||
"exit": 60 * 25
|
||||
}
|
||||
|
||||
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||
def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||
return True
|
||||
@@ -603,7 +595,7 @@ class AwesomeStrategy(IStrategy):
|
||||
return False
|
||||
|
||||
|
||||
def check_exit_timeout(self, pair: str, trade: Trade, order: 'Order',
|
||||
def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||
return True
|
||||
@@ -620,8 +612,7 @@ class AwesomeStrategy(IStrategy):
|
||||
### Custom order timeout example (using additional data)
|
||||
|
||||
``` python
|
||||
from datetime import datetime
|
||||
from freqtrade.persistence import Trade, Order
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -629,24 +620,24 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
|
||||
unfilledtimeout = {
|
||||
'entry': 60 * 25,
|
||||
'exit': 60 * 25
|
||||
"entry": 60 * 25,
|
||||
"exit": 60 * 25
|
||||
}
|
||||
|
||||
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||
def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
ob = self.dp.orderbook(pair, 1)
|
||||
current_price = ob['bids'][0][0]
|
||||
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_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||
def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
ob = self.dp.orderbook(pair, 1)
|
||||
current_price = ob['asks'][0][0]
|
||||
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
|
||||
@@ -665,12 +656,14 @@ This are the last methods that will be called before an order is placed.
|
||||
`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
|
||||
# Default imports
|
||||
|
||||
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],
|
||||
time_in_force: str, current_time: datetime, entry_tag: str | None,
|
||||
side: str, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a entry order.
|
||||
@@ -689,7 +682,7 @@ class AwesomeStrategy(IStrategy):
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param side: "long" or "short" - indicating the direction of the proposed trade
|
||||
: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
|
||||
@@ -711,8 +704,7 @@ The exit-reasons (if applicable) will be in the following sequence:
|
||||
* `trailing_stop_loss`
|
||||
|
||||
``` python
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
@@ -738,14 +730,14 @@ class AwesomeStrategy(IStrategy):
|
||||
or current rate for market orders.
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param exit_reason: Exit reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
'exit_signal', 'force_exit', 'emergency_exit']
|
||||
Can be any of ["roi", "stop_loss", "stoploss_on_exchange", "trailing_stop_loss",
|
||||
"exit_signal", "force_exit", "emergency_exit"]
|
||||
: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, then the exit-order is placed on the exchange.
|
||||
False aborts the process
|
||||
"""
|
||||
if exit_reason == 'force_exit' and trade.calc_profit_ratio(rate) < 0:
|
||||
if exit_reason == "force_exit" 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)
|
||||
@@ -771,7 +763,7 @@ This callback is **not** called when there is an open order (either buy or sell)
|
||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||
|
||||
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
||||
Adjustment orders can be assigned with a tag by returning a 2 element Tuple, with the first element being the adjustment amount, and the 2nd element the tag (e.g. `return 250, 'increase_favorable_conditions'`).
|
||||
Adjustment orders can be assigned with a tag by returning a 2 element Tuple, with the first element being the adjustment amount, and the 2nd element the tag (e.g. `return 250, "increase_favorable_conditions"`).
|
||||
|
||||
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
||||
|
||||
@@ -793,7 +785,7 @@ Returning a value more than the above (so remaining stake_amount would become ne
|
||||
!!! 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.
|
||||
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 calculation"
|
||||
Stoploss is still calculated from the initial opening price, not averaged price.
|
||||
@@ -811,9 +803,7 @@ Returning a value more than the above (so remaining stake_amount would become ne
|
||||
Trades with long duration and 10s or even 100ds of position adjustments are therefore not recommended, and should be closed at regular intervals to not affect performance.
|
||||
|
||||
``` python
|
||||
from freqtrade.persistence import Trade
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
# Default imports
|
||||
|
||||
class DigDeeperStrategy(IStrategy):
|
||||
|
||||
@@ -831,8 +821,8 @@ class DigDeeperStrategy(IStrategy):
|
||||
|
||||
# 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: Optional[float], max_stake: float,
|
||||
leverage: float, entry_tag: Optional[str], side: str,
|
||||
proposed_stake: float, min_stake: float | None, max_stake: float,
|
||||
leverage: float, entry_tag: str | None, side: str,
|
||||
**kwargs) -> float:
|
||||
|
||||
# We need to leave most of the funds for possible further DCA orders
|
||||
@@ -841,11 +831,11 @@ class DigDeeperStrategy(IStrategy):
|
||||
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float,
|
||||
min_stake: Optional[float], max_stake: float,
|
||||
min_stake: float | None, max_stake: float,
|
||||
current_entry_rate: float, current_exit_rate: float,
|
||||
current_entry_profit: float, current_exit_profit: float,
|
||||
**kwargs
|
||||
) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]:
|
||||
) -> float | None | tuple[float | None, str | None]:
|
||||
"""
|
||||
Custom trade adjustment logic, returning the stake amount that a trade should be
|
||||
increased or decreased.
|
||||
@@ -876,7 +866,7 @@ class DigDeeperStrategy(IStrategy):
|
||||
|
||||
if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
|
||||
# Take half of the profit at +5%
|
||||
return -(trade.stake_amount / 2), 'half_profit_5%'
|
||||
return -(trade.stake_amount / 2), "half_profit_5%"
|
||||
|
||||
if current_profit > -0.05:
|
||||
return None
|
||||
@@ -886,7 +876,7 @@ class DigDeeperStrategy(IStrategy):
|
||||
# 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']:
|
||||
if last_candle["close"] < previous_candle["close"]:
|
||||
return None
|
||||
|
||||
filled_entries = trade.select_filled_orders(trade.entry_side)
|
||||
@@ -901,10 +891,10 @@ class DigDeeperStrategy(IStrategy):
|
||||
# Hope you have a deep wallet!
|
||||
try:
|
||||
# This returns first order stake size
|
||||
stake_amount = filled_entries[0].stake_amount
|
||||
stake_amount = filled_entries[0].stake_amount_filled
|
||||
# This then calculates current safety order size
|
||||
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
|
||||
return stake_amount, '1/3rd_increase'
|
||||
return stake_amount, "1/3rd_increase"
|
||||
except Exception as exception:
|
||||
return None
|
||||
|
||||
@@ -951,16 +941,15 @@ If the cancellation of the original order fails, then the order will not be repl
|
||||
Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
||||
|
||||
```python
|
||||
from freqtrade.persistence import Trade
|
||||
from datetime import timedelta, datetime
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def adjust_entry_price(self, trade: Trade, order: Optional[Order], pair: str,
|
||||
def adjust_entry_price(self, trade: Trade, order: Order | None, pair: str,
|
||||
current_time: datetime, proposed_rate: float, current_order_rate: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
"""
|
||||
Entry price re-adjustment logic, returning the user desired limit price.
|
||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||
@@ -977,13 +966,18 @@ class AwesomeStrategy(IStrategy):
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param side: "long" or "short" - indicating the direction of the proposed trade
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
|
||||
"""
|
||||
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||
if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10)) > trade.open_date_utc:
|
||||
if (
|
||||
pair == "BTC/USDT"
|
||||
and entry_tag == "long_sma200"
|
||||
and side == "long"
|
||||
and (current_time - timedelta(minutes=10)) <= trade.open_date_utc
|
||||
):
|
||||
# just cancel the order if it has been filled more than half of the amount
|
||||
if order.filled > order.remaining:
|
||||
return None
|
||||
@@ -991,7 +985,7 @@ class AwesomeStrategy(IStrategy):
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
|
||||
current_candle = dataframe.iloc[-1].squeeze()
|
||||
# desired price
|
||||
return current_candle['sma_200']
|
||||
return current_candle["sma_200"]
|
||||
# default: maintain existing order
|
||||
return current_order_rate
|
||||
```
|
||||
@@ -1006,9 +1000,11 @@ Values that are above `max_leverage` will be adjusted to `max_leverage`.
|
||||
For markets / exchanges that don't support leverage, this method is ignored.
|
||||
|
||||
``` python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: str | None, side: str,
|
||||
**kwargs) -> float:
|
||||
"""
|
||||
Customize leverage for each new trade. This method is only called in futures mode.
|
||||
@@ -1019,7 +1015,7 @@ class AwesomeStrategy(IStrategy):
|
||||
:param proposed_leverage: A leverage proposed by the bot.
|
||||
:param max_leverage: Max leverage allowed on this pair
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param side: "long" or "short" - indicating the direction of the proposed trade
|
||||
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||
"""
|
||||
return 1.0
|
||||
@@ -1036,6 +1032,8 @@ It will be called independent of the order type (entry, exit, stoploss or positi
|
||||
Assuming that your strategy needs to store the high value of the candle at trade entry, this is possible with this callback as the following example show.
|
||||
|
||||
``` python
|
||||
# Default imports
|
||||
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
|
||||
"""
|
||||
@@ -1052,7 +1050,7 @@ class AwesomeStrategy(IStrategy):
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side):
|
||||
trade.set_custom_data(key='entry_candle_high', value=last_candle['high'])
|
||||
trade.set_custom_data(key="entry_candle_high", value=last_candle["high"])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -2,52 +2,93 @@
|
||||
|
||||
This page explains how to customize your strategies, add new indicators and set up trading rules.
|
||||
|
||||
Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates.
|
||||
If you haven't already, please familiarize yourself with:
|
||||
|
||||
- the [Freqtrade strategy 101](strategy-101.md), which provides a quick start to strategy development
|
||||
- the [Freqtrade bot basics](bot-basics.md), which provides overall info on how the bot operates
|
||||
|
||||
## 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 convert your strategy idea into your own strategy.
|
||||
|
||||
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`.
|
||||
This document intends to help you convert your ideas into a working strategy.
|
||||
|
||||
### Generating a strategy template
|
||||
|
||||
To get started, you can use the command:
|
||||
|
||||
```bash
|
||||
freqtrade new-strategy --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
This will create a new strategy called `AwesomeStrategy` from a template, which will be located using the filename `user_data/strategies/AwesomeStrategy.py`.
|
||||
|
||||
!!! Note
|
||||
This is just a template file, which will most likely not be profitable out of the box.
|
||||
There is a difference between the *name* of the strategy and the filename. In most commands, Freqtrade uses the *name* of the strategy, *not the filename*.
|
||||
|
||||
!!! Note
|
||||
The `new-strategy` command generates starting examples which will 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.
|
||||
`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 more complicated features defined.
|
||||
|
||||
### Anatomy of a strategy
|
||||
|
||||
A strategy file contains all the information needed to build a good strategy:
|
||||
A strategy file contains all the information needed to build the strategy logic:
|
||||
|
||||
- Candle data in OHLCV format
|
||||
- Indicators
|
||||
- Entry strategy rules
|
||||
- Exit strategy rules
|
||||
- Minimal ROI recommended
|
||||
- Stoploss strongly recommended
|
||||
- Entry logic
|
||||
- Signals
|
||||
- Exit logic
|
||||
- Signals
|
||||
- Minimal ROI
|
||||
- Callbacks ("custom functions")
|
||||
- Stoploss
|
||||
- Fixed/absolute
|
||||
- Trailing
|
||||
- Callbacks ("custom functions")
|
||||
- Pricing [optional]
|
||||
- Position adjustment [optional]
|
||||
|
||||
The bot also include a sample strategy called `SampleStrategy` you can update: `user_data/strategies/sample_strategy.py`.
|
||||
You can test it with the parameter: `--strategy SampleStrategy`
|
||||
The bot includes a sample strategy called `SampleStrategy` that you can use as a basis: `user_data/strategies/sample_strategy.py`.
|
||||
You can test it with the parameter: `--strategy SampleStrategy`. Remember that you use the strategy class name, not the filename.
|
||||
|
||||
Additionally, there is an attribute called `INTERFACE_VERSION`, which defines the version of the strategy interface the bot should use.
|
||||
The current version is 3 - which is also the default when it's not set explicitly in the strategy.
|
||||
|
||||
Future versions will require this to be set.
|
||||
You may see older strategies set to interface version 2, and these will need to be updated to v3 terminology as future versions will require this to be set.
|
||||
|
||||
Starting the bot in dry or live mode is accomplished using the `trade` command:
|
||||
|
||||
```bash
|
||||
freqtrade trade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
### Bot modes
|
||||
|
||||
Freqtrade strategies can be processed by the Freqtrade bot in 5 main modes:
|
||||
|
||||
- backtesting
|
||||
- hyperopting
|
||||
- dry ("forward testing")
|
||||
- live
|
||||
- FreqAI (not covered here)
|
||||
|
||||
Check the [configuration documentation](configuration.md) about how to set the bot to dry or live mode.
|
||||
|
||||
**Always use dry mode when testing as this gives you an idea of how your strategy will work in reality without risking capital.**
|
||||
|
||||
## Diving in deeper
|
||||
**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py)
|
||||
file as reference.**
|
||||
|
||||
!!! Note "Strategies and Backtesting"
|
||||
To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware
|
||||
To avoid problems and unexpected differences between backtesting and dry/live modes, please be aware
|
||||
that during backtesting the full time range is passed to the `populate_*()` methods at once.
|
||||
It is therefore best to use vectorized operations (across the whole dataframe, not loops) and
|
||||
avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle.
|
||||
@@ -57,14 +98,22 @@ 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.
|
||||
|
||||
??? Hint "Lookahead and recursive analysis"
|
||||
Freqtrade includes two helpful commands to help assess common lookahead (using future data) and
|
||||
recursive bias (variance in indicator values) issues. Before running a strategy in dry or live more,
|
||||
you should always use these commands first. Please check the relevant documentation for
|
||||
[lookahead](lookahead-analysis.md) and [recursive](recursive-analysis.md) analysis.
|
||||
|
||||
### 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.
|
||||
Pandas is a great library developed for processing large amounts of data in tabular format.
|
||||
|
||||
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).
|
||||
Each row in a dataframe corresponds to one candle on a chart, with the latest complete candle always being the last in the dataframe (sorted by date).
|
||||
|
||||
``` output
|
||||
If we were to look at the first few rows of the main dataframe using the pandas `head()` function, we would see:
|
||||
|
||||
```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
|
||||
@@ -74,20 +123,16 @@ Each row in a dataframe corresponds to one candle on a chart, with the latest ca
|
||||
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
|
||||
A dataframe is a table where columns are not single values, but a series of data values. As such, simple python comparisons like the following will not work:
|
||||
|
||||
``` python
|
||||
if dataframe['rsi'] > 30:
|
||||
dataframe['enter_long'] = 1
|
||||
```
|
||||
|
||||
The above section will fail with `The truth value of a Series is ambiguous. [...]`.
|
||||
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.
|
||||
This must instead be written in a pandas-compatible way, so the operation is performed across the whole dataframe, i.e. `vectorisation`.
|
||||
|
||||
``` python
|
||||
dataframe.loc[
|
||||
@@ -97,13 +142,38 @@ This must instead be written in a pandas-compatible way, so the operation is per
|
||||
|
||||
With this section, you have a new column in your dataframe, which has `1` assigned whenever RSI is above 30.
|
||||
|
||||
Freqtrade uses this new column as an entry signal, where it is assumed that a trade will subsequently open on the next open candle.
|
||||
|
||||
Pandas provides fast ways to calculate metrics, i.e. "vectorisation". To benefit from this speed, it is 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.
|
||||
|
||||
??? Hint "Signals vs Trades"
|
||||
- Signals are generated from indicators at candle close, and are intentions to enter a trade.
|
||||
- Trades are orders that are executed (on the exchange in live mode) where a trade will then open as close to next candle open as possible.
|
||||
|
||||
!!! Warning "Trade order assumptions"
|
||||
In backtesting, signals are generated on candle close. Trades are then initiated immeditely on next candle open.
|
||||
|
||||
In dry and live, this may be delayed due to all pair dataframes needing to be analysed first, then trade processing
|
||||
for each of those pairs happens. This means that in dry/live you need to be mindful of having as low a computation
|
||||
delay as possible, usually by running a low number of pairs and having a CPU with a good clock speed.
|
||||
|
||||
#### Why can't I see "real time" candle data?
|
||||
|
||||
Freqtrade does not store incomplete/unfinished candles in the dataframe.
|
||||
|
||||
The use of incomplete data for making strategy decisions is called "repainting" and you might see other platforms allow this.
|
||||
|
||||
Freqtrade does not. Only complete/finished candle data is available in the dataframe.
|
||||
|
||||
### Customize Indicators
|
||||
|
||||
Buy and sell signals need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||
Entry and exit signals need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||
|
||||
You should only add the indicators used in either `populate_entry_trend()`, `populate_exit_trend()`, or to populate another indicator, otherwise performance may suffer.
|
||||
|
||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||
It's important to always return the dataframe from these three functions without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||
|
||||
Sample:
|
||||
|
||||
@@ -124,7 +194,7 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||
stoch = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch['fastd']
|
||||
dataframe['fastk'] = stoch['fastk']
|
||||
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||
dataframe['bb_lower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
@@ -145,6 +215,8 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# remember to always return the dataframe
|
||||
return dataframe
|
||||
```
|
||||
|
||||
@@ -158,17 +230,19 @@ Out of the box, freqtrade installs the following technical libraries:
|
||||
|
||||
- [ta-lib](https://ta-lib.github.io/ta-lib-python/)
|
||||
- [pandas-ta](https://twopirllc.github.io/pandas-ta/)
|
||||
- [technical](https://github.com/freqtrade/technical/)
|
||||
- [technical](https://technical.freqtrade.io)
|
||||
|
||||
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 (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
|
||||
Some indicators have an unstable startup period in which there isn't enough candle data to calculate any values (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this unstable period is and uses whatever indicator values are in the dataframe.
|
||||
|
||||
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. In the case where a user includes higher timeframes with informative pairs, the `startup_candle_count` does not necessarily change. The value is the maximum period (in candles) that any of the informatives timeframes need to compute stable indicators.
|
||||
|
||||
You can use [recursive-analysis](recursive-analysis.md) to check and find the correct `startup_candle_count` to be used.
|
||||
You can use [recursive-analysis](recursive-analysis.md) to check and find the correct `startup_candle_count` to be used. When recursive analysis shows a variance of 0%, then you can be sure that you have enough startup candle data.
|
||||
|
||||
In this example strategy, this should be set to 400 (`startup_candle_count = 400`), since the minimum needed history for ema100 calculation to make sure the value is correct is 400 candles.
|
||||
|
||||
@@ -195,19 +269,22 @@ Let's try to backtest 1 month (January 2019) of 5m candles using an example stra
|
||||
freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m
|
||||
```
|
||||
|
||||
Assuming `startup_candle_count` is set to 400, backtesting knows it needs 400 candles to generate valid buy signals. It will load data from `20190101 - (400 * 5m)` - which is ~2018-12-30 11:40:00.
|
||||
If this data is available, indicators will be calculated with this extended timerange. The instable startup period (up to 2019-01-01 00:00:00) will then be removed before starting backtesting.
|
||||
Assuming `startup_candle_count` is set to 400, backtesting knows it needs 400 candles to generate valid entry signals. It will load data from `20190101 - (400 * 5m)` - which is ~2018-12-30 11:40:00.
|
||||
|
||||
!!! Note
|
||||
If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-02 09:20:00.
|
||||
If this data is available, indicators will be calculated with this extended timerange. The unstable startup period (up to 2019-01-01 00:00:00) will then be removed before backtesting is carried out.
|
||||
|
||||
!!! Note "Unavailable startup candle data"
|
||||
If data for the startup period is not available, then the timerange will be adjusted to account for this startup period. In our example, backtesting would then start from 2019-01-02 09:20:00.
|
||||
|
||||
### Entry signal rules
|
||||
|
||||
Edit the method `populate_entry_trend()` in your strategy file to update your entry strategy.
|
||||
|
||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. The strategy may then produce invalid values, or cease to work entirely.
|
||||
|
||||
This method will also define a new column, `"enter_long"` (`"enter_short"` for shorts), which needs to contain 1 for entries, and 0 for "no action". `enter_long` is a mandatory column that must be set even if the strategy is shorting only.
|
||||
This method will also define a new column, `"enter_long"` (`"enter_short"` for shorts), which needs to contain `1` for entries, and `0` for "no action". `enter_long` is a mandatory column that must be set even if the strategy is shorting only.
|
||||
|
||||
You can name your entry signals by using the `"enter_tag"` column, which can help debug and assess your strategy later.
|
||||
|
||||
Sample from `user_data/strategies/sample_strategy.py`:
|
||||
|
||||
@@ -232,12 +309,15 @@ def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFram
|
||||
```
|
||||
|
||||
??? Note "Enter short trades"
|
||||
Short-entries can be created by setting `enter_short` (corresponds to `enter_long` for long trades).
|
||||
Short entries can be created by setting `enter_short` (corresponds to `enter_long` for long trades).
|
||||
The `enter_tag` column remains identical.
|
||||
Short-trades need to be supported by your exchange and market configuration!
|
||||
Please make sure to set [`can_short`]() appropriately on your strategy if you intend to short.
|
||||
Shorting needs to be supported by your exchange and market configuration!
|
||||
Also, make sure you set [`can_short`](#can-short) appropriately on your strategy if you intend to short.
|
||||
|
||||
```python
|
||||
# allow both long and short trades
|
||||
can_short = True
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
@@ -261,17 +341,21 @@ def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFram
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Buying requires sellers to buy from - therefore volume needs to be > 0 (`dataframe['volume'] > 0`) to make sure that the bot does not buy/sell in no-activity periods.
|
||||
Buying requires sellers to buy from. Therefore volume needs to be > 0 (`dataframe['volume'] > 0`) to make sure that the bot does not buy/sell in no-activity periods.
|
||||
|
||||
### Exit signal rules
|
||||
|
||||
Edit the method `populate_exit_trend()` into your strategy file to update your exit strategy.
|
||||
|
||||
The exit-signal can be suppressed by setting `use_exit_signal` to false in the configuration or strategy.
|
||||
|
||||
`use_exit_signal` will not influence [signal collision rules](#colliding-signals) - which will still apply and can prevent entries.
|
||||
|
||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected. The strategy may then produce invalid values, or cease to work entirely.
|
||||
|
||||
This method will also define a new column, `"exit_long"` (`"exit_short"` for shorts), which needs to contain 1 for exits, and 0 for "no action".
|
||||
This method will also define a new column, `"exit_long"` (`"exit_short"` for shorts), which needs to contain `1` for exits, and `0` for "no action".
|
||||
|
||||
You can name your exit signals by using the `"exit_tag"` column, which can help debug and assess your strategy later.
|
||||
|
||||
Sample from `user_data/strategies/sample_strategy.py`:
|
||||
|
||||
@@ -295,11 +379,15 @@ def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||
```
|
||||
|
||||
??? Note "Exit short trades"
|
||||
Short-exits can be created by setting `exit_short` (corresponds to `exit_long`).
|
||||
Short exits can be created by setting `exit_short` (corresponds to `exit_long`).
|
||||
The `exit_tag` column remains identical.
|
||||
Short-trades need to be supported by your exchange and market configuration!
|
||||
Shorting needs to be supported by your exchange and market configuration!
|
||||
Also, make sure you set [`can_short`](#can-short) appropriately on your strategy if you intend to short.
|
||||
|
||||
```python
|
||||
# allow both long and short trades
|
||||
can_short = True
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[
|
||||
(
|
||||
@@ -322,9 +410,9 @@ def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||
|
||||
### Minimal ROI
|
||||
|
||||
This dict defines the minimal Return On Investment (ROI) a trade should reach before exiting, independent from the exit signal.
|
||||
The `minimal_roi` strategy variable defines the minimal Return On Investment (ROI) a trade should reach before exiting, independent from the exit signal.
|
||||
|
||||
It is of the following format, with the dict key (left side of the colon) being the minutes passed since the trade opened, and the value (right side of the colon) being the percentage.
|
||||
It is of the following format, i.e. a python `dict`, with the dict key (left side of the colon) being the minutes passed since the trade opened, and the value (right side of the colon) being the percentage.
|
||||
|
||||
```python
|
||||
minimal_roi = {
|
||||
@@ -344,14 +432,19 @@ The above configuration would therefore mean:
|
||||
|
||||
The calculation does include fees.
|
||||
|
||||
#### Disabling minimal ROI
|
||||
|
||||
To disable ROI completely, set it to an empty dictionary:
|
||||
|
||||
```python
|
||||
minimal_roi = {}
|
||||
```
|
||||
|
||||
#### Using calculations in minimal ROI
|
||||
|
||||
To use times based on candle duration (timeframe), the following snippet can be handy.
|
||||
This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
|
||||
|
||||
This will allow you to change the timeframe for the strategy, but the minimal ROI times will still be set as candles, e.g. after 3 candles.
|
||||
|
||||
``` python
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
@@ -368,9 +461,9 @@ class AwesomeStrategy(IStrategy):
|
||||
```
|
||||
|
||||
??? info "Orders that don't fill immediately"
|
||||
`minimal_roi` will take the `trade.open_date` as reference, which is the time the trade was initialized / the first order for this trade was placed.
|
||||
This will also hold true for limit orders that don't fill immediately (usually in combination with "off-spot" prices through `custom_entry_price()`), as well as for cases where the initial order is replaced through `adjust_entry_price()`.
|
||||
The time used will still be from the initial `trade.open_date` (when the initial order was first placed), not from the newly placed order date.
|
||||
`minimal_roi` will take the `trade.open_date` as reference, which is the time the trade was initialized, i.e. when the first order for this trade was placed.
|
||||
This will also hold true for limit orders that don't fill immediately (usually in combination with "off-spot" prices through `custom_entry_price()`), as well as for cases where the initial order price is replaced through `adjust_entry_price()`.
|
||||
The time used will still be from the initial `trade.open_date` (when the initial order was first placed), not from the newly placed or adjusted order date.
|
||||
|
||||
### Stoploss
|
||||
|
||||
@@ -386,33 +479,44 @@ For the full documentation on stoploss features, look at the dedicated [stoploss
|
||||
|
||||
### Timeframe
|
||||
|
||||
This is the set of candles the bot should download and use for the analysis.
|
||||
This is the periodicity of candles the bot should use in the strategy.
|
||||
|
||||
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
||||
|
||||
Please note that the same entry/exit signals may work well with one timeframe, but not with the others.
|
||||
Please note that the same entry/exit signals may work well with one timeframe, but not with others.
|
||||
|
||||
This setting is accessible within the strategy methods as the `self.timeframe` attribute.
|
||||
|
||||
### Can short
|
||||
|
||||
To use short signals in futures markets, you will have to let us know to do so by setting `can_short=True`.
|
||||
To use short signals in futures markets, you will have to set `can_short = True`.
|
||||
|
||||
Strategies which enable this will fail to load on spot markets.
|
||||
Disabling of this will have short signals ignored (also in futures markets).
|
||||
|
||||
If you have `1` values in the `enter_short` column to raise short signals, setting `can_short = False` (which is the default) will mean that these short signals are ignored, even if you have specified futures markets in your configuration.
|
||||
|
||||
### Metadata dict
|
||||
|
||||
The metadata-dict (available for `populate_entry_trend`, `populate_exit_trend`, `populate_indicators`) contains additional information.
|
||||
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
||||
The `metadata` dict (available for `populate_entry_trend`, `populate_exit_trend`, `populate_indicators`) contains additional information.
|
||||
Currently this is `pair`, which can be accessed using `metadata['pair']`, and will return a pair in the format `XRP/BTC` (or `XRP/BTC:BTC` for futures markets).
|
||||
|
||||
The Metadata-dict should not be modified and does not persist information across multiple calls.
|
||||
Instead, have a look at the [Storing information](strategy-advanced.md#storing-information-persistent) section.
|
||||
The metadata dict should not be modified and does not persist information across multiple functions in your strategy.
|
||||
|
||||
Instead, please check the [Storing information](strategy-advanced.md#storing-information-persistent) section.
|
||||
|
||||
--8<-- "includes/strategy-imports.md"
|
||||
|
||||
## Strategy file loading
|
||||
|
||||
By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`.
|
||||
By default, freqtrade will attempt to load strategies from all `.py` files within the `userdir` (default `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.
|
||||
Assuming your strategy is called `AwesomeStrategy`, stored in the file `user_data/strategies/AwesomeStrategy.py`, then you can start freqtrade in dry (or live, depending on your configuration) mode with:
|
||||
|
||||
```bash
|
||||
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.
|
||||
@@ -424,9 +528,11 @@ It will also include a "status" field, highlighting potential problems.
|
||||
|
||||
### Get data for non-tradeable pairs
|
||||
|
||||
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
|
||||
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies to see data on a wider timeframe.
|
||||
|
||||
OHLCV data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see below).
|
||||
These parts will **not** be traded unless they are also specified in the pair whitelist, or have been selected by Dynamic Whitelisting.
|
||||
|
||||
These pairs will **not** be traded unless they are also specified in the pair whitelist, or have been selected by Dynamic Whitelisting, e.g. `VolumePairlist`.
|
||||
|
||||
The pairs need to be specified as tuples in the format `("pair", "timeframe")`, with pair as the first and timeframe as the second argument.
|
||||
|
||||
@@ -466,18 +572,24 @@ 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.
|
||||
To easily define informative pairs, use the `@informative` decorator. All decorated `populate_indicators_*` methods run in isolation,
|
||||
and do not have access to data from other informative pairs. However, all informative dataframes for each pair are merged and passed to main `populate_indicators()` method.
|
||||
|
||||
!!! Note
|
||||
Do not use the `@informative` decorator if you need to use data from one informative pair when generating another informative pair. Instead, define informative pairs manually as described [in the DataProvider section](#complete-data-provider-sample).
|
||||
|
||||
When hyperopting, use of the hyperoptable parameter `.value` attribute is not supported. Please use the `.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,
|
||||
*,
|
||||
candle_type: Optional[CandleType] = None,
|
||||
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||
def informative(
|
||||
timeframe: str,
|
||||
asset: str = "",
|
||||
fmt: str | Callable[[Any], str] | None = None,
|
||||
*,
|
||||
candle_type: CandleType | str | None = None,
|
||||
ffill: bool = True,
|
||||
) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||
"""
|
||||
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||
define informative indicators.
|
||||
@@ -566,10 +678,6 @@ for more information.
|
||||
|
||||
```
|
||||
|
||||
!!! 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.
|
||||
|
||||
@@ -590,18 +698,15 @@ for more information.
|
||||
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!
|
||||
Methods tagged with the `@informative()` decorator must always have unique names! Reusing the same name (for example when copy-pasting already defined informative methods) will overwrite previously defined methods and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators created in methods higher up in the strategy file are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||
|
||||
### *merge_informative_pair()*
|
||||
|
||||
This method helps you merge an informative pair to a regular dataframe without lookahead bias.
|
||||
It's there to help you merge the dataframe in a safe and consistent way.
|
||||
This method helps you merge an informative pair to the regular main dataframe safely and consistently, without lookahead bias.
|
||||
|
||||
Options:
|
||||
|
||||
- Rename the columns for you to create unique columns
|
||||
- Rename the columns to create unique columns
|
||||
- Merge the dataframe without lookahead bias
|
||||
- Forward-fill (optional)
|
||||
|
||||
@@ -652,20 +757,20 @@ All columns of the informative dataframe will be available on the returning data
|
||||
```
|
||||
|
||||
!!! Warning "Informative timeframe < timeframe"
|
||||
Using informative timeframes smaller than the dataframe timeframe is not recommended with this method, as it will not use any of the additional information this would provide.
|
||||
To use the more detailed information properly, more advanced methods should be applied (which are out of scope for freqtrade documentation, as it'll depend on the respective need).
|
||||
Using informative timeframes smaller than the main dataframe timeframe is not recommended with this method, as it will not use any of the additional information this would provide.
|
||||
To use the more detailed information properly, more advanced methods should be applied (which are out of scope for this documentation).
|
||||
|
||||
## Additional data (DataProvider)
|
||||
|
||||
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
||||
|
||||
All methods return `None` in case of failure (do not raise an exception).
|
||||
All methods return `None` in case of failure, i.e. failures do not raise an exception.
|
||||
|
||||
Please always check the mode of operation to select the correct method to get data (samples see below).
|
||||
Please always check the mode of operation to select the correct method to get data (see below for examples).
|
||||
|
||||
!!! Warning "Hyperopt"
|
||||
Dataprovider is available during hyperopt, however it can only be used in `populate_indicators()` within a strategy.
|
||||
It is not available in `populate_buy()` and `populate_sell()` methods, nor in `populate_indicators()`, if this method located in the hyperopt file.
|
||||
!!! Warning "Hyperopt Limitations"
|
||||
The DataProvider is available during hyperopt, however it can only be used in `populate_indicators()` **within a strategy**, not within a hyperopt class file.
|
||||
It is also not available in `populate_entry_trend()` and `populate_exit_trend()` methods.
|
||||
|
||||
### Possible options for DataProvider
|
||||
|
||||
@@ -691,30 +796,31 @@ for pair, timeframe in self.dp.available_pairs:
|
||||
|
||||
### *current_whitelist()*
|
||||
|
||||
Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume.
|
||||
Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 exchange pairs by volume.
|
||||
|
||||
The strategy might look something like this:
|
||||
The strategy logic 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.*
|
||||
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to enter and exit.*
|
||||
|
||||
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-1000 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 users to just 500-1000 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
||||
|
||||
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.
|
||||
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! We have a problem!
|
||||
|
||||
This is where calling `self.dp.current_whitelist()` comes in handy.
|
||||
This is where calling `self.dp.current_whitelist()` comes in handy to retrieve only those pairs in the whitelist.
|
||||
|
||||
```python
|
||||
def informative_pairs(self):
|
||||
|
||||
# get access to all pairs available in whitelist.
|
||||
pairs = self.dp.current_whitelist()
|
||||
# Assign tf to each pair so they can be downloaded and cached for strategy.
|
||||
# Assign timeframe to each pair so they can be downloaded and cached for strategy.
|
||||
informative_pairs = [(pair, '1d') for pair in pairs]
|
||||
return informative_pairs
|
||||
```
|
||||
|
||||
??? Note "Plotting with current_whitelist"
|
||||
Current whitelist is not supported for `plot-dataframe`, as this command is usually used by providing an explicit pairlist - and would therefore make the return values of this method misleading.
|
||||
Current whitelist is not supported for `plot-dataframe`, as this command is usually used by providing an explicit pairlist and would therefore make the return values of this method misleading.
|
||||
It's also not supported for FreqUI visualization in [webserver mode](utils.md#webserver-mode), as the configuration for webserver mode doesn't require a pairlist to be set.
|
||||
|
||||
### *get_pair_dataframe(pair, timeframe)*
|
||||
|
||||
@@ -755,7 +861,7 @@ if self.dp.runmode.value in ('live', 'dry_run'):
|
||||
dataframe['best_ask'] = ob['asks'][0][0]
|
||||
```
|
||||
|
||||
The orderbook structure is aligned with the order structure from [ccxt](https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure), so the result will look as follows:
|
||||
The orderbook structure is aligned with the order structure from [ccxt](https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure), so the result will be formatted as follows:
|
||||
|
||||
``` js
|
||||
{
|
||||
@@ -773,7 +879,7 @@ The orderbook structure is aligned with the order structure from [ccxt](https://
|
||||
}
|
||||
```
|
||||
|
||||
Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position.
|
||||
Therefore, using `ob['bids'][0][0]` as demonstrated above will use the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position.
|
||||
|
||||
!!! Warning "Warning about backtesting"
|
||||
The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return up-to-date values.
|
||||
@@ -790,12 +896,12 @@ if self.dp.runmode.value in ('live', 'dry_run'):
|
||||
|
||||
!!! Warning
|
||||
Although the ticker data structure is a part of the ccxt Unified Interface, the values returned by this method can
|
||||
vary for different exchanges. For instance, many exchanges do not return `vwap` values, some exchanges
|
||||
does not always fills in the `last` field (so it can be None), etc. So you need to carefully verify the ticker
|
||||
vary for different exchanges. For instance, many exchanges do not return `vwap` values, and some exchanges
|
||||
do not always fill in the `last` field (so it can be None), etc. So you need to carefully verify the ticker
|
||||
data returned from the exchange and add appropriate error handling / defaults.
|
||||
|
||||
!!! Warning "Warning about backtesting"
|
||||
This method will always return up-to-date values - so usage during backtesting / hyperopt without runmode checks will lead to wrong results.
|
||||
This method will always return up-to-date / real-time values. As such, usage during backtesting / hyperopt without runmode checks will lead to wrong results, e.g. your whole dataframe will contain the same single value in all rows.
|
||||
|
||||
### Send Notification
|
||||
|
||||
@@ -814,7 +920,7 @@ Notifications will only be sent in trading modes (Live/Dry-run) - so this method
|
||||
!!! Warning "Spamming"
|
||||
You can spam yourself pretty good by setting `always_send=True` in this method. Use this with great care and only in conditions you know will not happen throughout a candle to avoid a message every 5 seconds.
|
||||
|
||||
### Complete Data-provider sample
|
||||
### Complete DataProvider sample
|
||||
|
||||
```python
|
||||
from freqtrade.strategy import IStrategy, merge_informative_pair
|
||||
@@ -881,14 +987,14 @@ class SampleStrategy(IStrategy):
|
||||
|
||||
## Additional data (Wallets)
|
||||
|
||||
The strategy provides access to the `wallets` object. This contains the current balances on the exchange.
|
||||
The strategy provides access to the `wallets` object. This contains the current balances of your wallets/accounts on the exchange.
|
||||
|
||||
!!! Note "Backtesting / Hyperopt"
|
||||
Wallets behaves differently depending on the function it's called.
|
||||
Wallets behaves differently depending on the function from which it is called.
|
||||
Within `populate_*()` methods, it'll return the full wallet as configured.
|
||||
Within [callbacks](strategy-callbacks.md), you'll get the wallet state corresponding to the actual simulated wallet at that point in the simulation process.
|
||||
|
||||
Please always check if `wallets` is available to avoid failures during backtesting.
|
||||
Always check if `wallets` is available to avoid failures during backtesting.
|
||||
|
||||
``` python
|
||||
if self.wallets:
|
||||
@@ -907,15 +1013,15 @@ if self.wallets:
|
||||
|
||||
## Additional data (Trades)
|
||||
|
||||
A history of Trades can be retrieved in the strategy by querying the database.
|
||||
A history of trades can be retrieved in the strategy by querying the database.
|
||||
|
||||
At the top of the file, import Trade.
|
||||
At the top of the file, import the required object:
|
||||
|
||||
```python
|
||||
from freqtrade.persistence import Trade
|
||||
```
|
||||
|
||||
The following example queries for the current pair and trades from today, however other filters can easily be added.
|
||||
The following example queries trades from today for the current pair (`metadata['pair']`). Other filters can easily be added.
|
||||
|
||||
``` python
|
||||
trades = Trade.get_trades_proxy(pair=metadata['pair'],
|
||||
@@ -933,7 +1039,9 @@ For a full list of available methods, please consult the [Trade object](trade-ob
|
||||
|
||||
## Prevent trades from happening for a specific pair
|
||||
|
||||
Freqtrade locks pairs automatically for the current candle (until that candle is over) when a pair is sold, preventing an immediate re-buy of that pair.
|
||||
Freqtrade locks pairs automatically for the current candle (until that candle is over) when a pair exits, preventing an immediate re-entry of that pair.
|
||||
|
||||
This is to prevent "waterfalls" of many and frequent trades within a single candle.
|
||||
|
||||
Locked pairs will show the message `Pair <pair> is currently locked.`.
|
||||
|
||||
@@ -944,7 +1052,7 @@ 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)` or `self.unlock_reason(<reason>)` - providing reason the pair was locked with.
|
||||
Locks can also be lifted manually, by calling `self.unlock_pair(pair)` or `self.unlock_reason(<reason>)`, providing the reason the pair was unlocked.
|
||||
`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)`.
|
||||
@@ -953,7 +1061,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
|
||||
|
||||
!!! Warning
|
||||
Manually locking pairs is not available during backtesting, only locks via Protections are allowed.
|
||||
Manually locking pairs is not available during backtesting. Only locks via Protections are allowed.
|
||||
|
||||
#### Pair locking example
|
||||
|
||||
@@ -963,7 +1071,7 @@ from datetime import timedelta, datetime, timezone
|
||||
# Put the above lines a the top of the strategy file, next to all the other imports
|
||||
# --------
|
||||
|
||||
# Within populate indicators (or populate_buy):
|
||||
# Within populate indicators (or populate_entry_trend):
|
||||
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||
# fetch closed trades for the last 2 days
|
||||
trades = Trade.get_trades_proxy(
|
||||
@@ -976,9 +1084,9 @@ if self.config['runmode'].value in ('live', 'dry_run'):
|
||||
self.lock_pair(metadata['pair'], until=datetime.now(timezone.utc) + timedelta(hours=12))
|
||||
```
|
||||
|
||||
## Print created dataframe
|
||||
## Print the main dataframe
|
||||
|
||||
To inspect the created dataframe, you can issue a print-statement in either `populate_entry_trend()` or `populate_exit_trend()`.
|
||||
To inspect the current main dataframe, you can issue a print-statement in either `populate_entry_trend()` or `populate_exit_trend()`.
|
||||
You may also want to print the pair so it's clear what data is currently shown.
|
||||
|
||||
``` python
|
||||
@@ -998,29 +1106,30 @@ def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFram
|
||||
return dataframe
|
||||
```
|
||||
|
||||
Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds).
|
||||
Printing more than a few rows is also possible by using `print(dataframe)` instead of `print(dataframe.tail())`. However this is not recommended, as can results in a lot of output (~500 lines per pair every 5 seconds).
|
||||
|
||||
## Common mistakes when developing strategies
|
||||
|
||||
### Peeking into the future while backtesting
|
||||
### Looking into the future while backtesting
|
||||
|
||||
Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.
|
||||
This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions.
|
||||
Backtesting analyzes the whole dataframe timerange at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not lookahead into the future, i.e. using data that would not be available in dry or live mode.
|
||||
|
||||
The following lists some common patterns which should be avoided to prevent frustration:
|
||||
This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods. Strategies that look into the future will perform well during backtesting, often with incredible profits or winrates, but will fail or perform badly in real conditions.
|
||||
|
||||
The following list contains some common patterns which should be avoided to prevent frustration:
|
||||
|
||||
- don't use `shift(-1)` or other negative values. This uses data from the future in backtesting, which is not available in dry or live modes.
|
||||
- don't use `.iloc[-1]` or any other absolute position in the dataframe within `populate_` functions, as this will be different between dry-run and backtesting. Absolute `iloc` indexing is safe to use in callbacks however - see [Strategy Callbacks](strategy-callbacks.md).
|
||||
- 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.
|
||||
- don't use functions that use all dataframe or column values, e.g. `dataframe['mean_volume'] = dataframe['volume'].mean()`. As backtesting uses the full dataframe, at any point in the dataframe, the `'mean_volume'` series would include data from the future. Use rolling() calculations instead, e.g. `dataframe['volume'].rolling(<window>).mean()`.
|
||||
- don't use `.resample('1h')`. This uses the left border of the period interval, so moves data from an hour boundary to the start of the hour. Use `.resample('1h', label='right')` instead.
|
||||
|
||||
!!! Tip "Identifying problems"
|
||||
You may also want to check the 2 helper commands [lookahead-analysis](lookahead-analysis.md) and [recursive-analysis](recursive-analysis.md), which can each help you figure out problems with your strategy in different ways.
|
||||
Please treat them as what they are - helpers to identify most common problems. A negative result of each does not guarantee that there's none of the above errors included.
|
||||
You should always use the two helper commands [lookahead-analysis](lookahead-analysis.md) and [recursive-analysis](recursive-analysis.md), which can each help you figure out problems with your strategy in different ways.
|
||||
Please treat them as what they are - helpers to identify most common problems. A negative result of each does not guarantee that there are none of the above errors included.
|
||||
|
||||
### Colliding signals
|
||||
|
||||
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
|
||||
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are set to `1`), freqtrade will do nothing and ignore the entry signal. This will avoid trades that enter, and exit immediately. Obviously, this can potentially lead to missed entries.
|
||||
|
||||
The following rules apply, and entry signals will be ignored if more than one of the 3 signals is set:
|
||||
|
||||
@@ -1029,11 +1138,11 @@ The following rules apply, and entry signals will be ignored if more than one of
|
||||
|
||||
## Further strategy ideas
|
||||
|
||||
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.
|
||||
To get additional ideas for strategies, head over to the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as examples, but results will depend on the current market situation, pairs used, etc. Therefore, these strategies should be considered only for learning purposes, not real world trading. Please backtest the strategy for your exchange/desired pairs first, then dry run to evaluate carefully, and use at your own risk.
|
||||
|
||||
## Next step
|
||||
Feel free to use any of them as inspiration for your own strategies. We're happy to accept Pull Requests containing new strategies to the repository.
|
||||
|
||||
## Next steps
|
||||
|
||||
Now you have a perfect strategy you probably want to backtest it.
|
||||
Your next step is to learn [How to use the Backtesting](backtesting.md).
|
||||
Your next step is to learn [how to use backtesting](backtesting.md).
|
||||
|
||||
@@ -13,19 +13,22 @@ Please follow the [documentation](https://www.freqtrade.io/en/stable/data-downlo
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Change directory
|
||||
# Modify this cell to insure that the output shows the correct path.
|
||||
# Define all paths relative to the project root shown in the cell output
|
||||
project_root = "somedir/freqtrade"
|
||||
i=0
|
||||
i = 0
|
||||
try:
|
||||
os.chdir(project_root)
|
||||
assert Path('LICENSE').is_file()
|
||||
except:
|
||||
while i<4 and (not Path('LICENSE').is_file()):
|
||||
os.chdir(Path(Path.cwd(), '../'))
|
||||
i+=1
|
||||
project_root = Path.cwd()
|
||||
if not Path("LICENSE").is_file():
|
||||
i = 0
|
||||
while i < 4 and (not Path("LICENSE").is_file()):
|
||||
os.chdir(Path(Path.cwd(), "../"))
|
||||
i += 1
|
||||
project_root = Path.cwd()
|
||||
except FileNotFoundError:
|
||||
print("Please define the project root relative to the current directory")
|
||||
print(Path.cwd())
|
||||
```
|
||||
|
||||
@@ -35,6 +38,7 @@ print(Path.cwd())
|
||||
```python
|
||||
from freqtrade.configuration import Configuration
|
||||
|
||||
|
||||
# Customize these according to your needs.
|
||||
|
||||
# Initialize empty configuration object
|
||||
@@ -58,12 +62,14 @@ pair = "BTC/USDT"
|
||||
from freqtrade.data.history import load_pair_history
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
candles = load_pair_history(datadir=data_location,
|
||||
timeframe=config["timeframe"],
|
||||
pair=pair,
|
||||
data_format = "json", # Make sure to update this to your data
|
||||
candle_type=CandleType.SPOT,
|
||||
)
|
||||
|
||||
candles = load_pair_history(
|
||||
datadir=data_location,
|
||||
timeframe=config["timeframe"],
|
||||
pair=pair,
|
||||
data_format="json", # Make sure to update this to your data
|
||||
candle_type=CandleType.SPOT,
|
||||
)
|
||||
|
||||
# Confirm success
|
||||
print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}")
|
||||
@@ -76,14 +82,16 @@ candles.head()
|
||||
|
||||
```python
|
||||
# Load strategy using values set above
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
|
||||
strategy = StrategyResolver.load_strategy(config)
|
||||
strategy.dp = DataProvider(config, None, None)
|
||||
strategy.ft_bot_start()
|
||||
|
||||
# Generate buy/sell signals using strategy
|
||||
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||
df = strategy.analyze_ticker(candles, {"pair": pair})
|
||||
df.tail()
|
||||
```
|
||||
|
||||
@@ -102,7 +110,7 @@ df.tail()
|
||||
```python
|
||||
# Report results
|
||||
print(f"Generated {df['enter_long'].sum()} entry signals")
|
||||
data = df.set_index('date', drop=False)
|
||||
data = df.set_index("date", drop=False)
|
||||
data.tail()
|
||||
```
|
||||
|
||||
@@ -119,10 +127,13 @@ Analyze a trades dataframe (also used below for plotting)
|
||||
```python
|
||||
from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
|
||||
|
||||
|
||||
# if backtest_dir points to a directory, it'll automatically load the last backtest file.
|
||||
backtest_dir = config["user_data_dir"] / "backtest_results"
|
||||
# backtest_dir can also point to a specific file
|
||||
# backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json"
|
||||
# backtest_dir can also point to a specific file
|
||||
# backtest_dir = (
|
||||
# config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json"
|
||||
# )
|
||||
```
|
||||
|
||||
|
||||
@@ -131,24 +142,24 @@ backtest_dir = config["user_data_dir"] / "backtest_results"
|
||||
# This contains all information used to generate the backtest result.
|
||||
stats = load_backtest_stats(backtest_dir)
|
||||
|
||||
strategy = 'SampleStrategy'
|
||||
# All statistics are available per strategy, so if `--strategy-list` was used during backtest, this will be reflected here as well.
|
||||
strategy = "SampleStrategy"
|
||||
# All statistics are available per strategy, so if `--strategy-list` was used during backtest,
|
||||
# this will be reflected here as well.
|
||||
# Example usages:
|
||||
print(stats['strategy'][strategy]['results_per_pair'])
|
||||
print(stats["strategy"][strategy]["results_per_pair"])
|
||||
# Get pairlist used for this backtest
|
||||
print(stats['strategy'][strategy]['pairlist'])
|
||||
print(stats["strategy"][strategy]["pairlist"])
|
||||
# Get market change (average change of all pairs from start to end of the backtest period)
|
||||
print(stats['strategy'][strategy]['market_change'])
|
||||
print(stats["strategy"][strategy]["market_change"])
|
||||
# Maximum drawdown ()
|
||||
print(stats['strategy'][strategy]['max_drawdown'])
|
||||
print(stats["strategy"][strategy]["max_drawdown"])
|
||||
# Maximum drawdown start and end
|
||||
print(stats['strategy'][strategy]['drawdown_start'])
|
||||
print(stats['strategy'][strategy]['drawdown_end'])
|
||||
print(stats["strategy"][strategy]["drawdown_start"])
|
||||
print(stats["strategy"][strategy]["drawdown_end"])
|
||||
|
||||
|
||||
# Get strategy comparison (only relevant if multiple strategies were compared)
|
||||
print(stats['strategy_comparison'])
|
||||
|
||||
print(stats["strategy_comparison"])
|
||||
```
|
||||
|
||||
|
||||
@@ -166,24 +177,25 @@ trades.groupby("pair")["exit_reason"].value_counts()
|
||||
```python
|
||||
# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)
|
||||
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.data.btanalysis import load_backtest_stats
|
||||
import plotly.express as px
|
||||
import pandas as pd
|
||||
|
||||
|
||||
# strategy = 'SampleStrategy'
|
||||
# config = Configuration.from_files(["user_data/config.json"])
|
||||
# backtest_dir = config["user_data_dir"] / "backtest_results"
|
||||
|
||||
stats = load_backtest_stats(backtest_dir)
|
||||
strategy_stats = stats['strategy'][strategy]
|
||||
strategy_stats = stats["strategy"][strategy]
|
||||
|
||||
df = pd.DataFrame(columns=['dates','equity'], data=strategy_stats['daily_profit'])
|
||||
df['equity_daily'] = df['equity'].cumsum()
|
||||
df = pd.DataFrame(columns=["dates", "equity"], data=strategy_stats["daily_profit"])
|
||||
df["equity_daily"] = df["equity"].cumsum()
|
||||
|
||||
fig = px.line(df, x="dates", y="equity_daily")
|
||||
fig.show()
|
||||
|
||||
```
|
||||
|
||||
### Load live trading results into a pandas dataframe
|
||||
@@ -194,6 +206,7 @@ In case you did already some trading and want to analyze your performance
|
||||
```python
|
||||
from freqtrade.data.btanalysis import load_trades_from_db
|
||||
|
||||
|
||||
# Fetch trades from database
|
||||
trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
||||
|
||||
@@ -202,7 +215,7 @@ trades.groupby("pair")["exit_reason"].value_counts()
|
||||
```
|
||||
|
||||
## Analyze the loaded trades for trade parallelism
|
||||
This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.
|
||||
This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with a very high `max_open_trades` setting.
|
||||
|
||||
`analyze_trade_parallelism()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle.
|
||||
|
||||
@@ -210,8 +223,9 @@ This can be useful to find the best `max_open_trades` parameter, when used with
|
||||
```python
|
||||
from freqtrade.data.btanalysis import analyze_trade_parallelism
|
||||
|
||||
|
||||
# Analyze the above
|
||||
parallel_trades = analyze_trade_parallelism(trades, '5m')
|
||||
parallel_trades = analyze_trade_parallelism(trades, "5m")
|
||||
|
||||
parallel_trades.plot()
|
||||
```
|
||||
@@ -222,23 +236,23 @@ Freqtrade offers interactive plotting capabilities based on plotly.
|
||||
|
||||
|
||||
```python
|
||||
from freqtrade.plot.plotting import generate_candlestick_graph
|
||||
from freqtrade.plot.plotting import generate_candlestick_graph
|
||||
|
||||
|
||||
# Limit graph period to keep plotly quick and reactive
|
||||
|
||||
# Filter trades to one pair
|
||||
trades_red = trades.loc[trades['pair'] == pair]
|
||||
trades_red = trades.loc[trades["pair"] == pair]
|
||||
|
||||
data_red = data['2019-06-01':'2019-06-10']
|
||||
data_red = data["2019-06-01":"2019-06-10"]
|
||||
# Generate candlestick graph
|
||||
graph = generate_candlestick_graph(pair=pair,
|
||||
data=data_red,
|
||||
trades=trades_red,
|
||||
indicators1=['sma20', 'ema50', 'ema55'],
|
||||
indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']
|
||||
)
|
||||
|
||||
|
||||
|
||||
graph = generate_candlestick_graph(
|
||||
pair=pair,
|
||||
data=data_red,
|
||||
trades=trades_red,
|
||||
indicators1=["sma20", "ema50", "ema55"],
|
||||
indicators2=["rsi", "macd", "macdsignal", "macdhist"],
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -248,7 +262,6 @@ graph = generate_candlestick_graph(pair=pair,
|
||||
|
||||
# Render graph in a separate window
|
||||
graph.show(renderer="browser")
|
||||
|
||||
```
|
||||
|
||||
## Plot average profit per trade as distribution graph
|
||||
@@ -257,12 +270,12 @@ graph.show(renderer="browser")
|
||||
```python
|
||||
import plotly.figure_factory as ff
|
||||
|
||||
|
||||
hist_data = [trades.profit_ratio]
|
||||
group_labels = ['profit_ratio'] # name of the dataset
|
||||
group_labels = ["profit_ratio"] # name of the dataset
|
||||
|
||||
fig = ff.create_distplot(hist_data, group_labels, bin_size=0.01)
|
||||
fig.show()
|
||||
|
||||
```
|
||||
|
||||
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.
|
||||
|
||||
@@ -214,8 +214,8 @@ class AwesomeStrategy(IStrategy):
|
||||
``` python hl_lines="4"
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: Optional[float], max_stake: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
proposed_stake: float, min_stake: float | None, max_stake: float,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
# ...
|
||||
return proposed_stake
|
||||
```
|
||||
@@ -237,7 +237,7 @@ After:
|
||||
``` python hl_lines="4"
|
||||
class AwesomeStrategy(IStrategy):
|
||||
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],
|
||||
time_in_force: str, current_time: datetime, entry_tag: str | None,
|
||||
side: str, **kwargs) -> bool:
|
||||
return True
|
||||
```
|
||||
@@ -280,8 +280,8 @@ After:
|
||||
|
||||
``` python hl_lines="3"
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_entry_price(self, pair: str, trade: Optional[Trade], current_time: datetime, proposed_rate: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
return proposed_rate
|
||||
```
|
||||
|
||||
@@ -312,7 +312,7 @@ After:
|
||||
``` python hl_lines="5 7"
|
||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> Optional[float]:
|
||||
**kwargs) -> float | None:
|
||||
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||
if current_profit > 0.10:
|
||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
||||
@@ -329,7 +329,7 @@ After:
|
||||
`order_time_in_force` attributes changed from `"buy"` to `"entry"` and `"sell"` to `"exit"`.
|
||||
|
||||
``` python
|
||||
order_time_in_force: Dict = {
|
||||
order_time_in_force: dict = {
|
||||
"buy": "gtc",
|
||||
"sell": "gtc",
|
||||
}
|
||||
@@ -338,7 +338,7 @@ After:
|
||||
After:
|
||||
|
||||
``` python hl_lines="2 3"
|
||||
order_time_in_force: Dict = {
|
||||
order_time_in_force: dict = {
|
||||
"entry": "GTC",
|
||||
"exit": "GTC",
|
||||
}
|
||||
@@ -780,7 +780,7 @@ class MyCoolFreqaiModel(BaseRegressionModel):
|
||||
|
||||
def predict(
|
||||
self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs
|
||||
) -> Tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||
) -> tuple[DataFrame, npt.NDArray[np.int_]]:
|
||||
|
||||
# ... your custom stuff
|
||||
|
||||
|
||||
@@ -11,3 +11,7 @@
|
||||
.rst-versions .rst-other-versions {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-version__list {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ For the Freqtrade configuration, you can then use 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 unpleasant surprises.
|
||||
|
||||
@@ -93,9 +94,12 @@ Example configuration showing the different settings:
|
||||
"trailing_stop_loss": "on",
|
||||
"stop_loss": "on",
|
||||
"stoploss_on_exchange": "on",
|
||||
"custom_exit": "silent",
|
||||
"partial_exit": "on"
|
||||
"custom_exit": "silent", // custom_exit without specifying an exit reason
|
||||
"partial_exit": "on",
|
||||
// "custom_exit_message": "silent", // Disable individual custom exit reasons
|
||||
"*": "off" // Disable all other exit reasons
|
||||
},
|
||||
// "exit": "off", // Simplistic configuration to disable all exit messages
|
||||
"exit_cancel": "on",
|
||||
"exit_fill": "off",
|
||||
"protection_trigger": "off",
|
||||
@@ -108,16 +112,16 @@ Example configuration showing the different settings:
|
||||
},
|
||||
```
|
||||
|
||||
`entry` notifications are sent when the order is placed, while `entry_fill` notifications are sent when the order is filled on the exchange.
|
||||
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
|
||||
`*_fill` notifications are off by default and must be explicitly enabled.
|
||||
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
||||
`strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification).
|
||||
`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`.
|
||||
|
||||
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||
`allow_custom_messages` completely disable strategy messages.
|
||||
`reload` allows you to disable reload-buttons on selected messages.
|
||||
* `entry` notifications are sent when the order is placed, while `entry_fill` notifications are sent when the order is filled on the exchange.
|
||||
* `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
|
||||
Exit messages (`exit` and `exit_fill`) can be further controlled at individual exit reasons level, with the specific exit reason as the key. the default for all exit reasons is `on` - but can be configured via special `*` key - which will act as a wildcard for all exit reasons that are not explicitly defined.
|
||||
* `*_fill` notifications are off by default and must be explicitly enabled.
|
||||
* `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
||||
* `strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification).
|
||||
* `show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`.
|
||||
* `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||
* `allow_custom_messages` completely disable strategy messages.
|
||||
* `reload` allows you to disable reload-buttons on selected messages.
|
||||
|
||||
## Create a custom keyboard (command shortcut buttons)
|
||||
|
||||
@@ -231,23 +235,23 @@ Once all positions are sold, run `/stop` to completely stop the bot.
|
||||
`/reload_config` resets "max_open_trades" to the value set in the configuration and resets this command.
|
||||
|
||||
!!! Warning
|
||||
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||
|
||||
### /status
|
||||
|
||||
For each open trade, the bot will send you the following message.
|
||||
Enter Tag is configurable via Strategy.
|
||||
|
||||
> **Trade ID:** `123` `(since 1 days ago)`
|
||||
> **Current Pair:** CVC/BTC
|
||||
> **Direction:** Long
|
||||
> **Leverage:** 1.0
|
||||
> **Amount:** `26.64180098`
|
||||
> **Enter Tag:** Awesome Long Signal
|
||||
> **Open Rate:** `0.00007489`
|
||||
> **Current Rate:** `0.00007489`
|
||||
> **Unrealized Profit:** `12.95%`
|
||||
> **Stoploss:** `0.00007389 (-0.02%)`
|
||||
> **Trade ID:** `123` `(since 1 days ago)`
|
||||
> **Current Pair:** CVC/BTC
|
||||
> **Direction:** Long
|
||||
> **Leverage:** 1.0
|
||||
> **Amount:** `26.64180098`
|
||||
> **Enter Tag:** Awesome Long Signal
|
||||
> **Open Rate:** `0.00007489`
|
||||
> **Current Rate:** `0.00007489`
|
||||
> **Unrealized Profit:** `12.95%`
|
||||
> **Stoploss:** `0.00007389 (-0.02%)`
|
||||
|
||||
### /status table
|
||||
|
||||
@@ -274,34 +278,34 @@ current max
|
||||
|
||||
Return a summary of your profit/loss and performance.
|
||||
|
||||
> **ROI:** Close trades
|
||||
> ∙ `0.00485701 BTC (2.2%) (15.2 Σ%)`
|
||||
> ∙ `62.968 USD`
|
||||
> **ROI:** All trades
|
||||
> ∙ `0.00255280 BTC (1.5%) (6.43 Σ%)`
|
||||
> ∙ `33.095 EUR`
|
||||
>
|
||||
> **Total Trade Count:** `138`
|
||||
> **Bot started:** `2022-07-11 18:40:44`
|
||||
> **First Trade opened:** `3 days ago`
|
||||
> **Latest Trade opened:** `2 minutes ago`
|
||||
> **Avg. Duration:** `2:33:45`
|
||||
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||
> **Trading volume:** `0.5 BTC`
|
||||
> **Profit factor:** `1.04`
|
||||
> **Win / Loss:** `102 / 36`
|
||||
> **Winrate:** `73.91%`
|
||||
> **Expectancy (Ratio):** `4.87 (1.66)`
|
||||
> **Max Drawdown:** `9.23% (0.01255 BTC)`
|
||||
> **ROI:** Close trades
|
||||
> ∙ `0.00485701 BTC (2.2%) (15.2 Σ%)`
|
||||
> ∙ `62.968 USD`
|
||||
> **ROI:** All trades
|
||||
> ∙ `0.00255280 BTC (1.5%) (6.43 Σ%)`
|
||||
> ∙ `33.095 EUR`
|
||||
>
|
||||
> **Total Trade Count:** `138`
|
||||
> **Bot started:** `2022-07-11 18:40:44`
|
||||
> **First Trade opened:** `3 days ago`
|
||||
> **Latest Trade opened:** `2 minutes ago`
|
||||
> **Avg. Duration:** `2:33:45`
|
||||
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||
> **Trading volume:** `0.5 BTC`
|
||||
> **Profit factor:** `1.04`
|
||||
> **Win / Loss:** `102 / 36`
|
||||
> **Winrate:** `73.91%`
|
||||
> **Expectancy (Ratio):** `4.87 (1.66)`
|
||||
> **Max Drawdown:** `9.23% (0.01255 BTC)`
|
||||
|
||||
The relative profit of `1.2%` is the average profit per trade.
|
||||
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
||||
Expectancy corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades).
|
||||
Expectancy Ratio is expected profit or loss of a subsequent trade based on the performance of all past trades.
|
||||
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||
Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
|
||||
The relative profit of `1.2%` is the average profit per trade.
|
||||
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
||||
**Starting capital(**) is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||
**Profit Factor** is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
||||
**Expectancy** corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades).
|
||||
**Expectancy Ratio** is expected profit or loss of a subsequent trade based on the performance of all past trades.
|
||||
**Max drawdown** corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||
**Bot started date** will refer to the date the bot was first started. For older bots, this will default to the first trade's open date.
|
||||
|
||||
### /forceexit <trade_id>
|
||||
|
||||
@@ -329,33 +333,34 @@ Note that for this to work, `force_entry_enable` needs to be set to true.
|
||||
### /performance
|
||||
|
||||
Return the performance of each crypto-currency the bot has sold.
|
||||
> Performance:
|
||||
> 1. `RCN/BTC 0.003 BTC (57.77%) (1)`
|
||||
> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)`
|
||||
> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)`
|
||||
> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)`
|
||||
> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)`
|
||||
> ...
|
||||
> Performance:
|
||||
> 1. `RCN/BTC 0.003 BTC (57.77%) (1)`
|
||||
> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)`
|
||||
> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)`
|
||||
> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)`
|
||||
> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)`
|
||||
> ...
|
||||
|
||||
### /balance
|
||||
|
||||
Return the balance of all crypto-currency your have on the exchange.
|
||||
|
||||
> **Currency:** BTC
|
||||
> **Available:** 3.05890234
|
||||
> **Balance:** 3.05890234
|
||||
> **Pending:** 0.0
|
||||
|
||||
> **Currency:** CVC
|
||||
> **Available:** 86.64180098
|
||||
> **Balance:** 86.64180098
|
||||
> **Pending:** 0.0
|
||||
> **Currency:** BTC
|
||||
> **Available:** 3.05890234
|
||||
> **Balance:** 3.05890234
|
||||
> **Pending:** 0.0
|
||||
>
|
||||
> **Currency:** CVC
|
||||
> **Available:** 86.64180098
|
||||
> **Balance:** 86.64180098
|
||||
> **Pending:** 0.0
|
||||
|
||||
### /daily <n>
|
||||
|
||||
Per default `/daily` will return the 7 last days. The example below if for `/daily 3`:
|
||||
|
||||
> **Daily Profit over the last 3 days:**
|
||||
|
||||
```
|
||||
Day (count) USDT USD Profit %
|
||||
-------------- ------------ ---------- ----------
|
||||
@@ -370,6 +375,7 @@ Per default `/weekly` will return the 8 last weeks, including the current week.
|
||||
from Monday. The example below if for `/weekly 3`:
|
||||
|
||||
> **Weekly Profit over the last 3 weeks (starting from Monday):**
|
||||
|
||||
```
|
||||
Monday (count) Profit BTC Profit USD Profit %
|
||||
------------- -------------- ------------ ----------
|
||||
@@ -396,18 +402,18 @@ Month (count) Profit BTC Profit USD Profit %
|
||||
|
||||
Shows the current whitelist
|
||||
|
||||
> Using whitelist `StaticPairList` with 22 pairs
|
||||
> Using whitelist `StaticPairList` with 22 pairs
|
||||
> `IOTA/BTC, NEO/BTC, TRX/BTC, VET/BTC, ADA/BTC, ETC/BTC, NCASH/BTC, DASH/BTC, XRP/BTC, XVG/BTC, EOS/BTC, LTC/BTC, OMG/BTC, BTG/BTC, LSK/BTC, ZEC/BTC, HOT/BTC, IOTX/BTC, XMR/BTC, AST/BTC, XLM/BTC, NANO/BTC`
|
||||
|
||||
### /blacklist [pair]
|
||||
|
||||
Shows the current blacklist.
|
||||
If Pair is set, then this pair will be added to the pairlist.
|
||||
Also supports multiple pairs, separated by a space.
|
||||
Also supports multiple pairs, separated by a space.
|
||||
Use `/reload_config` to reset the blacklist.
|
||||
|
||||
> Using blacklist `StaticPairList` with 2 pairs
|
||||
>`DODGE/BTC`, `HOT/BTC`.
|
||||
> Using blacklist `StaticPairList` with 2 pairs
|
||||
>`DODGE/BTC`, `HOT/BTC`.
|
||||
|
||||
### /edge
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ The following attributes / properties are available for each individual trade -
|
||||
| `open_rate` | float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments). |
|
||||
| `close_rate` | float | Close rate - only set when is_open = False. |
|
||||
| `stake_amount` | float | Amount in Stake (or Quote) currency. |
|
||||
| `amount` | float | Amount in Asset / Base currency that is currently owned. |
|
||||
| `amount` | float | Amount in Asset / Base currency that is currently owned. Will be 0.0 until the initial order fills. |
|
||||
| `open_date` | datetime | Timestamp when trade was opened **use `open_date_utc` instead** |
|
||||
| `open_date_utc` | datetime | Timestamp when trade was opened - in UTC. |
|
||||
| `close_date` | datetime | Timestamp when trade was closed **use `close_date_utc` instead** |
|
||||
@@ -130,20 +130,21 @@ Most properties here can be None as they are dependent on the exchange response.
|
||||
|
||||
| Attribute | DataType | Description |
|
||||
|------------|-------------|-------------|
|
||||
`trade` | Trade | Trade object this order is attached to
|
||||
`ft_pair` | string | Pair this order is for
|
||||
`ft_is_open` | boolean | is the order filled?
|
||||
`order_type` | string | Order type as defined on the exchange - usually market, limit or stoploss
|
||||
`status` | string | Status as defined by ccxt. Usually open, closed, expired or canceled
|
||||
`side` | string | Buy or Sell
|
||||
`price` | float | Price the order was placed at
|
||||
`average` | float | Average price the order filled at
|
||||
`amount` | float | Amount in base currency
|
||||
`filled` | float | Filled amount (in base currency)
|
||||
`remaining` | float | Remaining amount
|
||||
`cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*)
|
||||
`stake_amount` | float | Stake amount used for this order. *Added in 2023.7.*
|
||||
`order_date` | datetime | Order creation date **use `order_date_utc` instead**
|
||||
`order_date_utc` | datetime | Order creation date (in UTC)
|
||||
`order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead**
|
||||
`order_fill_date_utc` | datetime | Order fill date
|
||||
| `trade` | Trade | Trade object this order is attached to |
|
||||
| `ft_pair` | string | Pair this order is for |
|
||||
| `ft_is_open` | boolean | is the order filled? |
|
||||
| `order_type` | string | Order type as defined on the exchange - usually market, limit or stoploss |
|
||||
| `status` | string | Status as defined by ccxt. Usually open, closed, expired or canceled |
|
||||
| `side` | string | Buy or Sell |
|
||||
| `price` | float | Price the order was placed at |
|
||||
| `average` | float | Average price the order filled at |
|
||||
| `amount` | float | Amount in base currency |
|
||||
| `filled` | float | Filled amount (in base currency) |
|
||||
| `remaining` | float | Remaining amount |
|
||||
| `cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*) |
|
||||
| `stake_amount` | float | Stake amount used for this order. *Added in 2023.7.* |
|
||||
| `stake_amount_filled` | float | Filled Stake amount used for this order. *Added in 2024.11.* |
|
||||
| `order_date` | datetime | Order creation date **use `order_date_utc` instead** |
|
||||
| `order_date_utc` | datetime | Order creation date (in UTC) |
|
||||
| `order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead** |
|
||||
| `order_fill_date_utc` | datetime | Order fill date |
|
||||
|
||||
@@ -216,6 +216,45 @@ Example: Search dedicated strategy path.
|
||||
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
||||
```
|
||||
|
||||
## List Hyperopt-Loss functions
|
||||
|
||||
Use the `list-hyperoptloss` subcommand to see all hyperopt loss functions available.
|
||||
|
||||
It provides a quick list of all available loss functions in your environment.
|
||||
|
||||
This subcommand can be useful for finding problems in your environment with loading loss functions: modules with Hyperopt-Loss functions that contain errors and failed to load are printed in red (LOAD FAILED), while hyperopt-Loss functions with duplicate names are printed in yellow (DUPLICATE NAME).
|
||||
|
||||
```
|
||||
usage: freqtrade list-hyperoptloss [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-d PATH] [--userdir PATH]
|
||||
[--hyperopt-path PATH] [-1] [--no-color]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--hyperopt-path PATH Specify additional lookup path for Hyperopt Loss
|
||||
functions.
|
||||
-1, --one-column Print output in one column.
|
||||
--no-color Disable colorization of hyperopt results. May be
|
||||
useful if you are redirecting output to a file.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE, --log-file FILE
|
||||
Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
-c PATH, --config PATH
|
||||
Specify configuration file (default:
|
||||
`userdir/config.json` or `config.json` whichever
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH, --data-dir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
```
|
||||
|
||||
## List freqAI models
|
||||
|
||||
Use the `list-freqaimodels` subcommand to see all freqAI models available.
|
||||
@@ -418,8 +457,9 @@ Common arguments:
|
||||
|
||||
```
|
||||
|
||||
By default, only active pairs/markets are shown. Active pairs/markets are those that can currently be traded
|
||||
on the exchange. The see the list of all pairs/markets (not only the active ones), use the `-a`/`-all` option.
|
||||
By default, only active pairs/markets are shown. Active pairs/markets are those that can currently be traded on the exchange.
|
||||
You can use the `-a`/`-all` option to see the list of all pairs/markets, including the inactive ones.
|
||||
Pairs may be listed as untradeable if the smallest tradeable price for the market is very small, i.e. less than `1e-11` (`0.00000000001`)
|
||||
|
||||
Pairs/markets are sorted by its symbol string in the printed output.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
|
||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||
Otherwise, please follow the instructions below.
|
||||
|
||||
All instructions assume that python 3.9+ is installed and available.
|
||||
All instructions assume that python 3.10+ is installed and available.
|
||||
|
||||
## Clone the git repository
|
||||
|
||||
@@ -42,7 +42,7 @@ cd freqtrade
|
||||
|
||||
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
|
||||
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.9, 3.10, 3.11 and 3.12) and for 64bit Windows.
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.10, 3.11 and 3.12) and for 64bit Windows.
|
||||
These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade.
|
||||
|
||||
Other versions must be downloaded from the above link.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2024.7"
|
||||
__version__ = "2024.11"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
__main__.py for Freqtrade
|
||||
To launch Freqtrade as a module
|
||||
|
||||
> python -m freqtrade (with Python >= 3.9)
|
||||
> python -m freqtrade (with Python >= 3.10)
|
||||
"""
|
||||
|
||||
from freqtrade import main
|
||||
|
||||
@@ -15,6 +15,7 @@ from freqtrade.commands.data_commands import (
|
||||
start_convert_trades,
|
||||
start_download_data,
|
||||
start_list_data,
|
||||
start_list_trades_data,
|
||||
)
|
||||
from freqtrade.commands.db_commands import start_convert_db
|
||||
from freqtrade.commands.deploy_commands import (
|
||||
@@ -26,6 +27,7 @@ from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hype
|
||||
from freqtrade.commands.list_commands import (
|
||||
start_list_exchanges,
|
||||
start_list_freqAI_models,
|
||||
start_list_hyperopt_loss_functions,
|
||||
start_list_markets,
|
||||
start_list_strategies,
|
||||
start_list_timeframes,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
|
||||
@@ -10,13 +9,15 @@ from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
|
||||
def setup_analyze_configuration(args: dict[str, Any], method: RunMode) -> dict[str, Any]:
|
||||
"""
|
||||
Prepare the configuration for the entry/exit reason analysis module
|
||||
:param args: Cli args from Arguments()
|
||||
:param method: Bot running mode
|
||||
:return: Configuration
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
|
||||
config = setup_utils_configuration(args, method)
|
||||
|
||||
no_unlimited_runmodes = {
|
||||
@@ -47,7 +48,7 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
|
||||
return config
|
||||
|
||||
|
||||
def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
|
||||
def start_analysis_entries_exits(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Start analysis script
|
||||
:param args: Cli args from Arguments()
|
||||
|
||||
@@ -5,7 +5,7 @@ This module contains the argument manager class
|
||||
from argparse import ArgumentParser, Namespace, _ArgumentGroup
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS
|
||||
from freqtrade.constants import DEFAULT_CONFIG
|
||||
@@ -23,7 +23,7 @@ ARGS_STRATEGY = [
|
||||
|
||||
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
|
||||
|
||||
ARGS_WEBSERVER: List[str] = []
|
||||
ARGS_WEBSERVER: list[str] = []
|
||||
|
||||
ARGS_COMMON_OPTIMIZE = [
|
||||
"timeframe",
|
||||
@@ -37,7 +37,6 @@ ARGS_COMMON_OPTIMIZE = [
|
||||
|
||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + [
|
||||
"position_stacking",
|
||||
"use_max_market_positions",
|
||||
"enable_protections",
|
||||
"dry_run_wallet",
|
||||
"timeframe_detail",
|
||||
@@ -53,7 +52,6 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + [
|
||||
"hyperopt",
|
||||
"hyperopt_path",
|
||||
"position_stacking",
|
||||
"use_max_market_positions",
|
||||
"enable_protections",
|
||||
"dry_run_wallet",
|
||||
"timeframe_detail",
|
||||
@@ -117,7 +115,7 @@ ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]
|
||||
ARGS_BUILD_CONFIG = ["config"]
|
||||
ARGS_SHOW_CONFIG = ["user_data_dir", "config", "show_sensitive"]
|
||||
|
||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "strategy_path", "template"]
|
||||
|
||||
ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", "exchange"]
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"]
|
||||
@@ -132,7 +130,15 @@ ARGS_CONVERT_TRADES = [
|
||||
"trading_mode",
|
||||
]
|
||||
|
||||
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"]
|
||||
ARGS_LIST_DATA = [
|
||||
"exchange",
|
||||
"dataformat_ohlcv",
|
||||
"dataformat_trades",
|
||||
"trades",
|
||||
"pairs",
|
||||
"trading_mode",
|
||||
"show_timerange",
|
||||
]
|
||||
|
||||
ARGS_DOWNLOAD_DATA = [
|
||||
"pairs",
|
||||
@@ -220,6 +226,8 @@ ARGS_ANALYZE_ENTRIES_EXITS = [
|
||||
"enter_reason_list",
|
||||
"exit_reason_list",
|
||||
"indicator_list",
|
||||
"entry_only",
|
||||
"exit_only",
|
||||
"timerange",
|
||||
"analysis_rejected",
|
||||
"analysis_to_csv",
|
||||
@@ -232,8 +240,7 @@ ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_s
|
||||
ARGS_LOOKAHEAD_ANALYSIS = [
|
||||
a
|
||||
for a in ARGS_BACKTEST
|
||||
if a
|
||||
not in ("position_stacking", "use_max_market_positions", "backtest_cache", "backtest_breakdown")
|
||||
if a not in ("position_stacking", "backtest_cache", "backtest_breakdown")
|
||||
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
||||
|
||||
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
|
||||
@@ -248,6 +255,7 @@ NO_CONF_REQURIED = [
|
||||
"list-pairs",
|
||||
"list-strategies",
|
||||
"list-freqaimodels",
|
||||
"list-hyperoptloss",
|
||||
"list-data",
|
||||
"hyperopt-list",
|
||||
"hyperopt-show",
|
||||
@@ -267,11 +275,11 @@ class Arguments:
|
||||
Arguments Class. Manage the arguments received by the cli
|
||||
"""
|
||||
|
||||
def __init__(self, args: Optional[List[str]]) -> None:
|
||||
def __init__(self, args: list[str] | None) -> None:
|
||||
self.args = args
|
||||
self._parsed_arg: Optional[Namespace] = None
|
||||
self._parsed_arg: Namespace | None = None
|
||||
|
||||
def get_parsed_arg(self) -> Dict[str, Any]:
|
||||
def get_parsed_arg(self) -> dict[str, Any]:
|
||||
"""
|
||||
Return the list of arguments
|
||||
:return: List[str] List of arguments
|
||||
@@ -311,9 +319,7 @@ class Arguments:
|
||||
|
||||
return parsed_arg
|
||||
|
||||
def _build_args(
|
||||
self, optionlist: List[str], parser: Union[ArgumentParser, _ArgumentGroup]
|
||||
) -> None:
|
||||
def _build_args(self, optionlist: list[str], parser: ArgumentParser | _ArgumentGroup) -> None:
|
||||
for val in optionlist:
|
||||
opt = AVAILABLE_CLI_OPTIONS[val]
|
||||
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
|
||||
@@ -355,6 +361,7 @@ class Arguments:
|
||||
start_list_data,
|
||||
start_list_exchanges,
|
||||
start_list_freqAI_models,
|
||||
start_list_hyperopt_loss_functions,
|
||||
start_list_markets,
|
||||
start_list_strategies,
|
||||
start_list_timeframes,
|
||||
@@ -556,6 +563,15 @@ class Arguments:
|
||||
list_strategies_cmd.set_defaults(func=start_list_strategies)
|
||||
self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd)
|
||||
|
||||
# Add list-Hyperopt loss subcommand
|
||||
list_hyperopt_loss_cmd = subparsers.add_parser(
|
||||
"list-hyperoptloss",
|
||||
help="Print available hyperopt loss functions.",
|
||||
parents=[_common_parser],
|
||||
)
|
||||
list_hyperopt_loss_cmd.set_defaults(func=start_list_hyperopt_loss_functions)
|
||||
self._build_args(optionlist=ARGS_LIST_HYPEROPTS, parser=list_hyperopt_loss_cmd)
|
||||
|
||||
# Add list-freqAI Models subcommand
|
||||
list_freqaimodels_cmd = subparsers.add_parser(
|
||||
"list-freqaimodels",
|
||||
|
||||
@@ -1,261 +1,27 @@
|
||||
import logging
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
|
||||
from questionary import Separator, prompt
|
||||
|
||||
from freqtrade.configuration import sanitize_config
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.detect_environment import running_in_docker
|
||||
from freqtrade.configuration.directory_operations import chown_user_directory
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
|
||||
from freqtrade.util import render_template
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_is_int(val):
|
||||
try:
|
||||
_ = int(val)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def validate_is_float(val):
|
||||
try:
|
||||
_ = float(val)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ask_user_overwrite(config_path: Path) -> bool:
|
||||
questions = [
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "overwrite",
|
||||
"message": f"File {config_path} already exists. Overwrite?",
|
||||
"default": False,
|
||||
},
|
||||
]
|
||||
answers = prompt(questions)
|
||||
return answers["overwrite"]
|
||||
|
||||
|
||||
def ask_user_config() -> Dict[str, Any]:
|
||||
"""
|
||||
Ask user a few questions to build the configuration.
|
||||
Interactive questions built using https://github.com/tmbo/questionary
|
||||
:returns: Dict with keys to put into template
|
||||
"""
|
||||
questions: List[Dict[str, Any]] = [
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "dry_run",
|
||||
"message": "Do you want to enable Dry-run (simulated trades)?",
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_currency",
|
||||
"message": "Please insert your stake currency:",
|
||||
"default": "USDT",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_amount",
|
||||
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"default": "unlimited",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||
"filter": lambda val: (
|
||||
'"' + UNLIMITED_STAKE_AMOUNT + '"' if val == UNLIMITED_STAKE_AMOUNT else val
|
||||
),
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "max_open_trades",
|
||||
"message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):",
|
||||
"default": "3",
|
||||
"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",
|
||||
"name": "fiat_display_currency",
|
||||
"message": (
|
||||
"Please insert your display Currency for reporting "
|
||||
"(leave empty to disable FIAT conversion):"
|
||||
),
|
||||
"default": "USD",
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"name": "exchange_name",
|
||||
"message": "Select exchange",
|
||||
"choices": [
|
||||
"binance",
|
||||
"binanceus",
|
||||
"bingx",
|
||||
"gate",
|
||||
"htx",
|
||||
"kraken",
|
||||
"kucoin",
|
||||
"okx",
|
||||
Separator("------------------"),
|
||||
"other",
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "trading_mode",
|
||||
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
||||
"default": False,
|
||||
"filter": lambda val: "futures" if val else "spot",
|
||||
"when": lambda x: x["exchange_name"] in ["binance", "gate", "okx", "bybit"],
|
||||
},
|
||||
{
|
||||
"type": "autocomplete",
|
||||
"name": "exchange_name",
|
||||
"message": "Type your exchange name (Must be supported by ccxt)",
|
||||
"choices": available_exchanges(),
|
||||
"when": lambda x: x["exchange_name"] == "other",
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_key",
|
||||
"message": "Insert Exchange Key",
|
||||
"when": lambda x: not x["dry_run"],
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_secret",
|
||||
"message": "Insert Exchange Secret",
|
||||
"when": lambda x: not x["dry_run"],
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_key_password",
|
||||
"message": "Insert Exchange API Key password",
|
||||
"when": lambda x: not x["dry_run"] and x["exchange_name"] in ("kucoin", "okx"),
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "telegram",
|
||||
"message": "Do you want to enable Telegram?",
|
||||
"default": False,
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "telegram_token",
|
||||
"message": "Insert Telegram token",
|
||||
"when": lambda x: x["telegram"],
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "telegram_chat_id",
|
||||
"message": "Insert Telegram chat id",
|
||||
"when": lambda x: x["telegram"],
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "api_server",
|
||||
"message": "Do you want to enable the Rest API (includes FreqUI)?",
|
||||
"default": False,
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "api_server_listen_addr",
|
||||
"message": (
|
||||
"Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||
"otherwise best left untouched)"
|
||||
),
|
||||
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", # noqa: S104
|
||||
"when": lambda x: x["api_server"],
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "api_server_username",
|
||||
"message": "Insert api-server username",
|
||||
"default": "freqtrader",
|
||||
"when": lambda x: x["api_server"],
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "api_server_password",
|
||||
"message": "Insert api-server password",
|
||||
"when": lambda x: x["api_server"],
|
||||
},
|
||||
]
|
||||
answers = prompt(questions)
|
||||
|
||||
if not answers:
|
||||
# Interrupted questionary sessions return an empty dict.
|
||||
raise OperationalException("User interrupted interactive questions.")
|
||||
# Ensure default is set for non-futures exchanges
|
||||
answers["trading_mode"] = answers.get("trading_mode", "spot")
|
||||
answers["margin_mode"] = "isolated" if answers.get("trading_mode") == "futures" else ""
|
||||
# Force JWT token to be a random string
|
||||
answers["api_server_jwt_key"] = secrets.token_hex()
|
||||
answers["api_server_ws_token"] = secrets.token_urlsafe(25)
|
||||
|
||||
return answers
|
||||
|
||||
|
||||
def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Applies selections to the template and writes the result to config_path
|
||||
:param config_path: Path object for new config file. Should not exist yet
|
||||
:param selections: Dict containing selections taken by the user.
|
||||
"""
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
try:
|
||||
exchange_template = MAP_EXCHANGE_CHILDCLASS.get(
|
||||
selections["exchange_name"], selections["exchange_name"]
|
||||
)
|
||||
|
||||
selections["exchange"] = render_template(
|
||||
templatefile=f"subtemplates/exchange_{exchange_template}.j2", arguments=selections
|
||||
)
|
||||
except TemplateNotFound:
|
||||
selections["exchange"] = render_template(
|
||||
templatefile="subtemplates/exchange_generic.j2", arguments=selections
|
||||
)
|
||||
|
||||
config_text = render_template(templatefile="base_config.json.j2", arguments=selections)
|
||||
|
||||
logger.info(f"Writing config to `{config_path}`.")
|
||||
logger.info(
|
||||
"Please make sure to check the configuration contents and adjust settings to your needs."
|
||||
)
|
||||
|
||||
config_path.write_text(config_text)
|
||||
|
||||
|
||||
def start_new_config(args: Dict[str, Any]) -> None:
|
||||
def start_new_config(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Create a new strategy from a template
|
||||
Asking the user questions to fill out the template accordingly.
|
||||
"""
|
||||
|
||||
from freqtrade.configuration.deploy_config import (
|
||||
ask_user_config,
|
||||
ask_user_overwrite,
|
||||
deploy_new_config,
|
||||
)
|
||||
from freqtrade.configuration.directory_operations import chown_user_directory
|
||||
|
||||
config_path = Path(args["config"][0])
|
||||
chown_user_directory(config_path.parent)
|
||||
if config_path.exists():
|
||||
@@ -271,10 +37,11 @@ def start_new_config(args: Dict[str, Any]) -> None:
|
||||
deploy_new_config(config_path, selections)
|
||||
|
||||
|
||||
def start_show_config(args: Dict[str, Any]) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False)
|
||||
def start_show_config(args: dict[str, Any]) -> None:
|
||||
from freqtrade.configuration import sanitize_config
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
|
||||
# TODO: Sanitize from sensitive info before printing
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False)
|
||||
|
||||
print("Your combined configuration is:")
|
||||
config_sanitized = sanitize_config(
|
||||
|
||||
@@ -168,14 +168,6 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
action="store_true",
|
||||
default=False,
|
||||
),
|
||||
"use_max_market_positions": Arg(
|
||||
"--dmmp",
|
||||
"--disable-max-market-positions",
|
||||
help="Disable applying `max_open_trades` during backtest "
|
||||
"(same as setting `max_open_trades` to a very high number).",
|
||||
action="store_false",
|
||||
default=True,
|
||||
),
|
||||
"backtest_show_pair_list": Arg(
|
||||
"--show-pair-list",
|
||||
help="Show backtesting pairlist sorted by profit.",
|
||||
@@ -446,8 +438,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"download_trades": Arg(
|
||||
"--dl-trades",
|
||||
help="Download trades instead of OHLCV data. The bot will resample trades to the "
|
||||
"desired timeframe as specified as --timeframes/-t.",
|
||||
help="Download trades instead of OHLCV data.",
|
||||
action="store_true",
|
||||
),
|
||||
"trades": Arg(
|
||||
"--trades",
|
||||
help="Work on trades data instead of OHLCV data.",
|
||||
action="store_true",
|
||||
),
|
||||
"convert_trades": Arg(
|
||||
@@ -715,6 +711,12 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
nargs="+",
|
||||
default=[],
|
||||
),
|
||||
"entry_only": Arg(
|
||||
"--entry-only", help=("Only analyze entry signals."), action="store_true", default=False
|
||||
),
|
||||
"exit_only": Arg(
|
||||
"--exit-only", help=("Only analyze exit signals."), action="store_true", default=False
|
||||
),
|
||||
"analysis_rejected": Arg(
|
||||
"--rejected-signals",
|
||||
help="Analyse rejected signals",
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import logging
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Config
|
||||
from freqtrade.data.converter import (
|
||||
convert_ohlcv_format,
|
||||
convert_trades_format,
|
||||
convert_trades_to_ohlcv,
|
||||
)
|
||||
from freqtrade.data.history import download_data_main
|
||||
from freqtrade.enums import CandleType, RunMode, TradingMode
|
||||
from freqtrade.exceptions import ConfigurationError
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.util import print_rich_table
|
||||
from freqtrade.util.migrations import migrate_data
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,10 +26,13 @@ def _check_data_config_download_sanity(config: Config) -> None:
|
||||
)
|
||||
|
||||
|
||||
def start_download_data(args: Dict[str, Any]) -> None:
|
||||
def start_download_data(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Download data (former download_backtest_data.py script)
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.data.history import download_data_main
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
_check_data_config_download_sanity(config)
|
||||
@@ -52,7 +44,11 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
sys.exit("SIGINT received, aborting ...")
|
||||
|
||||
|
||||
def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||
def start_convert_trades(args: dict[str, Any]) -> None:
|
||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||
from freqtrade.data.converter import convert_trades_to_ohlcv
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
timerange = TimeRange()
|
||||
@@ -91,10 +87,14 @@ def start_convert_trades(args: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||
def start_convert_data(args: dict[str, Any], ohlcv: bool = True) -> None:
|
||||
"""
|
||||
Convert data from one format to another
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||
from freqtrade.util.migrations import migrate_data
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
if ohlcv:
|
||||
migrate_data(config)
|
||||
@@ -113,10 +113,17 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||
)
|
||||
|
||||
|
||||
def start_list_data(args: Dict[str, Any]) -> None:
|
||||
def start_list_data(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
List available backtest data
|
||||
List available OHLCV data
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.util import print_rich_table
|
||||
|
||||
if args["trades"]:
|
||||
start_list_trades_data(args)
|
||||
return
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
@@ -127,7 +134,6 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
||||
paircombs = dhc.ohlcv_get_available_data(
|
||||
config["datadir"], config.get("trading_mode", TradingMode.SPOT)
|
||||
)
|
||||
|
||||
if args["pairs"]:
|
||||
paircombs = [comb for comb in paircombs if comb[0] in args["pairs"]]
|
||||
title = f"Found {len(paircombs)} pair / timeframe combinations."
|
||||
@@ -171,3 +177,54 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
||||
summary=title,
|
||||
table_kwargs={"min_width": 50},
|
||||
)
|
||||
|
||||
|
||||
def start_list_trades_data(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
List available Trades data
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.util import print_rich_table
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
from freqtrade.data.history import get_datahandler
|
||||
|
||||
dhc = get_datahandler(config["datadir"], config["dataformat_trades"])
|
||||
|
||||
paircombs = dhc.trades_get_available_data(
|
||||
config["datadir"], config.get("trading_mode", TradingMode.SPOT)
|
||||
)
|
||||
|
||||
if args["pairs"]:
|
||||
paircombs = [comb for comb in paircombs if comb in args["pairs"]]
|
||||
|
||||
title = f"Found trades data for {len(paircombs)} {plural(len(paircombs), 'pair')}."
|
||||
if not config.get("show_timerange"):
|
||||
print_rich_table(
|
||||
[(pair, config.get("candle_type_def", CandleType.SPOT)) for pair in sorted(paircombs)],
|
||||
("Pair", "Type"),
|
||||
title,
|
||||
table_kwargs={"min_width": 50},
|
||||
)
|
||||
else:
|
||||
paircombs1 = [
|
||||
(pair, *dhc.trades_data_min_max(pair, config.get("trading_mode", TradingMode.SPOT)))
|
||||
for pair in paircombs
|
||||
]
|
||||
print_rich_table(
|
||||
[
|
||||
(
|
||||
pair,
|
||||
config.get("candle_type_def", CandleType.SPOT),
|
||||
start.strftime(DATETIME_PRINT_FORMAT),
|
||||
end.strftime(DATETIME_PRINT_FORMAT),
|
||||
str(length),
|
||||
)
|
||||
for pair, start, end, length in sorted(paircombs1, key=lambda x: (x[0]))
|
||||
],
|
||||
("Pair", "Type", "From", "To", "Trades"),
|
||||
summary=title,
|
||||
table_kwargs={"min_width": 50},
|
||||
)
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_convert_db(args: Dict[str, Any]) -> None:
|
||||
def start_convert_db(args: dict[str, Any]) -> None:
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import make_transient
|
||||
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.persistence import Order, Trade, init_db
|
||||
from freqtrade.persistence.migrations import set_sequence_ids
|
||||
from freqtrade.persistence.pairlock import PairLock
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
||||
from freqtrade.constants import USERPATH_STRATEGIES
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
from freqtrade.util import render_template, render_template_with_fallback
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,12 +15,14 @@ logger = logging.getLogger(__name__)
|
||||
req_timeout = 30
|
||||
|
||||
|
||||
def start_create_userdir(args: Dict[str, Any]) -> None:
|
||||
def start_create_userdir(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Create "user_data" directory to contain user data strategies, hyperopt, ...)
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
||||
|
||||
if "user_data_dir" in args and args["user_data_dir"]:
|
||||
userdir = create_userdata_dir(args["user_data_dir"], create_dir=True)
|
||||
copy_sample_files(userdir, overwrite=args["reset"])
|
||||
@@ -38,6 +35,8 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
||||
"""
|
||||
Deploy new strategy from template to strategy_path
|
||||
"""
|
||||
from freqtrade.util import render_template, render_template_with_fallback
|
||||
|
||||
fallback = "full"
|
||||
attributes = render_template_with_fallback(
|
||||
templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2",
|
||||
@@ -81,11 +80,20 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
||||
strategy_path.write_text(strategy_text)
|
||||
|
||||
|
||||
def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||
def start_new_strategy(args: dict[str, Any]) -> None:
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
if "strategy" in args and args["strategy"]:
|
||||
new_path = config["user_data_dir"] / USERPATH_STRATEGIES / (args["strategy"] + ".py")
|
||||
if "strategy_path" in args and args["strategy_path"]:
|
||||
strategy_dir = Path(args["strategy_path"])
|
||||
else:
|
||||
strategy_dir = config["user_data_dir"] / USERPATH_STRATEGIES
|
||||
if not strategy_dir.is_dir():
|
||||
logger.info(f"Creating strategy directory {strategy_dir}")
|
||||
strategy_dir.mkdir(parents=True)
|
||||
new_path = strategy_dir / (args["strategy"] + ".py")
|
||||
|
||||
if new_path.exists():
|
||||
raise OperationalException(
|
||||
@@ -98,80 +106,14 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||
raise ConfigurationError("`new-strategy` requires --strategy to be set.")
|
||||
|
||||
|
||||
def clean_ui_subdir(directory: Path):
|
||||
if directory.is_dir():
|
||||
logger.info("Removing UI directory content.")
|
||||
def start_install_ui(args: dict[str, Any]) -> None:
|
||||
from freqtrade.commands.deploy_ui import (
|
||||
clean_ui_subdir,
|
||||
download_and_install_ui,
|
||||
get_ui_download_url,
|
||||
read_ui_version,
|
||||
)
|
||||
|
||||
for p in reversed(list(directory.glob("**/*"))): # iterate contents from leaves to root
|
||||
if p.name in (".gitkeep", "fallback_file.html"):
|
||||
continue
|
||||
if p.is_file():
|
||||
p.unlink()
|
||||
elif p.is_dir():
|
||||
p.rmdir()
|
||||
|
||||
|
||||
def read_ui_version(dest_folder: Path) -> Optional[str]:
|
||||
file = dest_folder / ".uiversion"
|
||||
if not file.is_file():
|
||||
return None
|
||||
|
||||
with file.open("r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
logger.info(f"Downloading {dl_url}")
|
||||
resp = requests.get(dl_url, timeout=req_timeout).content
|
||||
dest_folder.mkdir(parents=True, exist_ok=True)
|
||||
with ZipFile(BytesIO(resp)) as zf:
|
||||
for fn in zf.filelist:
|
||||
with zf.open(fn) as x:
|
||||
destfile = dest_folder / fn.filename
|
||||
if fn.is_dir():
|
||||
destfile.mkdir(exist_ok=True)
|
||||
else:
|
||||
destfile.write_bytes(x.read())
|
||||
with (dest_folder / ".uiversion").open("w") as f:
|
||||
f.write(version)
|
||||
|
||||
|
||||
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
|
||||
|
||||
resp = requests.get(f"{base_url}releases", timeout=req_timeout)
|
||||
resp.raise_for_status()
|
||||
r = resp.json()
|
||||
|
||||
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"]
|
||||
|
||||
# URL not found - try assets url
|
||||
if not dl_url:
|
||||
assets = r[0]["assets_url"]
|
||||
resp = requests.get(assets, timeout=req_timeout)
|
||||
r = resp.json()
|
||||
dl_url = r[0]["browser_download_url"]
|
||||
|
||||
return dl_url, latest_version
|
||||
|
||||
|
||||
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(args.get("ui_version"))
|
||||
|
||||
83
freqtrade/commands/deploy_ui.py
Normal file
83
freqtrade/commands/deploy_ui.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Timeout for requests
|
||||
req_timeout = 30
|
||||
|
||||
|
||||
def clean_ui_subdir(directory: Path):
|
||||
if directory.is_dir():
|
||||
logger.info("Removing UI directory content.")
|
||||
|
||||
for p in reversed(list(directory.glob("**/*"))): # iterate contents from leaves to root
|
||||
if p.name in (".gitkeep", "fallback_file.html"):
|
||||
continue
|
||||
if p.is_file():
|
||||
p.unlink()
|
||||
elif p.is_dir():
|
||||
p.rmdir()
|
||||
|
||||
|
||||
def read_ui_version(dest_folder: Path) -> str | None:
|
||||
file = dest_folder / ".uiversion"
|
||||
if not file.is_file():
|
||||
return None
|
||||
|
||||
with file.open("r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
logger.info(f"Downloading {dl_url}")
|
||||
resp = requests.get(dl_url, timeout=req_timeout).content
|
||||
dest_folder.mkdir(parents=True, exist_ok=True)
|
||||
with ZipFile(BytesIO(resp)) as zf:
|
||||
for fn in zf.filelist:
|
||||
with zf.open(fn) as x:
|
||||
destfile = dest_folder / fn.filename
|
||||
if fn.is_dir():
|
||||
destfile.mkdir(exist_ok=True)
|
||||
else:
|
||||
destfile.write_bytes(x.read())
|
||||
with (dest_folder / ".uiversion").open("w") as f:
|
||||
f.write(version)
|
||||
|
||||
|
||||
def get_ui_download_url(version: str | None = None) -> tuple[str, str]:
|
||||
base_url = "https://api.github.com/repos/freqtrade/frequi/"
|
||||
# Get base UI Repo path
|
||||
|
||||
resp = requests.get(f"{base_url}releases", timeout=req_timeout)
|
||||
resp.raise_for_status()
|
||||
r = resp.json()
|
||||
|
||||
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"]
|
||||
|
||||
# URL not found - try assets url
|
||||
if not dl_url:
|
||||
assets = r[0]["assets_url"]
|
||||
resp = requests.get(assets, timeout=req_timeout)
|
||||
r = resp.json()
|
||||
dl_url = r[0]["browser_download_url"]
|
||||
|
||||
return dl_url, latest_version
|
||||
@@ -1,22 +1,21 @@
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.data.btanalysis import get_latest_hyperopt_file
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.optimize_reports import show_backtest_result
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
def start_hyperopt_list(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
List hyperopt epochs previously evaluated
|
||||
"""
|
||||
from freqtrade.optimize.hyperopt_output import HyperoptOutput
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.data.btanalysis import get_latest_hyperopt_file
|
||||
from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
@@ -57,11 +56,14 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
HyperoptTools.export_csv_file(config, epochs, export_csv)
|
||||
|
||||
|
||||
def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
def start_hyperopt_show(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Show details of a hyperopt epoch previously evaluated
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.data.btanalysis import get_latest_hyperopt_file
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
from freqtrade.optimize.optimize_reports import show_backtest_result
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
import csv
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Any
|
||||
|
||||
import rapidjson
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
from freqtrade.exchange import list_available_exchanges, market_is_active
|
||||
from freqtrade.misc import parse_db_uri_for_logging, plural
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.types.valid_exchanges_type import ValidExchangesType
|
||||
from freqtrade.util import print_rich_table
|
||||
from freqtrade.ft_types import ValidExchangesType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
def start_list_exchanges(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Print available exchanges
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
available_exchanges: List[ValidExchangesType] = list_available_exchanges(
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from freqtrade.exchange import list_available_exchanges
|
||||
|
||||
available_exchanges: list[ValidExchangesType] = list_available_exchanges(
|
||||
args["list_exchanges_all"]
|
||||
)
|
||||
|
||||
if args["print_one_column"]:
|
||||
print("\n".join([e["name"] for e in available_exchanges]))
|
||||
print("\n".join([e["classname"] for e in available_exchanges]))
|
||||
else:
|
||||
if args["list_exchanges_all"]:
|
||||
title = (
|
||||
@@ -46,14 +42,20 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
table = Table(title=title)
|
||||
|
||||
table.add_column("Exchange Name")
|
||||
table.add_column("Class Name")
|
||||
table.add_column("Markets")
|
||||
table.add_column("Reason")
|
||||
|
||||
for exchange in available_exchanges:
|
||||
name = Text(exchange["name"])
|
||||
if exchange["supported"]:
|
||||
name.append(" (Official)", style="italic")
|
||||
name.append(" (Supported)", style="italic")
|
||||
name.stylize("green bold")
|
||||
classname = Text(exchange["classname"])
|
||||
if exchange["is_alias"]:
|
||||
name.stylize("strike")
|
||||
classname.stylize("strike")
|
||||
classname.append(f" (use {exchange['alias_for']})", style="italic")
|
||||
|
||||
trade_modes = Text(
|
||||
", ".join(
|
||||
@@ -68,6 +70,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
|
||||
table.add_row(
|
||||
name,
|
||||
classname,
|
||||
trade_modes,
|
||||
exchange["comment"],
|
||||
style=None if exchange["valid"] else "red",
|
||||
@@ -78,9 +81,13 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
console.print(table)
|
||||
|
||||
|
||||
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
||||
def _print_objs_tabular(objs: list, print_colorized: bool) -> None:
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
names = [s["name"] for s in objs]
|
||||
objs_to_print: List[Dict[str, Union[Text, str]]] = [
|
||||
objs_to_print: list[dict[str, Text | str]] = [
|
||||
{
|
||||
"name": Text(s["name"] if s["name"] else "--"),
|
||||
"location": s["location_rel"],
|
||||
@@ -118,10 +125,13 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
||||
console.print(table)
|
||||
|
||||
|
||||
def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
def start_list_strategies(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Print files with Strategy custom classes available in the directory
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
strategy_objs = StrategyResolver.search_all_objects(
|
||||
@@ -141,13 +151,15 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
_print_objs_tabular(strategy_objs, config.get("print_colorized", False))
|
||||
|
||||
|
||||
def start_list_freqAI_models(args: Dict[str, Any]) -> None:
|
||||
def start_list_freqAI_models(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Print files with FreqAI models custom classes available in the directory
|
||||
"""
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
model_objs = FreqaiModelResolver.search_all_objects(config, not args["print_one_column"])
|
||||
# Sort alphabetically
|
||||
model_objs = sorted(model_objs, key=lambda x: x["name"])
|
||||
@@ -157,10 +169,31 @@ def start_list_freqAI_models(args: Dict[str, Any]) -> None:
|
||||
_print_objs_tabular(model_objs, config.get("print_colorized", False))
|
||||
|
||||
|
||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||
def start_list_hyperopt_loss_functions(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Print files with FreqAI models custom classes available in the directory
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
model_objs = HyperOptLossResolver.search_all_objects(config, not args["print_one_column"])
|
||||
# Sort alphabetically
|
||||
model_objs = sorted(model_objs, key=lambda x: x["name"])
|
||||
if args["print_one_column"]:
|
||||
print("\n".join([s["name"] for s in model_objs]))
|
||||
else:
|
||||
_print_objs_tabular(model_objs, config.get("print_colorized", False))
|
||||
|
||||
|
||||
def start_list_timeframes(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Print timeframes available on Exchange
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
# Do not use timeframe set in the config
|
||||
config["timeframe"] = None
|
||||
@@ -177,13 +210,19 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
||||
def start_list_markets(args: dict[str, Any], pairs_only: bool = False) -> None:
|
||||
"""
|
||||
Print pairs/markets on the exchange
|
||||
:param args: Cli args from Arguments()
|
||||
:param pairs_only: if True print only pairs, otherwise print all instruments (markets)
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.exchange import market_is_active
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.util import print_rich_table
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
# Init exchange
|
||||
@@ -274,6 +313,8 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
||||
elif args.get("print_one_column", False):
|
||||
print("\n".join(pairs.keys()))
|
||||
elif args.get("list_pairs_print_json", False):
|
||||
import rapidjson
|
||||
|
||||
print(rapidjson.dumps(list(pairs.keys()), default=str))
|
||||
elif args.get("print_csv", False):
|
||||
writer = csv.DictWriter(sys.stdout, fieldnames=headers)
|
||||
@@ -289,12 +330,14 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
||||
print(f"{summary_str}.")
|
||||
|
||||
|
||||
def start_show_trades(args: Dict[str, Any]) -> None:
|
||||
def start_show_trades(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Show trades
|
||||
"""
|
||||
import json
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.misc import parse_db_uri_for_logging
|
||||
from freqtrade.persistence import Trade, init_db
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
from freqtrade.util import fmt_coin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
|
||||
def setup_optimize_configuration(args: dict[str, Any], method: RunMode) -> dict[str, Any]:
|
||||
"""
|
||||
Prepare the configuration for the Hyperopt module
|
||||
:param args: Cli args from Arguments()
|
||||
:param method: Bot running mode
|
||||
:return: Configuration
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.util import fmt_coin
|
||||
|
||||
config = setup_utils_configuration(args, method)
|
||||
|
||||
no_unlimited_runmodes = {
|
||||
@@ -41,7 +42,7 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
||||
return config
|
||||
|
||||
|
||||
def start_backtesting(args: Dict[str, Any]) -> None:
|
||||
def start_backtesting(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Start Backtesting script
|
||||
:param args: Cli args from Arguments()
|
||||
@@ -60,10 +61,11 @@ def start_backtesting(args: Dict[str, Any]) -> None:
|
||||
backtesting.start()
|
||||
|
||||
|
||||
def start_backtesting_show(args: Dict[str, Any]) -> None:
|
||||
def start_backtesting_show(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Show previous backtest result
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
@@ -76,7 +78,7 @@ def start_backtesting_show(args: Dict[str, Any]) -> None:
|
||||
show_sorted_pairlist(config, results)
|
||||
|
||||
|
||||
def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||
def start_hyperopt(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Start hyperopt script
|
||||
:param args: Cli args from Arguments()
|
||||
@@ -121,7 +123,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||
# Same in Edge and Backtesting start() functions.
|
||||
|
||||
|
||||
def start_edge(args: Dict[str, Any]) -> None:
|
||||
def start_edge(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Start Edge script
|
||||
:param args: Cli args from Arguments()
|
||||
@@ -138,24 +140,26 @@ def start_edge(args: Dict[str, Any]) -> None:
|
||||
edge_cli.start()
|
||||
|
||||
|
||||
def start_lookahead_analysis(args: Dict[str, Any]) -> None:
|
||||
def start_lookahead_analysis(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Start the backtest bias tester script
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.optimize.analysis.lookahead_helpers import LookaheadAnalysisSubFunctions
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
LookaheadAnalysisSubFunctions.start(config)
|
||||
|
||||
|
||||
def start_recursive_analysis(args: Dict[str, Any]) -> None:
|
||||
def start_recursive_analysis(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Start the backtest recursive tester script
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.optimize.analysis.recursive_helpers import RecursiveAnalysisSubFunctions
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
import rapidjson
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_test_pairlist(args: Dict[str, Any]) -> None:
|
||||
def start_test_pairlist(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Test Pairlist configuration
|
||||
"""
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.persistence import FtNoDBContext
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import ConfigurationError
|
||||
|
||||
|
||||
def validate_plot_args(args: Dict[str, Any]) -> None:
|
||||
def validate_plot_args(args: dict[str, Any]) -> None:
|
||||
if not args.get("datadir") and not args.get("config"):
|
||||
raise ConfigurationError(
|
||||
"You need to specify either `--datadir` or `--config` "
|
||||
@@ -13,11 +12,12 @@ def validate_plot_args(args: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def start_plot_dataframe(args: Dict[str, Any]) -> None:
|
||||
def start_plot_dataframe(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Entrypoint for dataframe plotting
|
||||
"""
|
||||
# Import here to avoid errors if plot-dependencies are not installed.
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.plot.plotting import load_and_plot_trades
|
||||
|
||||
validate_plot_args(args)
|
||||
@@ -26,11 +26,12 @@ def start_plot_dataframe(args: Dict[str, Any]) -> None:
|
||||
load_and_plot_trades(config)
|
||||
|
||||
|
||||
def start_plot_profit(args: Dict[str, Any]) -> None:
|
||||
def start_plot_profit(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Entrypoint for plot_profit
|
||||
"""
|
||||
# Import here to avoid errors if plot-dependencies are not installed.
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.plot.plotting import plot_profit
|
||||
|
||||
validate_plot_args(args)
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.strategyupdater import StrategyUpdater
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_strategy_update(args: Dict[str, Any]) -> None:
|
||||
def start_strategy_update(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Start the strategy updating script
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if sys.version_info == (3, 8): # pragma: no cover
|
||||
sys.exit("Freqtrade strategy updater requires Python version >= 3.9")
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
@@ -49,6 +44,8 @@ def start_strategy_update(args: Dict[str, Any]) -> None:
|
||||
|
||||
|
||||
def start_conversion(strategy_obj, config):
|
||||
from freqtrade.strategy.strategyupdater import StrategyUpdater
|
||||
|
||||
print(f"Conversion of {Path(strategy_obj['location']).name} started.")
|
||||
instance_strategy_updater = StrategyUpdater()
|
||||
start = time.perf_counter()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import logging
|
||||
import signal
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_trading(args: Dict[str, Any]) -> int:
|
||||
def start_trading(args: dict[str, Any]) -> int:
|
||||
"""
|
||||
Main entry point for trading mode
|
||||
"""
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
|
||||
|
||||
def start_webserver(args: Dict[str, Any]) -> None:
|
||||
def start_webserver(args: dict[str, Any]) -> None:
|
||||
"""
|
||||
Main entry point for webserver mode
|
||||
"""
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# Required json-schema for user specified config
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.constants import (
|
||||
AVAILABLE_DATAHANDLERS,
|
||||
AVAILABLE_PAIRLISTS,
|
||||
AVAILABLE_PROTECTIONS,
|
||||
BACKTEST_BREAKDOWNS,
|
||||
DRY_RUN_WALLET,
|
||||
EXPORT_OPTIONS,
|
||||
@@ -24,7 +22,7 @@ from freqtrade.constants import (
|
||||
from freqtrade.enums import RPCMessageType
|
||||
|
||||
|
||||
__MESSAGE_TYPE_DICT: Dict[str, Dict[str, str]] = {x: {"type": "object"} for x in RPCMessageType}
|
||||
__MESSAGE_TYPE_DICT: dict[str, dict[str, str]] = {x: {"type": "object"} for x in RPCMessageType}
|
||||
|
||||
__IN_STRATEGY = "\nUsually specified in the strategy and missing in the configuration."
|
||||
|
||||
@@ -36,11 +34,6 @@ CONF_SCHEMA = {
|
||||
"type": ["integer", "number"],
|
||||
"minimum": -1,
|
||||
},
|
||||
"new_pairs_days": {
|
||||
"description": "Download data of new pairs for given number of days",
|
||||
"type": "integer",
|
||||
"default": 30,
|
||||
},
|
||||
"timeframe": {
|
||||
"description": (
|
||||
f"The timeframe to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). {__IN_STRATEGY}"
|
||||
@@ -185,6 +178,7 @@ CONF_SCHEMA = {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
# Lookahead analysis section
|
||||
"minimum_trade_amount": {
|
||||
"description": "Minimum amount for a trade - only used for lookahead-analysis",
|
||||
"type": "number",
|
||||
@@ -453,54 +447,7 @@ CONF_SCHEMA = {
|
||||
"required": ["method"],
|
||||
},
|
||||
},
|
||||
"protections": {
|
||||
"description": "Configuration for various protections.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"description": "Method used for the protection.",
|
||||
"type": "string",
|
||||
"enum": AVAILABLE_PROTECTIONS,
|
||||
},
|
||||
"stop_duration": {
|
||||
"description": (
|
||||
"Duration to lock the pair after a protection is triggered, "
|
||||
"in minutes."
|
||||
),
|
||||
"type": "number",
|
||||
"minimum": 0.0,
|
||||
},
|
||||
"stop_duration_candles": {
|
||||
"description": (
|
||||
"Duration to lock the pair after a protection is triggered, in "
|
||||
"number of candles."
|
||||
),
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
},
|
||||
"trade_limit": {
|
||||
"description": "Minimum number of trades required during lookback period.",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
},
|
||||
"lookback_period": {
|
||||
"description": "Period to look back for protection checks, in minutes.",
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
},
|
||||
"lookback_period_candles": {
|
||||
"description": (
|
||||
"Period to look back for protection checks, in number " "of candles."
|
||||
),
|
||||
"type": "number",
|
||||
"minimum": 1,
|
||||
},
|
||||
},
|
||||
"required": ["method"],
|
||||
},
|
||||
},
|
||||
# RPC section
|
||||
"telegram": {
|
||||
"description": "Telegram settings.",
|
||||
"type": "object",
|
||||
@@ -570,8 +517,11 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"exit_fill": {
|
||||
"description": "Telegram setting for exit fill signals.",
|
||||
"type": "string",
|
||||
"enum": TELEGRAM_SETTING_OPTIONS,
|
||||
"type": ["string", "object"],
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"enum": TELEGRAM_SETTING_OPTIONS,
|
||||
},
|
||||
"default": "on",
|
||||
},
|
||||
"exit_cancel": {
|
||||
@@ -701,6 +651,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
"required": ["enabled", "listen_ip_address", "listen_port", "username", "password"],
|
||||
},
|
||||
# end of RPC section
|
||||
"db_url": {
|
||||
"description": "Database connection URL.",
|
||||
"type": "string",
|
||||
@@ -734,7 +685,7 @@ CONF_SCHEMA = {
|
||||
"default": {},
|
||||
"properties": {
|
||||
"process_throttle_secs": {
|
||||
"description": "Throttle time in seconds for processing.",
|
||||
"description": "Minimum loop duration for one bot iteration in seconds.",
|
||||
"type": "integer",
|
||||
},
|
||||
"interval": {
|
||||
@@ -763,11 +714,26 @@ CONF_SCHEMA = {
|
||||
"description": f"Enable position adjustment. {__IN_STRATEGY}",
|
||||
"type": "boolean",
|
||||
},
|
||||
# Download data section
|
||||
"new_pairs_days": {
|
||||
"description": "Download data of new pairs for given number of days",
|
||||
"type": "integer",
|
||||
"default": 30,
|
||||
},
|
||||
"download_trades": {
|
||||
"description": "Download trades data by default (instead of ohlcv data).",
|
||||
"type": "boolean",
|
||||
},
|
||||
"max_entry_position_adjustment": {
|
||||
"description": f"Maximum entry position adjustment allowed. {__IN_STRATEGY}",
|
||||
"type": ["integer", "number"],
|
||||
"minimum": -1,
|
||||
},
|
||||
"add_config_files": {
|
||||
"description": "Additional configuration files to load.",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"orderflow": {
|
||||
"description": "Settings related to order flow.",
|
||||
"type": "object",
|
||||
@@ -853,6 +819,14 @@ CONF_SCHEMA = {
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": True,
|
||||
},
|
||||
"log_responses": {
|
||||
"description": (
|
||||
"Log responses from the exchange."
|
||||
"Useful/required to debug issues with order processing."
|
||||
),
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
},
|
||||
"unknown_fee_rate": {
|
||||
"description": "Fee rate for unknown markets.",
|
||||
"type": "number",
|
||||
@@ -1024,6 +998,13 @@ CONF_SCHEMA = {
|
||||
"type": "string",
|
||||
"default": "example",
|
||||
},
|
||||
"wait_for_training_iteration_on_reload": {
|
||||
"description": (
|
||||
"Wait for the next training iteration to complete after /reload or ctrl+c."
|
||||
),
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
},
|
||||
"feature_parameters": {
|
||||
"description": "The parameters used to engineer the feature set",
|
||||
"type": "object",
|
||||
|
||||
@@ -14,12 +14,16 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
||||
return config
|
||||
keys_to_remove = [
|
||||
"exchange.key",
|
||||
"exchange.api_key",
|
||||
"exchange.apiKey",
|
||||
"exchange.secret",
|
||||
"exchange.password",
|
||||
"exchange.uid",
|
||||
"exchange.account_id",
|
||||
"exchange.accountId",
|
||||
"exchange.wallet_address",
|
||||
"exchange.walletAddress",
|
||||
"exchange.private_key",
|
||||
"exchange.privateKey",
|
||||
"telegram.token",
|
||||
"telegram.chat_id",
|
||||
@@ -33,8 +37,10 @@ def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config:
|
||||
nested_config = config
|
||||
for nested_key in nested_keys[:-1]:
|
||||
nested_config = nested_config.get(nested_key, {})
|
||||
nested_config[nested_keys[-1]] = "REDACTED"
|
||||
if nested_keys[-1] in nested_config:
|
||||
nested_config[nested_keys[-1]] = "REDACTED"
|
||||
else:
|
||||
config[key] = "REDACTED"
|
||||
if key in config:
|
||||
config[key] = "REDACTED"
|
||||
|
||||
return config
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
|
||||
@@ -11,8 +11,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_utils_configuration(
|
||||
args: Dict[str, Any], method: RunMode, *, set_dry: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
args: dict[str, Any], method: RunMode, *, set_dry: bool = True
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Prepare the configuration for utils subcommands
|
||||
:param args: Cli args from Arguments()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from jsonschema import Draft4Validator, validators
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
@@ -43,7 +43,7 @@ def _extend_validator(validator_class):
|
||||
FreqtradeValidator = _extend_validator(Draft4Validator)
|
||||
|
||||
|
||||
def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> Dict[str, Any]:
|
||||
def validate_config_schema(conf: dict[str, Any], preliminary: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
Validate the configuration follow the Config Schema
|
||||
:param conf: Config in JSON format
|
||||
@@ -69,7 +69,7 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
|
||||
raise ValidationError(best_match(Draft4Validator(conf_schema).iter_errors(conf)).message)
|
||||
|
||||
|
||||
def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = False) -> None:
|
||||
def validate_config_consistency(conf: dict[str, Any], *, preliminary: bool = False) -> None:
|
||||
"""
|
||||
Validate the configuration consistency.
|
||||
Should be ran after loading both configuration and strategy,
|
||||
@@ -83,7 +83,6 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
|
||||
_validate_price_config(conf)
|
||||
_validate_edge(conf)
|
||||
_validate_whitelist(conf)
|
||||
_validate_protections(conf)
|
||||
_validate_unlimited_amount(conf)
|
||||
_validate_ask_orderbook(conf)
|
||||
_validate_freqai_hyperopt(conf)
|
||||
@@ -98,7 +97,7 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal
|
||||
validate_config_schema(conf, preliminary=preliminary)
|
||||
|
||||
|
||||
def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
|
||||
def _validate_unlimited_amount(conf: dict[str, Any]) -> None:
|
||||
"""
|
||||
If edge is disabled, either max_open_trades or stake_amount need to be set.
|
||||
:raise: ConfigurationError if config validation failed
|
||||
@@ -111,7 +110,7 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
|
||||
raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.")
|
||||
|
||||
|
||||
def _validate_price_config(conf: Dict[str, Any]) -> None:
|
||||
def _validate_price_config(conf: dict[str, Any]) -> None:
|
||||
"""
|
||||
When using market orders, price sides must be using the "other" side of the price
|
||||
"""
|
||||
@@ -127,7 +126,7 @@ def _validate_price_config(conf: Dict[str, Any]) -> None:
|
||||
raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".')
|
||||
|
||||
|
||||
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
||||
def _validate_trailing_stoploss(conf: dict[str, Any]) -> None:
|
||||
if conf.get("stoploss") == 0.0:
|
||||
raise ConfigurationError(
|
||||
"The config stoploss needs to be different from 0 to avoid problems with sell orders."
|
||||
@@ -160,7 +159,7 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_edge(conf: Dict[str, Any]) -> None:
|
||||
def _validate_edge(conf: dict[str, Any]) -> None:
|
||||
"""
|
||||
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
|
||||
"""
|
||||
@@ -174,7 +173,7 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
||||
def _validate_whitelist(conf: dict[str, Any]) -> None:
|
||||
"""
|
||||
Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does.
|
||||
"""
|
||||
@@ -195,26 +194,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
||||
raise ConfigurationError("StaticPairList requires pair_whitelist to be set.")
|
||||
|
||||
|
||||
def _validate_protections(conf: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Validate protection configuration validity
|
||||
"""
|
||||
|
||||
for prot in conf.get("protections", []):
|
||||
if "stop_duration" in prot and "stop_duration_candles" in prot:
|
||||
raise ConfigurationError(
|
||||
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
|
||||
f"Please fix the protection {prot.get('method')}"
|
||||
)
|
||||
|
||||
if "lookback_period" in prot and "lookback_period_candles" in prot:
|
||||
raise ConfigurationError(
|
||||
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
|
||||
f"Please fix the protection {prot.get('method')}"
|
||||
)
|
||||
|
||||
|
||||
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
||||
def _validate_ask_orderbook(conf: dict[str, Any]) -> None:
|
||||
ask_strategy = conf.get("exit_pricing", {})
|
||||
ob_min = ask_strategy.get("order_book_min")
|
||||
ob_max = ask_strategy.get("order_book_max")
|
||||
@@ -234,7 +214,7 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
|
||||
def validate_migrated_strategy_settings(conf: dict[str, Any]) -> None:
|
||||
_validate_time_in_force(conf)
|
||||
_validate_order_types(conf)
|
||||
_validate_unfilledtimeout(conf)
|
||||
@@ -242,7 +222,7 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
|
||||
_strategy_settings(conf)
|
||||
|
||||
|
||||
def _validate_time_in_force(conf: Dict[str, Any]) -> None:
|
||||
def _validate_time_in_force(conf: dict[str, Any]) -> None:
|
||||
time_in_force = conf.get("order_time_in_force", {})
|
||||
if "buy" in time_in_force or "sell" in time_in_force:
|
||||
if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||
@@ -263,7 +243,7 @@ def _validate_time_in_force(conf: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_order_types(conf: Dict[str, Any]) -> None:
|
||||
def _validate_order_types(conf: dict[str, Any]) -> None:
|
||||
order_types = conf.get("order_types", {})
|
||||
old_order_types = [
|
||||
"buy",
|
||||
@@ -298,7 +278,7 @@ def _validate_order_types(conf: Dict[str, Any]) -> None:
|
||||
process_deprecated_setting(conf, "order_types", o, "order_types", n)
|
||||
|
||||
|
||||
def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
|
||||
def _validate_unfilledtimeout(conf: dict[str, Any]) -> None:
|
||||
unfilledtimeout = conf.get("unfilledtimeout", {})
|
||||
if any(x in unfilledtimeout for x in ["buy", "sell"]):
|
||||
if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||
@@ -317,7 +297,7 @@ def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
|
||||
process_deprecated_setting(conf, "unfilledtimeout", o, "unfilledtimeout", n)
|
||||
|
||||
|
||||
def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
|
||||
def _validate_pricing_rules(conf: dict[str, Any]) -> None:
|
||||
if conf.get("ask_strategy") or conf.get("bid_strategy"):
|
||||
if conf.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT:
|
||||
raise ConfigurationError("Please migrate your pricing settings to use the new wording.")
|
||||
@@ -347,7 +327,7 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
|
||||
del conf["ask_strategy"]
|
||||
|
||||
|
||||
def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
|
||||
def _validate_freqai_hyperopt(conf: dict[str, Any]) -> None:
|
||||
freqai_enabled = conf.get("freqai", {}).get("enabled", False)
|
||||
analyze_per_epoch = conf.get("analyze_per_epoch", False)
|
||||
if analyze_per_epoch and freqai_enabled:
|
||||
@@ -356,7 +336,7 @@ def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) -> None:
|
||||
def _validate_freqai_include_timeframes(conf: dict[str, Any], preliminary: bool) -> None:
|
||||
freqai_enabled = conf.get("freqai", {}).get("enabled", False)
|
||||
if freqai_enabled:
|
||||
main_tf = conf.get("timeframe", "5m")
|
||||
@@ -387,7 +367,7 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool)
|
||||
)
|
||||
|
||||
|
||||
def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
|
||||
def _validate_freqai_backtest(conf: dict[str, Any]) -> None:
|
||||
if conf.get("runmode", RunMode.OTHER) == RunMode.BACKTEST:
|
||||
freqai_enabled = conf.get("freqai", {}).get("enabled", False)
|
||||
timerange = conf.get("timerange")
|
||||
@@ -410,7 +390,7 @@ def _validate_freqai_backtest(conf: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_consumers(conf: Dict[str, Any]) -> None:
|
||||
def _validate_consumers(conf: dict[str, Any]) -> None:
|
||||
emc_conf = conf.get("external_message_consumer", {})
|
||||
if emc_conf.get("enabled", False):
|
||||
if len(emc_conf.get("producers", [])) < 1:
|
||||
@@ -430,7 +410,7 @@ def _validate_consumers(conf: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_orderflow(conf: Dict[str, Any]) -> None:
|
||||
def _validate_orderflow(conf: dict[str, Any]) -> None:
|
||||
if conf.get("exchange", {}).get("use_public_trades"):
|
||||
if "orderflow" not in conf:
|
||||
raise ConfigurationError(
|
||||
@@ -438,7 +418,7 @@ def _validate_orderflow(conf: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _strategy_settings(conf: Dict[str, Any]) -> None:
|
||||
def _strategy_settings(conf: dict[str, Any]) -> None:
|
||||
process_deprecated_setting(conf, None, "use_sell_signal", None, "use_exit_signal")
|
||||
process_deprecated_setting(conf, None, "sell_profit_only", None, "exit_profit_only")
|
||||
process_deprecated_setting(conf, None, "sell_profit_offset", None, "exit_profit_offset")
|
||||
|
||||
@@ -5,9 +5,10 @@ This module contains the configuration class
|
||||
import ast
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
from typing import Any
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||
@@ -15,7 +16,14 @@ from freqtrade.configuration.directory_operations import create_datadir, create_
|
||||
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
|
||||
from freqtrade.configuration.load_config import load_file, load_from_files
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums import NON_UTIL_MODES, TRADE_MODES, CandleType, RunMode, TradingMode
|
||||
from freqtrade.enums import (
|
||||
NON_UTIL_MODES,
|
||||
TRADE_MODES,
|
||||
CandleType,
|
||||
MarginMode,
|
||||
RunMode,
|
||||
TradingMode,
|
||||
)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import setup_logging
|
||||
from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging
|
||||
@@ -30,9 +38,9 @@ class Configuration:
|
||||
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||
"""
|
||||
|
||||
def __init__(self, args: Dict[str, Any], runmode: Optional[RunMode] = None) -> None:
|
||||
def __init__(self, args: dict[str, Any], runmode: RunMode | None = None) -> None:
|
||||
self.args = args
|
||||
self.config: Optional[Config] = None
|
||||
self.config: Config | None = None
|
||||
self.runmode = runmode
|
||||
|
||||
def get_config(self) -> Config:
|
||||
@@ -46,7 +54,7 @@ class Configuration:
|
||||
return self.config
|
||||
|
||||
@staticmethod
|
||||
def from_files(files: List[str]) -> Dict[str, Any]:
|
||||
def from_files(files: list[str]) -> dict[str, Any]:
|
||||
"""
|
||||
Iterate through the config files passed in, loading all of them
|
||||
and merging their contents.
|
||||
@@ -61,7 +69,7 @@ class Configuration:
|
||||
c = Configuration({"config": files}, RunMode.OTHER)
|
||||
return c.get_config()
|
||||
|
||||
def load_config(self) -> Dict[str, Any]:
|
||||
def load_config(self) -> dict[str, Any]:
|
||||
"""
|
||||
Extract information for sys.argv and load the bot configuration
|
||||
:return: Configuration dictionary
|
||||
@@ -234,11 +242,7 @@ class Configuration:
|
||||
logstring="Parameter --enable-protections detected, enabling Protections. ...",
|
||||
)
|
||||
|
||||
if "use_max_market_positions" in self.args and not self.args["use_max_market_positions"]:
|
||||
config.update({"use_max_market_positions": False})
|
||||
logger.info("Parameter --disable-max-market-positions detected ...")
|
||||
logger.info("max_open_trades set to unlimited ...")
|
||||
elif "max_open_trades" in self.args and self.args["max_open_trades"]:
|
||||
if "max_open_trades" in self.args and self.args["max_open_trades"]:
|
||||
config.update({"max_open_trades": self.args["max_open_trades"]})
|
||||
logger.info(
|
||||
"Parameter --max-open-trades detected, overriding max_open_trades to: %s ...",
|
||||
@@ -389,6 +393,7 @@ class Configuration:
|
||||
config.get("trading_mode", "spot") or "spot"
|
||||
)
|
||||
config["trading_mode"] = TradingMode(config.get("trading_mode", "spot") or "spot")
|
||||
config["margin_mode"] = MarginMode(config.get("margin_mode", "") or "")
|
||||
self._args_to_config(
|
||||
config, argname="candle_types", logstring="Detected --candle-types: {}"
|
||||
)
|
||||
@@ -399,6 +404,8 @@ class Configuration:
|
||||
("enter_reason_list", "Analysis enter tag list: {}"),
|
||||
("exit_reason_list", "Analysis exit tag list: {}"),
|
||||
("indicator_list", "Analysis indicator list: {}"),
|
||||
("entry_only", "Only analyze entry signals: {}"),
|
||||
("exit_only", "Only analyze exit signals: {}"),
|
||||
("timerange", "Filter trades by timerange: {}"),
|
||||
("analysis_rejected", "Analyse rejected signals: {}"),
|
||||
("analysis_to_csv", "Store analysis tables to CSV: {}"),
|
||||
@@ -411,7 +418,7 @@ class Configuration:
|
||||
]
|
||||
self._args_to_config_loop(config, configurations)
|
||||
|
||||
def _args_to_config_loop(self, config, configurations: List[Tuple[str, str]]) -> None:
|
||||
def _args_to_config_loop(self, config, configurations: list[tuple[str, str]]) -> None:
|
||||
for argname, logstring in configurations:
|
||||
self._args_to_config(config, argname=argname, logstring=logstring)
|
||||
|
||||
@@ -445,8 +452,8 @@ class Configuration:
|
||||
config: Config,
|
||||
argname: str,
|
||||
logstring: str,
|
||||
logfun: Optional[Callable] = None,
|
||||
deprecated_msg: Optional[str] = None,
|
||||
logfun: Callable | None = None,
|
||||
deprecated_msg: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param config: Configuration dictionary
|
||||
@@ -468,7 +475,7 @@ class Configuration:
|
||||
else:
|
||||
logger.info(logstring.format(config[argname]))
|
||||
if deprecated_msg:
|
||||
warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning)
|
||||
warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning, stacklevel=1)
|
||||
|
||||
def _resolve_pairs_list(self, config: Config) -> None:
|
||||
"""
|
||||
|
||||
250
freqtrade/configuration/deploy_config.py
Normal file
250
freqtrade/configuration/deploy_config.py
Normal file
@@ -0,0 +1,250 @@
|
||||
import logging
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from questionary import Separator, prompt
|
||||
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_is_int(val):
|
||||
try:
|
||||
_ = int(val)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def validate_is_float(val):
|
||||
try:
|
||||
_ = float(val)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ask_user_overwrite(config_path: Path) -> bool:
|
||||
questions = [
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "overwrite",
|
||||
"message": f"File {config_path} already exists. Overwrite?",
|
||||
"default": False,
|
||||
},
|
||||
]
|
||||
answers = prompt(questions)
|
||||
return answers["overwrite"]
|
||||
|
||||
|
||||
def ask_user_config() -> dict[str, Any]:
|
||||
"""
|
||||
Ask user a few questions to build the configuration.
|
||||
Interactive questions built using https://github.com/tmbo/questionary
|
||||
:returns: Dict with keys to put into template
|
||||
"""
|
||||
|
||||
from freqtrade.configuration.detect_environment import running_in_docker
|
||||
from freqtrade.exchange import available_exchanges
|
||||
|
||||
questions: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "dry_run",
|
||||
"message": "Do you want to enable Dry-run (simulated trades)?",
|
||||
"default": True,
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_currency",
|
||||
"message": "Please insert your stake currency:",
|
||||
"default": "USDT",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_amount",
|
||||
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"default": "unlimited",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||
"filter": lambda val: (
|
||||
'"' + UNLIMITED_STAKE_AMOUNT + '"' if val == UNLIMITED_STAKE_AMOUNT else val
|
||||
),
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "max_open_trades",
|
||||
"message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):",
|
||||
"default": "3",
|
||||
"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",
|
||||
"name": "fiat_display_currency",
|
||||
"message": (
|
||||
"Please insert your display Currency for reporting "
|
||||
"(leave empty to disable FIAT conversion):"
|
||||
),
|
||||
"default": "USD",
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"name": "exchange_name",
|
||||
"message": "Select exchange",
|
||||
"choices": [
|
||||
"binance",
|
||||
"binanceus",
|
||||
"bingx",
|
||||
"gate",
|
||||
"htx",
|
||||
"kraken",
|
||||
"kucoin",
|
||||
"okx",
|
||||
Separator("------------------"),
|
||||
"other",
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "trading_mode",
|
||||
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
|
||||
"default": False,
|
||||
"filter": lambda val: "futures" if val else "spot",
|
||||
"when": lambda x: x["exchange_name"] in ["binance", "gate", "okx", "bybit"],
|
||||
},
|
||||
{
|
||||
"type": "autocomplete",
|
||||
"name": "exchange_name",
|
||||
"message": "Type your exchange name (Must be supported by ccxt)",
|
||||
"choices": available_exchanges(),
|
||||
"when": lambda x: x["exchange_name"] == "other",
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_key",
|
||||
"message": "Insert Exchange Key",
|
||||
"when": lambda x: not x["dry_run"],
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_secret",
|
||||
"message": "Insert Exchange Secret",
|
||||
"when": lambda x: not x["dry_run"],
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_key_password",
|
||||
"message": "Insert Exchange API Key password",
|
||||
"when": lambda x: not x["dry_run"] and x["exchange_name"] in ("kucoin", "okx"),
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "telegram",
|
||||
"message": "Do you want to enable Telegram?",
|
||||
"default": False,
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "telegram_token",
|
||||
"message": "Insert Telegram token",
|
||||
"when": lambda x: x["telegram"],
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "telegram_chat_id",
|
||||
"message": "Insert Telegram chat id",
|
||||
"when": lambda x: x["telegram"],
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "api_server",
|
||||
"message": "Do you want to enable the Rest API (includes FreqUI)?",
|
||||
"default": False,
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "api_server_listen_addr",
|
||||
"message": (
|
||||
"Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||
"otherwise best left untouched)"
|
||||
),
|
||||
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", # noqa: S104
|
||||
"when": lambda x: x["api_server"],
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "api_server_username",
|
||||
"message": "Insert api-server username",
|
||||
"default": "freqtrader",
|
||||
"when": lambda x: x["api_server"],
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "api_server_password",
|
||||
"message": "Insert api-server password",
|
||||
"when": lambda x: x["api_server"],
|
||||
},
|
||||
]
|
||||
answers = prompt(questions)
|
||||
|
||||
if not answers:
|
||||
# Interrupted questionary sessions return an empty dict.
|
||||
raise OperationalException("User interrupted interactive questions.")
|
||||
# Ensure default is set for non-futures exchanges
|
||||
answers["trading_mode"] = answers.get("trading_mode", "spot")
|
||||
answers["margin_mode"] = "isolated" if answers.get("trading_mode") == "futures" else ""
|
||||
# Force JWT token to be a random string
|
||||
answers["api_server_jwt_key"] = secrets.token_hex()
|
||||
answers["api_server_ws_token"] = secrets.token_urlsafe(25)
|
||||
|
||||
return answers
|
||||
|
||||
|
||||
def deploy_new_config(config_path: Path, selections: dict[str, Any]) -> None:
|
||||
"""
|
||||
Applies selections to the template and writes the result to config_path
|
||||
:param config_path: Path object for new config file. Should not exist yet
|
||||
:param selections: Dict containing selections taken by the user.
|
||||
"""
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.util import render_template
|
||||
|
||||
try:
|
||||
exchange_template = MAP_EXCHANGE_CHILDCLASS.get(
|
||||
selections["exchange_name"], selections["exchange_name"]
|
||||
)
|
||||
|
||||
selections["exchange"] = render_template(
|
||||
templatefile=f"subtemplates/exchange_{exchange_template}.j2", arguments=selections
|
||||
)
|
||||
except TemplateNotFound:
|
||||
selections["exchange"] = render_template(
|
||||
templatefile="subtemplates/exchange_generic.j2", arguments=selections
|
||||
)
|
||||
|
||||
config_text = render_template(templatefile="base_config.json.j2", arguments=selections)
|
||||
|
||||
logger.info(f"Writing config to `{config_path}`.")
|
||||
logger.info(
|
||||
"Please make sure to check the configuration contents and adjust settings to your needs."
|
||||
)
|
||||
|
||||
config_path.write_text(config_text)
|
||||
@@ -3,7 +3,6 @@ Functions to handle deprecated settings
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
@@ -14,9 +13,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def check_conflicting_settings(
|
||||
config: Config,
|
||||
section_old: Optional[str],
|
||||
section_old: str | None,
|
||||
name_old: str,
|
||||
section_new: Optional[str],
|
||||
section_new: str | None,
|
||||
name_new: str,
|
||||
) -> None:
|
||||
section_new_config = config.get(section_new, {}) if section_new else config
|
||||
@@ -34,7 +33,7 @@ def check_conflicting_settings(
|
||||
|
||||
|
||||
def process_removed_setting(
|
||||
config: Config, section1: str, name1: str, section2: Optional[str], name2: str
|
||||
config: Config, section1: str, name1: str, section2: str | None, name2: str
|
||||
) -> None:
|
||||
"""
|
||||
:param section1: Removed section
|
||||
@@ -54,9 +53,9 @@ def process_removed_setting(
|
||||
|
||||
def process_deprecated_setting(
|
||||
config: Config,
|
||||
section_old: Optional[str],
|
||||
section_old: str | None,
|
||||
name_old: str,
|
||||
section_new: Optional[str],
|
||||
section_new: str | None,
|
||||
name_new: str,
|
||||
) -> None:
|
||||
check_conflicting_settings(config, section_old, name_old, section_new, name_new)
|
||||
@@ -177,4 +176,6 @@ def process_temporary_deprecated_settings(config: Config) -> None:
|
||||
)
|
||||
|
||||
if "protections" in config:
|
||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||
raise ConfigurationError(
|
||||
"DEPRECATED: Setting 'protections' in the configuration is deprecated."
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from freqtrade.configuration.detect_environment import running_in_docker
|
||||
from freqtrade.constants import (
|
||||
@@ -18,7 +17,7 @@ from freqtrade.exceptions import OperationalException
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_datadir(config: Config, datadir: Optional[str] = None) -> Path:
|
||||
def create_datadir(config: Config, datadir: str | None = None) -> Path:
|
||||
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
|
||||
if not datadir:
|
||||
# set datadir
|
||||
@@ -82,6 +81,11 @@ def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
|
||||
for f in sub_dirs:
|
||||
subfolder = folder / f
|
||||
if not subfolder.is_dir():
|
||||
if subfolder.exists() or subfolder.is_symlink():
|
||||
raise OperationalException(
|
||||
f"File `{subfolder}` exists already and is not a directory. "
|
||||
"Freqtrade requires this to be a directory."
|
||||
)
|
||||
subfolder.mkdir(parents=False)
|
||||
return folder
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from freqtrade.constants import ENV_VAR_PREFIX
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
@@ -24,7 +24,7 @@ def _get_var_typed(val):
|
||||
return val
|
||||
|
||||
|
||||
def _flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str, Any]:
|
||||
def _flat_vars_to_nested_dict(env_dict: dict[str, Any], prefix: str) -> dict[str, Any]:
|
||||
"""
|
||||
Environment variables must be prefixed with FREQTRADE.
|
||||
FREQTRADE__{section}__{key}
|
||||
@@ -33,7 +33,7 @@ def _flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str
|
||||
:return: Nested dict based on available and relevant variables.
|
||||
"""
|
||||
no_convert = ["CHAT_ID", "PASSWORD"]
|
||||
relevant_vars: Dict[str, Any] = {}
|
||||
relevant_vars: dict[str, Any] = {}
|
||||
|
||||
for env_var, val in sorted(env_dict.items()):
|
||||
if env_var.startswith(prefix):
|
||||
@@ -51,7 +51,7 @@ def _flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str
|
||||
return relevant_vars
|
||||
|
||||
|
||||
def enironment_vars_to_dict() -> Dict[str, Any]:
|
||||
def enironment_vars_to_dict() -> dict[str, Any]:
|
||||
"""
|
||||
Read environment variables and return a nested dict for relevant variables
|
||||
Relevant variables must follow the FREQTRADE__{section}__{key} pattern
|
||||
|
||||
@@ -7,7 +7,7 @@ import re
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
import rapidjson
|
||||
|
||||
@@ -42,7 +42,7 @@ def log_config_error_range(path: str, errmsg: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def load_file(path: Path) -> Dict[str, Any]:
|
||||
def load_file(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
with path.open("r") as file:
|
||||
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||
@@ -51,7 +51,7 @@ def load_file(path: Path) -> Dict[str, Any]:
|
||||
return config
|
||||
|
||||
|
||||
def load_config_file(path: str) -> Dict[str, Any]:
|
||||
def load_config_file(path: str) -> dict[str, Any]:
|
||||
"""
|
||||
Loads a config file from the given path
|
||||
:param path: path as str
|
||||
@@ -78,8 +78,8 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
||||
|
||||
|
||||
def load_from_files(
|
||||
files: List[str], base_path: Optional[Path] = None, level: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
files: list[str], base_path: Path | None = None, level: int = 0
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Recursively load configuration files if specified.
|
||||
Sub-files are assumed to be relative to the initial config.
|
||||
|
||||
@@ -5,7 +5,6 @@ This module contains the argument manager class
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -25,24 +24,24 @@ class TimeRange:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
starttype: Optional[str] = None,
|
||||
stoptype: Optional[str] = None,
|
||||
starttype: str | None = None,
|
||||
stoptype: str | None = None,
|
||||
startts: int = 0,
|
||||
stopts: int = 0,
|
||||
):
|
||||
self.starttype: Optional[str] = starttype
|
||||
self.stoptype: Optional[str] = stoptype
|
||||
self.starttype: str | None = starttype
|
||||
self.stoptype: str | None = stoptype
|
||||
self.startts: int = startts
|
||||
self.stopts: int = stopts
|
||||
|
||||
@property
|
||||
def startdt(self) -> Optional[datetime]:
|
||||
def startdt(self) -> datetime | None:
|
||||
if self.startts:
|
||||
return datetime.fromtimestamp(self.startts, tz=timezone.utc)
|
||||
return None
|
||||
|
||||
@property
|
||||
def stopdt(self) -> Optional[datetime]:
|
||||
def stopdt(self) -> datetime | None:
|
||||
if self.stopts:
|
||||
return datetime.fromtimestamp(self.stopts, tz=timezone.utc)
|
||||
return None
|
||||
@@ -120,7 +119,7 @@ class TimeRange:
|
||||
self.starttype = "date"
|
||||
|
||||
@classmethod
|
||||
def parse_timerange(cls, text: Optional[str]) -> Self:
|
||||
def parse_timerange(cls, text: str | None) -> Self:
|
||||
"""
|
||||
Parse the value of the argument --timerange to determine what is the range desired
|
||||
:param text: value from --timerange
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
bot constants
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple
|
||||
from typing import Any, Literal
|
||||
|
||||
from freqtrade.enums import CandleType, PriceType
|
||||
|
||||
@@ -38,10 +38,12 @@ HYPEROPT_LOSS_BUILTIN = [
|
||||
"MaxDrawDownHyperOptLoss",
|
||||
"MaxDrawDownRelativeHyperOptLoss",
|
||||
"ProfitDrawDownHyperOptLoss",
|
||||
"MultiMetricHyperOptLoss",
|
||||
]
|
||||
AVAILABLE_PAIRLISTS = [
|
||||
"StaticPairList",
|
||||
"VolumePairList",
|
||||
"PercentChangePairList",
|
||||
"ProducerPairList",
|
||||
"RemotePairList",
|
||||
"MarketCapPairList",
|
||||
@@ -56,7 +58,6 @@ AVAILABLE_PAIRLISTS = [
|
||||
"SpreadFilter",
|
||||
"VolatilityFilter",
|
||||
]
|
||||
AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]
|
||||
AVAILABLE_DATAHANDLERS = ["json", "jsongz", "hdf5", "feather", "parquet"]
|
||||
BACKTEST_BREAKDOWNS = ["day", "week", "month"]
|
||||
BACKTEST_CACHE_AGE = ["none", "day", "week", "month"]
|
||||
@@ -97,7 +98,7 @@ DL_DATA_TIMEFRAMES = ["1m", "5m"]
|
||||
|
||||
ENV_VAR_PREFIX = "FREQTRADE__"
|
||||
|
||||
CANCELED_EXCHANGE_STATES = ("cancelled", "canceled", "expired")
|
||||
CANCELED_EXCHANGE_STATES = ("cancelled", "canceled", "expired", "rejected")
|
||||
NON_OPEN_EXCHANGE_STATES = CANCELED_EXCHANGE_STATES + ("closed",)
|
||||
|
||||
# Define decimals per coin for outputs
|
||||
@@ -187,14 +188,14 @@ CANCEL_REASON = {
|
||||
}
|
||||
|
||||
# List of pairs with their timeframes
|
||||
PairWithTimeframe = Tuple[str, str, CandleType]
|
||||
ListPairsWithTimeframes = List[PairWithTimeframe]
|
||||
PairWithTimeframe = tuple[str, str, CandleType]
|
||||
ListPairsWithTimeframes = list[PairWithTimeframe]
|
||||
|
||||
# Type for trades list
|
||||
TradeList = List[List]
|
||||
TradeList = list[list]
|
||||
# ticks, pair, timeframe, CandleType
|
||||
TickWithTimeframe = Tuple[str, str, CandleType, Optional[int], Optional[int]]
|
||||
ListTicksWithTimeframes = List[TickWithTimeframe]
|
||||
TickWithTimeframe = tuple[str, str, CandleType, int | None, int | None]
|
||||
ListTicksWithTimeframes = list[TickWithTimeframe]
|
||||
|
||||
LongShort = Literal["long", "short"]
|
||||
EntryExit = Literal["entry", "exit"]
|
||||
@@ -203,9 +204,9 @@ MakerTaker = Literal["maker", "taker"]
|
||||
BidAsk = Literal["bid", "ask"]
|
||||
OBLiteral = Literal["asks", "bids"]
|
||||
|
||||
Config = Dict[str, Any]
|
||||
Config = dict[str, Any]
|
||||
# Exchange part of the configuration.
|
||||
ExchangeConfig = Dict[str, Any]
|
||||
ExchangeConfig = dict[str, Any]
|
||||
IntOrInf = float
|
||||
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ import logging
|
||||
from copy import copy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Literal, Optional, Union
|
||||
from typing import Any, Literal
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
|
||||
from freqtrade.exceptions import ConfigurationError, OperationalException
|
||||
from freqtrade.ft_types import BacktestHistoryEntryType, BacktestResultType
|
||||
from freqtrade.misc import file_dump_json, json_load
|
||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||
from freqtrade.types import BacktestHistoryEntryType, BacktestResultType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -53,7 +53,7 @@ BT_DATA_COLUMNS = [
|
||||
]
|
||||
|
||||
|
||||
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
|
||||
def get_latest_optimize_filename(directory: Path | str, variant: str) -> str:
|
||||
"""
|
||||
Get latest backtest export based on '.last_result.json'.
|
||||
:param directory: Directory to search for last result
|
||||
@@ -84,7 +84,7 @@ def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> s
|
||||
return data[f"latest_{variant}"]
|
||||
|
||||
|
||||
def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
|
||||
def get_latest_backtest_filename(directory: Path | str) -> str:
|
||||
"""
|
||||
Get latest backtest export based on '.last_result.json'.
|
||||
:param directory: Directory to search for last result
|
||||
@@ -97,7 +97,7 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
|
||||
return get_latest_optimize_filename(directory, "backtest")
|
||||
|
||||
|
||||
def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
|
||||
def get_latest_hyperopt_filename(directory: Path | str) -> str:
|
||||
"""
|
||||
Get latest hyperopt export based on '.last_result.json'.
|
||||
:param directory: Directory to search for last result
|
||||
@@ -114,9 +114,7 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
|
||||
return "hyperopt_results.pickle"
|
||||
|
||||
|
||||
def get_latest_hyperopt_file(
|
||||
directory: Union[Path, str], predef_filename: Optional[str] = None
|
||||
) -> Path:
|
||||
def get_latest_hyperopt_file(directory: Path | str, predef_filename: str | None = None) -> Path:
|
||||
"""
|
||||
Get latest hyperopt export based on '.last_result.json'.
|
||||
:param directory: Directory to search for last result
|
||||
@@ -137,7 +135,7 @@ def get_latest_hyperopt_file(
|
||||
return directory / get_latest_hyperopt_filename(directory)
|
||||
|
||||
|
||||
def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
def load_backtest_metadata(filename: Path | str) -> dict[str, Any]:
|
||||
"""
|
||||
Read metadata dictionary from backtest results file without reading and deserializing entire
|
||||
file.
|
||||
@@ -154,7 +152,7 @@ def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
raise OperationalException("Unexpected error while loading backtest metadata.") from e
|
||||
|
||||
|
||||
def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
|
||||
def load_backtest_stats(filename: Path | str) -> BacktestResultType:
|
||||
"""
|
||||
Load backtest statistics file.
|
||||
:param filename: pathlib.Path object, or string pointing to the file.
|
||||
@@ -176,7 +174,7 @@ def load_backtest_stats(filename: Union[Path, str]) -> BacktestResultType:
|
||||
return data
|
||||
|
||||
|
||||
def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
|
||||
def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: dict[str, Any]):
|
||||
"""
|
||||
Load one strategy from multi-strategy result and merge it with results
|
||||
:param strategy_name: Name of the strategy contained in the result
|
||||
@@ -195,12 +193,12 @@ def load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
|
||||
break
|
||||
|
||||
|
||||
def _get_backtest_files(dirname: Path) -> List[Path]:
|
||||
def _get_backtest_files(dirname: Path) -> list[Path]:
|
||||
# Weird glob expression here avoids including .meta.json files.
|
||||
return list(reversed(sorted(dirname.glob("backtest-result-*-[0-9][0-9].json"))))
|
||||
|
||||
|
||||
def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||
def _extract_backtest_result(filename: Path) -> list[BacktestHistoryEntryType]:
|
||||
metadata = load_backtest_metadata(filename)
|
||||
return [
|
||||
{
|
||||
@@ -220,14 +218,14 @@ def _extract_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||
]
|
||||
|
||||
|
||||
def get_backtest_result(filename: Path) -> List[BacktestHistoryEntryType]:
|
||||
def get_backtest_result(filename: Path) -> list[BacktestHistoryEntryType]:
|
||||
"""
|
||||
Get backtest result read from metadata file
|
||||
"""
|
||||
return _extract_backtest_result(filename)
|
||||
|
||||
|
||||
def get_backtest_resultlist(dirname: Path) -> List[BacktestHistoryEntryType]:
|
||||
def get_backtest_resultlist(dirname: Path) -> list[BacktestHistoryEntryType]:
|
||||
"""
|
||||
Get list of backtest results read from metadata files
|
||||
"""
|
||||
@@ -244,12 +242,13 @@ def delete_backtest_result(file_abs: Path):
|
||||
"""
|
||||
# *.meta.json
|
||||
logger.info(f"Deleting backtest result file: {file_abs.name}")
|
||||
file_abs_meta = file_abs.with_suffix(".meta.json")
|
||||
file_abs.unlink()
|
||||
file_abs_meta.unlink()
|
||||
|
||||
for file in file_abs.parent.glob(f"{file_abs.stem}*"):
|
||||
logger.info(f"Deleting file: {file}")
|
||||
file.unlink()
|
||||
|
||||
|
||||
def update_backtest_metadata(filename: Path, strategy: str, content: Dict[str, Any]):
|
||||
def update_backtest_metadata(filename: Path, strategy: str, content: dict[str, Any]):
|
||||
"""
|
||||
Updates backtest metadata file with new content.
|
||||
:raises: ValueError if metadata file does not exist, or strategy is not in this file.
|
||||
@@ -275,8 +274,8 @@ def get_backtest_market_change(filename: Path, include_ts: bool = True) -> pd.Da
|
||||
|
||||
|
||||
def find_existing_backtest_stats(
|
||||
dirname: Union[Path, str], run_ids: Dict[str, str], min_backtest_date: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
dirname: Path | str, run_ids: dict[str, str], min_backtest_date: datetime | None = 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.
|
||||
@@ -287,7 +286,7 @@ def find_existing_backtest_stats(
|
||||
# Copy so we can modify this dict without affecting parent scope.
|
||||
run_ids = copy(run_ids)
|
||||
dirname = Path(dirname)
|
||||
results: Dict[str, Any] = {
|
||||
results: dict[str, Any] = {
|
||||
"metadata": {},
|
||||
"strategy": {},
|
||||
"strategy_comparison": [],
|
||||
@@ -344,7 +343,7 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
|
||||
return df
|
||||
|
||||
|
||||
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
||||
def load_backtest_data(filename: Path | str, strategy: str | None = None) -> pd.DataFrame:
|
||||
"""
|
||||
Load backtest data file.
|
||||
:param filename: pathlib.Path object, or string pointing to a file or directory
|
||||
@@ -401,7 +400,15 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
|
||||
|
||||
timeframe_freq = timeframe_to_resample_freq(timeframe)
|
||||
dates = [
|
||||
pd.Series(pd.date_range(row[1]["open_date"], row[1]["close_date"], freq=timeframe_freq))
|
||||
pd.Series(
|
||||
pd.date_range(
|
||||
row[1]["open_date"],
|
||||
row[1]["close_date"],
|
||||
freq=timeframe_freq,
|
||||
# Exclude right boundary - the date is the candle open date.
|
||||
inclusive="left",
|
||||
)
|
||||
)
|
||||
for row in results[["open_date", "close_date"]].iterrows()
|
||||
]
|
||||
deltas = [len(x) for x in dates]
|
||||
@@ -430,7 +437,7 @@ def evaluate_result_multi(
|
||||
return df_final[df_final["open_trades"] > max_open_trades]
|
||||
|
||||
|
||||
def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
|
||||
def trade_list_to_dataframe(trades: list[Trade] | list[LocalTrade]) -> pd.DataFrame:
|
||||
"""
|
||||
Convert list of Trade objects to pandas Dataframe
|
||||
:param trades: List of trade objects
|
||||
@@ -444,7 +451,7 @@ def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.
|
||||
return df
|
||||
|
||||
|
||||
def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataFrame:
|
||||
def load_trades_from_db(db_url: str, strategy: str | None = None) -> pd.DataFrame:
|
||||
"""
|
||||
Load trades from a DB (using dburl)
|
||||
:param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite)
|
||||
@@ -467,7 +474,7 @@ def load_trades(
|
||||
db_url: str,
|
||||
exportfilename: Path,
|
||||
no_trades: bool = False,
|
||||
strategy: Optional[str] = None,
|
||||
strategy: str | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Based on configuration option 'trade_source':
|
||||
|
||||
@@ -3,7 +3,6 @@ Functions to convert data from one format to another
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -158,8 +157,8 @@ def trim_dataframe(
|
||||
|
||||
|
||||
def trim_dataframes(
|
||||
preprocessed: Dict[str, DataFrame], timerange, startup_candles: int
|
||||
) -> Dict[str, DataFrame]:
|
||||
preprocessed: dict[str, DataFrame], timerange, startup_candles: int
|
||||
) -> dict[str, DataFrame]:
|
||||
"""
|
||||
Trim startup period from analyzed dataframes
|
||||
:param preprocessed: Dict of pair: dataframe
|
||||
@@ -167,7 +166,7 @@ def trim_dataframes(
|
||||
:param startup_candles: Startup-candles that should be removed
|
||||
:return: Dict of trimmed dataframes
|
||||
"""
|
||||
processed: Dict[str, DataFrame] = {}
|
||||
processed: dict[str, DataFrame] = {}
|
||||
|
||||
for pair, df in preprocessed.items():
|
||||
trimed_df = trim_dataframe(df, timerange, startup_candles=startup_candles)
|
||||
|
||||
@@ -7,12 +7,11 @@ import time
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS
|
||||
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, Config
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import DependencyException
|
||||
|
||||
@@ -62,11 +61,11 @@ def _calculate_ohlcv_candle_start_and_end(df: pd.DataFrame, timeframe: str):
|
||||
|
||||
|
||||
def populate_dataframe_with_trades(
|
||||
cached_grouped_trades: OrderedDict[Tuple[datetime, datetime], pd.DataFrame],
|
||||
config,
|
||||
cached_grouped_trades: OrderedDict[tuple[datetime, datetime], pd.DataFrame],
|
||||
config: Config,
|
||||
dataframe: pd.DataFrame,
|
||||
trades: pd.DataFrame,
|
||||
) -> Tuple[pd.DataFrame, OrderedDict[Tuple[datetime, datetime], pd.DataFrame]]:
|
||||
) -> tuple[pd.DataFrame, OrderedDict[tuple[datetime, datetime], pd.DataFrame]]:
|
||||
"""
|
||||
Populates a dataframe with trades
|
||||
:param dataframe: Dataframe to populate
|
||||
@@ -78,6 +77,8 @@ def populate_dataframe_with_trades(
|
||||
|
||||
# create columns for trades
|
||||
_init_dataframe_with_trades_columns(dataframe)
|
||||
if trades is None or trades.empty:
|
||||
return dataframe, cached_grouped_trades
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
@@ -88,7 +89,7 @@ def populate_dataframe_with_trades(
|
||||
max_candles = config_orderflow["max_candles"]
|
||||
start_date = dataframe.tail(max_candles).date.iat[0]
|
||||
# slice of trades that are before current ohlcv candles to make groupby faster
|
||||
trades = trades.loc[trades.candle_start >= start_date]
|
||||
trades = trades.loc[trades["candle_start"] >= start_date]
|
||||
trades.reset_index(inplace=True, drop=True)
|
||||
|
||||
# group trades by candle start
|
||||
|
||||
@@ -4,7 +4,6 @@ Functions to convert data from one format to another
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
import pandas as pd
|
||||
from pandas import DataFrame, to_datetime
|
||||
@@ -34,7 +33,7 @@ def trades_df_remove_duplicates(trades: pd.DataFrame) -> pd.DataFrame:
|
||||
return trades.drop_duplicates(subset=["timestamp", "id"])
|
||||
|
||||
|
||||
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
||||
def trades_dict_to_list(trades: list[dict]) -> TradeList:
|
||||
"""
|
||||
Convert fetch_trades result into a List (to be more memory efficient).
|
||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||
@@ -91,8 +90,8 @@ def trades_to_ohlcv(trades: DataFrame, timeframe: str) -> DataFrame:
|
||||
|
||||
|
||||
def convert_trades_to_ohlcv(
|
||||
pairs: List[str],
|
||||
timeframes: List[str],
|
||||
pairs: list[str],
|
||||
timeframes: list[str],
|
||||
datadir: Path,
|
||||
timerange: TimeRange,
|
||||
erase: bool,
|
||||
|
||||
@@ -8,7 +8,7 @@ Common Interface for bot and strategy to access data.
|
||||
import logging
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any
|
||||
|
||||
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
||||
|
||||
@@ -23,7 +23,7 @@ from freqtrade.data.history import get_datahandler, load_pair_history
|
||||
from freqtrade.enums import CandleType, RPCMessageType, RunMode, TradingMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange, timeframe_to_prev_date, timeframe_to_seconds
|
||||
from freqtrade.exchange.types import OrderBook
|
||||
from freqtrade.exchange.exchange_types import OrderBook
|
||||
from freqtrade.misc import append_candles_to_dataframe
|
||||
from freqtrade.rpc import RPCManager
|
||||
from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg
|
||||
@@ -40,23 +40,23 @@ class DataProvider:
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
exchange: Optional[Exchange],
|
||||
exchange: Exchange | None,
|
||||
pairlists=None,
|
||||
rpc: Optional[RPCManager] = None,
|
||||
rpc: RPCManager | None = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._exchange = exchange
|
||||
self._pairlists = pairlists
|
||||
self.__rpc = rpc
|
||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||
self.__slice_index: Optional[int] = None
|
||||
self.__slice_date: Optional[datetime] = None
|
||||
self.__cached_pairs: dict[PairWithTimeframe, tuple[DataFrame, datetime]] = {}
|
||||
self.__slice_index: int | None = None
|
||||
self.__slice_date: datetime | None = None
|
||||
|
||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||
self.__producer_pairs_df: Dict[
|
||||
str, Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]
|
||||
self.__cached_pairs_backtesting: dict[PairWithTimeframe, DataFrame] = {}
|
||||
self.__producer_pairs_df: dict[
|
||||
str, dict[PairWithTimeframe, tuple[DataFrame, datetime]]
|
||||
] = {}
|
||||
self.__producer_pairs: Dict[str, List[str]] = {}
|
||||
self.__producer_pairs: dict[str, list[str]] = {}
|
||||
self._msg_queue: deque = deque()
|
||||
|
||||
self._default_candle_type = self._config.get("candle_type_def", CandleType.SPOT)
|
||||
@@ -101,7 +101,7 @@ class DataProvider:
|
||||
self.__cached_pairs[pair_key] = (dataframe, datetime.now(timezone.utc))
|
||||
|
||||
# For multiple producers we will want to merge the pairlists instead of overwriting
|
||||
def _set_producer_pairs(self, pairlist: List[str], producer_name: str = "default"):
|
||||
def _set_producer_pairs(self, pairlist: list[str], producer_name: str = "default"):
|
||||
"""
|
||||
Set the pairs received to later be used.
|
||||
|
||||
@@ -109,7 +109,7 @@ class DataProvider:
|
||||
"""
|
||||
self.__producer_pairs[producer_name] = pairlist
|
||||
|
||||
def get_producer_pairs(self, producer_name: str = "default") -> List[str]:
|
||||
def get_producer_pairs(self, producer_name: str = "default") -> list[str]:
|
||||
"""
|
||||
Get the pairs cached from the producer
|
||||
|
||||
@@ -177,7 +177,7 @@ class DataProvider:
|
||||
timeframe: str,
|
||||
candle_type: CandleType,
|
||||
producer_name: str = "default",
|
||||
) -> Tuple[bool, int]:
|
||||
) -> tuple[bool, int]:
|
||||
"""
|
||||
Append a candle to the existing external dataframe. The incoming dataframe
|
||||
must have at least 1 candle.
|
||||
@@ -255,10 +255,10 @@ class DataProvider:
|
||||
def get_producer_df(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: Optional[str] = None,
|
||||
candle_type: Optional[CandleType] = None,
|
||||
timeframe: str | None = None,
|
||||
candle_type: CandleType | None = None,
|
||||
producer_name: str = "default",
|
||||
) -> Tuple[DataFrame, datetime]:
|
||||
) -> tuple[DataFrame, datetime]:
|
||||
"""
|
||||
Get the pair data from producers.
|
||||
|
||||
@@ -349,7 +349,7 @@ class DataProvider:
|
||||
return total_candles
|
||||
|
||||
def get_pair_dataframe(
|
||||
self, pair: str, timeframe: Optional[str] = None, candle_type: str = ""
|
||||
self, pair: str, timeframe: str | None = None, candle_type: str = ""
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Return pair candle (OHLCV) data, either live or cached historical -- depending
|
||||
@@ -377,7 +377,7 @@ class DataProvider:
|
||||
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
||||
return data
|
||||
|
||||
def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
|
||||
def get_analyzed_dataframe(self, pair: str, timeframe: str) -> tuple[DataFrame, datetime]:
|
||||
"""
|
||||
Retrieve the analyzed dataframe. Returns the full dataframe in trade mode (live / dry),
|
||||
and the last 1000 candles (up to the time evaluated at this moment) in all other modes.
|
||||
@@ -408,7 +408,7 @@ class DataProvider:
|
||||
"""
|
||||
return RunMode(self._config.get("runmode", RunMode.OTHER))
|
||||
|
||||
def current_whitelist(self) -> List[str]:
|
||||
def current_whitelist(self) -> list[str]:
|
||||
"""
|
||||
fetch latest available whitelist.
|
||||
|
||||
@@ -437,7 +437,7 @@ class DataProvider:
|
||||
def refresh(
|
||||
self,
|
||||
pairlist: ListPairsWithTimeframes,
|
||||
helping_pairs: Optional[ListPairsWithTimeframes] = None,
|
||||
helping_pairs: ListPairsWithTimeframes | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Refresh data, called with each cycle
|
||||
@@ -471,7 +471,7 @@ class DataProvider:
|
||||
return list(self._exchange._klines.keys())
|
||||
|
||||
def ohlcv(
|
||||
self, pair: str, timeframe: Optional[str] = None, copy: bool = True, candle_type: str = ""
|
||||
self, pair: str, timeframe: str | None = None, copy: bool = True, candle_type: str = ""
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Get candle (OHLCV) data for the given pair as DataFrame
|
||||
@@ -497,7 +497,7 @@ class DataProvider:
|
||||
return DataFrame()
|
||||
|
||||
def trades(
|
||||
self, pair: str, timeframe: Optional[str] = None, copy: bool = True, candle_type: str = ""
|
||||
self, pair: str, timeframe: str | None = None, copy: bool = True, candle_type: str = ""
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Get candle (TRADES) data for the given pair as DataFrame
|
||||
@@ -520,22 +520,16 @@ class DataProvider:
|
||||
return self._exchange.trades(
|
||||
(pair, timeframe or self._config["timeframe"], _candle_type), copy=copy
|
||||
)
|
||||
elif self.runmode in (RunMode.BACKTEST, RunMode.HYPEROPT):
|
||||
_candle_type = (
|
||||
CandleType.from_string(candle_type)
|
||||
if candle_type != ""
|
||||
else self._config["candle_type_def"]
|
||||
)
|
||||
else:
|
||||
data_handler = get_datahandler(
|
||||
self._config["datadir"], data_format=self._config["dataformat_trades"]
|
||||
)
|
||||
trades_df = data_handler.trades_load(pair, TradingMode.FUTURES)
|
||||
trades_df = data_handler.trades_load(
|
||||
pair, self._config.get("trading_mode", TradingMode.SPOT)
|
||||
)
|
||||
return trades_df
|
||||
|
||||
else:
|
||||
return DataFrame()
|
||||
|
||||
def market(self, pair: str) -> Optional[Dict[str, Any]]:
|
||||
def market(self, pair: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
Return market data for the pair
|
||||
:param pair: Pair to get the data for
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import joblib
|
||||
import pandas as pd
|
||||
@@ -8,6 +7,7 @@ import pandas as pd
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.btanalysis import (
|
||||
BT_DATA_COLUMNS,
|
||||
get_latest_backtest_filename,
|
||||
load_backtest_data,
|
||||
load_backtest_stats,
|
||||
@@ -47,9 +47,14 @@ def _load_signal_candles(backtest_dir: Path):
|
||||
return _load_backtest_analysis_data(backtest_dir, "signals")
|
||||
|
||||
|
||||
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
|
||||
analysed_trades_dict = {}
|
||||
analysed_trades_dict[strategy_name] = {}
|
||||
def _load_exit_signal_candles(backtest_dir: Path) -> dict[str, dict[str, pd.DataFrame]]:
|
||||
return _load_backtest_analysis_data(backtest_dir, "exited")
|
||||
|
||||
|
||||
def _process_candles_and_indicators(
|
||||
pairlist, strategy_name, trades, signal_candles, date_col: str = "open_date"
|
||||
):
|
||||
analysed_trades_dict: dict[str, dict] = {strategy_name: {}}
|
||||
|
||||
try:
|
||||
logger.info(f"Processing {strategy_name} : {len(pairlist)} pairs")
|
||||
@@ -57,7 +62,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
||||
for pair in pairlist:
|
||||
if pair in signal_candles[strategy_name]:
|
||||
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
||||
pair, trades, signal_candles[strategy_name][pair]
|
||||
pair, trades, signal_candles[strategy_name][pair], date_col
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
||||
@@ -65,7 +70,9 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
|
||||
return analysed_trades_dict
|
||||
|
||||
|
||||
def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: pd.DataFrame):
|
||||
def _analyze_candles_and_indicators(
|
||||
pair: str, trades: pd.DataFrame, signal_candles: pd.DataFrame, date_col: str = "open_date"
|
||||
) -> pd.DataFrame:
|
||||
buyf = signal_candles
|
||||
|
||||
if len(buyf) > 0:
|
||||
@@ -75,8 +82,8 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
|
||||
trades_inds = pd.DataFrame()
|
||||
|
||||
if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
|
||||
for t, v in trades_red.open_date.items():
|
||||
allinds = buyf.loc[(buyf["date"] < v)]
|
||||
for t, v in trades_red.iterrows():
|
||||
allinds = buyf.loc[(buyf["date"] < v[date_col])]
|
||||
if allinds.shape[0] > 0:
|
||||
tmp_inds = allinds.iloc[[-1]]
|
||||
|
||||
@@ -235,7 +242,7 @@ def _select_rows_by_tags(df, enter_reason_list, exit_reason_list):
|
||||
|
||||
def prepare_results(
|
||||
analysed_trades, stratname, enter_reason_list, exit_reason_list, timerange=None
|
||||
):
|
||||
) -> pd.DataFrame:
|
||||
res_df = pd.DataFrame()
|
||||
for pair, trades in analysed_trades[stratname].items():
|
||||
if trades.shape[0] > 0:
|
||||
@@ -252,8 +259,11 @@ def prepare_results(
|
||||
|
||||
def print_results(
|
||||
res_df: pd.DataFrame,
|
||||
analysis_groups: List[str],
|
||||
indicator_list: List[str],
|
||||
exit_df: pd.DataFrame,
|
||||
analysis_groups: list[str],
|
||||
indicator_list: list[str],
|
||||
entry_only: bool,
|
||||
exit_only: bool,
|
||||
csv_path: Path,
|
||||
rejected_signals=None,
|
||||
to_csv=False,
|
||||
@@ -278,9 +288,11 @@ def print_results(
|
||||
for ind in indicator_list:
|
||||
if ind in res_df:
|
||||
available_inds.append(ind)
|
||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||
|
||||
merged_df = _merge_dfs(res_df, exit_df, available_inds, entry_only, exit_only)
|
||||
|
||||
_print_table(
|
||||
res_df[ilist],
|
||||
merged_df,
|
||||
sortcols=["exit_reason"],
|
||||
show_index=False,
|
||||
name="Indicators:",
|
||||
@@ -291,6 +303,36 @@ def print_results(
|
||||
print("\\No trades to show")
|
||||
|
||||
|
||||
def _merge_dfs(
|
||||
entry_df: pd.DataFrame,
|
||||
exit_df: pd.DataFrame,
|
||||
available_inds: list[str],
|
||||
entry_only: bool,
|
||||
exit_only: bool,
|
||||
):
|
||||
merge_on = ["pair", "open_date"]
|
||||
signal_wide_indicators = list(set(available_inds) - set(BT_DATA_COLUMNS))
|
||||
columns_to_keep = merge_on + ["enter_reason", "exit_reason"]
|
||||
|
||||
if exit_df is None or exit_df.empty or entry_only is True:
|
||||
return entry_df[columns_to_keep + available_inds]
|
||||
|
||||
if exit_only is True:
|
||||
return pd.merge(
|
||||
entry_df[columns_to_keep],
|
||||
exit_df[merge_on + signal_wide_indicators],
|
||||
on=merge_on,
|
||||
suffixes=(" (entry)", " (exit)"),
|
||||
)
|
||||
|
||||
return pd.merge(
|
||||
entry_df[columns_to_keep + available_inds],
|
||||
exit_df[merge_on + signal_wide_indicators],
|
||||
on=merge_on,
|
||||
suffixes=(" (entry)", " (exit)"),
|
||||
)
|
||||
|
||||
|
||||
def _print_table(
|
||||
df: pd.DataFrame, sortcols=None, *, show_index=False, name=None, to_csv=False, csv_path: Path
|
||||
):
|
||||
@@ -316,9 +358,16 @@ def process_entry_exit_reasons(config: Config):
|
||||
enter_reason_list = config.get("enter_reason_list", ["all"])
|
||||
exit_reason_list = config.get("exit_reason_list", ["all"])
|
||||
indicator_list = config.get("indicator_list", [])
|
||||
entry_only = config.get("entry_only", False)
|
||||
exit_only = config.get("exit_only", False)
|
||||
do_rejected = config.get("analysis_rejected", False)
|
||||
to_csv = config.get("analysis_to_csv", False)
|
||||
csv_path = Path(config.get("analysis_csv_path", config["exportfilename"]))
|
||||
|
||||
if entry_only is True and exit_only is True:
|
||||
raise OperationalException(
|
||||
"Cannot use --entry-only and --exit-only at the same time. Please choose one."
|
||||
)
|
||||
if to_csv and not csv_path.is_dir():
|
||||
raise OperationalException(f"Specified directory {csv_path} does not exist.")
|
||||
|
||||
@@ -333,6 +382,7 @@ def process_entry_exit_reasons(config: Config):
|
||||
|
||||
if trades is not None and not trades.empty:
|
||||
signal_candles = _load_signal_candles(config["exportfilename"])
|
||||
exit_signals = _load_exit_signal_candles(config["exportfilename"])
|
||||
|
||||
rej_df = None
|
||||
if do_rejected:
|
||||
@@ -345,22 +395,35 @@ def process_entry_exit_reasons(config: Config):
|
||||
timerange=timerange,
|
||||
)
|
||||
|
||||
analysed_trades_dict = _process_candles_and_indicators(
|
||||
config["exchange"]["pair_whitelist"], strategy_name, trades, signal_candles
|
||||
)
|
||||
|
||||
res_df = prepare_results(
|
||||
analysed_trades_dict,
|
||||
strategy_name,
|
||||
entry_df = _generate_dfs(
|
||||
config["exchange"]["pair_whitelist"],
|
||||
enter_reason_list,
|
||||
exit_reason_list,
|
||||
timerange=timerange,
|
||||
signal_candles,
|
||||
strategy_name,
|
||||
timerange,
|
||||
trades,
|
||||
"open_date",
|
||||
)
|
||||
|
||||
exit_df = _generate_dfs(
|
||||
config["exchange"]["pair_whitelist"],
|
||||
enter_reason_list,
|
||||
exit_reason_list,
|
||||
exit_signals,
|
||||
strategy_name,
|
||||
timerange,
|
||||
trades,
|
||||
"close_date",
|
||||
)
|
||||
|
||||
print_results(
|
||||
res_df,
|
||||
entry_df,
|
||||
exit_df,
|
||||
analysis_groups,
|
||||
indicator_list,
|
||||
entry_only,
|
||||
exit_only,
|
||||
rejected_signals=rej_df,
|
||||
to_csv=to_csv,
|
||||
csv_path=csv_path,
|
||||
@@ -368,3 +431,30 @@ def process_entry_exit_reasons(config: Config):
|
||||
|
||||
except ValueError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
|
||||
def _generate_dfs(
|
||||
pairlist: list,
|
||||
enter_reason_list: list,
|
||||
exit_reason_list: list,
|
||||
signal_candles: dict,
|
||||
strategy_name: str,
|
||||
timerange: TimeRange,
|
||||
trades: pd.DataFrame,
|
||||
date_col: str,
|
||||
) -> pd.DataFrame:
|
||||
analysed_trades_dict = _process_candles_and_indicators(
|
||||
pairlist,
|
||||
strategy_name,
|
||||
trades,
|
||||
signal_candles,
|
||||
date_col,
|
||||
)
|
||||
res_df = prepare_results(
|
||||
analysed_trades_dict,
|
||||
strategy_name,
|
||||
enter_reason_list,
|
||||
exit_reason_list,
|
||||
timerange=timerange,
|
||||
)
|
||||
return res_df
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from pandas import DataFrame, read_feather, to_datetime
|
||||
|
||||
@@ -37,7 +36,7 @@ class FeatherDataHandler(IDataHandler):
|
||||
)
|
||||
|
||||
def _ohlcv_load(
|
||||
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
|
||||
self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Internal method used to load data for one pair from disk.
|
||||
@@ -59,20 +58,25 @@ class FeatherDataHandler(IDataHandler):
|
||||
)
|
||||
if not filename.exists():
|
||||
return DataFrame(columns=self._columns)
|
||||
|
||||
pairdata = read_feather(filename)
|
||||
pairdata.columns = self._columns
|
||||
pairdata = pairdata.astype(
|
||||
dtype={
|
||||
"open": "float",
|
||||
"high": "float",
|
||||
"low": "float",
|
||||
"close": "float",
|
||||
"volume": "float",
|
||||
}
|
||||
)
|
||||
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
||||
return pairdata
|
||||
try:
|
||||
pairdata = read_feather(filename)
|
||||
pairdata.columns = self._columns
|
||||
pairdata = pairdata.astype(
|
||||
dtype={
|
||||
"open": "float",
|
||||
"high": "float",
|
||||
"low": "float",
|
||||
"close": "float",
|
||||
"volume": "float",
|
||||
}
|
||||
)
|
||||
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
||||
return pairdata
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error loading data from {filename}. Exception: {e}. Returning empty dataframe."
|
||||
)
|
||||
return DataFrame(columns=self._columns)
|
||||
|
||||
def ohlcv_append(
|
||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||
@@ -108,7 +112,7 @@ class FeatherDataHandler(IDataHandler):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _trades_load(
|
||||
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
||||
self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -45,7 +44,7 @@ class HDF5DataHandler(IDataHandler):
|
||||
)
|
||||
|
||||
def _ohlcv_load(
|
||||
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
|
||||
self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Internal method used to load data for one pair from disk.
|
||||
@@ -69,28 +68,36 @@ class HDF5DataHandler(IDataHandler):
|
||||
)
|
||||
if not filename.exists():
|
||||
return pd.DataFrame(columns=self._columns)
|
||||
where = []
|
||||
if timerange:
|
||||
if timerange.starttype == "date":
|
||||
where.append(f"date >= Timestamp({timerange.startts * 1e9})")
|
||||
if timerange.stoptype == "date":
|
||||
where.append(f"date <= Timestamp({timerange.stopts * 1e9})")
|
||||
try:
|
||||
where = []
|
||||
if timerange:
|
||||
if timerange.starttype == "date":
|
||||
where.append(f"date >= Timestamp({timerange.startts * 1e9})")
|
||||
if timerange.stoptype == "date":
|
||||
where.append(f"date <= Timestamp({timerange.stopts * 1e9})")
|
||||
|
||||
pairdata = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||
pairdata = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||
|
||||
if list(pairdata.columns) != self._columns:
|
||||
raise ValueError("Wrong dataframe format")
|
||||
pairdata = pairdata.astype(
|
||||
dtype={
|
||||
"open": "float",
|
||||
"high": "float",
|
||||
"low": "float",
|
||||
"close": "float",
|
||||
"volume": "float",
|
||||
}
|
||||
)
|
||||
pairdata = pairdata.reset_index(drop=True)
|
||||
return pairdata
|
||||
if list(pairdata.columns) != self._columns:
|
||||
raise ValueError("Wrong dataframe format")
|
||||
pairdata = pairdata.astype(
|
||||
dtype={
|
||||
"open": "float",
|
||||
"high": "float",
|
||||
"low": "float",
|
||||
"close": "float",
|
||||
"volume": "float",
|
||||
}
|
||||
)
|
||||
pairdata = pairdata.reset_index(drop=True)
|
||||
return pairdata
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error loading data from {filename}. Exception: {e}. Returning empty dataframe."
|
||||
)
|
||||
return pd.DataFrame(columns=self._columns)
|
||||
|
||||
def ohlcv_append(
|
||||
self, pair: str, timeframe: str, data: pd.DataFrame, candle_type: CandleType
|
||||
@@ -134,7 +141,7 @@ class HDF5DataHandler(IDataHandler):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _trades_load(
|
||||
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
||||
self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Load a pair from h5 file.
|
||||
|
||||
@@ -10,9 +10,8 @@ from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple, Type
|
||||
|
||||
from pandas import DataFrame
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade import misc
|
||||
from freqtrade.configuration import TimeRange
|
||||
@@ -32,6 +31,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class IDataHandler(ABC):
|
||||
_OHLCV_REGEX = r"^([a-zA-Z_\d-]+)\-(\d+[a-zA-Z]{1,2})\-?([a-zA-Z_]*)?(?=\.)"
|
||||
_TRADES_REGEX = r"^([a-zA-Z_\d-]+)\-(trades)?(?=\.)"
|
||||
|
||||
def __init__(self, datadir: Path) -> None:
|
||||
self._datadir = datadir
|
||||
@@ -70,7 +70,7 @@ class IDataHandler(ABC):
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
|
||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> list[str]:
|
||||
"""
|
||||
Returns a list of all pairs with ohlcv data available in this datadir
|
||||
for the specified timeframe
|
||||
@@ -106,7 +106,7 @@ class IDataHandler(ABC):
|
||||
|
||||
def ohlcv_data_min_max(
|
||||
self, pair: str, timeframe: str, candle_type: CandleType
|
||||
) -> Tuple[datetime, datetime, int]:
|
||||
) -> tuple[datetime, datetime, int]:
|
||||
"""
|
||||
Returns the min and max timestamp for the given pair and timeframe.
|
||||
:param pair: Pair to get min/max for
|
||||
@@ -125,7 +125,7 @@ class IDataHandler(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def _ohlcv_load(
|
||||
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
|
||||
self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Internal method used to load data for one pair from disk.
|
||||
@@ -167,7 +167,51 @@ class IDataHandler(ABC):
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def trades_get_pairs(cls, datadir: Path) -> List[str]:
|
||||
def trades_get_available_data(cls, datadir: Path, trading_mode: TradingMode) -> list[str]:
|
||||
"""
|
||||
Returns a list of all pairs with ohlcv data available in this datadir
|
||||
:param datadir: Directory to search for ohlcv files
|
||||
:param trading_mode: trading-mode to be used
|
||||
:return: List of Tuples of (pair, timeframe, CandleType)
|
||||
"""
|
||||
if trading_mode == TradingMode.FUTURES:
|
||||
datadir = datadir.joinpath("futures")
|
||||
_tmp = [
|
||||
re.search(cls._TRADES_REGEX, p.name)
|
||||
for p in datadir.glob(f"*.{cls._get_file_extension()}")
|
||||
]
|
||||
return [
|
||||
cls.rebuild_pair_from_filename(match[1])
|
||||
for match in _tmp
|
||||
if match and len(match.groups()) > 1
|
||||
]
|
||||
|
||||
def trades_data_min_max(
|
||||
self,
|
||||
pair: str,
|
||||
trading_mode: TradingMode,
|
||||
) -> tuple[datetime, datetime, int]:
|
||||
"""
|
||||
Returns the min and max timestamp for the given pair's trades data.
|
||||
:param pair: Pair to get min/max for
|
||||
:param trading_mode: Trading mode to use (used to determine the filename)
|
||||
:return: (min, max, len)
|
||||
"""
|
||||
df = self._trades_load(pair, trading_mode)
|
||||
if df.empty:
|
||||
return (
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
datetime.fromtimestamp(0, tz=timezone.utc),
|
||||
0,
|
||||
)
|
||||
return (
|
||||
to_datetime(df.iloc[0]["timestamp"], unit="ms", utc=True).to_pydatetime(),
|
||||
to_datetime(df.iloc[-1]["timestamp"], unit="ms", utc=True).to_pydatetime(),
|
||||
len(df),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def trades_get_pairs(cls, datadir: Path) -> list[str]:
|
||||
"""
|
||||
Returns a list of all pairs for which trade data is available in this
|
||||
:param datadir: Directory to search for ohlcv files
|
||||
@@ -202,7 +246,7 @@ class IDataHandler(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def _trades_load(
|
||||
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
||||
self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
@@ -237,7 +281,7 @@ class IDataHandler(ABC):
|
||||
return False
|
||||
|
||||
def trades_load(
|
||||
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
||||
self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
@@ -247,9 +291,13 @@ class IDataHandler(ABC):
|
||||
:param timerange: Timerange to load trades for - currently not implemented
|
||||
:return: List of trades
|
||||
"""
|
||||
trades = trades_df_remove_duplicates(
|
||||
self._trades_load(pair, trading_mode, timerange=timerange)
|
||||
)
|
||||
try:
|
||||
trades = self._trades_load(pair, trading_mode, timerange=timerange)
|
||||
except Exception:
|
||||
logger.exception(f"Error loading trades for {pair}")
|
||||
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||
|
||||
trades = trades_df_remove_duplicates(trades)
|
||||
|
||||
trades = trades_convert_types(trades)
|
||||
return trades
|
||||
@@ -321,7 +369,7 @@ class IDataHandler(ABC):
|
||||
timeframe: str,
|
||||
candle_type: CandleType,
|
||||
*,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
timerange: TimeRange | None = None,
|
||||
fill_missing: bool = True,
|
||||
drop_incomplete: bool = False,
|
||||
startup_candles: int = 0,
|
||||
@@ -483,7 +531,7 @@ class IDataHandler(ABC):
|
||||
Path(old_name).rename(new_name)
|
||||
|
||||
|
||||
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
||||
def get_datahandlerclass(datatype: str) -> type[IDataHandler]:
|
||||
"""
|
||||
Get datahandler class.
|
||||
Could be done using Resolvers, but since this may be called often and resolvers
|
||||
@@ -517,7 +565,7 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
|
||||
|
||||
|
||||
def get_datahandler(
|
||||
datadir: Path, data_format: Optional[str] = None, data_handler: Optional[IDataHandler] = None
|
||||
datadir: Path, data_format: str | None = None, data_handler: IDataHandler | None = None
|
||||
) -> IDataHandler:
|
||||
"""
|
||||
:param datadir: Folder to save data
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from pandas import DataFrame, read_json, to_datetime
|
||||
@@ -45,7 +44,7 @@ class JsonDataHandler(IDataHandler):
|
||||
)
|
||||
|
||||
def _ohlcv_load(
|
||||
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
|
||||
self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Internal method used to load data for one pair from disk.
|
||||
@@ -119,7 +118,7 @@ class JsonDataHandler(IDataHandler):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _trades_load(
|
||||
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
||||
self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from pandas import DataFrame, read_parquet, to_datetime
|
||||
|
||||
@@ -35,7 +34,7 @@ class ParquetDataHandler(IDataHandler):
|
||||
data.reset_index(drop=True).loc[:, self._columns].to_parquet(filename)
|
||||
|
||||
def _ohlcv_load(
|
||||
self, pair: str, timeframe: str, timerange: Optional[TimeRange], candle_type: CandleType
|
||||
self, pair: str, timeframe: str, timerange: TimeRange | None, candle_type: CandleType
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Internal method used to load data for one pair from disk.
|
||||
@@ -57,20 +56,25 @@ class ParquetDataHandler(IDataHandler):
|
||||
)
|
||||
if not filename.exists():
|
||||
return DataFrame(columns=self._columns)
|
||||
|
||||
pairdata = read_parquet(filename)
|
||||
pairdata.columns = self._columns
|
||||
pairdata = pairdata.astype(
|
||||
dtype={
|
||||
"open": "float",
|
||||
"high": "float",
|
||||
"low": "float",
|
||||
"close": "float",
|
||||
"volume": "float",
|
||||
}
|
||||
)
|
||||
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
||||
return pairdata
|
||||
try:
|
||||
pairdata = read_parquet(filename)
|
||||
pairdata.columns = self._columns
|
||||
pairdata = pairdata.astype(
|
||||
dtype={
|
||||
"open": "float",
|
||||
"high": "float",
|
||||
"low": "float",
|
||||
"close": "float",
|
||||
"volume": "float",
|
||||
}
|
||||
)
|
||||
pairdata["date"] = to_datetime(pairdata["date"], unit="ms", utc=True)
|
||||
return pairdata
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error loading data from {filename}. Exception: {e}. Returning empty dataframe."
|
||||
)
|
||||
return DataFrame(columns=self._columns)
|
||||
|
||||
def ohlcv_append(
|
||||
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType
|
||||
@@ -106,7 +110,7 @@ class ParquetDataHandler(IDataHandler):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _trades_load(
|
||||
self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None
|
||||
self, pair: str, trading_mode: TradingMode, timerange: TimeRange | None = None
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
|
||||
@@ -2,22 +2,14 @@ import logging
|
||||
import operator
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame, concat
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import (
|
||||
DATETIME_PRINT_FORMAT,
|
||||
DEFAULT_DATAFRAME_COLUMNS,
|
||||
DL_DATA_TIMEFRAMES,
|
||||
DOCS_LINK,
|
||||
Config,
|
||||
)
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, DOCS_LINK, Config
|
||||
from freqtrade.data.converter import (
|
||||
clean_ohlcv_dataframe,
|
||||
convert_trades_to_ohlcv,
|
||||
ohlcv_to_dataframe,
|
||||
trades_df_remove_duplicates,
|
||||
trades_list_to_df,
|
||||
)
|
||||
@@ -26,8 +18,9 @@ from freqtrade.enums import CandleType, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.util import dt_now, dt_ts, format_ms_time, get_progress_tracker
|
||||
from freqtrade.util import dt_now, dt_ts, format_ms_time
|
||||
from freqtrade.util.migrations import migrate_data
|
||||
from freqtrade.util.progress_tracker import CustomProgress, retrieve_progress_tracker
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,12 +31,12 @@ def load_pair_history(
|
||||
timeframe: str,
|
||||
datadir: Path,
|
||||
*,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
timerange: TimeRange | None = None,
|
||||
fill_up_missing: bool = True,
|
||||
drop_incomplete: bool = False,
|
||||
startup_candles: int = 0,
|
||||
data_format: Optional[str] = None,
|
||||
data_handler: Optional[IDataHandler] = None,
|
||||
data_format: str | None = None,
|
||||
data_handler: IDataHandler | None = None,
|
||||
candle_type: CandleType = CandleType.SPOT,
|
||||
) -> DataFrame:
|
||||
"""
|
||||
@@ -78,16 +71,16 @@ def load_pair_history(
|
||||
def load_data(
|
||||
datadir: Path,
|
||||
timeframe: str,
|
||||
pairs: List[str],
|
||||
pairs: list[str],
|
||||
*,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
timerange: TimeRange | None = None,
|
||||
fill_up_missing: bool = True,
|
||||
startup_candles: int = 0,
|
||||
fail_without_data: bool = False,
|
||||
data_format: str = "feather",
|
||||
candle_type: CandleType = CandleType.SPOT,
|
||||
user_futures_funding_rate: Optional[int] = None,
|
||||
) -> Dict[str, DataFrame]:
|
||||
user_futures_funding_rate: int | None = None,
|
||||
) -> dict[str, DataFrame]:
|
||||
"""
|
||||
Load ohlcv history data for a list of pairs.
|
||||
|
||||
@@ -102,7 +95,7 @@ def load_data(
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
:return: dict(<pair>:<Dataframe>)
|
||||
"""
|
||||
result: Dict[str, DataFrame] = {}
|
||||
result: dict[str, DataFrame] = {}
|
||||
if startup_candles > 0 and timerange:
|
||||
logger.info(f"Using indicator startup period: {startup_candles} ...")
|
||||
|
||||
@@ -136,10 +129,10 @@ def refresh_data(
|
||||
*,
|
||||
datadir: Path,
|
||||
timeframe: str,
|
||||
pairs: List[str],
|
||||
pairs: list[str],
|
||||
exchange: Exchange,
|
||||
data_format: Optional[str] = None,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
data_format: str | None = None,
|
||||
timerange: TimeRange | None = None,
|
||||
candle_type: CandleType,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -169,11 +162,11 @@ def refresh_data(
|
||||
def _load_cached_data_for_updating(
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
timerange: Optional[TimeRange],
|
||||
timerange: TimeRange | None,
|
||||
data_handler: IDataHandler,
|
||||
candle_type: CandleType,
|
||||
prepend: bool = False,
|
||||
) -> Tuple[DataFrame, Optional[int], Optional[int]]:
|
||||
) -> tuple[DataFrame, int | None, int | None]:
|
||||
"""
|
||||
Load cached data to download more data.
|
||||
If timerange is passed in, checks whether data from an before the stored data will be
|
||||
@@ -201,14 +194,21 @@ def _load_cached_data_for_updating(
|
||||
candle_type=candle_type,
|
||||
)
|
||||
if not data.empty:
|
||||
if not prepend and start and start < data.iloc[0]["date"]:
|
||||
# Earlier data than existing data requested, redownload all
|
||||
data = DataFrame(columns=DEFAULT_DATAFRAME_COLUMNS)
|
||||
if prepend:
|
||||
end = data.iloc[0]["date"]
|
||||
else:
|
||||
if prepend:
|
||||
end = data.iloc[0]["date"]
|
||||
else:
|
||||
start = data.iloc[-1]["date"]
|
||||
if start and start < data.iloc[0]["date"]:
|
||||
# Earlier data than existing data requested, Update start date
|
||||
logger.info(
|
||||
f"{pair}, {timeframe}, {candle_type}: "
|
||||
f"Requested start date {start:{DATETIME_PRINT_FORMAT}} earlier than local "
|
||||
f"data start date {data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}. "
|
||||
f"Use `--prepend` to download data prior "
|
||||
f"to {data.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}, or "
|
||||
"`--erase` to redownload all data."
|
||||
)
|
||||
start = data.iloc[-1]["date"]
|
||||
|
||||
start_ms = int(start.timestamp() * 1000) if start else None
|
||||
end_ms = int(end.timestamp() * 1000) if end else None
|
||||
return data, start_ms, end_ms
|
||||
@@ -221,8 +221,8 @@ def _download_pair_history(
|
||||
exchange: Exchange,
|
||||
timeframe: str = "5m",
|
||||
new_pairs_days: int = 30,
|
||||
data_handler: Optional[IDataHandler] = None,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
data_handler: IDataHandler | None = None,
|
||||
timerange: TimeRange | None = None,
|
||||
candle_type: CandleType,
|
||||
erase: bool = False,
|
||||
prepend: bool = False,
|
||||
@@ -273,7 +273,7 @@ def _download_pair_history(
|
||||
)
|
||||
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
new_data = exchange.get_historic_ohlcv(
|
||||
new_dataframe = exchange.get_historic_ohlcv(
|
||||
pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=(
|
||||
@@ -285,10 +285,6 @@ def _download_pair_history(
|
||||
candle_type=candle_type,
|
||||
until_ms=until_ms if until_ms else None,
|
||||
)
|
||||
# TODO: Maybe move parsing to exchange class (?)
|
||||
new_dataframe = ohlcv_to_dataframe(
|
||||
new_data, timeframe, pair, fill_missing=False, drop_incomplete=True
|
||||
)
|
||||
if data.empty:
|
||||
data = new_dataframe
|
||||
else:
|
||||
@@ -323,25 +319,28 @@ def _download_pair_history(
|
||||
|
||||
def refresh_backtest_ohlcv_data(
|
||||
exchange: Exchange,
|
||||
pairs: List[str],
|
||||
timeframes: List[str],
|
||||
pairs: list[str],
|
||||
timeframes: list[str],
|
||||
datadir: Path,
|
||||
trading_mode: str,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
timerange: TimeRange | None = None,
|
||||
new_pairs_days: int = 30,
|
||||
erase: bool = False,
|
||||
data_format: Optional[str] = None,
|
||||
data_format: str | None = None,
|
||||
prepend: bool = False,
|
||||
) -> List[str]:
|
||||
progress_tracker: CustomProgress | None = None,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
||||
Used by freqtrade download-data subcommand.
|
||||
:return: List of pairs that are not available.
|
||||
"""
|
||||
progress_tracker = retrieve_progress_tracker(progress_tracker)
|
||||
|
||||
pairs_not_available = []
|
||||
data_handler = get_datahandler(datadir, data_format)
|
||||
candle_type = CandleType.get_default(trading_mode)
|
||||
with get_progress_tracker() as progress:
|
||||
with progress_tracker as progress:
|
||||
tf_length = len(timeframes) if trading_mode != "futures" else len(timeframes) + 2
|
||||
timeframe_task = progress.add_task("Timeframe", total=tf_length)
|
||||
pair_task = progress.add_task("Downloading data...", total=len(pairs))
|
||||
@@ -351,7 +350,7 @@ def refresh_backtest_ohlcv_data(
|
||||
progress.update(timeframe_task, completed=0)
|
||||
|
||||
if pair not in exchange.markets:
|
||||
pairs_not_available.append(pair)
|
||||
pairs_not_available.append(f"{pair}: Pair not available on exchange.")
|
||||
logger.info(f"Skipping pair {pair}...")
|
||||
continue
|
||||
for timeframe in timeframes:
|
||||
@@ -409,7 +408,7 @@ def _download_trades_history(
|
||||
pair: str,
|
||||
*,
|
||||
new_pairs_days: int = 30,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
timerange: TimeRange | None = None,
|
||||
data_handler: IDataHandler,
|
||||
trading_mode: TradingMode,
|
||||
) -> bool:
|
||||
@@ -417,104 +416,101 @@ def _download_trades_history(
|
||||
Download trade history from the exchange.
|
||||
Appends to previously downloaded trades data.
|
||||
"""
|
||||
try:
|
||||
until = None
|
||||
since = 0
|
||||
if timerange:
|
||||
if timerange.starttype == "date":
|
||||
since = timerange.startts * 1000
|
||||
if timerange.stoptype == "date":
|
||||
until = timerange.stopts * 1000
|
||||
until = None
|
||||
since = 0
|
||||
if timerange:
|
||||
if timerange.starttype == "date":
|
||||
since = timerange.startts * 1000
|
||||
if timerange.stoptype == "date":
|
||||
until = timerange.stopts * 1000
|
||||
|
||||
trades = data_handler.trades_load(pair, trading_mode)
|
||||
trades = data_handler.trades_load(pair, trading_mode)
|
||||
|
||||
# TradesList columns are defined in constants.DEFAULT_TRADES_COLUMNS
|
||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||
# TradesList columns are defined in constants.DEFAULT_TRADES_COLUMNS
|
||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||
|
||||
if not trades.empty and since > 0 and since < trades.iloc[0]["timestamp"]:
|
||||
# since is before the first trade
|
||||
logger.info(
|
||||
f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than "
|
||||
f"available data. Redownloading trades for {pair}..."
|
||||
)
|
||||
trades = trades_list_to_df([])
|
||||
|
||||
from_id = trades.iloc[-1]["id"] if not trades.empty else None
|
||||
if not trades.empty and since < trades.iloc[-1]["timestamp"]:
|
||||
# Reset since to the last available point
|
||||
# - 5 seconds (to ensure we're getting all trades)
|
||||
since = trades.iloc[-1]["timestamp"] - (5 * 1000)
|
||||
logger.info(
|
||||
f"Using last trade date -5s - Downloading trades for {pair} "
|
||||
f"since: {format_ms_time(since)}."
|
||||
)
|
||||
|
||||
if not since:
|
||||
since = dt_ts(dt_now() - timedelta(days=new_pairs_days))
|
||||
|
||||
logger.debug(
|
||||
"Current Start: %s",
|
||||
"None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||
if not trades.empty and since > 0 and since < trades.iloc[0]["timestamp"]:
|
||||
# since is before the first trade
|
||||
raise ValueError(
|
||||
f"Start {format_ms_time(since)} earlier than "
|
||||
f"available data ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}). "
|
||||
f"Please use `--erase` if you'd like to redownload {pair}."
|
||||
)
|
||||
logger.debug(
|
||||
"Current End: %s",
|
||||
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||
)
|
||||
logger.info(f"Current Amount of trades: {len(trades)}")
|
||||
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
new_trades = exchange.get_historic_trades(
|
||||
pair=pair,
|
||||
since=since,
|
||||
until=until,
|
||||
from_id=from_id,
|
||||
from_id = trades.iloc[-1]["id"] if not trades.empty else None
|
||||
if not trades.empty and since < trades.iloc[-1]["timestamp"]:
|
||||
# Reset since to the last available point
|
||||
# - 5 seconds (to ensure we're getting all trades)
|
||||
since = trades.iloc[-1]["timestamp"] - (5 * 1000)
|
||||
logger.info(
|
||||
f"Using last trade date -5s - Downloading trades for {pair} "
|
||||
f"since: {format_ms_time(since)}."
|
||||
)
|
||||
new_trades_df = trades_list_to_df(new_trades[1])
|
||||
trades = concat([trades, new_trades_df], axis=0)
|
||||
# Remove duplicates to make sure we're not storing data we don't need
|
||||
trades = trades_df_remove_duplicates(trades)
|
||||
data_handler.trades_store(pair, trades, trading_mode)
|
||||
|
||||
logger.debug(
|
||||
"New Start: %s",
|
||||
"None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||
)
|
||||
logger.debug(
|
||||
"New End: %s",
|
||||
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||
)
|
||||
logger.info(f"New Amount of trades: {len(trades)}")
|
||||
return True
|
||||
if not since:
|
||||
since = dt_ts(dt_now() - timedelta(days=new_pairs_days))
|
||||
|
||||
except Exception:
|
||||
logger.exception(f'Failed to download and store historic trades for pair: "{pair}". ')
|
||||
return False
|
||||
logger.debug(
|
||||
"Current Start: %s",
|
||||
"None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||
)
|
||||
logger.debug(
|
||||
"Current End: %s",
|
||||
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||
)
|
||||
logger.info(f"Current Amount of trades: {len(trades)}")
|
||||
|
||||
# Default since_ms to 30 days if nothing is given
|
||||
new_trades = exchange.get_historic_trades(
|
||||
pair=pair,
|
||||
since=since,
|
||||
until=until,
|
||||
from_id=from_id,
|
||||
)
|
||||
new_trades_df = trades_list_to_df(new_trades[1])
|
||||
trades = concat([trades, new_trades_df], axis=0)
|
||||
# Remove duplicates to make sure we're not storing data we don't need
|
||||
trades = trades_df_remove_duplicates(trades)
|
||||
data_handler.trades_store(pair, trades, trading_mode)
|
||||
|
||||
logger.debug(
|
||||
"New Start: %s",
|
||||
"None" if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||
)
|
||||
logger.debug(
|
||||
"New End: %s",
|
||||
"None" if trades.empty else f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}",
|
||||
)
|
||||
logger.info(f"New Amount of trades: {len(trades)}")
|
||||
return True
|
||||
|
||||
|
||||
def refresh_backtest_trades_data(
|
||||
exchange: Exchange,
|
||||
pairs: List[str],
|
||||
pairs: list[str],
|
||||
datadir: Path,
|
||||
timerange: TimeRange,
|
||||
trading_mode: TradingMode,
|
||||
new_pairs_days: int = 30,
|
||||
erase: bool = False,
|
||||
data_format: str = "feather",
|
||||
) -> List[str]:
|
||||
progress_tracker: CustomProgress | None = None,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Refresh stored trades data for backtesting and hyperopt operations.
|
||||
Used by freqtrade download-data subcommand.
|
||||
:return: List of pairs that are not available.
|
||||
"""
|
||||
progress_tracker = retrieve_progress_tracker(progress_tracker)
|
||||
pairs_not_available = []
|
||||
data_handler = get_datahandler(datadir, data_format=data_format)
|
||||
with get_progress_tracker() as progress:
|
||||
with progress_tracker as progress:
|
||||
pair_task = progress.add_task("Downloading data...", total=len(pairs))
|
||||
for pair in pairs:
|
||||
progress.update(pair_task, description=f"Downloading trades [{pair}]")
|
||||
if pair not in exchange.markets:
|
||||
pairs_not_available.append(pair)
|
||||
pairs_not_available.append(f"{pair}: Pair not available on exchange.")
|
||||
logger.info(f"Skipping pair {pair}...")
|
||||
continue
|
||||
|
||||
@@ -523,20 +519,28 @@ def refresh_backtest_trades_data(
|
||||
logger.info(f"Deleting existing data for pair {pair}.")
|
||||
|
||||
logger.info(f"Downloading trades for pair {pair}.")
|
||||
_download_trades_history(
|
||||
exchange=exchange,
|
||||
pair=pair,
|
||||
new_pairs_days=new_pairs_days,
|
||||
timerange=timerange,
|
||||
data_handler=data_handler,
|
||||
trading_mode=trading_mode,
|
||||
)
|
||||
try:
|
||||
_download_trades_history(
|
||||
exchange=exchange,
|
||||
pair=pair,
|
||||
new_pairs_days=new_pairs_days,
|
||||
timerange=timerange,
|
||||
data_handler=data_handler,
|
||||
trading_mode=trading_mode,
|
||||
)
|
||||
except ValueError as e:
|
||||
pairs_not_available.append(f"{pair}: {str(e)}")
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Failed to download and store historic trades for pair: "{pair}". '
|
||||
)
|
||||
|
||||
progress.update(pair_task, advance=1)
|
||||
|
||||
return pairs_not_available
|
||||
|
||||
|
||||
def get_timerange(data: Dict[str, DataFrame]) -> Tuple[datetime, datetime]:
|
||||
def get_timerange(data: dict[str, DataFrame]) -> tuple[datetime, datetime]:
|
||||
"""
|
||||
Get the maximum common timerange for the given backtest data.
|
||||
|
||||
@@ -582,23 +586,35 @@ def validate_backtest_data(
|
||||
|
||||
|
||||
def download_data_main(config: Config) -> None:
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
|
||||
download_data(config, exchange)
|
||||
|
||||
|
||||
def download_data(
|
||||
config: Config,
|
||||
exchange: Exchange,
|
||||
*,
|
||||
progress_tracker: CustomProgress | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Download data function. Used from both cli and API.
|
||||
"""
|
||||
timerange = TimeRange()
|
||||
if "days" in config:
|
||||
time_since = (datetime.now() - timedelta(days=config["days"])).strftime("%Y%m%d")
|
||||
timerange = TimeRange.parse_timerange(f"{time_since}-")
|
||||
|
||||
if "timerange" in config:
|
||||
timerange = timerange.parse_timerange(config["timerange"])
|
||||
timerange = TimeRange.parse_timerange(config["timerange"])
|
||||
|
||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||
config["stake_currency"] = ""
|
||||
|
||||
pairs_not_available: List[str] = []
|
||||
pairs_not_available: list[str] = []
|
||||
|
||||
# Init exchange
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(config, validate=False)
|
||||
available_pairs = [
|
||||
p
|
||||
for p in exchange.get_markets(
|
||||
@@ -610,20 +626,18 @@ def download_data_main(config: Config) -> None:
|
||||
if "timeframes" not in config:
|
||||
config["timeframes"] = DL_DATA_TIMEFRAMES
|
||||
|
||||
# Manual validations of relevant settings
|
||||
if not config["exchange"].get("skip_pair_validation", False):
|
||||
exchange.validate_pairs(expanded_pairs)
|
||||
logger.info(
|
||||
f"About to download pairs: {expanded_pairs}, "
|
||||
f"intervals: {config['timeframes']} to {config['datadir']}"
|
||||
)
|
||||
|
||||
if len(expanded_pairs) == 0:
|
||||
logger.warning(
|
||||
"No pairs available for download. "
|
||||
"Please make sure you're using the correct Pair naming for your selected trade mode. \n"
|
||||
f"More info: {DOCS_LINK}/bot-basics/#pair-naming"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"About to download pairs: {expanded_pairs}, "
|
||||
f"intervals: {config['timeframes']} to {config['datadir']}"
|
||||
)
|
||||
|
||||
for timeframe in config["timeframes"]:
|
||||
exchange.validate_timeframes(timeframe)
|
||||
@@ -645,6 +659,7 @@ def download_data_main(config: Config) -> None:
|
||||
erase=bool(config.get("erase")),
|
||||
data_format=config["dataformat_trades"],
|
||||
trading_mode=config.get("trading_mode", TradingMode.SPOT),
|
||||
progress_tracker=progress_tracker,
|
||||
)
|
||||
|
||||
if config.get("convert_trades") or not exchange.get_option("ohlcv_has_history", True):
|
||||
@@ -680,10 +695,12 @@ def download_data_main(config: Config) -> None:
|
||||
data_format=config["dataformat_ohlcv"],
|
||||
trading_mode=config.get("trading_mode", "spot"),
|
||||
prepend=config.get("prepend_data", False),
|
||||
progress_tracker=progress_tracker,
|
||||
)
|
||||
finally:
|
||||
if pairs_not_available:
|
||||
logger.info(
|
||||
f"Pairs [{','.join(pairs_not_available)}] not available "
|
||||
f"on exchange {exchange.name}."
|
||||
errors = "\n" + ("\n".join(pairs_not_available))
|
||||
logger.warning(
|
||||
f"Encountered a problem downloading the following pairs from {exchange.name}: "
|
||||
f"{errors}"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Dict, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -11,7 +10,7 @@ import pandas as pd
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def calculate_market_change(data: Dict[str, pd.DataFrame], column: str = "close") -> float:
|
||||
def calculate_market_change(data: dict[str, pd.DataFrame], column: str = "close") -> float:
|
||||
"""
|
||||
Calculate market change based on "column".
|
||||
Calculation is done by taking the first non-null and the last non-null element of each column
|
||||
@@ -32,7 +31,7 @@ def calculate_market_change(data: Dict[str, pd.DataFrame], column: str = "close"
|
||||
|
||||
|
||||
def combine_dataframes_by_column(
|
||||
data: Dict[str, pd.DataFrame], column: str = "close"
|
||||
data: dict[str, pd.DataFrame], column: str = "close"
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Combine multiple dataframes "column"
|
||||
@@ -50,7 +49,7 @@ def combine_dataframes_by_column(
|
||||
|
||||
|
||||
def combined_dataframes_with_rel_mean(
|
||||
data: Dict[str, pd.DataFrame], fromdt: datetime, todt: datetime, column: str = "close"
|
||||
data: dict[str, pd.DataFrame], fromdt: datetime, todt: datetime, column: str = "close"
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Combine multiple dataframes "column"
|
||||
@@ -70,7 +69,7 @@ def combined_dataframes_with_rel_mean(
|
||||
|
||||
|
||||
def combine_dataframes_with_mean(
|
||||
data: Dict[str, pd.DataFrame], column: str = "close"
|
||||
data: dict[str, pd.DataFrame], column: str = "close"
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Combine multiple dataframes "column"
|
||||
@@ -222,7 +221,7 @@ def calculate_max_drawdown(
|
||||
)
|
||||
|
||||
|
||||
def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]:
|
||||
def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> tuple[float, float]:
|
||||
"""
|
||||
Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_percent)
|
||||
@@ -255,15 +254,15 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
||||
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||
|
||||
|
||||
def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]:
|
||||
def calculate_expectancy(trades: pd.DataFrame) -> tuple[float, float]:
|
||||
"""
|
||||
Calculate expectancy
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||
:return: expectancy, expectancy_ratio
|
||||
"""
|
||||
|
||||
expectancy = 0
|
||||
expectancy_ratio = 100
|
||||
expectancy = 0.0
|
||||
expectancy_ratio = 100.0
|
||||
|
||||
if len(trades) > 0:
|
||||
winning_trades = trades.loc[trades["profit_abs"] > 0]
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, NamedTuple
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import numpy as np
|
||||
import utils_find_1st as utf1st
|
||||
@@ -44,7 +44,7 @@ class Edge:
|
||||
Author: https://github.com/mishaker
|
||||
"""
|
||||
|
||||
_cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
||||
_cached_pairs: dict[str, Any] = {} # Keeps a list of pairs
|
||||
|
||||
def __init__(self, config: Config, exchange, strategy) -> None:
|
||||
self.config = config
|
||||
@@ -52,7 +52,7 @@ class Edge:
|
||||
self.strategy: IStrategy = strategy
|
||||
|
||||
self.edge_config = self.config.get("edge", {})
|
||||
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
||||
self._cached_pairs: dict[str, Any] = {} # Keeps a list of pairs
|
||||
self._final_pairs: list = []
|
||||
|
||||
# checking max_open_trades. it should be -1 as with Edge
|
||||
@@ -93,7 +93,7 @@ class Edge:
|
||||
except IndexError:
|
||||
self.fee = None
|
||||
|
||||
def calculate(self, pairs: List[str]) -> bool:
|
||||
def calculate(self, pairs: list[str]) -> bool:
|
||||
if self.fee is None and pairs:
|
||||
self.fee = self.exchange.get_fee(pairs[0])
|
||||
|
||||
@@ -104,7 +104,7 @@ class Edge:
|
||||
):
|
||||
return False
|
||||
|
||||
data: Dict[str, Any] = {}
|
||||
data: dict[str, Any] = {}
|
||||
logger.info("Using stake_currency: %s ...", self.config["stake_currency"])
|
||||
logger.info("Using local backtesting data (using whitelist in given config) ...")
|
||||
|
||||
@@ -231,7 +231,7 @@ class Edge:
|
||||
)
|
||||
return self.strategy.stoploss
|
||||
|
||||
def adjust(self, pairs: List[str]) -> list:
|
||||
def adjust(self, pairs: list[str]) -> list:
|
||||
"""
|
||||
Filters out and sorts "pairs" according to Edge calculated pairs
|
||||
"""
|
||||
@@ -260,7 +260,7 @@ class Edge:
|
||||
|
||||
return self._final_pairs
|
||||
|
||||
def accepted_pairs(self) -> List[Dict[str, Any]]:
|
||||
def accepted_pairs(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
return a list of accepted pairs along with their winrate, expectancy and stoploss
|
||||
"""
|
||||
@@ -322,7 +322,7 @@ class Edge:
|
||||
|
||||
return result
|
||||
|
||||
def _process_expectancy(self, results: DataFrame) -> Dict[str, Any]:
|
||||
def _process_expectancy(self, results: DataFrame) -> dict[str, Any]:
|
||||
"""
|
||||
This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs
|
||||
The calculation will be done per pair and per strategy.
|
||||
|
||||
@@ -11,3 +11,6 @@ class MarginMode(str, Enum):
|
||||
CROSS = "cross"
|
||||
ISOLATED = "isolated"
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
@@ -10,3 +10,6 @@ class TradingMode(str, Enum):
|
||||
SPOT = "spot"
|
||||
MARGIN = "margin"
|
||||
FUTURES = "futures"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
@@ -39,7 +39,9 @@ from freqtrade.exchange.exchange_utils_timeframe import (
|
||||
from freqtrade.exchange.gate import Gate
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.htx import Htx
|
||||
from freqtrade.exchange.hyperliquid import Hyperliquid
|
||||
from freqtrade.exchange.idex import Idex
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.lbank import Lbank
|
||||
from freqtrade.exchange.okx import Okx
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user