mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Compare commits
581 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
157cb7d982 | ||
|
|
27af9455f5 | ||
|
|
98ff572afe | ||
|
|
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 | ||
|
|
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
|
||||
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -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
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
|
||||
- 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
|
||||
@@ -384,7 +384,6 @@ jobs:
|
||||
- name: Documentation build
|
||||
run: |
|
||||
pip install -r docs/requirements-docs.txt
|
||||
pip install mkdocs
|
||||
mkdocs build
|
||||
|
||||
- name: Discord notification
|
||||
@@ -427,7 +426,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 +537,12 @@ jobs:
|
||||
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||
uses: pypa/gh-action-pypi-publish@v1.10.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.10.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.11.2"
|
||||
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.20240914
|
||||
- types-tabulate==0.9.0.20240106
|
||||
- types-python-dateutil==2.9.0.20240316
|
||||
- SQLAlchemy==2.0.31
|
||||
- types-python-dateutil==2.9.0.20240906
|
||||
- SQLAlchemy==2.0.35
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -31,9 +31,10 @@ repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.5.4'
|
||||
rev: 'v0.6.7'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12.4-slim-bookworm as base
|
||||
FROM python:3.12.6-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/
|
||||
|
||||
18
README.md
18
README.md
@@ -30,6 +30,7 @@ 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] [Kraken](https://kraken.com/)
|
||||
@@ -86,41 +87,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
|
||||
|
||||
|
||||
@@ -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}')"
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -609,6 +605,10 @@
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"unlock_at": {
|
||||
"description": "Time when trading will be unlocked regularly. Format: HH:MM",
|
||||
"type": "string"
|
||||
},
|
||||
"trade_limit": {
|
||||
"description": "Minimum number of trades required during lookback period.",
|
||||
"type": "number",
|
||||
@@ -1064,7 +1064,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 +1105,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 +1122,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 +1224,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"
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -293,6 +293,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 +399,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 +454,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 +533,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%)
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,7 +219,7 @@ 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.
|
||||
|
||||
@@ -481,21 +481,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.
|
||||
|
||||
@@ -255,18 +255,24 @@ The configuration parameter `exchange.unknown_fee_rate` can be used to specify t
|
||||
## 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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
@@ -277,14 +360,21 @@ 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.
|
||||
|
||||
`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
|
||||
|
||||
|
||||
@@ -36,6 +36,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 +45,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 +98,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 +121,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
|
||||
```
|
||||
@@ -42,6 +42,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
||||
- [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] [Kraken](https://kraken.com/)
|
||||
|
||||
@@ -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.36
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.8.1
|
||||
pymdown-extensions==10.10.1
|
||||
jinja2==3.1.4
|
||||
mike==2.1.3
|
||||
|
||||
@@ -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,6 +86,8 @@ 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,
|
||||
@@ -92,13 +97,13 @@ class AwesomeStrategy(IStrategy):
|
||||
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.
|
||||
@@ -168,7 +175,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 +202,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,7 +210,7 @@ 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]:
|
||||
"""
|
||||
@@ -236,8 +240,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,7 +248,7 @@ 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]:
|
||||
|
||||
@@ -263,8 +266,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,7 +274,7 @@ 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]:
|
||||
|
||||
@@ -293,8 +295,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 +303,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]:
|
||||
|
||||
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 +321,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,7 +329,7 @@ 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]:
|
||||
|
||||
@@ -353,9 +353,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,7 +361,7 @@ 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]:
|
||||
|
||||
@@ -384,15 +382,17 @@ 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]:
|
||||
|
||||
@@ -400,7 +400,7 @@ class AwesomeStrategy(IStrategy):
|
||||
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 +429,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,7 +437,7 @@ 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]:
|
||||
|
||||
@@ -469,38 +466,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]:
|
||||
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,19 +513,18 @@ 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,
|
||||
def custom_entry_price(self, pair: str, trade: Optional[Trade], current_time: datetime, proposed_rate: float,
|
||||
entry_tag: Optional[str], 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
|
||||
|
||||
@@ -542,7 +534,7 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
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 +571,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 +579,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 +594,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 +611,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 +619,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,6 +655,8 @@ 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
|
||||
@@ -689,7 +681,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 +703,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 +729,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 +762,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 +784,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 +802,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):
|
||||
|
||||
@@ -876,7 +865,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 +875,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)
|
||||
@@ -904,7 +893,7 @@ class DigDeeperStrategy(IStrategy):
|
||||
stake_amount = filled_entries[0].stake_amount
|
||||
# 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,8 +940,7 @@ 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):
|
||||
|
||||
@@ -977,13 +965,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 +984,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,6 +999,8 @@ 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,
|
||||
@@ -1019,7 +1014,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 +1031,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 +1049,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
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ 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.
|
||||
|
||||
@@ -407,6 +407,8 @@ Currently this is `pair`, which can be accessed using `metadata['pair']` - and w
|
||||
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.
|
||||
|
||||
--8<-- "includes/strategy-imports.md"
|
||||
|
||||
## Strategy file loading
|
||||
|
||||
By default, freqtrade will attempt to load strategies from all `.py` files within `user_data/strategies`.
|
||||
@@ -715,6 +717,7 @@ This is where calling `self.dp.current_whitelist()` comes in handy.
|
||||
|
||||
??? 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.
|
||||
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)*
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -11,3 +11,7 @@
|
||||
.rst-versions .rst-other-versions {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.md-version__list {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
@@ -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,20 @@ 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.* |
|
||||
| `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 |
|
||||
|
||||
@@ -418,8 +418,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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2024.7.1"
|
||||
__version__ = "2024.9"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -132,7 +132,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 +228,8 @@ ARGS_ANALYZE_ENTRIES_EXITS = [
|
||||
"enter_reason_list",
|
||||
"exit_reason_list",
|
||||
"indicator_list",
|
||||
"entry_only",
|
||||
"exit_only",
|
||||
"timerange",
|
||||
"analysis_rejected",
|
||||
"analysis_to_csv",
|
||||
|
||||
@@ -274,8 +274,6 @@ def start_new_config(args: Dict[str, Any]) -> None:
|
||||
def start_show_config(args: Dict[str, Any]) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False)
|
||||
|
||||
# TODO: Sanitize from sensitive info before printing
|
||||
|
||||
print("Your combined configuration is:")
|
||||
config_sanitized = sanitize_config(
|
||||
config["original_config"], show_sensitive=args.get("show_sensitive", False)
|
||||
|
||||
@@ -446,8 +446,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 +719,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",
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.misc import plural
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.util import print_rich_table
|
||||
@@ -115,9 +116,13 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
|
||||
|
||||
def start_list_data(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
List available backtest data
|
||||
List available OHLCV data
|
||||
"""
|
||||
|
||||
if args["trades"]:
|
||||
start_list_trades_data(args)
|
||||
return
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
from freqtrade.data.history import get_datahandler
|
||||
@@ -127,7 +132,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 +175,51 @@ 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
|
||||
"""
|
||||
|
||||
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},
|
||||
)
|
||||
|
||||
@@ -12,9 +12,9 @@ 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.ft_types import ValidExchangesType
|
||||
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
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
)
|
||||
|
||||
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 +46,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 +74,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",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.configuration.asyncio_config import asyncio_setup
|
||||
from freqtrade.configuration.config_secrets import sanitize_config
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
|
||||
10
freqtrade/configuration/asyncio_config.py
Normal file
10
freqtrade/configuration/asyncio_config.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import sys
|
||||
|
||||
|
||||
def asyncio_setup() -> None: # pragma: no cover
|
||||
# Set eventloop for win32 setups
|
||||
|
||||
if sys.platform == "win32":
|
||||
import asyncio
|
||||
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
@@ -36,11 +36,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 +180,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",
|
||||
@@ -480,6 +476,12 @@ CONF_SCHEMA = {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
},
|
||||
"unlock_at": {
|
||||
"description": (
|
||||
"Time when trading will be unlocked regularly. Format: HH:MM"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"trade_limit": {
|
||||
"description": "Minimum number of trades required during lookback period.",
|
||||
"type": "number",
|
||||
@@ -501,6 +503,7 @@ CONF_SCHEMA = {
|
||||
"required": ["method"],
|
||||
},
|
||||
},
|
||||
# RPC section
|
||||
"telegram": {
|
||||
"description": "Telegram settings.",
|
||||
"type": "object",
|
||||
@@ -701,6 +704,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 +738,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 +767,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 +872,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",
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
import logging
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
from jsonschema import Draft4Validator, validators
|
||||
@@ -201,16 +202,32 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
|
||||
"""
|
||||
|
||||
for prot in conf.get("protections", []):
|
||||
parsed_unlock_at = None
|
||||
if (config_unlock_at := prot.get("unlock_at")) is not None:
|
||||
try:
|
||||
parsed_unlock_at = datetime.strptime(config_unlock_at, "%H:%M")
|
||||
except ValueError:
|
||||
raise ConfigurationError(f"Invalid date format for unlock_at: {config_unlock_at}.")
|
||||
|
||||
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')}"
|
||||
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')}"
|
||||
f"Please fix the protection {prot.get('method')}."
|
||||
)
|
||||
|
||||
if parsed_unlock_at is not None and (
|
||||
"stop_duration" in prot or "stop_duration_candles" in prot
|
||||
):
|
||||
raise ConfigurationError(
|
||||
"Protections must specify either `unlock_at`, `stop_duration` or "
|
||||
"`stop_duration_candles`.\n"
|
||||
f"Please fix the protection {prot.get('method')}."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,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
|
||||
@@ -389,6 +396,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 +407,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: {}"),
|
||||
@@ -468,7 +478,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:
|
||||
"""
|
||||
|
||||
@@ -82,6 +82,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
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ HYPEROPT_LOSS_BUILTIN = [
|
||||
AVAILABLE_PAIRLISTS = [
|
||||
"StaticPairList",
|
||||
"VolumePairList",
|
||||
"PercentChangePairList",
|
||||
"ProducerPairList",
|
||||
"RemotePairList",
|
||||
"MarketCapPairList",
|
||||
|
||||
@@ -13,10 +13,10 @@ 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__)
|
||||
@@ -401,7 +401,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]
|
||||
|
||||
@@ -12,7 +12,7 @@ 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
|
||||
|
||||
@@ -63,7 +63,7 @@ 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,
|
||||
config: Config,
|
||||
dataframe: pd.DataFrame,
|
||||
trades: pd.DataFrame,
|
||||
) -> Tuple[pd.DataFrame, OrderedDict[Tuple[datetime, datetime], pd.DataFrame]]:
|
||||
@@ -78,6 +78,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 +90,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
|
||||
|
||||
@@ -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
|
||||
@@ -520,21 +520,15 @@ 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]]:
|
||||
"""
|
||||
Return market data for the pair
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import Dict, List
|
||||
|
||||
import joblib
|
||||
import pandas as pd
|
||||
@@ -8,6 +8,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 +48,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 +63,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 +71,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 +83,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 +243,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 +260,11 @@ def prepare_results(
|
||||
|
||||
def print_results(
|
||||
res_df: pd.DataFrame,
|
||||
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 +289,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 +304,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 +359,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 +383,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 +396,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 +432,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
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 +32,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
|
||||
@@ -166,6 +167,50 @@ class IDataHandler(ABC):
|
||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
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]:
|
||||
"""
|
||||
@@ -247,9 +292,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
|
||||
|
||||
@@ -17,7 +17,6 @@ from freqtrade.constants import (
|
||||
from freqtrade.data.converter import (
|
||||
clean_ohlcv_dataframe,
|
||||
convert_trades_to_ohlcv,
|
||||
ohlcv_to_dataframe,
|
||||
trades_df_remove_duplicates,
|
||||
trades_list_to_df,
|
||||
)
|
||||
@@ -273,7 +272,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 +284,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:
|
||||
@@ -610,9 +605,6 @@ 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']}"
|
||||
|
||||
@@ -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,6 +39,7 @@ 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
|
||||
|
||||
@@ -11,7 +11,7 @@ from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.types import OHLCVResponse, Tickers
|
||||
from freqtrade.exchange.exchange_types import FtHas, OHLCVResponse, Tickers
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Binance(Exchange):
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
@@ -30,9 +30,9 @@ class Binance(Exchange):
|
||||
"trades_pagination_arg": "fromId",
|
||||
"trades_has_history": True,
|
||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||
"ws.enabled": True,
|
||||
"ws_enabled": True,
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
_ft_has_futures: FtHas = {
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC"],
|
||||
"tickers_have_price": False,
|
||||
@@ -43,7 +43,7 @@ class Binance(Exchange):
|
||||
PriceType.LAST: "CONTRACT_PRICE",
|
||||
PriceType.MARK: "MARK_PRICE",
|
||||
},
|
||||
"ws.enabled": False,
|
||||
"ws_enabled": False,
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||
@@ -192,7 +192,7 @@ class Binance(Exchange):
|
||||
if maintenance_amt is None:
|
||||
raise OperationalException(
|
||||
"Parameter maintenance_amt is required by Binance.liquidation_price"
|
||||
f"for {self.trading_mode.value}"
|
||||
f"for {self.trading_mode}"
|
||||
)
|
||||
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
"""Bingx exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -15,7 +15,7 @@ class Bingx(Exchange):
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Bitmart exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -15,7 +15,7 @@ class Bitmart(Exchange):
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"stoploss_on_exchange": False, # Bitmart API does not support stoploss orders
|
||||
"ohlcv_candle_limit": 200,
|
||||
"trades_has_history": False, # Endpoint doesn't seem to support pagination
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Kucoin exchange subclass."""
|
||||
"""Bitvavo exchange subclass."""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from ccxt import DECIMAL_PLACES
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -19,6 +21,14 @@ class Bitvavo(Exchange):
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 1440,
|
||||
}
|
||||
|
||||
@property
|
||||
def precisionMode(self) -> int:
|
||||
"""
|
||||
Exchange ccxt precisionMode
|
||||
Override due to https://github.com/ccxt/ccxt/issues/20408
|
||||
"""
|
||||
return DECIMAL_PLACES
|
||||
|
||||
@@ -11,6 +11,7 @@ from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, ExchangeError, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
from freqtrade.util.datetime_helpers import dt_now, dt_ts
|
||||
|
||||
|
||||
@@ -29,14 +30,14 @@ class Bybit(Exchange):
|
||||
|
||||
unified_account = False
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"ohlcv_has_history": True,
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
"ws.enabled": True,
|
||||
"ws_enabled": True,
|
||||
"trades_has_history": False, # Endpoint doesn't support pagination
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
_ft_has_futures: FtHas = {
|
||||
"ohlcv_has_history": True,
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
"funding_fee_timeframe": "8h",
|
||||
@@ -89,10 +90,8 @@ class Bybit(Exchange):
|
||||
# Returns a tuple of bools, first for margin, second for Account
|
||||
if is_unified and len(is_unified) > 1 and is_unified[1]:
|
||||
self.unified_account = True
|
||||
logger.info("Bybit: Unified account.")
|
||||
raise OperationalException(
|
||||
"Bybit: Unified account is not supported. "
|
||||
"Please use a standard (sub)account."
|
||||
logger.info(
|
||||
"Bybit: Unified account. Assuming dedicated subaccount for this bot."
|
||||
)
|
||||
else:
|
||||
self.unified_account = False
|
||||
@@ -239,7 +238,13 @@ class Bybit(Exchange):
|
||||
return orders
|
||||
|
||||
def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
|
||||
if self.exchange_has("fetchOrder"):
|
||||
# Set acknowledged to True to avoid ccxt exception
|
||||
params = {"acknowledged": True}
|
||||
|
||||
order = super().fetch_order(order_id, pair, params)
|
||||
if not order:
|
||||
order = self.fetch_order_emulated(order_id, pair, {})
|
||||
if (
|
||||
order.get("status") == "canceled"
|
||||
and order.get("filled") == 0.0
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""CoinbasePro exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -19,6 +19,6 @@ class Coinbasepro(Exchange):
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 300,
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ SUPPORTED_EXCHANGES = [
|
||||
"binance",
|
||||
"bingx",
|
||||
"bitmart",
|
||||
"bybit",
|
||||
"gate",
|
||||
"htx",
|
||||
"kraken",
|
||||
@@ -163,6 +164,10 @@ F = TypeVar("F", bound=Callable[..., Any])
|
||||
def retrier(_func: F) -> F: ...
|
||||
|
||||
|
||||
@overload
|
||||
def retrier(_func: F, *, retries=API_RETRY_COUNT) -> F: ...
|
||||
|
||||
|
||||
@overload
|
||||
def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]: ...
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Crypto.com exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -14,6 +14,6 @@ class Cryptocom(Exchange):
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 300,
|
||||
}
|
||||
|
||||
@@ -67,6 +67,15 @@ from freqtrade.exchange.common import (
|
||||
retrier,
|
||||
retrier_async,
|
||||
)
|
||||
from freqtrade.exchange.exchange_types import (
|
||||
CcxtBalances,
|
||||
CcxtPosition,
|
||||
FtHas,
|
||||
OHLCVResponse,
|
||||
OrderBook,
|
||||
Ticker,
|
||||
Tickers,
|
||||
)
|
||||
from freqtrade.exchange.exchange_utils import (
|
||||
ROUND,
|
||||
ROUND_DOWN,
|
||||
@@ -88,7 +97,6 @@ from freqtrade.exchange.exchange_utils_timeframe import (
|
||||
timeframe_to_seconds,
|
||||
)
|
||||
from freqtrade.exchange.exchange_ws import ExchangeWS
|
||||
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
|
||||
from freqtrade.misc import (
|
||||
chunks,
|
||||
deep_merge_dicts,
|
||||
@@ -96,7 +104,6 @@ from freqtrade.misc import (
|
||||
file_load_json,
|
||||
safe_value_fallback2,
|
||||
)
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.util import dt_from_ts, dt_now
|
||||
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
|
||||
from freqtrade.util.periodic_cache import PeriodicCache
|
||||
@@ -115,10 +122,11 @@ class Exchange:
|
||||
# Dict to specify which options each exchange implements
|
||||
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
||||
# or by specifying them in the configuration.
|
||||
_ft_has_default: Dict = {
|
||||
_ft_has_default: FtHas = {
|
||||
"stoploss_on_exchange": False,
|
||||
"stop_price_param": "stopLossPrice", # Used for stoploss_on_exchange request
|
||||
"stop_price_prop": "stopLossPrice", # Used for stoploss_on_exchange response parsing
|
||||
"stoploss_order_types": {},
|
||||
"order_time_in_force": ["GTC"],
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
@@ -128,6 +136,7 @@ class Exchange:
|
||||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||
"tickers_have_quoteVolume": True,
|
||||
"tickers_have_percentage": True,
|
||||
"tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers
|
||||
"tickers_have_price": True,
|
||||
"trades_limit": 1000, # Limit for 1 call to fetch_trades
|
||||
@@ -146,10 +155,10 @@ class Exchange:
|
||||
"marketOrderRequiresPrice": False,
|
||||
"exchange_has_overrides": {}, # Dictionary overriding ccxt's "has".
|
||||
# Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False}
|
||||
"ws.enabled": False, # Set to true for exchanges with tested websocket support
|
||||
"ws_enabled": False, # Set to true for exchanges with tested websocket support
|
||||
}
|
||||
_ft_has: Dict = {}
|
||||
_ft_has_futures: Dict = {}
|
||||
_ft_has: FtHas = {}
|
||||
_ft_has_futures: FtHas = {}
|
||||
|
||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
@@ -253,7 +262,7 @@ class Exchange:
|
||||
exchange_conf.get("ccxt_async_config", {}), ccxt_async_config
|
||||
)
|
||||
self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config)
|
||||
self._has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws.enabled"]
|
||||
self._has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws_enabled"]
|
||||
if (
|
||||
self._config["runmode"] in TRADE_MODES
|
||||
and exchange_conf.get("enable_ws", True)
|
||||
@@ -315,20 +324,19 @@ class Exchange:
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop
|
||||
|
||||
def validate_config(self, config):
|
||||
def validate_config(self, config: Config) -> None:
|
||||
# Check if timeframe is available
|
||||
self.validate_timeframes(config.get("timeframe"))
|
||||
|
||||
# Check if all pairs are available
|
||||
self.validate_stakecurrency(config["stake_currency"])
|
||||
if not config["exchange"].get("skip_pair_validation"):
|
||||
self.validate_pairs(config["exchange"]["pair_whitelist"])
|
||||
self.validate_ordertypes(config.get("order_types", {}))
|
||||
self.validate_order_time_in_force(config.get("order_time_in_force", {}))
|
||||
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
|
||||
self.validate_pricing(config["exit_pricing"])
|
||||
self.validate_pricing(config["entry_pricing"])
|
||||
self.validate_orderflow(config["exchange"])
|
||||
self.validate_freqai(config)
|
||||
|
||||
def _init_ccxt(
|
||||
self, exchange_config: Dict[str, Any], sync: bool, ccxt_kwargs: Dict[str, Any]
|
||||
@@ -352,14 +360,18 @@ class Exchange:
|
||||
raise OperationalException(f"Exchange {name} is not supported by ccxt")
|
||||
|
||||
ex_config = {
|
||||
"apiKey": exchange_config.get("apiKey", exchange_config.get("key")),
|
||||
"apiKey": exchange_config.get(
|
||||
"api_key", exchange_config.get("apiKey", exchange_config.get("key"))
|
||||
),
|
||||
"secret": exchange_config.get("secret"),
|
||||
"password": exchange_config.get("password"),
|
||||
"uid": exchange_config.get("uid", ""),
|
||||
"accountId": exchange_config.get("accountId", ""),
|
||||
"accountId": exchange_config.get("account_id", exchange_config.get("accountId", "")),
|
||||
# DEX attributes:
|
||||
"walletAddress": exchange_config.get("walletAddress"),
|
||||
"privateKey": exchange_config.get("privateKey"),
|
||||
"walletAddress": exchange_config.get(
|
||||
"wallet_address", exchange_config.get("walletAddress")
|
||||
),
|
||||
"privateKey": exchange_config.get("private_key", exchange_config.get("privateKey")),
|
||||
}
|
||||
if ccxt_kwargs:
|
||||
logger.info("Applying additional ccxt config: %s", ccxt_kwargs)
|
||||
@@ -411,7 +423,17 @@ class Exchange:
|
||||
|
||||
@property
|
||||
def precisionMode(self) -> int:
|
||||
"""exchange ccxt precisionMode"""
|
||||
"""Exchange ccxt precisionMode"""
|
||||
return self._api.precisionMode
|
||||
|
||||
@property
|
||||
def precision_mode_price(self) -> int:
|
||||
"""
|
||||
Exchange ccxt precisionMode used for price
|
||||
Workaround for ccxt limitation to not have precisionMode for price
|
||||
if it differs for an exchange
|
||||
Might need to be updated if https://github.com/ccxt/ccxt/issues/20408 is fixed.
|
||||
"""
|
||||
return self._api.precisionMode
|
||||
|
||||
def additional_exchange_init(self) -> None:
|
||||
@@ -443,7 +465,7 @@ class Exchange:
|
||||
"""
|
||||
return int(
|
||||
self._ft_has.get("ohlcv_candle_limit_per_timeframe", {}).get(
|
||||
timeframe, self._ft_has.get("ohlcv_candle_limit")
|
||||
timeframe, str(self._ft_has.get("ohlcv_candle_limit"))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -541,7 +563,7 @@ class Exchange:
|
||||
else:
|
||||
return self._trades[pair_interval]
|
||||
else:
|
||||
return DataFrame()
|
||||
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||
|
||||
def get_contract_size(self, pair: str) -> Optional[float]:
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
@@ -598,11 +620,21 @@ class Exchange:
|
||||
if self._exchange_ws:
|
||||
self._exchange_ws.reset_connections()
|
||||
|
||||
async def _api_reload_markets(self, reload: bool = False) -> Dict[str, Any]:
|
||||
try:
|
||||
return await self._api_async.load_markets(reload=reload, params={})
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Error in reload_markets due to {e.__class__.__name__}. Message: {e}"
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise TemporaryError(e) from e
|
||||
|
||||
def _load_async_markets(self, reload: bool = False) -> Dict[str, Any]:
|
||||
try:
|
||||
markets = self.loop.run_until_complete(
|
||||
self._api_async.load_markets(reload=reload, params={})
|
||||
)
|
||||
markets = self.loop.run_until_complete(self._api_reload_markets(reload=reload))
|
||||
|
||||
if isinstance(markets, Exception):
|
||||
raise markets
|
||||
@@ -626,8 +658,10 @@ class Exchange:
|
||||
return None
|
||||
logger.debug("Performing scheduled market reload..")
|
||||
try:
|
||||
# on initial load, we retry 3 times to ensure we get the markets
|
||||
retries: int = 3 if force else 0
|
||||
# Reload async markets, then assign them to sync api
|
||||
self._markets = self._load_async_markets(reload=True)
|
||||
self._markets = retrier(self._load_async_markets, retries=retries)(reload=True)
|
||||
self._api.set_markets(self._api_async.markets, self._api_async.currencies)
|
||||
# Assign options array, as it contains some temporary information from the exchange.
|
||||
self._api.options = self._api_async.options
|
||||
@@ -665,54 +699,6 @@ class Exchange:
|
||||
f"Available currencies are: {', '.join(quote_currencies)}"
|
||||
)
|
||||
|
||||
def validate_pairs(self, pairs: List[str]) -> None:
|
||||
"""
|
||||
Checks if all given pairs are tradable on the current exchange.
|
||||
:param pairs: list of pairs
|
||||
:raise: OperationalException if one pair is not available
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if not self.markets:
|
||||
logger.warning("Unable to validate pairs (assuming they are correct).")
|
||||
return
|
||||
extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True)
|
||||
invalid_pairs = []
|
||||
for pair in extended_pairs:
|
||||
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
|
||||
if self.markets and pair not in self.markets:
|
||||
raise OperationalException(
|
||||
f"Pair {pair} is not available on {self.name} {self.trading_mode.value}. "
|
||||
f"Please remove {pair} from your whitelist."
|
||||
)
|
||||
|
||||
# From ccxt Documentation:
|
||||
# markets.info: An associative array of non-common market properties,
|
||||
# including fees, rates, limits and other general market information.
|
||||
# The internal info array is different for each particular market,
|
||||
# its contents depend on the exchange.
|
||||
# It can also be a string or similar ... so we need to verify that first.
|
||||
elif isinstance(self.markets[pair].get("info"), dict) and self.markets[pair].get(
|
||||
"info", {}
|
||||
).get("prohibitedIn", False):
|
||||
# Warn users about restricted pairs in whitelist.
|
||||
# We cannot determine reliably if Users are affected.
|
||||
logger.warning(
|
||||
f"Pair {pair} is restricted for some users on this exchange."
|
||||
f"Please check if you are impacted by this restriction "
|
||||
f"on the exchange and eventually remove {pair} from your whitelist."
|
||||
)
|
||||
if (
|
||||
self._config["stake_currency"]
|
||||
and self.get_pair_quote_currency(pair) != self._config["stake_currency"]
|
||||
):
|
||||
invalid_pairs.append(pair)
|
||||
if invalid_pairs:
|
||||
raise OperationalException(
|
||||
f"Stake-currency '{self._config['stake_currency']}' not compatible with "
|
||||
f"pair-whitelist. Please remove the following pairs: {invalid_pairs}"
|
||||
)
|
||||
|
||||
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
|
||||
"""
|
||||
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
|
||||
@@ -804,6 +790,13 @@ class Exchange:
|
||||
f"Trade data not available for {self.name}. Can't use orderflow feature."
|
||||
)
|
||||
|
||||
def validate_freqai(self, config: Config) -> None:
|
||||
freqai_enabled = config.get("freqai", {}).get("enabled", False)
|
||||
if freqai_enabled and not self._ft_has["ohlcv_has_history"]:
|
||||
raise ConfigurationError(
|
||||
f"Historic OHLCV data not available for {self.name}. Can't use freqAI."
|
||||
)
|
||||
|
||||
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
|
||||
"""
|
||||
Checks if required startup_candles is more than ohlcv_candle_limit().
|
||||
@@ -858,7 +851,7 @@ class Exchange:
|
||||
):
|
||||
mm_value = margin_mode and margin_mode.value
|
||||
raise OperationalException(
|
||||
f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
|
||||
f"Freqtrade does not support {mm_value} {trading_mode} on {self.name}"
|
||||
)
|
||||
|
||||
def get_option(self, param: str, default: Optional[Any] = None) -> Any:
|
||||
@@ -908,7 +901,10 @@ class Exchange:
|
||||
For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
|
||||
"""
|
||||
return price_to_precision(
|
||||
price, self.get_precision_price(pair), self.precisionMode, rounding_mode=rounding_mode
|
||||
price,
|
||||
self.get_precision_price(pair),
|
||||
self.precision_mode_price,
|
||||
rounding_mode=rounding_mode,
|
||||
)
|
||||
|
||||
def price_get_one_pip(self, pair: str, price: float) -> float:
|
||||
@@ -1645,7 +1641,7 @@ class Exchange:
|
||||
return order
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> dict:
|
||||
def get_balances(self) -> CcxtBalances:
|
||||
try:
|
||||
balances = self._api.fetch_balance()
|
||||
# Remove additional info from ccxt results
|
||||
@@ -1665,7 +1661,7 @@ class Exchange:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]:
|
||||
def fetch_positions(self, pair: Optional[str] = None) -> List[CcxtPosition]:
|
||||
"""
|
||||
Fetch positions from the exchange.
|
||||
If no pair is given, all positions are returned.
|
||||
@@ -1677,7 +1673,7 @@ class Exchange:
|
||||
symbols = []
|
||||
if pair:
|
||||
symbols.append(pair)
|
||||
positions: List[Dict] = self._api.fetch_positions(symbols)
|
||||
positions: List[CcxtPosition] = self._api.fetch_positions(symbols)
|
||||
self._log_exchange_response("fetch_positions", positions)
|
||||
return positions
|
||||
except ccxt.DDoSProtection as e:
|
||||
@@ -2227,7 +2223,7 @@ class Exchange:
|
||||
candle_type: CandleType,
|
||||
is_new_pair: bool = False,
|
||||
until_ms: Optional[int] = None,
|
||||
) -> List:
|
||||
) -> DataFrame:
|
||||
"""
|
||||
Get candle history using asyncio and returns the list of candles.
|
||||
Handles all async work for this.
|
||||
@@ -2237,7 +2233,7 @@ class Exchange:
|
||||
:param since_ms: Timestamp in milliseconds to get history from
|
||||
:param until_ms: Timestamp in milliseconds to get history up to
|
||||
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||
:return: List with candle (OHLCV) data
|
||||
:return: Dataframe with candle (OHLCV) data
|
||||
"""
|
||||
pair, _, _, data, _ = self.loop.run_until_complete(
|
||||
self._async_get_historic_ohlcv(
|
||||
@@ -2250,7 +2246,7 @@ class Exchange:
|
||||
)
|
||||
)
|
||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||
return data
|
||||
return ohlcv_to_dataframe(data, timeframe, pair, fill_missing=False, drop_incomplete=True)
|
||||
|
||||
async def _async_get_historic_ohlcv(
|
||||
self,
|
||||
@@ -2469,17 +2465,17 @@ class Exchange:
|
||||
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
||||
|
||||
# Gather coroutines to run
|
||||
input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
|
||||
ohlcv_dl_jobs, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
|
||||
|
||||
results_df = {}
|
||||
# Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling
|
||||
for input_coro in chunks(input_coroutines, 100):
|
||||
for dl_jobs_batch in chunks(ohlcv_dl_jobs, 100):
|
||||
|
||||
async def gather_stuff(coro):
|
||||
async def gather_coroutines(coro):
|
||||
return await asyncio.gather(*coro, return_exceptions=True)
|
||||
|
||||
with self._loop_lock:
|
||||
results = self.loop.run_until_complete(gather_stuff(input_coro))
|
||||
results = self.loop.run_until_complete(gather_coroutines(dl_jobs_batch))
|
||||
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
@@ -2607,12 +2603,13 @@ class Exchange:
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not fetch historical candle (OHLCV) data "
|
||||
f"for pair {pair} due to {e.__class__.__name__}. "
|
||||
f"for {pair}, {timeframe}, {candle_type} due to {e.__class__.__name__}. "
|
||||
f"Message: {e}"
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(
|
||||
f"Could not fetch historical candle (OHLCV) data for pair {pair}. Message: {e}"
|
||||
f"Could not fetch historical candle (OHLCV) data for "
|
||||
f"{pair}, {timeframe}, {candle_type}. Message: {e}"
|
||||
) from e
|
||||
|
||||
async def _fetch_funding_rate_history(
|
||||
@@ -2677,6 +2674,94 @@ class Exchange:
|
||||
self._trades[(pair, timeframe, c_type)] = trades_df
|
||||
return trades_df
|
||||
|
||||
async def _build_trades_dl_jobs(
|
||||
self, pairwt: PairWithTimeframe, data_handler, cache: bool
|
||||
) -> Tuple[PairWithTimeframe, Optional[DataFrame]]:
|
||||
"""
|
||||
Build coroutines to refresh trades for (they're then called through async.gather)
|
||||
"""
|
||||
pair, timeframe, candle_type = pairwt
|
||||
since_ms = None
|
||||
new_ticks: List = []
|
||||
all_stored_ticks_df = DataFrame(columns=DEFAULT_TRADES_COLUMNS + ["date"])
|
||||
first_candle_ms = self.needed_candle_for_trades_ms(timeframe, candle_type)
|
||||
# refresh, if
|
||||
# a. not in _trades
|
||||
# b. no cache used
|
||||
# c. need new data
|
||||
is_in_cache = (pair, timeframe, candle_type) in self._trades
|
||||
if (
|
||||
not is_in_cache
|
||||
or not cache
|
||||
or self._now_is_time_to_refresh_trades(pair, timeframe, candle_type)
|
||||
):
|
||||
logger.debug(f"Refreshing TRADES data for {pair}")
|
||||
# fetch trades since latest _trades and
|
||||
# store together with existing trades
|
||||
try:
|
||||
until = None
|
||||
from_id = None
|
||||
if is_in_cache:
|
||||
from_id = self._trades[(pair, timeframe, candle_type)].iloc[-1]["id"]
|
||||
until = dt_ts() # now
|
||||
|
||||
else:
|
||||
until = int(timeframe_to_prev_date(timeframe).timestamp()) * 1000
|
||||
all_stored_ticks_df = data_handler.trades_load(
|
||||
f"{pair}-cached", self.trading_mode
|
||||
)
|
||||
|
||||
if not all_stored_ticks_df.empty:
|
||||
if (
|
||||
all_stored_ticks_df.iloc[-1]["timestamp"] > first_candle_ms
|
||||
and all_stored_ticks_df.iloc[0]["timestamp"] <= first_candle_ms
|
||||
):
|
||||
# Use cache and populate further
|
||||
last_cached_ms = all_stored_ticks_df.iloc[-1]["timestamp"]
|
||||
from_id = all_stored_ticks_df.iloc[-1]["id"]
|
||||
# only use cached if it's closer than first_candle_ms
|
||||
since_ms = (
|
||||
last_cached_ms
|
||||
if last_cached_ms > first_candle_ms
|
||||
else first_candle_ms
|
||||
)
|
||||
else:
|
||||
# Skip cache, it's too old
|
||||
all_stored_ticks_df = DataFrame(
|
||||
columns=DEFAULT_TRADES_COLUMNS + ["date"]
|
||||
)
|
||||
|
||||
# from_id overrules with exchange set to id paginate
|
||||
[_, new_ticks] = await self._async_get_trade_history(
|
||||
pair,
|
||||
since=since_ms if since_ms else first_candle_ms,
|
||||
until=until,
|
||||
from_id=from_id,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception(f"Refreshing TRADES data for {pair} failed")
|
||||
return pairwt, None
|
||||
|
||||
if new_ticks:
|
||||
all_stored_ticks_list = all_stored_ticks_df[DEFAULT_TRADES_COLUMNS].values.tolist()
|
||||
all_stored_ticks_list.extend(new_ticks)
|
||||
trades_df = self._process_trades_df(
|
||||
pair,
|
||||
timeframe,
|
||||
candle_type,
|
||||
all_stored_ticks_list,
|
||||
cache,
|
||||
first_required_candle_date=first_candle_ms,
|
||||
)
|
||||
data_handler.trades_store(
|
||||
f"{pair}-cached", trades_df[DEFAULT_TRADES_COLUMNS], self.trading_mode
|
||||
)
|
||||
return pairwt, trades_df
|
||||
else:
|
||||
logger.error(f"No new ticks for {pair}")
|
||||
return pairwt, None
|
||||
|
||||
def refresh_latest_trades(
|
||||
self,
|
||||
pair_list: ListPairsWithTimeframes,
|
||||
@@ -2697,90 +2782,25 @@ class Exchange:
|
||||
self._config["datadir"], data_format=self._config["dataformat_trades"]
|
||||
)
|
||||
logger.debug("Refreshing TRADES data for %d pairs", len(pair_list))
|
||||
since_ms = None
|
||||
results_df = {}
|
||||
for pair, timeframe, candle_type in set(pair_list):
|
||||
new_ticks: List = []
|
||||
all_stored_ticks_df = DataFrame(columns=DEFAULT_TRADES_COLUMNS + ["date"])
|
||||
first_candle_ms = self.needed_candle_for_trades_ms(timeframe, candle_type)
|
||||
# refresh, if
|
||||
# a. not in _trades
|
||||
# b. no cache used
|
||||
# c. need new data
|
||||
is_in_cache = (pair, timeframe, candle_type) in self._trades
|
||||
if (
|
||||
not is_in_cache
|
||||
or not cache
|
||||
or self._now_is_time_to_refresh_trades(pair, timeframe, candle_type)
|
||||
):
|
||||
logger.debug(f"Refreshing TRADES data for {pair}")
|
||||
# fetch trades since latest _trades and
|
||||
# store together with existing trades
|
||||
try:
|
||||
until = None
|
||||
from_id = None
|
||||
if is_in_cache:
|
||||
from_id = self._trades[(pair, timeframe, candle_type)].iloc[-1]["id"]
|
||||
until = dt_ts() # now
|
||||
trades_dl_jobs = []
|
||||
for pair_wt in set(pair_list):
|
||||
trades_dl_jobs.append(self._build_trades_dl_jobs(pair_wt, data_handler, cache))
|
||||
|
||||
else:
|
||||
until = int(timeframe_to_prev_date(timeframe).timestamp()) * 1000
|
||||
all_stored_ticks_df = data_handler.trades_load(
|
||||
f"{pair}-cached", self.trading_mode
|
||||
)
|
||||
async def gather_coroutines(coro):
|
||||
return await asyncio.gather(*coro, return_exceptions=True)
|
||||
|
||||
if not all_stored_ticks_df.empty:
|
||||
if (
|
||||
all_stored_ticks_df.iloc[-1]["timestamp"] > first_candle_ms
|
||||
and all_stored_ticks_df.iloc[0]["timestamp"] <= first_candle_ms
|
||||
):
|
||||
# Use cache and populate further
|
||||
last_cached_ms = all_stored_ticks_df.iloc[-1]["timestamp"]
|
||||
from_id = all_stored_ticks_df.iloc[-1]["id"]
|
||||
# only use cached if it's closer than first_candle_ms
|
||||
since_ms = (
|
||||
last_cached_ms
|
||||
if last_cached_ms > first_candle_ms
|
||||
else first_candle_ms
|
||||
)
|
||||
else:
|
||||
# Skip cache, it's too old
|
||||
all_stored_ticks_df = DataFrame(
|
||||
columns=DEFAULT_TRADES_COLUMNS + ["date"]
|
||||
)
|
||||
for dl_job_chunk in chunks(trades_dl_jobs, 100):
|
||||
with self._loop_lock:
|
||||
results = self.loop.run_until_complete(gather_coroutines(dl_job_chunk))
|
||||
|
||||
# from_id overrules with exchange set to id paginate
|
||||
[_, new_ticks] = self.get_historic_trades(
|
||||
pair,
|
||||
since=since_ms if since_ms else first_candle_ms,
|
||||
until=until,
|
||||
from_id=from_id,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception(f"Refreshing TRADES data for {pair} failed")
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||
continue
|
||||
|
||||
if new_ticks:
|
||||
all_stored_ticks_list = all_stored_ticks_df[
|
||||
DEFAULT_TRADES_COLUMNS
|
||||
].values.tolist()
|
||||
all_stored_ticks_list.extend(new_ticks)
|
||||
trades_df = self._process_trades_df(
|
||||
pair,
|
||||
timeframe,
|
||||
candle_type,
|
||||
all_stored_ticks_list,
|
||||
cache,
|
||||
first_required_candle_date=first_candle_ms,
|
||||
)
|
||||
results_df[(pair, timeframe, candle_type)] = trades_df
|
||||
data_handler.trades_store(
|
||||
f"{pair}-cached", trades_df[DEFAULT_TRADES_COLUMNS], self.trading_mode
|
||||
)
|
||||
|
||||
else:
|
||||
logger.error(f"No new ticks for {pair}")
|
||||
pairwt, trades_df = res
|
||||
if trades_df is not None:
|
||||
results_df[pairwt] = trades_df
|
||||
|
||||
return results_df
|
||||
|
||||
@@ -3574,7 +3594,7 @@ class Exchange:
|
||||
Wherein, "+" or "-" depends on whether the contract goes long or short:
|
||||
"-" for long, and "+" for short.
|
||||
|
||||
okex: https://www.okex.com/support/hc/en-us/articles/
|
||||
okex: https://www.okx.com/support/hc/en-us/articles/
|
||||
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
|
||||
|
||||
:param pair: Pair to calculate liquidation price for
|
||||
|
||||
98
freqtrade/exchange/exchange_types.py
Normal file
98
freqtrade/exchange/exchange_types.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from typing import Dict, List, Optional, Tuple, TypedDict
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
|
||||
class FtHas(TypedDict, total=False):
|
||||
order_time_in_force: List[str]
|
||||
exchange_has_overrides: Dict[str, bool]
|
||||
marketOrderRequiresPrice: bool
|
||||
|
||||
# Stoploss on exchange
|
||||
stoploss_on_exchange: bool
|
||||
stop_price_param: str
|
||||
stop_price_prop: str
|
||||
stop_price_type_field: str
|
||||
stop_price_type_value_mapping: Dict
|
||||
stoploss_order_types: Dict[str, str]
|
||||
# ohlcv
|
||||
ohlcv_params: Dict
|
||||
ohlcv_candle_limit: int
|
||||
ohlcv_has_history: bool
|
||||
ohlcv_partial_candle: bool
|
||||
ohlcv_require_since: bool
|
||||
ohlcv_volume_currency: str
|
||||
ohlcv_candle_limit_per_timeframe: Dict[str, int]
|
||||
# Tickers
|
||||
tickers_have_quoteVolume: bool
|
||||
tickers_have_percentage: bool
|
||||
tickers_have_bid_ask: bool
|
||||
tickers_have_price: bool
|
||||
# Trades
|
||||
trades_limit: int
|
||||
trades_pagination: str
|
||||
trades_pagination_arg: str
|
||||
trades_has_history: bool
|
||||
trades_pagination_overlap: bool
|
||||
# Orderbook
|
||||
l2_limit_range: Optional[List[int]]
|
||||
l2_limit_range_required: bool
|
||||
# Futures
|
||||
ccxt_futures_name: str # usually swap
|
||||
mark_ohlcv_price: str
|
||||
mark_ohlcv_timeframe: str
|
||||
funding_fee_timeframe: str
|
||||
floor_leverage: bool
|
||||
needs_trading_fees: bool
|
||||
order_props_in_contracts: List[str]
|
||||
|
||||
# Websocket control
|
||||
ws_enabled: bool
|
||||
|
||||
|
||||
class Ticker(TypedDict):
|
||||
symbol: str
|
||||
ask: Optional[float]
|
||||
askVolume: Optional[float]
|
||||
bid: Optional[float]
|
||||
bidVolume: Optional[float]
|
||||
last: Optional[float]
|
||||
quoteVolume: Optional[float]
|
||||
baseVolume: Optional[float]
|
||||
percentage: Optional[float]
|
||||
# Several more - only listing required.
|
||||
|
||||
|
||||
Tickers = Dict[str, Ticker]
|
||||
|
||||
|
||||
class OrderBook(TypedDict):
|
||||
symbol: str
|
||||
bids: List[Tuple[float, float]]
|
||||
asks: List[Tuple[float, float]]
|
||||
timestamp: Optional[int]
|
||||
datetime: Optional[str]
|
||||
nonce: Optional[int]
|
||||
|
||||
|
||||
class CcxtBalance(TypedDict):
|
||||
free: float
|
||||
used: float
|
||||
total: float
|
||||
|
||||
|
||||
CcxtBalances = Dict[str, CcxtBalance]
|
||||
|
||||
|
||||
class CcxtPosition(TypedDict):
|
||||
symbol: str
|
||||
side: str
|
||||
contracts: float
|
||||
leverage: float
|
||||
collateral: Optional[float]
|
||||
initialMargin: Optional[float]
|
||||
liquidationPrice: Optional[float]
|
||||
|
||||
|
||||
# pair, timeframe, candleType, OHLCV, drop last?,
|
||||
OHLCVResponse = Tuple[str, str, CandleType, List, bool]
|
||||
@@ -2,6 +2,7 @@
|
||||
Exchange support utils
|
||||
"""
|
||||
|
||||
import inspect
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil, floor
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
@@ -25,7 +26,7 @@ from freqtrade.exchange.common import (
|
||||
SUPPORTED_EXCHANGES,
|
||||
)
|
||||
from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.types import ValidExchangesType
|
||||
from freqtrade.ft_types import ValidExchangesType
|
||||
from freqtrade.util import FtPrecise
|
||||
|
||||
|
||||
@@ -53,9 +54,9 @@ def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[st
|
||||
return [x for x in exchanges if validate_exchange(x)[0]]
|
||||
|
||||
|
||||
def validate_exchange(exchange: str) -> Tuple[bool, str, bool]:
|
||||
def validate_exchange(exchange: str) -> Tuple[bool, str, Optional[ccxt.Exchange]]:
|
||||
"""
|
||||
returns: can_use, reason
|
||||
returns: can_use, reason, exchange_object
|
||||
with Reason including both missing and missing_opt
|
||||
"""
|
||||
try:
|
||||
@@ -64,11 +65,10 @@ def validate_exchange(exchange: str) -> Tuple[bool, str, bool]:
|
||||
ex_mod = getattr(ccxt.async_support, exchange.lower())()
|
||||
|
||||
if not ex_mod or not ex_mod.has:
|
||||
return False, "", False
|
||||
return False, "", None
|
||||
|
||||
result = True
|
||||
reason = ""
|
||||
is_dex = getattr(ex_mod, "dex", False)
|
||||
missing = [
|
||||
k
|
||||
for k, v in EXCHANGE_HAS_REQUIRED.items()
|
||||
@@ -87,19 +87,24 @@ def validate_exchange(exchange: str) -> Tuple[bool, str, bool]:
|
||||
if missing_opt:
|
||||
reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. "
|
||||
|
||||
return result, reason, is_dex
|
||||
return result, reason, ex_mod
|
||||
|
||||
|
||||
def _build_exchange_list_entry(
|
||||
exchange_name: str, exchangeClasses: Dict[str, Any]
|
||||
) -> ValidExchangesType:
|
||||
valid, comment, is_dex = validate_exchange(exchange_name)
|
||||
valid, comment, ex_mod = validate_exchange(exchange_name)
|
||||
result: ValidExchangesType = {
|
||||
"name": exchange_name,
|
||||
"name": getattr(ex_mod, "name", exchange_name),
|
||||
"classname": exchange_name,
|
||||
"valid": valid,
|
||||
"supported": exchange_name.lower() in SUPPORTED_EXCHANGES,
|
||||
"comment": comment,
|
||||
"dex": is_dex,
|
||||
"dex": getattr(ex_mod, "dex", False),
|
||||
"is_alias": getattr(ex_mod, "alias", False),
|
||||
"alias_for": inspect.getmro(ex_mod.__class__)[1]().id
|
||||
if getattr(ex_mod, "alias", False)
|
||||
else None,
|
||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||
}
|
||||
if resolved := exchangeClasses.get(exchange_name.lower()):
|
||||
|
||||
@@ -11,7 +11,7 @@ import ccxt
|
||||
from freqtrade.constants import Config, PairWithTimeframe
|
||||
from freqtrade.enums.candletype import CandleType
|
||||
from freqtrade.exchange.exchange import timeframe_to_seconds
|
||||
from freqtrade.exchange.types import OHLCVResponse
|
||||
from freqtrade.exchange.exchange_types import OHLCVResponse
|
||||
from freqtrade.util import dt_ts, format_ms_time
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, PriceType, TradingMode
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
|
||||
|
||||
@@ -23,7 +24,7 @@ class Gate(Exchange):
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"order_time_in_force": ["GTC", "IOC"],
|
||||
"stoploss_on_exchange": True,
|
||||
@@ -34,7 +35,7 @@ class Gate(Exchange):
|
||||
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
|
||||
}
|
||||
|
||||
_ft_has_futures: Dict = {
|
||||
_ft_has_futures: FtHas = {
|
||||
"needs_trading_fees": True,
|
||||
"marketOrderRequiresPrice": False,
|
||||
"stop_price_type_field": "price_type",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -17,6 +17,6 @@ class Hitbtc(Exchange):
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Dict
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -16,7 +17,7 @@ class Htx(Exchange):
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from ccxt import SIGNIFICANT_DIGITS
|
||||
|
||||
from freqtrade.enums import TradingMode
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -14,11 +18,28 @@ class Hyperliquid(Exchange):
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
# Only the most recent 5000 candles are available according to the
|
||||
# exchange's API documentation.
|
||||
"ohlcv_has_history": True,
|
||||
"ohlcv_has_history": False,
|
||||
"ohlcv_candle_limit": 5000,
|
||||
"trades_has_history": False, # Trades endpoint doesn't seem available.
|
||||
"exchange_has_overrides": {"fetchTrades": False},
|
||||
}
|
||||
|
||||
@property
|
||||
def _ccxt_config(self) -> Dict:
|
||||
# Parameters to add directly to ccxt sync/async initialization.
|
||||
# ccxt defaults to swap mode.
|
||||
config = {}
|
||||
if self.trading_mode == TradingMode.SPOT:
|
||||
config.update({"options": {"defaultType": "spot"}})
|
||||
config.update(super()._ccxt_config)
|
||||
return config
|
||||
|
||||
@property
|
||||
def precision_mode_price(self) -> int:
|
||||
"""
|
||||
Override the default precision mode for price.
|
||||
"""
|
||||
return SIGNIFICANT_DIGITS
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Idex exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -15,6 +15,6 @@ class Idex(Exchange):
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.exchange.exchange_types import CcxtBalances, FtHas, Tickers
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Kraken(Exchange):
|
||||
_params: Dict = {"trading_agreement": "agree"}
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stop_price_param": "stopLossPrice",
|
||||
"stop_price_prop": "stopLossPrice",
|
||||
@@ -57,7 +57,7 @@ class Kraken(Exchange):
|
||||
return super().get_tickers(symbols=symbols, cached=cached)
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> dict:
|
||||
def get_balances(self) -> CcxtBalances:
|
||||
if self._config["dry_run"]:
|
||||
return {}
|
||||
|
||||
@@ -78,6 +78,7 @@ class Kraken(Exchange):
|
||||
# x["side"], x["amount"],
|
||||
)
|
||||
for x in orders
|
||||
if x["remaining"] is not None and (x["side"] == "sell" or x["price"] is not None)
|
||||
]
|
||||
for bal in balances:
|
||||
if not isinstance(balances[bal], dict):
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Dict
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,7 +21,7 @@ class Kucoin(Exchange):
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
|
||||
@@ -13,7 +13,8 @@ from freqtrade.exceptions import (
|
||||
TemporaryError,
|
||||
)
|
||||
from freqtrade.exchange import Exchange, date_minus_candles
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.common import API_RETRY_COUNT, retrier
|
||||
from freqtrade.exchange.exchange_types import FtHas
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
from freqtrade.util import dt_now, dt_ts
|
||||
|
||||
@@ -27,15 +28,16 @@ class Okx(Exchange):
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
_ft_has: FtHas = {
|
||||
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
"funding_fee_timeframe": "8h",
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
|
||||
"ws_enabled": True,
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
_ft_has_futures: FtHas = {
|
||||
"tickers_have_quoteVolume": False,
|
||||
"stop_price_type_field": "slTriggerPxType",
|
||||
"stop_price_type_value_mapping": {
|
||||
@@ -43,6 +45,7 @@ class Okx(Exchange):
|
||||
PriceType.MARK: "index",
|
||||
PriceType.INDEX: "mark",
|
||||
},
|
||||
"ws_enabled": True,
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||
@@ -205,6 +208,7 @@ class Okx(Exchange):
|
||||
order["type"] = "stoploss"
|
||||
return order
|
||||
|
||||
@retrier(retries=API_RETRY_COUNT)
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
|
||||
if self._config["dry_run"]:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
@@ -214,8 +218,20 @@ class Okx(Exchange):
|
||||
order_reg = self._api.fetch_order(order_id, pair, params=params1)
|
||||
self._log_exchange_response("fetch_stoploss_order", order_reg)
|
||||
return self._convert_stop_order(pair, order_id, order_reg)
|
||||
except ccxt.OrderNotFound:
|
||||
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||
pass
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
return self._fetch_stop_order_fallback(order_id, pair)
|
||||
|
||||
def _fetch_stop_order_fallback(self, order_id: str, pair: str) -> Dict:
|
||||
params2 = {"stop": True, "ordType": "conditional"}
|
||||
for method in (
|
||||
self._api.fetch_open_orders,
|
||||
@@ -228,8 +244,16 @@ class Okx(Exchange):
|
||||
if orders_f:
|
||||
order = orders_f[0]
|
||||
return self._convert_stop_order(pair, order_id, order)
|
||||
except ccxt.BaseError:
|
||||
except (ccxt.OrderNotFound, ccxt.InvalidOrder):
|
||||
pass
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not get order due to {e.__class__.__name__}. Message: {e}"
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
|
||||
|
||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
from typing import Dict, List, Optional, Tuple, TypedDict
|
||||
|
||||
from freqtrade.enums import CandleType
|
||||
|
||||
|
||||
class Ticker(TypedDict):
|
||||
symbol: str
|
||||
ask: Optional[float]
|
||||
askVolume: Optional[float]
|
||||
bid: Optional[float]
|
||||
bidVolume: Optional[float]
|
||||
last: Optional[float]
|
||||
quoteVolume: Optional[float]
|
||||
baseVolume: Optional[float]
|
||||
# Several more - only listing required.
|
||||
|
||||
|
||||
class OrderBook(TypedDict):
|
||||
symbol: str
|
||||
bids: List[Tuple[float, float]]
|
||||
asks: List[Tuple[float, float]]
|
||||
timestamp: Optional[int]
|
||||
datetime: Optional[str]
|
||||
nonce: Optional[int]
|
||||
|
||||
|
||||
Tickers = Dict[str, Ticker]
|
||||
|
||||
# pair, timeframe, candleType, OHLCV, drop last?,
|
||||
OHLCVResponse = Tuple[str, str, CandleType, List, bool]
|
||||
@@ -86,9 +86,6 @@ class BasePyTorchRegressor(BasePyTorchModel):
|
||||
dk.feature_pipeline = self.define_data_pipeline(threads=dk.thread_count)
|
||||
dk.label_pipeline = self.define_label_pipeline(threads=dk.thread_count)
|
||||
|
||||
dd["train_labels"], _, _ = dk.label_pipeline.fit_transform(dd["train_labels"])
|
||||
dd["test_labels"], _, _ = dk.label_pipeline.transform(dd["test_labels"])
|
||||
|
||||
(dd["train_features"], dd["train_labels"], dd["train_weights"]) = (
|
||||
dk.feature_pipeline.fit_transform(
|
||||
dd["train_features"], dd["train_labels"], dd["train_weights"]
|
||||
|
||||
@@ -141,7 +141,7 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
||||
pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list)
|
||||
pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df)
|
||||
|
||||
if self.freqai_info.get("DI_threshold", 0) > 0:
|
||||
if self.ft_params.get("DI_threshold", 0) > 0:
|
||||
dk.DI_values = dk.feature_pipeline["di"].di_values
|
||||
else:
|
||||
dk.DI_values = np.zeros(outliers.shape[0])
|
||||
|
||||
@@ -22,6 +22,7 @@ from freqtrade.edge import Edge
|
||||
from freqtrade.enums import (
|
||||
ExitCheckTuple,
|
||||
ExitType,
|
||||
MarginMode,
|
||||
RPCMessageType,
|
||||
SignalDirection,
|
||||
State,
|
||||
@@ -61,7 +62,7 @@ from freqtrade.rpc.rpc_types import (
|
||||
)
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.util import MeasureTime
|
||||
from freqtrade.util import FtPrecise, MeasureTime
|
||||
from freqtrade.util.migrations.binance_mig import migrate_binance_futures_names
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -108,6 +109,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
PairLocks.timeframe = self.config["timeframe"]
|
||||
|
||||
self.trading_mode: TradingMode = self.config.get("trading_mode", TradingMode.SPOT)
|
||||
self.margin_mode: MarginMode = self.config.get("margin_mode", MarginMode.NONE)
|
||||
self.last_process: Optional[datetime] = None
|
||||
|
||||
# RPC runs in separate threads, can start handling external commands just after
|
||||
@@ -374,6 +376,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if trade.exchange != self.exchange.id:
|
||||
continue
|
||||
trade.precision_mode = self.exchange.precisionMode
|
||||
trade.precision_mode_price = self.exchange.precision_mode_price
|
||||
trade.amount_precision = self.exchange.get_precision_amount(trade.pair)
|
||||
trade.price_precision = self.exchange.get_precision_price(trade.pair)
|
||||
trade.contract_size = self.exchange.get_contract_size(trade.pair)
|
||||
@@ -541,7 +544,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
else:
|
||||
trade.exit_reason = prev_exit_reason
|
||||
total = self.wallets.get_total(trade.base_currency) if trade.base_currency else 0
|
||||
total = (
|
||||
self.wallets.get_owned(trade.pair, trade.base_currency)
|
||||
if trade.base_currency
|
||||
else 0
|
||||
)
|
||||
if total < trade.amount:
|
||||
if trade.fully_canceled_entry_order_count == len(trade.orders):
|
||||
logger.warning(
|
||||
@@ -777,7 +784,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
if stake_amount is not None and stake_amount < 0.0:
|
||||
# We should decrease our position
|
||||
amount = self.exchange.amount_to_contract_precision(
|
||||
trade.pair, abs(float(stake_amount * trade.amount / trade.stake_amount))
|
||||
trade.pair,
|
||||
abs(
|
||||
float(
|
||||
FtPrecise(stake_amount)
|
||||
* FtPrecise(trade.amount)
|
||||
/ FtPrecise(trade.stake_amount)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
if amount == 0.0:
|
||||
@@ -973,7 +987,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
base_currency=base_currency,
|
||||
stake_currency=self.config["stake_currency"],
|
||||
stake_amount=stake_amount,
|
||||
amount=amount,
|
||||
amount=0,
|
||||
is_open=True,
|
||||
amount_requested=amount_requested,
|
||||
fee_open=fee,
|
||||
@@ -992,6 +1006,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount_precision=self.exchange.get_precision_amount(pair),
|
||||
price_precision=self.exchange.get_precision_price(pair),
|
||||
precision_mode=self.exchange.precisionMode,
|
||||
precision_mode_price=self.exchange.precision_mode_price,
|
||||
contract_size=self.exchange.get_contract_size(pair),
|
||||
)
|
||||
stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair)
|
||||
@@ -2210,7 +2225,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
# TODO: should shorting/leverage be supported by Edge,
|
||||
# then this will need to be fixed.
|
||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||
if order.ft_order_side == trade.entry_side or (trade.amount > 0 and trade.is_open):
|
||||
if (
|
||||
order.ft_order_side == trade.entry_side
|
||||
or (trade.amount > 0 and trade.is_open)
|
||||
or self.margin_mode == MarginMode.CROSS
|
||||
):
|
||||
# Must also run for partial exits
|
||||
# TODO: Margin will need to use interest_rate as well.
|
||||
# interest_rate = self.exchange.get_interest_rate()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# flake8: noqa: F401
|
||||
from freqtrade.types.backtest_result_type import (
|
||||
from freqtrade.ft_types.backtest_result_type import (
|
||||
BacktestHistoryEntryType,
|
||||
BacktestMetadataType,
|
||||
BacktestResultType,
|
||||
get_BacktestResultType_default,
|
||||
)
|
||||
from freqtrade.types.valid_exchanges_type import ValidExchangesType
|
||||
from freqtrade.ft_types.valid_exchanges_type import ValidExchangesType
|
||||
@@ -1,5 +1,5 @@
|
||||
# Used for list-exchanges
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
@@ -11,8 +11,11 @@ class TradeModeType(TypedDict):
|
||||
|
||||
class ValidExchangesType(TypedDict):
|
||||
name: str
|
||||
classname: str
|
||||
valid: bool
|
||||
supported: bool
|
||||
comment: str
|
||||
dex: bool
|
||||
is_alias: bool
|
||||
alias_for: Optional[str]
|
||||
trade_modes: List[TradeModeType]
|
||||
@@ -15,6 +15,7 @@ if sys.version_info < (3, 9): # pragma: no cover
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.commands import Arguments
|
||||
from freqtrade.configuration import asyncio_setup
|
||||
from freqtrade.constants import DOCS_LINK
|
||||
from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException
|
||||
from freqtrade.loggers import setup_logging_pre
|
||||
@@ -33,6 +34,7 @@ def main(sysargv: Optional[List[str]] = None) -> None:
|
||||
return_code: Any = 1
|
||||
try:
|
||||
setup_logging_pre()
|
||||
asyncio_setup()
|
||||
arguments = Arguments(sysargv)
|
||||
args = arguments.get_parsed_arg()
|
||||
|
||||
|
||||
@@ -128,7 +128,10 @@ def round_dict(d, n):
|
||||
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
|
||||
|
||||
|
||||
def safe_value_fallback(obj: dict, key1: str, key2: Optional[str] = None, default_value=None):
|
||||
DictMap = Union[Dict[str, Any], Mapping[str, Any]]
|
||||
|
||||
|
||||
def safe_value_fallback(obj: DictMap, key1: str, key2: Optional[str] = None, default_value=None):
|
||||
"""
|
||||
Search a value in obj, return this if it's not None.
|
||||
Then search key2 in obj - return that if it's not none - then use default_value.
|
||||
@@ -142,10 +145,7 @@ def safe_value_fallback(obj: dict, key1: str, key2: Optional[str] = None, defaul
|
||||
return default_value
|
||||
|
||||
|
||||
dictMap = Union[Dict[str, Any], Mapping[str, Any]]
|
||||
|
||||
|
||||
def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, default_value=None):
|
||||
def safe_value_fallback2(dict1: DictMap, dict2: DictMap, key1: str, key2: str, default_value=None):
|
||||
"""
|
||||
Search a value in dict1, return this if it's not None.
|
||||
Fall back to dict2 - return key2 from dict2 if it's not None.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
import pandas as pd
|
||||
from rich.text import Text
|
||||
@@ -19,7 +19,9 @@ logger = logging.getLogger(__name__)
|
||||
class LookaheadAnalysisSubFunctions:
|
||||
@staticmethod
|
||||
def text_table_lookahead_analysis_instances(
|
||||
config: Dict[str, Any], lookahead_instances: List[LookaheadAnalysis]
|
||||
config: Dict[str, Any],
|
||||
lookahead_instances: List[LookaheadAnalysis],
|
||||
caption: Union[str, None] = None,
|
||||
):
|
||||
headers = [
|
||||
"filename",
|
||||
@@ -65,7 +67,9 @@ class LookaheadAnalysisSubFunctions:
|
||||
]
|
||||
)
|
||||
|
||||
print_rich_table(data, headers, summary="Lookahead Analysis")
|
||||
print_rich_table(
|
||||
data, headers, summary="Lookahead Analysis", table_kwargs={"caption": caption}
|
||||
)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
@@ -239,8 +243,24 @@ class LookaheadAnalysisSubFunctions:
|
||||
|
||||
# report the results
|
||||
if lookaheadAnalysis_instances:
|
||||
caption: Union[str, None] = None
|
||||
if any(
|
||||
[
|
||||
any(
|
||||
[
|
||||
indicator.startswith("&")
|
||||
for indicator in inst.current_analysis.false_indicators
|
||||
]
|
||||
)
|
||||
for inst in lookaheadAnalysis_instances
|
||||
]
|
||||
):
|
||||
caption = (
|
||||
"Any indicators in 'biased_indicators' which are used within "
|
||||
"set_freqai_targets() can be ignored."
|
||||
)
|
||||
LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances(
|
||||
config, lookaheadAnalysis_instances
|
||||
config, lookaheadAnalysis_instances, caption=caption
|
||||
)
|
||||
if config.get("lookahead_analysis_exportfilename") is not None:
|
||||
LookaheadAnalysisSubFunctions.export_to_csv(config, lookaheadAnalysis_instances)
|
||||
|
||||
@@ -14,6 +14,7 @@ from freqtrade.loggers.set_log_levels import (
|
||||
)
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.optimize.base_analysis import BaseAnalysis, VarHolder
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,10 +22,19 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class RecursiveAnalysis(BaseAnalysis):
|
||||
def __init__(self, config: Dict[str, Any], strategy_obj: Dict):
|
||||
self._startup_candle = config.get("startup_candle", [199, 399, 499, 999, 1999])
|
||||
self._startup_candle = list(
|
||||
map(int, config.get("startup_candle", [199, 399, 499, 999, 1999]))
|
||||
)
|
||||
|
||||
super().__init__(config, strategy_obj)
|
||||
|
||||
strat = StrategyResolver.load_strategy(config)
|
||||
self._strat_scc = strat.startup_candle_count
|
||||
|
||||
if self._strat_scc not in self._startup_candle:
|
||||
self._startup_candle.append(self._strat_scc)
|
||||
self._startup_candle.sort()
|
||||
|
||||
self.partial_varHolder_array: List[VarHolder] = []
|
||||
self.partial_varHolder_lookahead_array: List[VarHolder] = []
|
||||
|
||||
@@ -58,9 +68,13 @@ class RecursiveAnalysis(BaseAnalysis):
|
||||
values_diff = compare_df.loc[indicator]
|
||||
values_diff_self = values_diff.loc["self"]
|
||||
values_diff_other = values_diff.loc["other"]
|
||||
diff = (values_diff_other - values_diff_self) / values_diff_self * 100
|
||||
|
||||
self.dict_recursive[indicator][part.startup_candle] = f"{diff:.3f}%"
|
||||
if values_diff_self and values_diff_other:
|
||||
diff = (values_diff_other - values_diff_self) / values_diff_self * 100
|
||||
str_diff = f"{diff:.3f}%"
|
||||
else:
|
||||
str_diff = "NaN"
|
||||
self.dict_recursive[indicator][part.startup_candle] = str_diff
|
||||
|
||||
else:
|
||||
logger.info("No variance on indicator(s) found due to recursive formula.")
|
||||
@@ -174,7 +188,7 @@ class RecursiveAnalysis(BaseAnalysis):
|
||||
start_date_partial = end_date_full - timedelta(minutes=int(timeframe_minutes))
|
||||
|
||||
for startup_candle in self._startup_candle:
|
||||
self.fill_partial_varholder(start_date_partial, int(startup_candle))
|
||||
self.fill_partial_varholder(start_date_partial, startup_candle)
|
||||
|
||||
# Restore verbosity, so it's not too quiet for the next strategy
|
||||
restore_verbosity_for_bias_tester()
|
||||
|
||||
@@ -17,9 +17,13 @@ class RecursiveAnalysisSubFunctions:
|
||||
@staticmethod
|
||||
def text_table_recursive_analysis_instances(recursive_instances: List[RecursiveAnalysis]):
|
||||
startups = recursive_instances[0]._startup_candle
|
||||
strat_scc = recursive_instances[0]._strat_scc
|
||||
headers = ["Indicators"]
|
||||
for candle in startups:
|
||||
headers.append(str(candle))
|
||||
if candle == strat_scc:
|
||||
headers.append(f"{candle} (from strategy)")
|
||||
else:
|
||||
headers.append(str(candle))
|
||||
|
||||
data = []
|
||||
for inst in recursive_instances:
|
||||
|
||||
@@ -36,6 +36,7 @@ from freqtrade.exchange import (
|
||||
timeframe_to_seconds,
|
||||
)
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
from freqtrade.ft_types import BacktestResultType, get_BacktestResultType_default
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
||||
from freqtrade.optimize.bt_progress import BTProgress
|
||||
@@ -61,7 +62,6 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.types import BacktestResultType, get_BacktestResultType_default
|
||||
from freqtrade.util import FtPrecise
|
||||
from freqtrade.util.migrations import migrate_data
|
||||
from freqtrade.wallets import Wallets
|
||||
@@ -122,6 +122,7 @@ class Backtesting:
|
||||
self.processed_dfs: Dict[str, Dict] = {}
|
||||
self.rejected_dict: Dict[str, List] = {}
|
||||
self.rejected_df: Dict[str, Dict] = {}
|
||||
self.exited_dfs: Dict[str, Dict] = {}
|
||||
|
||||
self._exchange_name = self.config["exchange"]["name"]
|
||||
if not exchange:
|
||||
@@ -181,6 +182,7 @@ class Backtesting:
|
||||
self.fee = max(fee for fee in fees if fee is not None)
|
||||
logger.info(f"Using fee {self.fee:.4%} - worst case fee from exchange (lowest tier).")
|
||||
self.precision_mode = self.exchange.precisionMode
|
||||
self.precision_mode_price = self.exchange.precision_mode_price
|
||||
|
||||
if self.config.get("freqai_backtest_live_models", False):
|
||||
from freqtrade.freqai.utils import get_timerange_backtest_live_models
|
||||
@@ -329,15 +331,15 @@ class Backtesting:
|
||||
else:
|
||||
self.detail_data = {}
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
self.funding_fee_timeframe: str = self.exchange.get_option("funding_fee_timeframe")
|
||||
self.funding_fee_timeframe_secs: int = timeframe_to_seconds(self.funding_fee_timeframe)
|
||||
funding_fee_timeframe: str = self.exchange.get_option("funding_fee_timeframe")
|
||||
self.funding_fee_timeframe_secs: int = timeframe_to_seconds(funding_fee_timeframe)
|
||||
mark_timeframe: str = self.exchange.get_option("mark_ohlcv_timeframe")
|
||||
|
||||
# Load additional futures data.
|
||||
funding_rates_dict = history.load_data(
|
||||
datadir=self.config["datadir"],
|
||||
pairs=self.pairlists.whitelist,
|
||||
timeframe=self.funding_fee_timeframe,
|
||||
timeframe=funding_fee_timeframe,
|
||||
timerange=self.timerange,
|
||||
startup_candles=0,
|
||||
fail_without_data=True,
|
||||
@@ -785,7 +787,7 @@ class Backtesting:
|
||||
)
|
||||
if rate is not None and rate != close_rate:
|
||||
close_rate = price_to_precision(
|
||||
rate, trade.price_precision, self.precision_mode
|
||||
rate, trade.price_precision, self.precision_mode_price
|
||||
)
|
||||
# We can't place orders lower than current low.
|
||||
# freqtrade does not support this in live, and the order would fill immediately
|
||||
@@ -929,7 +931,9 @@ class Backtesting:
|
||||
# We can't place orders higher than current high (otherwise it'd be a stop limit entry)
|
||||
# which freqtrade does not support in live.
|
||||
if new_rate is not None and new_rate != propose_rate:
|
||||
propose_rate = price_to_precision(new_rate, price_precision, self.precision_mode)
|
||||
propose_rate = price_to_precision(
|
||||
new_rate, price_precision, self.precision_mode_price
|
||||
)
|
||||
if direction == "short":
|
||||
propose_rate = max(propose_rate, row[LOW_IDX])
|
||||
else:
|
||||
@@ -1095,7 +1099,7 @@ class Backtesting:
|
||||
open_rate_requested=propose_rate,
|
||||
open_date=current_time,
|
||||
stake_amount=stake_amount,
|
||||
amount=amount,
|
||||
amount=0,
|
||||
amount_requested=amount,
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
@@ -1109,6 +1113,7 @@ class Backtesting:
|
||||
amount_precision=precision_amount,
|
||||
price_precision=precision_price,
|
||||
precision_mode=self.precision_mode,
|
||||
precision_mode_price=self.precision_mode_price,
|
||||
contract_size=contract_size,
|
||||
orders=[],
|
||||
)
|
||||
@@ -1161,12 +1166,10 @@ class Backtesting:
|
||||
self._exit_trade(
|
||||
trade, exit_row, exit_row[OPEN_IDX], trade.amount, ExitType.FORCE_EXIT.value
|
||||
)
|
||||
trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade)
|
||||
|
||||
trade.close_date = exit_row[DATE_IDX].to_pydatetime()
|
||||
trade.exit_reason = ExitType.FORCE_EXIT.value
|
||||
trade.close(exit_row[OPEN_IDX], show_msg=False)
|
||||
LocalTrade.close_bt_trade(trade)
|
||||
self._process_exit_order(
|
||||
trade.orders[-1], trade, exit_row[DATE_IDX].to_pydatetime(), exit_row, pair
|
||||
)
|
||||
|
||||
def trade_slot_available(self, open_trade_count: int) -> bool:
|
||||
# Always allow trades when max_open_trades is enabled.
|
||||
@@ -1332,10 +1335,9 @@ class Backtesting:
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
end_date: datetime,
|
||||
open_trade_count_start: int,
|
||||
trade_dir: Optional[LongShort],
|
||||
is_first: bool = True,
|
||||
) -> int:
|
||||
) -> None:
|
||||
"""
|
||||
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
|
||||
|
||||
@@ -1345,7 +1347,6 @@ class Backtesting:
|
||||
# 1. Manage currently open orders of active trades
|
||||
if self.manage_open_orders(t, current_time, row):
|
||||
# Close trade
|
||||
open_trade_count_start -= 1
|
||||
LocalTrade.remove_bt_trade(t)
|
||||
self.wallets.update()
|
||||
|
||||
@@ -1361,13 +1362,9 @@ class Backtesting:
|
||||
and trade_dir is not None
|
||||
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
|
||||
):
|
||||
if self.trade_slot_available(open_trade_count_start):
|
||||
if self.trade_slot_available(LocalTrade.bt_open_open_trade_count):
|
||||
trade = self._enter_trade(pair, row, trade_dir)
|
||||
if trade:
|
||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||
# This emulates previous behavior - not sure if this is correct
|
||||
# Prevents entering if the trade-slot was freed in this candle
|
||||
open_trade_count_start += 1
|
||||
self.wallets.update()
|
||||
else:
|
||||
self._collate_rejected(pair, row)
|
||||
@@ -1386,7 +1383,28 @@ class Backtesting:
|
||||
order = trade.select_order(trade.exit_side, is_open=True)
|
||||
if order:
|
||||
self._process_exit_order(order, trade, current_time, row, pair)
|
||||
return open_trade_count_start
|
||||
|
||||
def time_pair_generator(
|
||||
self, start_date: datetime, end_date: datetime, increment: timedelta, pairs: List[str]
|
||||
):
|
||||
"""
|
||||
Backtest time and pair generator
|
||||
"""
|
||||
current_time = start_date + increment
|
||||
self.progress.init_step(
|
||||
BacktestState.BACKTEST, int((end_date - start_date) / self.timeframe_td)
|
||||
)
|
||||
while current_time <= end_date:
|
||||
is_first = True
|
||||
# Pairs that have open trades should be processed first
|
||||
new_pairlist = list(dict.fromkeys([t.pair for t in LocalTrade.bt_trades_open] + pairs))
|
||||
|
||||
for pair in new_pairlist:
|
||||
yield current_time, pair, is_first
|
||||
is_first = False
|
||||
|
||||
self.progress.increment()
|
||||
current_time += increment
|
||||
|
||||
def backtest(self, processed: Dict, start_date: datetime, end_date: datetime) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -1411,87 +1429,75 @@ class Backtesting:
|
||||
|
||||
# Indexes per pair, so some pairs are allowed to have a missing start.
|
||||
indexes: Dict = defaultdict(int)
|
||||
current_time = start_date + self.timeframe_td
|
||||
|
||||
self.progress.init_step(
|
||||
BacktestState.BACKTEST, int((end_date - start_date) / self.timeframe_td)
|
||||
)
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
while current_time <= end_date:
|
||||
open_trade_count_start = LocalTrade.bt_open_open_trade_count
|
||||
self.check_abort()
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
|
||||
current_time=current_time
|
||||
)
|
||||
for i, pair in enumerate(data):
|
||||
row_index = indexes[pair]
|
||||
row = self.validate_row(data, pair, row_index, current_time)
|
||||
if not row:
|
||||
for current_time, pair, is_first in self.time_pair_generator(
|
||||
start_date, end_date, self.timeframe_td, list(data.keys())
|
||||
):
|
||||
if is_first:
|
||||
self.check_abort()
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
|
||||
current_time=current_time
|
||||
)
|
||||
row_index = indexes[pair]
|
||||
row = self.validate_row(data, pair, row_index, current_time)
|
||||
if not row:
|
||||
continue
|
||||
|
||||
row_index += 1
|
||||
indexes[pair] = row_index
|
||||
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index)
|
||||
self.dataprovider._set_dataframe_max_date(current_time)
|
||||
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
|
||||
|
||||
if (
|
||||
(trade_dir is not None or len(LocalTrade.bt_trades_open_pp[pair]) > 0)
|
||||
and self.timeframe_detail
|
||||
and pair in self.detail_data
|
||||
):
|
||||
# Spread out into detail timeframe.
|
||||
# Should only happen when we are either in a trade for this pair
|
||||
# or when we got the signal for a new trade.
|
||||
exit_candle_end = current_detail_time + self.timeframe_td
|
||||
|
||||
detail_data = self.detail_data[pair]
|
||||
detail_data = detail_data.loc[
|
||||
(detail_data["date"] >= current_detail_time)
|
||||
& (detail_data["date"] < exit_candle_end)
|
||||
].copy()
|
||||
if len(detail_data) == 0:
|
||||
# Fall back to "regular" data if no detail data was found for this candle
|
||||
self.backtest_loop(row, pair, current_time, end_date, trade_dir)
|
||||
continue
|
||||
|
||||
row_index += 1
|
||||
indexes[pair] = row_index
|
||||
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index)
|
||||
self.dataprovider._set_dataframe_max_date(current_time)
|
||||
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
|
||||
|
||||
if (
|
||||
(trade_dir is not None or len(LocalTrade.bt_trades_open_pp[pair]) > 0)
|
||||
and self.timeframe_detail
|
||||
and pair in self.detail_data
|
||||
):
|
||||
# Spread out into detail timeframe.
|
||||
# Should only happen when we are either in a trade for this pair
|
||||
# or when we got the signal for a new trade.
|
||||
exit_candle_end = current_detail_time + self.timeframe_td
|
||||
|
||||
detail_data = self.detail_data[pair]
|
||||
detail_data = detail_data.loc[
|
||||
(detail_data["date"] >= current_detail_time)
|
||||
& (detail_data["date"] < exit_candle_end)
|
||||
].copy()
|
||||
if len(detail_data) == 0:
|
||||
# Fall back to "regular" data if no detail data was found for this candle
|
||||
open_trade_count_start = self.backtest_loop(
|
||||
row, pair, current_time, end_date, open_trade_count_start, trade_dir
|
||||
)
|
||||
continue
|
||||
detail_data.loc[:, "enter_long"] = row[LONG_IDX]
|
||||
detail_data.loc[:, "exit_long"] = row[ELONG_IDX]
|
||||
detail_data.loc[:, "enter_short"] = row[SHORT_IDX]
|
||||
detail_data.loc[:, "exit_short"] = row[ESHORT_IDX]
|
||||
detail_data.loc[:, "enter_tag"] = row[ENTER_TAG_IDX]
|
||||
detail_data.loc[:, "exit_tag"] = row[EXIT_TAG_IDX]
|
||||
is_first = True
|
||||
current_time_det = current_time
|
||||
for det_row in detail_data[HEADERS].values.tolist():
|
||||
self.dataprovider._set_dataframe_max_date(current_time_det)
|
||||
open_trade_count_start = self.backtest_loop(
|
||||
det_row,
|
||||
pair,
|
||||
current_time_det,
|
||||
end_date,
|
||||
open_trade_count_start,
|
||||
trade_dir,
|
||||
is_first,
|
||||
)
|
||||
current_time_det += self.timeframe_detail_td
|
||||
is_first = False
|
||||
else:
|
||||
self.dataprovider._set_dataframe_max_date(current_time)
|
||||
open_trade_count_start = self.backtest_loop(
|
||||
row, pair, current_time, end_date, open_trade_count_start, trade_dir
|
||||
detail_data.loc[:, "enter_long"] = row[LONG_IDX]
|
||||
detail_data.loc[:, "exit_long"] = row[ELONG_IDX]
|
||||
detail_data.loc[:, "enter_short"] = row[SHORT_IDX]
|
||||
detail_data.loc[:, "exit_short"] = row[ESHORT_IDX]
|
||||
detail_data.loc[:, "enter_tag"] = row[ENTER_TAG_IDX]
|
||||
detail_data.loc[:, "exit_tag"] = row[EXIT_TAG_IDX]
|
||||
is_first = True
|
||||
current_time_det = current_time
|
||||
for det_row in detail_data[HEADERS].values.tolist():
|
||||
self.dataprovider._set_dataframe_max_date(current_time_det)
|
||||
self.backtest_loop(
|
||||
det_row,
|
||||
pair,
|
||||
current_time_det,
|
||||
end_date,
|
||||
trade_dir,
|
||||
is_first,
|
||||
)
|
||||
|
||||
# Move time one configured time_interval ahead.
|
||||
self.progress.increment()
|
||||
current_time += self.timeframe_td
|
||||
current_time_det += self.timeframe_detail_td
|
||||
is_first = False
|
||||
else:
|
||||
self.dataprovider._set_dataframe_max_date(current_time)
|
||||
self.backtest_loop(row, pair, current_time, end_date, trade_dir)
|
||||
|
||||
self.handle_left_open(LocalTrade.bt_trades_open_pp, data=data)
|
||||
self.wallets.update()
|
||||
|
||||
results = trade_list_to_dataframe(LocalTrade.trades)
|
||||
results = trade_list_to_dataframe(LocalTrade.bt_trades)
|
||||
return {
|
||||
"results": results,
|
||||
"config": self.strategy.config,
|
||||
@@ -1559,11 +1565,14 @@ class Backtesting:
|
||||
and self.dataprovider.runmode == RunMode.BACKTEST
|
||||
):
|
||||
self.processed_dfs[strategy_name] = generate_trade_signal_candles(
|
||||
preprocessed_tmp, results
|
||||
preprocessed_tmp, results, "open_date"
|
||||
)
|
||||
self.rejected_df[strategy_name] = generate_rejected_signals(
|
||||
preprocessed_tmp, self.rejected_dict
|
||||
)
|
||||
self.exited_dfs[strategy_name] = generate_trade_signal_candles(
|
||||
preprocessed_tmp, results, "close_date"
|
||||
)
|
||||
|
||||
return min_date, max_date
|
||||
|
||||
@@ -1639,7 +1648,11 @@ class Backtesting:
|
||||
and self.dataprovider.runmode == RunMode.BACKTEST
|
||||
):
|
||||
store_backtest_analysis_results(
|
||||
self.config["exportfilename"], self.processed_dfs, self.rejected_df, dt_appendix
|
||||
self.config["exportfilename"],
|
||||
self.processed_dfs,
|
||||
self.rejected_df,
|
||||
self.exited_dfs,
|
||||
dt_appendix,
|
||||
)
|
||||
|
||||
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
||||
|
||||
@@ -17,7 +17,6 @@ import rapidjson
|
||||
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
|
||||
from joblib.externals import cloudpickle
|
||||
from pandas import DataFrame
|
||||
from rich.align import Align
|
||||
from rich.console import Console
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config
|
||||
@@ -80,7 +79,7 @@ class Hyperopt:
|
||||
self.max_open_trades_space: List[Dimension] = []
|
||||
self.dimensions: List[Dimension] = []
|
||||
|
||||
self._hyper_out: HyperoptOutput = HyperoptOutput()
|
||||
self._hyper_out: HyperoptOutput = HyperoptOutput(streaming=True)
|
||||
|
||||
self.config = config
|
||||
self.min_date: datetime
|
||||
@@ -168,7 +167,9 @@ class Hyperopt:
|
||||
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
|
||||
self.hyperopt_pickle_magic(modules.__bases__)
|
||||
|
||||
def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict:
|
||||
def _get_params_dict(
|
||||
self, dimensions: List[Dimension], raw_params: List[Any]
|
||||
) -> Dict[str, Any]:
|
||||
# Ensure the number of dimensions match
|
||||
# the number of parameters in the list.
|
||||
if len(raw_params) != len(dimensions):
|
||||
@@ -317,7 +318,7 @@ class Hyperopt:
|
||||
+ self.max_open_trades_space
|
||||
)
|
||||
|
||||
def assign_params(self, params_dict: Dict, category: str) -> None:
|
||||
def assign_params(self, params_dict: Dict[str, Any], category: str) -> None:
|
||||
"""
|
||||
Assign hyperoptable parameters
|
||||
"""
|
||||
@@ -404,7 +405,12 @@ class Hyperopt:
|
||||
)
|
||||
|
||||
def _get_results_dict(
|
||||
self, backtesting_results, min_date, max_date, params_dict, processed: Dict[str, DataFrame]
|
||||
self,
|
||||
backtesting_results: Dict[str, Any],
|
||||
min_date: datetime,
|
||||
max_date: datetime,
|
||||
params_dict: Dict[str, Any],
|
||||
processed: Dict[str, DataFrame],
|
||||
) -> Dict[str, Any]:
|
||||
params_details = self._get_params_details(params_dict)
|
||||
|
||||
@@ -628,7 +634,7 @@ class Hyperopt:
|
||||
# Define progressbar
|
||||
with get_progress_tracker(
|
||||
console=console,
|
||||
cust_objs=[Align.center(self._hyper_out.table)],
|
||||
cust_callables=[self._hyper_out],
|
||||
) as pbar:
|
||||
task = pbar.add_task("Epochs", total=self.total_epochs)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import sys
|
||||
from typing import List, Optional, Union
|
||||
from os import get_terminal_size
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from rich.align import Align
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
@@ -11,7 +13,16 @@ from freqtrade.util import fmt_coin
|
||||
|
||||
|
||||
class HyperoptOutput:
|
||||
def __init__(self):
|
||||
def __init__(self, streaming=False) -> None:
|
||||
self._results: List[Any] = []
|
||||
self._streaming = streaming
|
||||
self.__init_table()
|
||||
|
||||
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
||||
return Align.center(self.table)
|
||||
|
||||
def __init_table(self) -> None:
|
||||
"""Initialize table"""
|
||||
self.table = Table(
|
||||
title="Hyperopt results",
|
||||
)
|
||||
@@ -26,17 +37,6 @@ class HyperoptOutput:
|
||||
self.table.add_column("Objective", justify="right")
|
||||
self.table.add_column("Max Drawdown (Acct)", justify="right")
|
||||
|
||||
def _add_row(self, data: List[Union[str, Text]]):
|
||||
"""Add single row"""
|
||||
row_to_add: List[Union[str, Text]] = [r if isinstance(r, Text) else str(r) for r in data]
|
||||
|
||||
self.table.add_row(*row_to_add)
|
||||
|
||||
def _add_rows(self, data: List[List[Union[str, Text]]]):
|
||||
"""add multiple rows"""
|
||||
for row in data:
|
||||
self._add_row(row)
|
||||
|
||||
def print(self, console: Optional[Console] = None, *, print_colorized=True):
|
||||
if not console:
|
||||
console = Console(
|
||||
@@ -55,8 +55,28 @@ class HyperoptOutput:
|
||||
) -> None:
|
||||
"""Format one or multiple rows and add them"""
|
||||
stake_currency = config["stake_currency"]
|
||||
self._results.extend(results)
|
||||
|
||||
for r in results:
|
||||
max_rows: Optional[int] = None
|
||||
|
||||
if self._streaming:
|
||||
try:
|
||||
ts = get_terminal_size()
|
||||
# Get terminal size.
|
||||
# Account for header, borders, and for the progress bar.
|
||||
# This assumes that lines don't wrap.
|
||||
if ts.columns < 148:
|
||||
# If the terminal is too small, we can't display the table properly.
|
||||
# We will halve the number of rows to display.
|
||||
max_rows = -(int(ts.lines / 2) - 6)
|
||||
else:
|
||||
max_rows = -(ts.lines - 6)
|
||||
except OSError:
|
||||
# If we can't get the terminal size, we will just display the last 10 rows.
|
||||
pass
|
||||
|
||||
self.__init_table()
|
||||
for r in self._results[max_rows:]:
|
||||
self.table.add_row(
|
||||
*[
|
||||
# "Best":
|
||||
|
||||
@@ -2,8 +2,8 @@ import logging
|
||||
from typing import Any, Dict, List, Literal, Union
|
||||
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats
|
||||
from freqtrade.types import BacktestResultType
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin, print_rich_table
|
||||
|
||||
|
||||
@@ -263,12 +263,32 @@ def text_table_add_metrics(strat_results: Dict) -> None:
|
||||
else []
|
||||
)
|
||||
|
||||
trading_mode = (
|
||||
(
|
||||
[
|
||||
(
|
||||
"Trading Mode",
|
||||
(
|
||||
""
|
||||
if not strat_results.get("margin_mode")
|
||||
or strat_results.get("trading_mode", "spot") == "spot"
|
||||
else f"{strat_results['margin_mode'].capitalize()} "
|
||||
)
|
||||
+ f"{strat_results['trading_mode'].capitalize()}",
|
||||
)
|
||||
]
|
||||
)
|
||||
if "trading_mode" in strat_results
|
||||
else []
|
||||
)
|
||||
|
||||
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
|
||||
# command stores these results and newer version of freqtrade must be able to handle old
|
||||
# results with missing new fields.
|
||||
metrics = [
|
||||
("Backtesting from", strat_results["backtest_start"]),
|
||||
("Backtesting to", strat_results["backtest_end"]),
|
||||
*trading_mode,
|
||||
("Max open trades", strat_results["max_open_trades"]),
|
||||
("", ""), # Empty line to improve readability
|
||||
(
|
||||
|
||||
@@ -5,9 +5,9 @@ from typing import Dict, Optional
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
from freqtrade.misc import file_dump_joblib, file_dump_json
|
||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||
from freqtrade.types import BacktestResultType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -90,7 +90,12 @@ def _store_backtest_analysis_data(
|
||||
|
||||
|
||||
def store_backtest_analysis_results(
|
||||
recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict], dtappendix: str
|
||||
recordfilename: Path,
|
||||
candles: Dict[str, Dict],
|
||||
trades: Dict[str, Dict],
|
||||
exited: Dict[str, Dict],
|
||||
dtappendix: str,
|
||||
) -> None:
|
||||
_store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals")
|
||||
_store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected")
|
||||
_store_backtest_analysis_data(recordfilename, exited, dtappendix, "exited")
|
||||
|
||||
@@ -17,7 +17,7 @@ from freqtrade.data.metrics import (
|
||||
calculate_sharpe,
|
||||
calculate_sortino,
|
||||
)
|
||||
from freqtrade.types import BacktestResultType
|
||||
from freqtrade.ft_types import BacktestResultType
|
||||
from freqtrade.util import decimals_per_coin, fmt_coin
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_trade_signal_candles(
|
||||
preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any]
|
||||
) -> DataFrame:
|
||||
preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any], date_col: str
|
||||
) -> Dict[str, DataFrame]:
|
||||
signal_candles_only = {}
|
||||
for pair in preprocessed_df.keys():
|
||||
signal_candles_only_df = DataFrame()
|
||||
@@ -36,8 +36,8 @@ def generate_trade_signal_candles(
|
||||
pairresults = resdf.loc[(resdf["pair"] == pair)]
|
||||
|
||||
if pairdf.shape[0] > 0:
|
||||
for t, v in pairresults.open_date.items():
|
||||
allinds = pairdf.loc[(pairdf["date"] < v)]
|
||||
for t, v in pairresults.iterrows():
|
||||
allinds = pairdf.loc[(pairdf["date"] < v[date_col])]
|
||||
signal_inds = allinds.iloc[[-1]]
|
||||
signal_candles_only_df = concat(
|
||||
[signal_candles_only_df.infer_objects(), signal_inds.infer_objects()]
|
||||
@@ -504,6 +504,8 @@ def generate_strategy_stats(
|
||||
"exit_profit_only": config["exit_profit_only"],
|
||||
"exit_profit_offset": config["exit_profit_offset"],
|
||||
"ignore_roi_if_entry_signal": config["ignore_roi_if_entry_signal"],
|
||||
"trading_mode": config["trading_mode"],
|
||||
"margin_mode": config["margin_mode"],
|
||||
**periodic_breakdown,
|
||||
**daily_stats,
|
||||
**trade_stats,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user