Compare commits
471 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a0ee490957 | ||
|
5938514e5d | ||
|
d73f5f75fc | ||
|
5726886b06 | ||
|
3fbf716f85 | ||
|
6a033bd01e | ||
|
5b7a1f8642 | ||
|
37a1cd5d38 | ||
|
78096c9eff | ||
|
29c6f182f6 | ||
|
9059502303 | ||
|
08b1f04ed5 | ||
|
ec445776e9 | ||
|
6319c104fe | ||
|
f4f204d849 | ||
|
4c268847d4 | ||
|
4d72632524 | ||
|
b59906b117 | ||
|
2431c9f195 | ||
|
9657028633 | ||
|
c61b2a83c1 | ||
|
72a1e27fc6 | ||
|
a217d84e0f | ||
|
5da218e383 | ||
|
954c468191 | ||
|
0353f070f9 | ||
|
90d5af9a35 | ||
|
766ef90b56 | ||
|
e85dc63263 | ||
|
422d560189 | ||
|
9a6d8977de | ||
|
ff9c8fe234 | ||
|
692e91a26d | ||
|
d7903f012f | ||
|
fcca637107 | ||
|
2bf49445b7 | ||
|
30cc69c880 | ||
|
8cfb6ddd51 | ||
|
f768bdea50 | ||
|
553c868d7f | ||
|
b0de4d333e | ||
|
707d0ef795 | ||
|
277828bf0e | ||
|
6fc770d97d | ||
|
4237acf5b6 | ||
|
abddb0db66 | ||
|
3ce05c0d54 | ||
|
fd23ab3d64 | ||
|
dd0db7ee5d | ||
|
a0fb43c6ca | ||
|
c91a9a92f2 | ||
|
1da091dea3 | ||
|
879bf47b32 | ||
|
c46ef637c3 | ||
|
ec03531771 | ||
|
ab88217186 | ||
|
cee4ed541b | ||
|
ec9dbc550e | ||
|
c1895a0fc2 | ||
|
73f044d1e2 | ||
|
eab7f8f694 | ||
|
713e7819f7 | ||
|
518a59ad41 | ||
|
42a2fdc1c5 | ||
|
216f75bbb9 | ||
|
e4ca42faec | ||
|
7e6aa9390a | ||
|
e88c4701bb | ||
|
bb6ae682fc | ||
|
5dc78a0c66 | ||
|
f81df19b93 | ||
|
dfa61b7ad2 | ||
|
f2a1d9d2fc | ||
|
1fdb656334 | ||
|
d84ef34740 | ||
|
11f08b0053 | ||
|
56fb25c5e5 | ||
|
564e0b9a1a | ||
|
12c12d42df | ||
|
853c3a4433 | ||
|
d7395e873b | ||
|
4b2c1a9b8e | ||
|
e715f2a253 | ||
|
9525a5b96c | ||
|
9c50d0c250 | ||
|
6d1604d6fa | ||
|
fb6beb90e8 | ||
|
124e97f3b9 | ||
|
5fc993231a | ||
|
3a98fb72a4 | ||
|
982deeedf0 | ||
|
54ef36a497 | ||
|
4ce1375bf3 | ||
|
457e738b4a | ||
|
994c3c3a4c | ||
|
c0811ae896 | ||
|
90ad178932 | ||
|
57ea0c322f | ||
|
f7bae81d96 | ||
|
e4ec5679a1 | ||
|
4e2b1764b8 | ||
|
315ea1e116 | ||
|
35eda8c8c7 | ||
|
3ce5197e8d | ||
|
c9ba52d732 | ||
|
e8e8ef4872 | ||
|
a12c3ecc9b | ||
|
8afb3c4b70 | ||
|
3cdd06f562 | ||
|
b13bd87625 | ||
|
51643ed56c | ||
|
22e0728ac2 | ||
|
81039fce28 | ||
|
d8f48cf0e3 | ||
|
236dc48000 | ||
|
0017b3438e | ||
|
3675df8344 | ||
|
fd6bf591f8 | ||
|
dad4a49e81 | ||
|
ebb0b8aa3f | ||
|
432c3df17e | ||
|
50479d0b44 | ||
|
a5f90a409c | ||
|
4c4604f837 | ||
|
8c9159f596 | ||
|
a19c33ba54 | ||
|
7251a3ab19 | ||
|
982534ddc7 | ||
|
5844f5a7fa | ||
|
366247dff3 | ||
|
fb376153a2 | ||
|
b2f289e404 | ||
|
a1c9a4d619 | ||
|
362dc20406 | ||
|
e1f846f22f | ||
|
e0092a85e9 | ||
|
be93c75e44 | ||
|
aac05029e1 | ||
|
93fcaac19f | ||
|
79ca6135a2 | ||
|
2d66987ac7 | ||
|
8c83c258a5 | ||
|
71ff214adf | ||
|
880474594e | ||
|
10d0987f49 | ||
|
6bd495a32a | ||
|
fb78caf801 | ||
|
a04875eb55 | ||
|
3f0032498e | ||
|
76a59bf2b6 | ||
|
8347219990 | ||
|
64ec1b6f8c | ||
|
765e72715b | ||
|
44f8d7abf2 | ||
|
771193cbe4 | ||
|
4daa4b9e63 | ||
|
01b5fe9f97 | ||
|
1d24d3d5ee | ||
|
c519ecf8df | ||
|
a8f28ffb11 | ||
|
ea5c7e7ed6 | ||
|
2173ff0133 | ||
|
4e049f65f2 | ||
|
63f2494936 | ||
|
35267de88a | ||
|
eb0362c29e | ||
|
493fb35073 | ||
|
91779ee0cc | ||
|
103a8e827e | ||
|
2f92838c39 | ||
|
b4130dfabb | ||
|
c489e6825c | ||
|
68f13173bc | ||
|
e64ccd8fc1 | ||
|
19ad165483 | ||
|
93c1dff71b | ||
|
f59ba92920 | ||
|
ab5e63cbdd | ||
|
b65a15d8b4 | ||
|
87fa49d529 | ||
|
b0c4f079c2 | ||
|
1cbe303434 | ||
|
525aa234dc | ||
|
da5f8c87ae | ||
|
4cc1f2b4a4 | ||
|
ab9a4375cc | ||
|
2a0c95a2e7 | ||
|
b25a161e22 | ||
|
7f13eec5d3 | ||
|
bf1e78fcc8 | ||
|
89d7e36d64 | ||
|
6682d44f05 | ||
|
45c6f90691 | ||
|
9e0ab9c2ca | ||
|
26451e8c01 | ||
|
d0504c47ef | ||
|
c64ebeb6e2 | ||
|
c17595b314 | ||
|
20878290a0 | ||
|
c14d8ea827 | ||
|
a6b4b8bfd9 | ||
|
1895230afe | ||
|
89581ad25c | ||
|
0a52d7c24f | ||
|
f79b30e886 | ||
|
19b3e8a8c5 | ||
|
482e65453f | ||
|
b4d869e8c4 | ||
|
ac0dada962 | ||
|
c6f38bc2f3 | ||
|
a38b72af91 | ||
|
6b2bcd9bdc | ||
|
ef9c1addcf | ||
|
b3a4b0fbde | ||
|
3e10f7e2d8 | ||
|
07ce6bf3a6 | ||
|
2ce458810b | ||
|
07d71f014f | ||
|
6d96b11279 | ||
|
df1c0540ab | ||
|
0d8e105a33 | ||
|
58ecb34a66 | ||
|
fbf8eb4526 | ||
|
1f3ccc2587 | ||
|
c4be52d1c3 | ||
|
63844d39f6 | ||
|
7fb570cc58 | ||
|
68dd349094 | ||
|
3745966c6c | ||
|
23d21d8ace | ||
|
4b36276e4f | ||
|
8a9407bac9 | ||
|
60b476611c | ||
|
9691563066 | ||
|
aafb868cc4 | ||
|
c2b7577e94 | ||
|
3e296dee24 | ||
|
345c7ab64b | ||
|
90f1845eaf | ||
|
0f9bfcf8b0 | ||
|
4ee467f857 | ||
|
3026583ed4 | ||
|
5582737093 | ||
|
04b4deab58 | ||
|
56759cea7b | ||
|
34456b9798 | ||
|
127f470bc3 | ||
|
40ad451019 | ||
|
695a1e21bf | ||
|
ba5abb20bd | ||
|
19158ba7da | ||
|
f7087feeb1 | ||
|
dc0b4d07d4 | ||
|
9951f51079 | ||
|
d97fc1e484 | ||
|
ffd60f392b | ||
|
9469c6dfa9 | ||
|
2fb9f6e2f4 | ||
|
acb00cd072 | ||
|
6e41add40e | ||
|
9871268529 | ||
|
4164f93853 | ||
|
81715d0b9d | ||
|
37e3d20357 | ||
|
9758bed250 | ||
|
f471915828 | ||
|
6ab99369f2 | ||
|
f08d673a52 | ||
|
17daba321b | ||
|
faff40577a | ||
|
3ea4b2ba00 | ||
|
cf80cabc84 | ||
|
3cc510f1a8 | ||
|
0264d77d86 | ||
|
f24a951ec5 | ||
|
4115121c24 | ||
|
4b65206e6b | ||
|
4a75f9bb5b | ||
|
6b2ef36a56 | ||
|
abddb3ef25 | ||
|
c6af4f6c6b | ||
|
108a6cb897 | ||
|
a058737c72 | ||
|
c08bbcb16d | ||
|
d644c12a55 | ||
|
6da3c07c2d | ||
|
3878e5186e | ||
|
a10fd66906 | ||
|
d8607b2ce8 | ||
|
7125793249 | ||
|
e7b6a996df | ||
|
37d4545123 | ||
|
dda8276589 | ||
|
322ea2481e | ||
|
ed6776c5cd | ||
|
4f10a88529 | ||
|
fa4ec9f83e | ||
|
3406b889b6 | ||
|
8405ccc15e | ||
|
88172fab82 | ||
|
123971d271 | ||
|
c456cfc312 | ||
|
0f7ddabec8 | ||
|
bb472ff98b | ||
|
db5a944396 | ||
|
c7147311f8 | ||
|
0a6c0c429a | ||
|
20cc60bfde | ||
|
dbf7f34ecb | ||
|
b098ce4e76 | ||
|
ae11be3970 | ||
|
e03784d98d | ||
|
bfc3968ab3 | ||
|
fad253ad51 | ||
|
bdbac37be7 | ||
|
f6267c7514 | ||
|
4e83d5c4c6 | ||
|
61c076563f | ||
|
65d025923d | ||
|
0b6aedbc4c | ||
|
039d6384ed | ||
|
124e9519e4 | ||
|
3f160c7144 | ||
|
cf27968b97 | ||
|
5bfb9edf02 | ||
|
8bb42a07ce | ||
|
58e4255ae3 | ||
|
895b912c71 | ||
|
a5f796bc97 | ||
|
519c256b88 | ||
|
927ac24f82 | ||
|
f17942b68f | ||
|
f3c603073e | ||
|
5919992ad2 | ||
|
9e6ed5ada0 | ||
|
47f641d12f | ||
|
b89a993890 | ||
|
6a227fe9eb | ||
|
70f8bff8ce | ||
|
d2d5590252 | ||
|
59626b4ffc | ||
|
bad25b753c | ||
|
32e8e3b242 | ||
|
756112c84d | ||
|
3bd0c3d009 | ||
|
be240566ba | ||
|
faf16a64e5 | ||
|
0ae4eccea5 | ||
|
7eaadb2630 | ||
|
cf70b34ff0 | ||
|
5393c55b51 | ||
|
6532aba765 | ||
|
66de30f042 | ||
|
b9356a5564 | ||
|
612b88e993 | ||
|
0985b11267 | ||
|
84d082033b | ||
|
0aeebc9d53 | ||
|
90c194de1f | ||
|
4d28f0ed59 | ||
|
eee5f174fc | ||
|
cb3b0cf311 | ||
|
f243ad4af0 | ||
|
f9f519fd3c | ||
|
d9b9eecd4d | ||
|
2cf781f3dd | ||
|
ad0e4a8567 | ||
|
b73768acd1 | ||
|
3c41223333 | ||
|
a661e0db6e | ||
|
ceed3c663b | ||
|
a6454cfc39 | ||
|
091bf7c4d2 | ||
|
544e0da6c2 | ||
|
800b2eeaf0 | ||
|
74a5cb3c21 | ||
|
6410a6528b | ||
|
bc3e6deb1c | ||
|
b644233ead | ||
|
b3dafb378e | ||
|
16146357b3 | ||
|
42e24d8b4b | ||
|
00939b63f2 | ||
|
53fb8b05e7 | ||
|
d9c9b7d7fc | ||
|
3d8c3ffd38 | ||
|
5284112b69 | ||
|
f11f5d17e9 | ||
|
dfc17f2bd1 | ||
|
4ab03f7e37 | ||
|
e70a742005 | ||
|
adb33e763b | ||
|
c981641441 | ||
|
d8d8261f1b | ||
|
3c5f06d5c0 | ||
|
5ead95b06b | ||
|
3e5659b32f | ||
|
b63eda3a2b | ||
|
8485964bb7 | ||
|
eeee92275b | ||
|
d4fa50c605 | ||
|
ae48012831 | ||
|
5b4931897d | ||
|
3a19e1610d | ||
|
7fd3fc98c0 | ||
|
849b8197a9 | ||
|
4afcea9a1c | ||
|
4f05d31b94 | ||
|
b3f057e7c0 | ||
|
059c32b067 | ||
|
056bc93bc6 | ||
|
e78be75e8c | ||
|
047df0c212 | ||
|
e9ef9a6d28 | ||
|
90a61b1765 | ||
|
6f8519d0a3 | ||
|
c5e3348b89 | ||
|
1ccc89d1e9 | ||
|
b1cbc75e93 | ||
|
6abd352c0f | ||
|
ab3c753415 | ||
|
499af5c42b | ||
|
35bf2a59a8 | ||
|
138b126d03 | ||
|
aa34889c04 | ||
|
71838dc51a | ||
|
dad98d43be | ||
|
25c527ee67 | ||
|
4675d85b90 | ||
|
34c8a5afaf | ||
|
b7ba2f115e | ||
|
0fcbe097c0 | ||
|
7a0cb95ffb | ||
|
b9c2489b73 | ||
|
ba0fa1120a | ||
|
acfaa39e54 | ||
|
8032257fdf | ||
|
aea5da0c73 | ||
|
65fc094c9f | ||
|
5fe18be4b5 | ||
|
dd923c3471 | ||
|
65b4705b67 | ||
|
b01daa8bbc | ||
|
dd809f756b | ||
|
643b6b950e | ||
|
25e329623f | ||
|
46f2a20a98 | ||
|
235c1afd09 | ||
|
f5a660f845 | ||
|
49886874aa | ||
|
759a350d73 | ||
|
1ea29a918a | ||
|
db1e676639 | ||
|
66a7070170 | ||
|
5d04d6ffa7 | ||
|
cbfedf8b29 | ||
|
c558fc0b17 | ||
|
3d8b2d601d | ||
|
edf9c08f06 | ||
|
ed30c023cd | ||
|
d31d38a85f | ||
|
ec526b3f96 | ||
|
7d04005218 | ||
|
104711a9bf | ||
|
9e63bdbac9 | ||
|
ab786abf7f | ||
|
f705293353 | ||
|
365479f5e0 | ||
|
8b0a02db8e | ||
|
8b485d197a | ||
|
638bed3dac |
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,14 +2,16 @@ Thank you for sending your pull request. But first, have you included
|
|||||||
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Explain in one sentence the goal of this PR
|
Explain in one sentence the goal of this PR
|
||||||
|
|
||||||
Solve the issue: #___
|
Solve the issue: #___
|
||||||
|
|
||||||
## Quick changelog
|
## Quick changelog
|
||||||
|
|
||||||
- <change log #1>
|
- <change log 1>
|
||||||
- <change log #2>
|
- <change log 1>
|
||||||
|
|
||||||
## What's new?
|
## What's new?
|
||||||
|
|
||||||
*Explain in details what this PR solve or improve. You can include visuals.*
|
*Explain in details what this PR solve or improve. You can include visuals.*
|
||||||
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -87,7 +87,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
- name: Flake8
|
||||||
run: |
|
run: |
|
||||||
@@ -180,7 +180,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
- name: Flake8
|
||||||
run: |
|
run: |
|
||||||
@@ -247,7 +247,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp config_examples/config_bittrex.example.json config.json
|
cp config_examples/config_bittrex.example.json config.json
|
||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||||
|
|
||||||
- name: Flake8
|
- name: Flake8
|
||||||
run: |
|
run: |
|
||||||
|
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- script:
|
- script:
|
||||||
- cp config_examples/config_bittrex.example.json config.json
|
- cp config_examples/config_bittrex.example.json config.json
|
||||||
- freqtrade create-userdir --userdir user_data
|
- freqtrade create-userdir --userdir user_data
|
||||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily
|
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily
|
||||||
name: hyperopt
|
name: hyperopt
|
||||||
- script: flake8
|
- script: flake8
|
||||||
name: flake8
|
name: flake8
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.9.6-slim-buster as base
|
FROM python:3.9.7-slim-buster as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
@@ -13,7 +13,7 @@ RUN mkdir /freqtrade \
|
|||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \
|
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
&& useradd -u 1000 -G sudo -U -m -s /bin/bash ftuser \
|
||||||
&& chown ftuser:ftuser /freqtrade \
|
&& chown ftuser:ftuser /freqtrade \
|
||||||
# Allow sudoers
|
# Allow sudoers
|
||||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
||||||
|
13
README.md
13
README.md
@@ -26,10 +26,11 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
|
|
||||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
|
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#blacklists))
|
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
@@ -37,7 +38,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
|||||||
Exchanges confirmed working by the community:
|
Exchanges confirmed working by the community:
|
||||||
|
|
||||||
- [X] [Bitvavo](https://bitvavo.com/)
|
- [X] [Bitvavo](https://bitvavo.com/)
|
||||||
- [X] [Kukoin](https://www.kucoin.com/)
|
- [X] [Kucoin](https://www.kucoin.com/)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -78,22 +79,22 @@ For any other type of installation please refer to [Installation doc](https://ww
|
|||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-V]
|
usage: freqtrade [-h] [-V]
|
||||||
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
{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}
|
||||||
...
|
...
|
||||||
|
|
||||||
Free, open source crypto trading bot
|
Free, open source crypto trading bot
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
{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 Trade module.
|
trade Trade module.
|
||||||
create-userdir Create user-data directory.
|
create-userdir Create user-data directory.
|
||||||
new-config Create new config
|
new-config Create new config
|
||||||
new-hyperopt Create new hyperopt
|
|
||||||
new-strategy Create new strategy
|
new-strategy Create new strategy
|
||||||
download-data Download backtesting data.
|
download-data Download backtesting data.
|
||||||
convert-data Convert candle (OHLCV) data from one format to
|
convert-data Convert candle (OHLCV) data from one format to
|
||||||
another.
|
another.
|
||||||
convert-trade-data Convert trade data from one format to another.
|
convert-trade-data Convert trade data from one format to another.
|
||||||
|
list-data List downloaded data.
|
||||||
backtesting Backtesting module.
|
backtesting Backtesting module.
|
||||||
edge Edge module.
|
edge Edge module.
|
||||||
hyperopt Hyperopt module.
|
hyperopt Hyperopt module.
|
||||||
@@ -107,8 +108,10 @@ positional arguments:
|
|||||||
list-timeframes Print available timeframes for the exchange.
|
list-timeframes Print available timeframes for the exchange.
|
||||||
show-trades Show trades.
|
show-trades Show trades.
|
||||||
test-pairlist Test your pairlist configuration.
|
test-pairlist Test your pairlist configuration.
|
||||||
|
install-ui Install FreqUI
|
||||||
plot-dataframe Plot candles with indicators.
|
plot-dataframe Plot candles with indicators.
|
||||||
plot-profit Generate plot showing profits.
|
plot-profit Generate plot showing profits.
|
||||||
|
webserver Webserver module.
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@@ -12,9 +12,12 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
|||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
||||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& which sudo && sudo make install || make install \
|
&& which sudo && sudo make install || make install
|
||||||
&& cd ..
|
if [ -x "$(command -v apt-get)" ]; then
|
||||||
|
echo "Updating library path using ldconfig"
|
||||||
|
sudo ldconfig
|
||||||
|
fi
|
||||||
|
cd .. && rm -rf ./ta-lib/
|
||||||
else
|
else
|
||||||
echo "TA-lib already installed, skipping installation"
|
echo "TA-lib already installed, skipping installation"
|
||||||
fi
|
fi
|
||||||
# && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
|
||||||
|
@@ -37,12 +37,12 @@ fi
|
|||||||
# Tag image for upload and next build step
|
# Tag image for upload and next build step
|
||||||
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
|
||||||
|
|
||||||
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
|
||||||
|
|
||||||
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy
|
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "failed running backtest"
|
echo "failed running backtest"
|
||||||
@@ -63,18 +63,16 @@ echo "create manifests"
|
|||||||
docker manifest create --amend ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
docker manifest create --amend ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG}
|
docker manifest push -p ${IMAGE_NAME}:${TAG}
|
||||||
|
|
||||||
docker manifest create --amend ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT}
|
docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT}
|
||||||
docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
|
docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT}
|
||||||
|
|
||||||
Tag as latest for develop builds
|
# Tag as latest for develop builds
|
||||||
if [ "${TAG}" = "develop" ]; then
|
if [ "${TAG}" = "develop" ]; then
|
||||||
docker tag ${IMAGE_NAME}:develop ${IMAGE_NAME}:latest
|
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
||||||
docker push ${IMAGE_NAME}:latest
|
docker manifest push -p ${IMAGE_NAME}:latest
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker images
|
docker images
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
# Cleanup old images from arm64 node.
|
||||||
echo "failed building image"
|
docker image prune -a --force --filter "until=24h"
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
@@ -48,12 +48,12 @@ fi
|
|||||||
# Tag image for upload and next build step
|
# Tag image for upload and next build step
|
||||||
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG
|
||||||
|
|
||||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||||
|
|
||||||
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
|
||||||
|
|
||||||
# Run backtest
|
# Run backtest
|
||||||
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy
|
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "failed running backtest"
|
echo "failed running backtest"
|
||||||
|
@@ -78,33 +78,6 @@
|
|||||||
"refresh_period": 1440
|
"refresh_period": 1440
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"protections": [
|
|
||||||
{
|
|
||||||
"method": "StoplossGuard",
|
|
||||||
"lookback_period_candles": 60,
|
|
||||||
"trade_limit": 4,
|
|
||||||
"stop_duration_candles": 60,
|
|
||||||
"only_per_pair": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "CooldownPeriod",
|
|
||||||
"stop_duration_candles": 20
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "MaxDrawdown",
|
|
||||||
"lookback_period_candles": 200,
|
|
||||||
"trade_limit": 20,
|
|
||||||
"stop_duration_candles": 10,
|
|
||||||
"max_allowed_drawdown": 0.2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "LowProfitPairs",
|
|
||||||
"lookback_period_candles": 360,
|
|
||||||
"trade_limit": 1,
|
|
||||||
"stop_duration_candles": 2,
|
|
||||||
"required_profit": 0.02
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "binance",
|
"name": "binance",
|
||||||
"sandbox": false,
|
"sandbox": false,
|
||||||
@@ -176,7 +149,9 @@
|
|||||||
},
|
},
|
||||||
"sell_fill": "on",
|
"sell_fill": "on",
|
||||||
"buy_cancel": "on",
|
"buy_cancel": "on",
|
||||||
"sell_cancel": "on"
|
"sell_cancel": "on",
|
||||||
|
"protection_trigger": "off",
|
||||||
|
"protection_trigger_global": "on"
|
||||||
},
|
},
|
||||||
"reload": true,
|
"reload": true,
|
||||||
"balance_dust_level": 0.01
|
"balance_dust_level": 0.01
|
||||||
@@ -201,7 +176,7 @@
|
|||||||
"heartbeat_interval": 60
|
"heartbeat_interval": 60
|
||||||
},
|
},
|
||||||
"disable_dataframe_checks": false,
|
"disable_dataframe_checks": false,
|
||||||
"strategy": "DefaultStrategy",
|
"strategy": "SampleStrategy",
|
||||||
"strategy_path": "user_data/strategies/",
|
"strategy_path": "user_data/strategies/",
|
||||||
"dataformat_ohlcv": "json",
|
"dataformat_ohlcv": "json",
|
||||||
"dataformat_trades": "jsongz"
|
"dataformat_trades": "jsongz"
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
ARG sourceimage=develop
|
ARG sourceimage=freqtradeorg/freqtrade
|
||||||
FROM freqtradeorg/freqtrade:${sourceimage}
|
ARG sourcetag=develop
|
||||||
|
FROM ${sourceimage}:${sourcetag}
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements-plot.txt /freqtrade/
|
COPY requirements-plot.txt /freqtrade/
|
||||||
|
@@ -67,10 +67,10 @@ Currently, the arguments are:
|
|||||||
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
|
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
|
This function is called once per epoch - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
|
||||||
|
|
||||||
!!! Note
|
!!! Note "`*args` and `**kwargs`"
|
||||||
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later.
|
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface in the future.
|
||||||
|
|
||||||
## Overriding pre-defined spaces
|
## Overriding pre-defined spaces
|
||||||
|
|
||||||
@@ -80,10 +80,56 @@ To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_sp
|
|||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
class HyperOpt:
|
class HyperOpt:
|
||||||
# Define a custom stoploss space.
|
# Define a custom stoploss space.
|
||||||
def stoploss_space(self):
|
def stoploss_space():
|
||||||
return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')]
|
return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')]
|
||||||
|
|
||||||
|
# Define custom ROI space
|
||||||
|
def roi_space() -> List[Dimension]:
|
||||||
|
return [
|
||||||
|
Integer(10, 120, name='roi_t1'),
|
||||||
|
Integer(10, 60, name='roi_t2'),
|
||||||
|
Integer(10, 40, name='roi_t3'),
|
||||||
|
SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'),
|
||||||
|
SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'),
|
||||||
|
SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'),
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
All overrides are optional and can be mixed/matched as necessary.
|
||||||
|
|
||||||
|
### Overriding Base estimator
|
||||||
|
|
||||||
|
You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
class HyperOpt:
|
||||||
|
def generate_estimator():
|
||||||
|
return "RF"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Possible values are either one of "GP", "RF", "ET", "GBRT" (Details can be found in the [scikit-optimize documentation](https://scikit-optimize.github.io/)), or "an instance of a class that inherits from `RegressorMixin` (from sklearn) and where the `predict` method has an optional `return_std` argument, which returns `std(Y | x)` along with `E[Y | x]`".
|
||||||
|
|
||||||
|
Some research will be necessary to find additional Regressors.
|
||||||
|
|
||||||
|
Example for `ExtraTreesRegressor` ("ET") with additional parameters:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
class HyperOpt:
|
||||||
|
def generate_estimator():
|
||||||
|
from skopt.learning import ExtraTreesRegressor
|
||||||
|
# Corresponds to "ET" - but allows additional parameters.
|
||||||
|
return ExtraTreesRegressor(n_estimators=100)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used.
|
||||||
|
If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters.
|
||||||
|
|
||||||
## Space options
|
## Space options
|
||||||
|
|
||||||
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
|
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
|
||||||
@@ -105,281 +151,3 @@ from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal,
|
|||||||
Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`).
|
Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`).
|
||||||
|
|
||||||
A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`).
|
A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Legacy Hyperopt
|
|
||||||
|
|
||||||
This Section explains the configuration of an explicit Hyperopt file (separate to the strategy).
|
|
||||||
|
|
||||||
!!! Warning "Deprecated / legacy mode"
|
|
||||||
Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted.
|
|
||||||
Please read the [main hyperopt page](hyperopt.md) for more details.
|
|
||||||
|
|
||||||
### Prepare hyperopt file
|
|
||||||
|
|
||||||
Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar.
|
|
||||||
|
|
||||||
!!! Tip "About this page"
|
|
||||||
For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class.
|
|
||||||
|
|
||||||
#### Create a Custom Hyperopt File
|
|
||||||
|
|
||||||
The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`.
|
|
||||||
|
|
||||||
Let assume you want a hyperopt file `AwesomeHyperopt.py`:
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
freqtrade new-hyperopt --hyperopt AwesomeHyperopt
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Legacy Hyperopt checklist
|
|
||||||
|
|
||||||
Checklist on all tasks / possibilities in hyperopt
|
|
||||||
|
|
||||||
Depending on the space you want to optimize, only some of the below are required:
|
|
||||||
|
|
||||||
* fill `buy_strategy_generator` - for buy signal optimization
|
|
||||||
* fill `indicator_space` - for buy signal optimization
|
|
||||||
* fill `sell_strategy_generator` - for sell signal optimization
|
|
||||||
* fill `sell_indicator_space` - for sell signal optimization
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work.
|
|
||||||
|
|
||||||
Optional in hyperopt - can also be loaded from a strategy (recommended):
|
|
||||||
|
|
||||||
* `populate_indicators` - fallback to create indicators
|
|
||||||
* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy
|
|
||||||
* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods.
|
|
||||||
Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead.
|
|
||||||
|
|
||||||
Rarely you may also need to override:
|
|
||||||
|
|
||||||
* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default)
|
|
||||||
* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
|
|
||||||
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
|
|
||||||
* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
|
|
||||||
|
|
||||||
#### Defining a buy signal optimization
|
|
||||||
|
|
||||||
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
|
||||||
Bands to trigger your buys. And you also wonder should you use RSI or ADX to
|
|
||||||
help with those buy decisions. If you decide to use RSI or ADX, which values
|
|
||||||
should I use for them? So let's use hyperparameter optimization to solve this
|
|
||||||
mystery.
|
|
||||||
|
|
||||||
We will start by defining a search space:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching strategy parameters
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(20, 40, name='adx-value'),
|
|
||||||
Integer(20, 40, name='rsi-value'),
|
|
||||||
Categorical([True, False], name='adx-enabled'),
|
|
||||||
Categorical([True, False], name='rsi-enabled'),
|
|
||||||
Categorical(['bb_lower', 'macd_cross_signal'], name='trigger')
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Above definition says: I have five parameters I want you to randomly combine
|
|
||||||
to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40.
|
|
||||||
Then we have three category variables. First two are either `True` or `False`.
|
|
||||||
We use these to either enable or disable the ADX and RSI guards.
|
|
||||||
The last one we call `trigger` and use it to decide which buy trigger we want to use.
|
|
||||||
|
|
||||||
So let's write the buy strategy generator using these values:
|
|
||||||
|
|
||||||
```python
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
conditions = []
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'adx-enabled' in params and params['adx-enabled']:
|
|
||||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
|
||||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if 'trigger' in params:
|
|
||||||
if params['trigger'] == 'bb_lower':
|
|
||||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macd'], dataframe['macdsignal']
|
|
||||||
))
|
|
||||||
|
|
||||||
# Check that volume is not 0
|
|
||||||
conditions.append(dataframe['volume'] > 0)
|
|
||||||
|
|
||||||
if conditions:
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, conditions),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
```
|
|
||||||
|
|
||||||
Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations.
|
|
||||||
It will use the given historical data and make buys based on the buy signals generated with the above function.
|
|
||||||
Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)).
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
|
||||||
When you want to test an indicator that isn't used by the bot currently, remember to
|
|
||||||
add it to the `populate_indicators()` method in your strategy or hyperopt file.
|
|
||||||
|
|
||||||
#### Sell optimization
|
|
||||||
|
|
||||||
Similar to the buy-signal above, sell-signals can also be optimized.
|
|
||||||
Place the corresponding settings into the following methods
|
|
||||||
|
|
||||||
* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing.
|
|
||||||
* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters.
|
|
||||||
|
|
||||||
The configuration and rules are the same than for buy signals.
|
|
||||||
To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`.
|
|
||||||
|
|
||||||
### Execute Hyperopt
|
|
||||||
|
|
||||||
Once you have updated your hyperopt configuration you can run it.
|
|
||||||
Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results.
|
|
||||||
|
|
||||||
We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade hyperopt --config config.json --hyperopt <hyperoptname> --hyperopt-loss <hyperoptlossname> --strategy <strategyname> -e 500 --spaces all
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `<hyperoptname>` as the name of the custom hyperopt used.
|
|
||||||
|
|
||||||
The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs.
|
|
||||||
Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results.
|
|
||||||
|
|
||||||
The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
Hyperopt will store hyperopt results with the timestamp of the hyperopt start time.
|
|
||||||
Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename <filename>` to read and display older hyperopt results.
|
|
||||||
You can find a list of filenames with `ls -l user_data/hyperopt_results/`.
|
|
||||||
|
|
||||||
#### Running Hyperopt using methods from a strategy
|
|
||||||
|
|
||||||
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Understand the Hyperopt Result
|
|
||||||
|
|
||||||
Once Hyperopt is completed you can use the result to create a new strategy.
|
|
||||||
Given the following result from hyperopt:
|
|
||||||
|
|
||||||
```
|
|
||||||
Best result:
|
|
||||||
|
|
||||||
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722%). Avg duration 180.4 mins. Objective: 1.94367
|
|
||||||
|
|
||||||
Buy hyperspace params:
|
|
||||||
{ 'adx-value': 44,
|
|
||||||
'rsi-value': 29,
|
|
||||||
'adx-enabled': False,
|
|
||||||
'rsi-enabled': True,
|
|
||||||
'trigger': 'bb_lower'}
|
|
||||||
```
|
|
||||||
|
|
||||||
You should understand this result like:
|
|
||||||
|
|
||||||
* The buy trigger that worked best was `bb_lower`.
|
|
||||||
* You should not use ADX because `adx-enabled: False`)
|
|
||||||
* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
|
|
||||||
|
|
||||||
You have to look inside your strategy file into `buy_strategy_generator()`
|
|
||||||
method, what those values match to.
|
|
||||||
|
|
||||||
So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block:
|
|
||||||
|
|
||||||
```python
|
|
||||||
(dataframe['rsi'] < 29.0)
|
|
||||||
```
|
|
||||||
|
|
||||||
Translating your whole hyperopt result as the new buy-signal would then look like:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe['rsi'] < 29.0) & # rsi-value
|
|
||||||
dataframe['close'] < dataframe['bb_lowerband'] # trigger
|
|
||||||
),
|
|
||||||
'buy'] = 1
|
|
||||||
return dataframe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validate backtesting results
|
|
||||||
|
|
||||||
Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected.
|
|
||||||
|
|
||||||
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
|
||||||
|
|
||||||
Should results don't match, please double-check to make sure you transferred all conditions correctly.
|
|
||||||
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
|
|
||||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
|
|
||||||
|
|
||||||
### Sharing methods with your strategy
|
|
||||||
|
|
||||||
Hyperopt classes provide access to the Strategy via the `strategy` class attribute.
|
|
||||||
This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users.
|
|
||||||
|
|
||||||
``` python
|
|
||||||
from pandas import DataFrame
|
|
||||||
from freqtrade.strategy.interface import IStrategy
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
|
|
||||||
class MyAwesomeStrategy(IStrategy):
|
|
||||||
|
|
||||||
buy_params = {
|
|
||||||
'rsi-value': 30,
|
|
||||||
'adx-value': 35,
|
|
||||||
}
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
return self.buy_strategy_generator(self.buy_params, dataframe, metadata)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) &
|
|
||||||
dataframe['adx'] > params['adx-value']) &
|
|
||||||
dataframe['volume'] > 0
|
|
||||||
)
|
|
||||||
, 'buy'] = 1
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
class MyAwesomeHyperOpt(IHyperOpt):
|
|
||||||
...
|
|
||||||
@staticmethod
|
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by Hyperopt.
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
# Call strategy's buy strategy generator
|
|
||||||
return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata)
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
```
|
|
||||||
|
@@ -18,6 +18,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[-p PAIRS [PAIRS ...]] [--eps] [--dmmp]
|
[-p PAIRS [PAIRS ...]] [--eps] [--dmmp]
|
||||||
[--enable-protections]
|
[--enable-protections]
|
||||||
[--dry-run-wallet DRY_RUN_WALLET]
|
[--dry-run-wallet DRY_RUN_WALLET]
|
||||||
|
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
[--export {none,trades}] [--export-filename PATH]
|
[--export {none,trades}] [--export-filename PATH]
|
||||||
|
|
||||||
@@ -55,6 +56,9 @@ optional arguments:
|
|||||||
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
|
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
|
||||||
Starting balance, used for backtesting / hyperopt and
|
Starting balance, used for backtesting / hyperopt and
|
||||||
dry-runs.
|
dry-runs.
|
||||||
|
--timeframe-detail TIMEFRAME_DETAIL
|
||||||
|
Specify detail timeframe for backtesting (`1m`, `5m`,
|
||||||
|
`30m`, `1h`, `1d`).
|
||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that ticker-interval needs to be
|
backtest. Please note that ticker-interval needs to be
|
||||||
@@ -62,7 +66,7 @@ optional arguments:
|
|||||||
this together with `--export trades`, the strategy-
|
this together with `--export trades`, the strategy-
|
||||||
name is injected into the filename (so `backtest-
|
name is injected into the filename (so `backtest-
|
||||||
data.json` becomes `backtest-data-
|
data.json` becomes `backtest-data-
|
||||||
DefaultStrategy.json`
|
SampleStrategy.json`
|
||||||
--export {none,trades}
|
--export {none,trades}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--export-filename PATH
|
--export-filename PATH
|
||||||
@@ -425,7 +429,12 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||||
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
||||||
|
|
||||||
### Assumptions made by backtesting
|
### Further backtest-result analysis
|
||||||
|
|
||||||
|
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
||||||
|
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
|
||||||
|
|
||||||
|
## Assumptions made by backtesting
|
||||||
|
|
||||||
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
|
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
|
||||||
|
|
||||||
@@ -456,10 +465,30 @@ Also, keep in mind that past results don't guarantee future success.
|
|||||||
|
|
||||||
In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions.
|
In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions.
|
||||||
|
|
||||||
### Further backtest-result analysis
|
### Improved backtest accuracy
|
||||||
|
|
||||||
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?).
|
||||||
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
|
So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close).
|
||||||
|
|
||||||
|
While backtesting does take some assumptions (read above) about this - this can never be perfect, and will always be biased in one way or the other.
|
||||||
|
To mitigate this, freqtrade can use a lower (faster) timeframe to simulate intra-candle movements.
|
||||||
|
|
||||||
|
To utilize this, you can append `--timeframe-detail 5m` to your regular backtesting command.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-detail 5m
|
||||||
|
```
|
||||||
|
|
||||||
|
This will load 1h data as well as 5m data for the timeframe. The strategy will be analyzed with the 1h timeframe - and for every "open trade candle" (candles where a trade is open) the 5m data will be used to simulate intra-candle movements.
|
||||||
|
All callback functions (`custom_sell()`, `custom_stoploss()`, ... ) will be running for each 5m candle once the trade is opened (so 12 times in the above example of 1h timeframe, and 5m detailed timeframe).
|
||||||
|
|
||||||
|
`--timeframe-detail` must be smaller than the original timeframe, otherwise backtesting will fail to start.
|
||||||
|
|
||||||
|
Obviously this will require more memory (5m data is bigger than 1h data), and will also impact runtime (depending on the amount of trades and trade durations).
|
||||||
|
Also, data must be available / downloaded already.
|
||||||
|
|
||||||
|
!!! Tip
|
||||||
|
You can use this function as the last part of strategy development, to ensure your strategy is not exploiting one of the [backtesting assumptions](#assumptions-made-by-backtesting). Strategies that perform similarly well with this mode have a good chance to perform well in dry/live modes too (although only forward-testing (dry-mode) can really confirm a strategy).
|
||||||
|
|
||||||
## Backtesting multiple strategies
|
## Backtesting multiple strategies
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ This page provides you some basic concepts on how Freqtrade works and operates.
|
|||||||
* **Strategy**: Your trading strategy, telling the bot what to do.
|
* **Strategy**: Your trading strategy, telling the bot what to do.
|
||||||
* **Trade**: Open position.
|
* **Trade**: Open position.
|
||||||
* **Open Order**: Order which is currently placed on the exchange, and is not yet complete.
|
* **Open Order**: Order which is currently placed on the exchange, and is not yet complete.
|
||||||
* **Pair**: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT).
|
* **Pair**: Tradable pair, usually in the format of Base/Quote (e.g. XRP/USDT).
|
||||||
* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
|
* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
|
||||||
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
||||||
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
||||||
@@ -35,12 +35,13 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
|
|||||||
* Calls `check_buy_timeout()` strategy callback for open buy orders.
|
* Calls `check_buy_timeout()` strategy callback for open buy orders.
|
||||||
* Calls `check_sell_timeout()` strategy callback for open sell orders.
|
* Calls `check_sell_timeout()` strategy callback for open sell orders.
|
||||||
* Verifies existing positions and eventually places sell orders.
|
* Verifies existing positions and eventually places sell orders.
|
||||||
* Considers stoploss, ROI and sell-signal.
|
* Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
|
||||||
* Determine sell-price based on `ask_strategy` configuration setting.
|
* Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
|
||||||
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
|
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||||
* Verifies buy signal trying to enter new positions.
|
* Verifies buy signal trying to enter new positions.
|
||||||
* Determine buy-price based on `bid_strategy` configuration setting.
|
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
|
||||||
|
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||||
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
|
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
|
||||||
|
|
||||||
This loop will be repeated again and again until the bot is stopped.
|
This loop will be repeated again and again until the bot is stopped.
|
||||||
@@ -52,9 +53,10 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
* Load historic data for configured pairlist.
|
* Load historic data for configured pairlist.
|
||||||
* Calls `bot_loop_start()` once.
|
* Calls `bot_loop_start()` once.
|
||||||
* Calculate indicators (calls `populate_indicators()` once per pair).
|
* Calculate indicators (calls `populate_indicators()` once per pair).
|
||||||
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair)
|
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
|
||||||
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy)
|
|
||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
|
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||||
|
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
||||||
* Generate backtest report output
|
* Generate backtest report output
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@@ -12,22 +12,22 @@ This page explains the different parameters of the bot and how to run it.
|
|||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-V]
|
usage: freqtrade [-h] [-V]
|
||||||
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
{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}
|
||||||
...
|
...
|
||||||
|
|
||||||
Free, open source crypto trading bot
|
Free, open source crypto trading bot
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
|
{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 Trade module.
|
trade Trade module.
|
||||||
create-userdir Create user-data directory.
|
create-userdir Create user-data directory.
|
||||||
new-config Create new config
|
new-config Create new config
|
||||||
new-hyperopt Create new hyperopt
|
|
||||||
new-strategy Create new strategy
|
new-strategy Create new strategy
|
||||||
download-data Download backtesting data.
|
download-data Download backtesting data.
|
||||||
convert-data Convert candle (OHLCV) data from one format to
|
convert-data Convert candle (OHLCV) data from one format to
|
||||||
another.
|
another.
|
||||||
convert-trade-data Convert trade data from one format to another.
|
convert-trade-data Convert trade data from one format to another.
|
||||||
|
list-data List downloaded data.
|
||||||
backtesting Backtesting module.
|
backtesting Backtesting module.
|
||||||
edge Edge module.
|
edge Edge module.
|
||||||
hyperopt Hyperopt module.
|
hyperopt Hyperopt module.
|
||||||
@@ -41,8 +41,10 @@ positional arguments:
|
|||||||
list-timeframes Print available timeframes for the exchange.
|
list-timeframes Print available timeframes for the exchange.
|
||||||
show-trades Show trades.
|
show-trades Show trades.
|
||||||
test-pairlist Test your pairlist configuration.
|
test-pairlist Test your pairlist configuration.
|
||||||
|
install-ui Install FreqUI
|
||||||
plot-dataframe Plot candles with indicators.
|
plot-dataframe Plot candles with indicators.
|
||||||
plot-profit Generate plot showing profits.
|
plot-profit Generate plot showing profits.
|
||||||
|
webserver Webserver module.
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
@@ -11,6 +11,37 @@ Per default, the bot loads the configuration from the `config.json` file, locate
|
|||||||
|
|
||||||
You can specify a different configuration file used by the bot with the `-c/--config` command-line option.
|
You can specify a different configuration file used by the bot with the `-c/--config` command-line option.
|
||||||
|
|
||||||
|
If you used the [Quick start](installation.md/#quick-start) method for installing
|
||||||
|
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
|
||||||
|
|
||||||
|
If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file.
|
||||||
|
|
||||||
|
The Freqtrade configuration file is to be written in JSON format.
|
||||||
|
|
||||||
|
Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters.
|
||||||
|
|
||||||
|
Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates the syntax of the configuration file at startup and will warn you if you made any errors editing it, pointing out problematic lines.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
Set options in the Freqtrade configuration via environment variables.
|
||||||
|
This takes priority over the corresponding value in configuration or strategy.
|
||||||
|
|
||||||
|
Environment variables must be prefixed with `FREQTRADE__` to be loaded to the freqtrade configuration.
|
||||||
|
|
||||||
|
`__` serves as level separator, so the format used should correspond to `FREQTRADE__{section}__{key}`.
|
||||||
|
As such - an environment variable defined as `export FREQTRADE__STAKE_AMOUNT=200` would result in `{stake_amount: 200}`.
|
||||||
|
|
||||||
|
A more complex example might be `export FREQTRADE__EXCHANGE__KEY=<yourExchangeKey>` to keep your exchange key secret. This will move the value to the `exchange.key` section of the configuration.
|
||||||
|
Using this scheme, all configuration settings will also be available as environment variables.
|
||||||
|
|
||||||
|
Please note that Environment variables will overwrite corresponding settings in your configuration, but command line Arguments will always win.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable.
|
||||||
|
|
||||||
|
### Multiple configuration files
|
||||||
|
|
||||||
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
|
Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
|
||||||
|
|
||||||
!!! Tip "Use multiple configuration files to keep secrets secret"
|
!!! Tip "Use multiple configuration files to keep secrets secret"
|
||||||
@@ -22,17 +53,6 @@ Multiple configuration files can be specified and used by the bot or the bot can
|
|||||||
The 2nd file should only specify what you intend to override.
|
The 2nd file should only specify what you intend to override.
|
||||||
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
|
If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`).
|
||||||
|
|
||||||
If you used the [Quick start](installation.md/#quick-start) method for installing
|
|
||||||
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
|
|
||||||
|
|
||||||
If the default configuration file is not created we recommend you to use `freqtrade new-config --config config.json` to generate a basic configuration file.
|
|
||||||
|
|
||||||
The Freqtrade configuration file is to be written in JSON format.
|
|
||||||
|
|
||||||
Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters.
|
|
||||||
|
|
||||||
Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates the syntax of the configuration file at startup and will warn you if you made any errors editing it, pointing out problematic lines.
|
|
||||||
|
|
||||||
## Configuration parameters
|
## Configuration parameters
|
||||||
|
|
||||||
The table below will list all configuration parameters available.
|
The table below will list all configuration parameters available.
|
||||||
@@ -41,6 +61,7 @@ Freqtrade can also load many options via command line (CLI) arguments (check out
|
|||||||
The prevalence for all Options is as follows:
|
The prevalence for all Options is as follows:
|
||||||
|
|
||||||
- CLI arguments override any other option
|
- CLI arguments override any other option
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
- Configuration files are used in sequence (the last file wins) and override Strategy configurations.
|
- Configuration files are used in sequence (the last file wins) and override Strategy configurations.
|
||||||
- Strategy configurations are only used if they are not set via configuration or command-line arguments. These options are marked with [Strategy Override](#parameters-in-the-strategy) in the below table.
|
- Strategy configurations are only used if they are not set via configuration or command-line arguments. These options are marked with [Strategy Override](#parameters-in-the-strategy) in the below table.
|
||||||
|
|
||||||
@@ -84,11 +105,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||||
| `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
| `sell_profit_offset` | Sell-signal is only active above this value. Only active in combination with `sell_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
||||||
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
||||||
| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
| `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||||
|
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
||||||
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
||||||
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
|
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
|
||||||
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
@@ -422,8 +444,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
This is ongoing work. For now, it is supported only for binance.
|
This is ongoing work. For now, it is supported only for binance and kucoin.
|
||||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values.
|
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
||||||
|
|
||||||
### Exchange configuration
|
### Exchange configuration
|
||||||
|
|
||||||
@@ -526,9 +548,10 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
|
|||||||
|
|
||||||
## Switch to production mode
|
## Switch to production mode
|
||||||
|
|
||||||
In production mode, the bot will engage your money. Be careful, since a wrong
|
In production mode, the bot will engage your money. Be careful, since a wrong strategy can lose all your money.
|
||||||
strategy can lose all your money. Be aware of what you are doing when
|
Be aware of what you are doing when you run it in production mode.
|
||||||
you run it in production mode.
|
|
||||||
|
When switching to Production mode, please make sure to use a different / fresh database to avoid dry-run trades messing with your exchange money and eventually tainting your statistics.
|
||||||
|
|
||||||
### Setup your exchange account
|
### Setup your exchange account
|
||||||
|
|
||||||
|
@@ -38,3 +38,8 @@ Since only quoteVolume can be compared between assets, the other options (bidVol
|
|||||||
|
|
||||||
Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early.
|
Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early.
|
||||||
As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7.
|
As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7.
|
||||||
|
|
||||||
|
### Legacy Hyperopt mode
|
||||||
|
|
||||||
|
Using separate hyperopt files was deprecated in 2021.4 and was removed in 2021.9.
|
||||||
|
Please switch to the new [Parametrized Strategies](hyperopt.md) to benefit from the new hyperopt interface.
|
||||||
|
@@ -240,11 +240,18 @@ The `IProtection` parent class provides a helper method for this in `calculate_l
|
|||||||
!!! Note
|
!!! Note
|
||||||
This section is a Work in Progress and is not a complete guide on how to test a new exchange with Freqtrade.
|
This section is a Work in Progress and is not a complete guide on how to test a new exchange with Freqtrade.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Make sure to use an up-to-date version of CCXT before running any of the below tests.
|
||||||
|
You can get the latest version of ccxt by running `pip install -U ccxt` with activated virtual environment.
|
||||||
|
Native docker is not supported for these tests, however the available dev-container will support all required actions and eventually necessary changes.
|
||||||
|
|
||||||
Most exchanges supported by CCXT should work out of the box.
|
Most exchanges supported by CCXT should work out of the box.
|
||||||
|
|
||||||
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
|
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
|
||||||
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
||||||
|
|
||||||
|
Also try to use `freqtrade download-data` for an extended timerange and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
|
||||||
|
|
||||||
### Stoploss On Exchange
|
### Stoploss On Exchange
|
||||||
|
|
||||||
Check if the new exchange supports Stoploss on Exchange orders through their API.
|
Check if the new exchange supports Stoploss on Exchange orders through their API.
|
||||||
|
@@ -149,6 +149,24 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the
|
|||||||
|
|
||||||
You can then run `docker-compose build` to build the docker image, and run it using the commands described above.
|
You can then run `docker-compose build` to build the docker image, and run it using the commands described above.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Docker on Windows
|
||||||
|
|
||||||
|
* Error: `"Timestamp for this request is outside of the recvWindow."`
|
||||||
|
* The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past.
|
||||||
|
To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so).
|
||||||
|
A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler.
|
||||||
|
```
|
||||||
|
taskkill /IM "Docker Desktop.exe" /F
|
||||||
|
wsl --shutdown
|
||||||
|
start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting.
|
||||||
|
Best use a linux-VPS for running freqtrade reliably.
|
||||||
|
|
||||||
## Plotting with docker-compose
|
## Plotting with docker-compose
|
||||||
|
|
||||||
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
|
Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file.
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss.
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
WHen using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
|
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
|
||||||
|
@@ -4,6 +4,8 @@ This page combines common gotchas and informations which are exchange-specific a
|
|||||||
|
|
||||||
## Binance
|
## Binance
|
||||||
|
|
||||||
|
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
|
||||||
@@ -56,6 +58,12 @@ Bittrex does not support market orders. If you have a message at the bot startup
|
|||||||
Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment.
|
Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment.
|
||||||
Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected.
|
Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected.
|
||||||
|
|
||||||
|
### Volume pairlist
|
||||||
|
|
||||||
|
Bittrex does not support the direct usage of VolumePairList. This can however be worked around by using the advanced mode with `lookback_days: 1` (or more), which will emulate 24h volume.
|
||||||
|
|
||||||
|
Read more in the [pairlist documentation](plugins.md#volumepairlist-advanced-mode).
|
||||||
|
|
||||||
### Restricted markets
|
### Restricted markets
|
||||||
|
|
||||||
Bittrex split its exchange into US and International versions.
|
Bittrex split its exchange into US and International versions.
|
||||||
@@ -105,7 +113,7 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll
|
|||||||
|
|
||||||
## Kucoin
|
## Kucoin
|
||||||
|
|
||||||
Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
@@ -113,8 +121,12 @@ Kucoin requries a passphrase for each api key, you will therefore need to add th
|
|||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"password": "your_exchange_api_key_password",
|
"password": "your_exchange_api_key_password",
|
||||||
|
// ...
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
### Kucoin Blacklists
|
### Kucoin Blacklists
|
||||||
|
|
||||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||||
@@ -158,6 +170,8 @@ For example, to test the order type `FOK` with Kraken, and modify candle limit t
|
|||||||
"order_time_in_force": ["gtc", "fok"],
|
"order_time_in_force": ["gtc", "fok"],
|
||||||
"ohlcv_candle_limit": 200
|
"ohlcv_candle_limit": 200
|
||||||
}
|
}
|
||||||
|
//...
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
|
@@ -167,7 +167,7 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce
|
|||||||
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
|
freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why does it take a long time to run hyperopt?
|
### Why does it take a long time to run hyperopt?
|
||||||
|
135
docs/hyperopt.md
135
docs/hyperopt.md
@@ -44,11 +44,10 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
[--data-format-ohlcv {json,jsongz,hdf5}]
|
[--data-format-ohlcv {json,jsongz,hdf5}]
|
||||||
[--max-open-trades INT]
|
[--max-open-trades INT]
|
||||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||||
[-p PAIRS [PAIRS ...]] [--hyperopt NAME]
|
[-p PAIRS [PAIRS ...]] [--hyperopt-path PATH]
|
||||||
[--hyperopt-path PATH] [--eps] [--dmmp]
|
[--eps] [--dmmp] [--enable-protections]
|
||||||
[--enable-protections]
|
|
||||||
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
|
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
|
||||||
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
|
[--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
|
||||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||||
[--random-state INT] [--min-trades INT]
|
[--random-state INT] [--min-trades INT]
|
||||||
[--hyperopt-loss NAME] [--disable-param-export]
|
[--hyperopt-loss NAME] [--disable-param-export]
|
||||||
@@ -73,10 +72,8 @@ optional arguments:
|
|||||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||||
Limit command to these pairs. Pairs are space-
|
Limit command to these pairs. Pairs are space-
|
||||||
separated.
|
separated.
|
||||||
--hyperopt NAME Specify hyperopt class name which will be used by the
|
--hyperopt-path PATH Specify additional lookup path for Hyperopt Loss
|
||||||
bot.
|
functions.
|
||||||
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
|
|
||||||
Hyperopt Loss functions.
|
|
||||||
--eps, --enable-position-stacking
|
--eps, --enable-position-stacking
|
||||||
Allow buying the same pair multiple times (position
|
Allow buying the same pair multiple times (position
|
||||||
stacking).
|
stacking).
|
||||||
@@ -92,7 +89,7 @@ optional arguments:
|
|||||||
Starting balance, used for backtesting / hyperopt and
|
Starting balance, used for backtesting / hyperopt and
|
||||||
dry-runs.
|
dry-runs.
|
||||||
-e INT, --epochs INT Specify number of epochs (default: 100).
|
-e INT, --epochs INT Specify number of epochs (default: 100).
|
||||||
--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]
|
--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]
|
||||||
Specify which parameters to hyperopt. Space-separated
|
Specify which parameters to hyperopt. Space-separated
|
||||||
list.
|
list.
|
||||||
--print-all Print all results, not only the best ones.
|
--print-all Print all results, not only the best ones.
|
||||||
@@ -253,7 +250,7 @@ We continue to define hyperoptable parameters:
|
|||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy")
|
buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy")
|
||||||
buy_rsi = IntParameter(20, 40, default=30, space="buy")
|
buy_rsi = IntParameter(20, 40, default=30, space="buy")
|
||||||
buy_adx_enabled = CategoricalParameter([True, False], default=True, space="buy")
|
buy_adx_enabled = BooleanParameter(default=True, space="buy")
|
||||||
buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy")
|
buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy")
|
||||||
buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy")
|
buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy")
|
||||||
```
|
```
|
||||||
@@ -316,6 +313,7 @@ There are four parameter types each suited for different purposes.
|
|||||||
* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases.
|
* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases.
|
||||||
* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities.
|
* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities.
|
||||||
* `CategoricalParameter` - defines a parameter with a predetermined number of choices.
|
* `CategoricalParameter` - defines a parameter with a predetermined number of choices.
|
||||||
|
* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters.
|
||||||
|
|
||||||
!!! Tip "Disabling parameter optimization"
|
!!! Tip "Disabling parameter optimization"
|
||||||
Each parameter takes two boolean parameters:
|
Each parameter takes two boolean parameters:
|
||||||
@@ -326,7 +324,7 @@ There are four parameter types each suited for different purposes.
|
|||||||
!!! Warning
|
!!! Warning
|
||||||
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
|
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
|
||||||
|
|
||||||
### Optimizing an indicator parameter
|
## Optimizing an indicator parameter
|
||||||
|
|
||||||
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
|
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
|
||||||
|
|
||||||
@@ -336,8 +334,8 @@ from functools import reduce
|
|||||||
|
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
|
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
|
IStrategy, IntParameter)
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
@@ -413,6 +411,98 @@ While this strategy is most likely too simple to provide consistent profit, it s
|
|||||||
While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values).
|
While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values).
|
||||||
You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space.
|
You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space.
|
||||||
|
|
||||||
|
## Optimizing protections
|
||||||
|
|
||||||
|
Freqtrade can also optimize protections. How you optimize protections is up to you, and the following should be considered as example only.
|
||||||
|
|
||||||
|
The strategy will simply need to define the "protections" entry as property returning a list of protection configurations.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from pandas import DataFrame
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
|
||||||
|
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
|
IStrategy, IntParameter)
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
stoploss = -0.05
|
||||||
|
timeframe = '15m'
|
||||||
|
# Define the parameter spaces
|
||||||
|
cooldown_lookback = IntParameter(2, 48, default=5, space="protection", optimize=True)
|
||||||
|
stop_duration = IntParameter(12, 200, default=5, space="protection", optimize=True)
|
||||||
|
use_stop_protection = BooleanParameter(default=True, space="protection", optimize=True)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protections(self):
|
||||||
|
prot = []
|
||||||
|
|
||||||
|
prot.append({
|
||||||
|
"method": "CooldownPeriod",
|
||||||
|
"stop_duration_candles": self.cooldown_lookback.value
|
||||||
|
})
|
||||||
|
if self.use_stop_protection.value:
|
||||||
|
prot.append({
|
||||||
|
"method": "StoplossGuard",
|
||||||
|
"lookback_period_candles": 24 * 3,
|
||||||
|
"trade_limit": 4,
|
||||||
|
"stop_duration_candles": self.stop_duration.value,
|
||||||
|
"only_per_pair": False
|
||||||
|
})
|
||||||
|
|
||||||
|
return prot
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# ...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then run hyperopt as follows:
|
||||||
|
`freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy --spaces protection`
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
The protection space is not part of the default space, and is only available with the Parameters Hyperopt interface, not with the legacy hyperopt interface (which required separate hyperopt files).
|
||||||
|
Freqtrade will also automatically change the "--enable-protections" flag if the protection space is selected.
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
If protections are defined as property, entries from the configuration will be ignored.
|
||||||
|
It is therefore recommended to not define protections in the configuration.
|
||||||
|
|
||||||
|
### Migrating from previous property setups
|
||||||
|
|
||||||
|
A migration from a previous setup is pretty simple, and can be accomplished by converting the protections entry to a property.
|
||||||
|
In simple terms, the following configuration will be converted to the below.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
protections = [
|
||||||
|
{
|
||||||
|
"method": "CooldownPeriod",
|
||||||
|
"stop_duration_candles": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Result
|
||||||
|
|
||||||
|
``` python
|
||||||
|
class MyAwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"method": "CooldownPeriod",
|
||||||
|
"stop_duration_candles": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
You will then obviously also change potential interesting entries to parameters to allow hyper-optimization.
|
||||||
|
|
||||||
## Loss-functions
|
## Loss-functions
|
||||||
|
|
||||||
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
||||||
@@ -465,7 +555,7 @@ For example, to use one month of data, pass `--timerange 20210101-20210201` (fro
|
|||||||
Full command:
|
Full command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade hyperopt --hyperopt <hyperoptname> --strategy <strategyname> --timerange 20210101-20210201
|
freqtrade hyperopt --strategy <strategyname> --timerange 20210101-20210201
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Hyperopt with Smaller Search Space
|
### Running Hyperopt with Smaller Search Space
|
||||||
@@ -483,7 +573,8 @@ Legal values are:
|
|||||||
* `roi`: just optimize the minimal profit table for your strategy
|
* `roi`: just optimize the minimal profit table for your strategy
|
||||||
* `stoploss`: search for the best stoploss value
|
* `stoploss`: search for the best stoploss value
|
||||||
* `trailing`: search for the best trailing stop values
|
* `trailing`: search for the best trailing stop values
|
||||||
* `default`: `all` except `trailing`
|
* `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
|
||||||
|
* `default`: `all` except `trailing` and `protection`
|
||||||
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
* space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||||
|
|
||||||
The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy.
|
The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy.
|
||||||
@@ -586,11 +677,11 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f
|
|||||||
|
|
||||||
These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used.
|
These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used.
|
||||||
|
|
||||||
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
|
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
|
||||||
|
|
||||||
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps).
|
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps).
|
||||||
|
|
||||||
A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
|
A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
||||||
|
|
||||||
!!! Note "Reduced search space"
|
!!! Note "Reduced search space"
|
||||||
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
||||||
@@ -632,7 +723,7 @@ If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimiza
|
|||||||
|
|
||||||
If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default.
|
If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default.
|
||||||
|
|
||||||
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
|
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
||||||
|
|
||||||
!!! Note "Reduced search space"
|
!!! Note "Reduced search space"
|
||||||
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
||||||
@@ -670,10 +761,10 @@ As stated in the comment, you can also use it as the values of the corresponding
|
|||||||
|
|
||||||
If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases.
|
If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases.
|
||||||
|
|
||||||
Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
|
Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces).
|
||||||
|
|
||||||
!!! Note "Reduced search space"
|
!!! Note "Reduced search space"
|
||||||
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
|
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs.
|
||||||
|
|
||||||
### Reproducible results
|
### Reproducible results
|
||||||
|
|
||||||
@@ -733,8 +824,8 @@ After you run Hyperopt for the desired amount of epochs, you can later list all
|
|||||||
|
|
||||||
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
|
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
|
||||||
|
|
||||||
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||||
|
|
||||||
Should results don't match, please double-check to make sure you transferred all conditions correctly.
|
Should results not match, please double-check to make sure you transferred all conditions correctly.
|
||||||
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
|
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
|
||||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
|
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
|
||||||
|
@@ -58,7 +58,7 @@ This option must be configured along with `exchange.skip_pair_validation` in the
|
|||||||
|
|
||||||
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
|
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
|
||||||
|
|
||||||
When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange.
|
When used in the leading position of the chain of Pairlist Handlers, the `pair_whitelist` configuration setting is ignored. Instead, `VolumePairList` selects the top assets from all available markets with matching stake-currency on the exchange.
|
||||||
|
|
||||||
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
|
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
|
||||||
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
|
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
|
||||||
@@ -74,11 +74,16 @@ Filtering instances (not the first position in the list) will not apply any cach
|
|||||||
"method": "VolumePairList",
|
"method": "VolumePairList",
|
||||||
"number_assets": 20,
|
"number_assets": 20,
|
||||||
"sort_key": "quoteVolume",
|
"sort_key": "quoteVolume",
|
||||||
|
"min_value": 0,
|
||||||
"refresh_period": 1800
|
"refresh_period": 1800
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange.
|
||||||
|
|
||||||
|
### VolumePairList Advanced mode
|
||||||
|
|
||||||
`VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles.
|
`VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles.
|
||||||
|
|
||||||
For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days:
|
For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days:
|
||||||
@@ -89,6 +94,7 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl
|
|||||||
"method": "VolumePairList",
|
"method": "VolumePairList",
|
||||||
"number_assets": 20,
|
"number_assets": 20,
|
||||||
"sort_key": "quoteVolume",
|
"sort_key": "quoteVolume",
|
||||||
|
"min_value": 0,
|
||||||
"refresh_period": 86400,
|
"refresh_period": 86400,
|
||||||
"lookback_days": 7
|
"lookback_days": 7
|
||||||
}
|
}
|
||||||
@@ -101,6 +107,24 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl
|
|||||||
!!! Warning "Performance implications when using lookback range"
|
!!! Warning "Performance implications when using lookback range"
|
||||||
If used in first position in combination with lookback, the computation of the range based volume 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 `VolumeFilter` to narrow the pairlist down for further range volume calculation.
|
If used in first position in combination with lookback, the computation of the range based volume 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 `VolumeFilter` to narrow the pairlist down for further range volume calculation.
|
||||||
|
|
||||||
|
??? Tip "Unsupported exchanges (Bittrex, Gemini)"
|
||||||
|
On some exchanges (like Bittrex and Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume.
|
||||||
|
To roughly simulate 24h volume, you can use the following configuration.
|
||||||
|
Please note that These pairlists will only refresh once per day.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlists": [
|
||||||
|
{
|
||||||
|
"method": "VolumePairList",
|
||||||
|
"number_assets": 20,
|
||||||
|
"sort_key": "quoteVolume",
|
||||||
|
"min_value": 0,
|
||||||
|
"refresh_period": 86400,
|
||||||
|
"lookback_days": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
More sophisticated approach can be used, by using `lookback_timeframe` for candle size and `lookback_period` which specifies the amount of candles. This example will build the volume pairs based on a rolling period of 3 days of 1h candles:
|
More sophisticated approach can be used, by using `lookback_timeframe` for candle size and `lookback_period` which specifies the amount of candles. This example will build the volume pairs based on a rolling period of 3 days of 1h candles:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -109,6 +133,7 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
|
|||||||
"method": "VolumePairList",
|
"method": "VolumePairList",
|
||||||
"number_assets": 20,
|
"number_assets": 20,
|
||||||
"sort_key": "quoteVolume",
|
"sort_key": "quoteVolume",
|
||||||
|
"min_value": 0,
|
||||||
"refresh_period": 3600,
|
"refresh_period": 3600,
|
||||||
"lookback_timeframe": "1h",
|
"lookback_timeframe": "1h",
|
||||||
"lookback_period": 72
|
"lookback_period": 72
|
||||||
@@ -140,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
|
// ...
|
||||||
{
|
{
|
||||||
"method": "OffsetFilter",
|
"method": "OffsetFilter",
|
||||||
"offset": 10
|
"offset": 10
|
||||||
@@ -165,6 +191,19 @@ Sorts pairs by past trade performance, as follows:
|
|||||||
|
|
||||||
Trade count is used as a tie breaker.
|
Trade count is used as a tie breaker.
|
||||||
|
|
||||||
|
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
||||||
|
Not defining this parameter (or setting it to 0) will use all-time performance.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlists": [
|
||||||
|
// ...
|
||||||
|
{
|
||||||
|
"method": "PerformanceFilter",
|
||||||
|
"minutes": 1440 // rolling 24h
|
||||||
|
}
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`PerformanceFilter` does not support backtesting mode.
|
`PerformanceFilter` does not support backtesting mode.
|
||||||
|
|
||||||
@@ -221,10 +260,10 @@ If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio
|
|||||||
|
|
||||||
#### RangeStabilityFilter
|
#### RangeStabilityFilter
|
||||||
|
|
||||||
Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
|
Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change` or above `max_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
|
||||||
|
|
||||||
In the below example:
|
In the below example:
|
||||||
If the trading range over the last 10 days is <1%, remove the pair from the whitelist.
|
If the trading range over the last 10 days is <1% or >99%, remove the pair from the whitelist.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
@@ -232,6 +271,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit
|
|||||||
"method": "RangeStabilityFilter",
|
"method": "RangeStabilityFilter",
|
||||||
"lookback_days": 10,
|
"lookback_days": 10,
|
||||||
"min_rate_of_change": 0.01,
|
"min_rate_of_change": 0.01,
|
||||||
|
"max_rate_of_change": 0.99,
|
||||||
"refresh_period": 1440
|
"refresh_period": 1440
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -239,6 +279,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit
|
|||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit.
|
This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit.
|
||||||
|
Additionally, it can also be used to automatically remove pairs with extreme high/low variance over a given amount of time.
|
||||||
|
|
||||||
#### VolatilityFilter
|
#### VolatilityFilter
|
||||||
|
|
||||||
|
@@ -15,6 +15,10 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
|||||||
!!! Note "Backtesting"
|
!!! Note "Backtesting"
|
||||||
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
|
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
|
||||||
|
|
||||||
|
!!! Warning "Setting protections from the configuration"
|
||||||
|
Setting protections from the configuration via `"protections": [],` key should be considered deprecated and will be removed in a future version.
|
||||||
|
It is also no longer guaranteed that your protections apply to the strategy in cases where the strategy defines [protections as property](hyperopt.md#optimizing-protections).
|
||||||
|
|
||||||
### Available Protections
|
### Available Protections
|
||||||
|
|
||||||
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
|
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
|
||||||
@@ -47,7 +51,9 @@ This applies across all pairs, unless `only_per_pair` is set to true, which will
|
|||||||
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
|
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
protections = [
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "StoplossGuard",
|
"method": "StoplossGuard",
|
||||||
"lookback_period_candles": 24,
|
"lookback_period_candles": 24,
|
||||||
@@ -55,7 +61,7 @@ protections = [
|
|||||||
"stop_duration_candles": 4,
|
"stop_duration_candles": 4,
|
||||||
"only_per_pair": False
|
"only_per_pair": False
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
@@ -69,7 +75,9 @@ protections = [
|
|||||||
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
protections = [
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "MaxDrawdown",
|
"method": "MaxDrawdown",
|
||||||
"lookback_period_candles": 48,
|
"lookback_period_candles": 48,
|
||||||
@@ -77,7 +85,7 @@ protections = [
|
|||||||
"stop_duration_candles": 12,
|
"stop_duration_candles": 12,
|
||||||
"max_allowed_drawdown": 0.2
|
"max_allowed_drawdown": 0.2
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Low Profit Pairs
|
#### Low Profit Pairs
|
||||||
@@ -88,7 +96,9 @@ If that ratio is below `required_profit`, that pair will be locked for `stop_dur
|
|||||||
The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles.
|
The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
protections = [
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "LowProfitPairs",
|
"method": "LowProfitPairs",
|
||||||
"lookback_period_candles": 6,
|
"lookback_period_candles": 6,
|
||||||
@@ -96,7 +106,7 @@ protections = [
|
|||||||
"stop_duration": 60,
|
"stop_duration": 60,
|
||||||
"required_profit": 0.02
|
"required_profit": 0.02
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Cooldown Period
|
#### Cooldown Period
|
||||||
@@ -106,12 +116,14 @@ protections = [
|
|||||||
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
|
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
protections = [
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
"stop_duration_candles": 2
|
"stop_duration_candles": 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
@@ -136,7 +148,10 @@ from freqtrade.strategy import IStrategy
|
|||||||
|
|
||||||
class AwesomeStrategy(IStrategy)
|
class AwesomeStrategy(IStrategy)
|
||||||
timeframe = '1h'
|
timeframe = '1h'
|
||||||
protections = [
|
|
||||||
|
@property
|
||||||
|
def protections(self):
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
"stop_duration_candles": 5
|
"stop_duration_candles": 5
|
||||||
|
@@ -36,10 +36,11 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
|
|||||||
|
|
||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#blacklists))
|
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
@@ -47,7 +48,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
|||||||
Exchanges confirmed working by the community:
|
Exchanges confirmed working by the community:
|
||||||
|
|
||||||
- [X] [Bitvavo](https://bitvavo.com/)
|
- [X] [Bitvavo](https://bitvavo.com/)
|
||||||
- [X] [Kukoin](https://www.kucoin.com/)
|
- [X] [Kucoin](https://www.kucoin.com/)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.2
|
mkdocs==1.2.2
|
||||||
mkdocs-material==7.2.1
|
mkdocs-material==7.3.0
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==8.2
|
pymdown-extensions==8.2
|
||||||
|
@@ -110,7 +110,7 @@ DELETE FROM trades WHERE id = 31;
|
|||||||
Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems.
|
Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems.
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
`pip install psycopg2`
|
`pip install psycopg2-binary`
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
`... --db-url postgresql+psycopg2://<username>:<password>@localhost:5432/<database>`
|
`... --db-url postgresql+psycopg2://<username>:<password>@localhost:5432/<database>`
|
||||||
|
@@ -114,6 +114,36 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
|
See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks.
|
||||||
|
|
||||||
|
## Buy Tag
|
||||||
|
|
||||||
|
When your strategy has multiple buy signals, you can name the signal that triggered.
|
||||||
|
Then you can access you buy signal on `custom_sell`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['rsi'] < 35) &
|
||||||
|
(dataframe['volume'] > 0)
|
||||||
|
),
|
||||||
|
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
|
current_profit: float, **kwargs):
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
last_candle = dataframe.iloc[-1].squeeze()
|
||||||
|
if trade.buy_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
|
||||||
|
return 'sell_signal_rsi'
|
||||||
|
return None
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
`buy_tag` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
|
|
||||||
## Custom stoploss
|
## Custom stoploss
|
||||||
|
|
||||||
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
|
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
|
||||||
@@ -258,6 +288,12 @@ Stoploss values returned from `custom_stoploss()` always specify a percentage re
|
|||||||
|
|
||||||
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
|
### Calculating stoploss percentage from absolute price
|
||||||
|
|
||||||
|
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||||
|
|
||||||
|
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
#### Stepped stoploss
|
#### Stepped stoploss
|
||||||
|
|
||||||
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
||||||
@@ -327,6 +363,55 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Custom order price rules
|
||||||
|
|
||||||
|
By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy.
|
||||||
|
|
||||||
|
You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration.
|
||||||
|
|
||||||
|
### Custom order entry and exit price example
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def custom_entry_price(self, pair: str, current_time: datetime,
|
||||||
|
proposed_rate, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||||
|
|
||||||
|
return new_entryprice
|
||||||
|
|
||||||
|
def custom_exit_price(self, pair: str, trade: Trade,
|
||||||
|
current_time: datetime, proposed_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
|
timeframe=self.timeframe)
|
||||||
|
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
||||||
|
|
||||||
|
return new_exitprice
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
||||||
|
|
||||||
|
!!! Example
|
||||||
|
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98.
|
||||||
|
|
||||||
|
!!! Warning "No backtesting support"
|
||||||
|
Custom entry-prices are currently not supported during backtesting.
|
||||||
|
|
||||||
## Custom order timeout rules
|
## Custom order timeout rules
|
||||||
|
|
||||||
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
||||||
@@ -616,3 +701,33 @@ The variable 'content', will contain the strategy file in a BASE64 encoded form.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Please ensure that 'NameOfStrategy' is identical to the strategy name!
|
Please ensure that 'NameOfStrategy' is identical to the strategy name!
|
||||||
|
|
||||||
|
## Performance warning
|
||||||
|
|
||||||
|
When executing a strategy, one can sometimes be greeted by the following in the logs
|
||||||
|
|
||||||
|
> PerformanceWarning: DataFrame is highly fragmented.
|
||||||
|
|
||||||
|
This is a warning from [`pandas`](https://github.com/pandas-dev/pandas) and as the warning continues to say:
|
||||||
|
use `pd.concat(axis=1)`.
|
||||||
|
This can have slight performance implications, which are usually only visible during hyperopt (when optimizing an indicator).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
for val in self.buy_ema_short.range:
|
||||||
|
dataframe[f'ema_short_{val}'] = ta.EMA(dataframe, timeperiod=val)
|
||||||
|
```
|
||||||
|
|
||||||
|
should be rewritten to
|
||||||
|
|
||||||
|
```python
|
||||||
|
frames = [dataframe]
|
||||||
|
for val in self.buy_ema_short.range:
|
||||||
|
frames.append({
|
||||||
|
f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Append columns to existing dataframe
|
||||||
|
merged_frame = pd.concat(frames, axis=1)
|
||||||
|
```
|
||||||
|
@@ -639,6 +639,167 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
|||||||
|
|
||||||
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
|
||||||
|
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
|
||||||
|
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in
|
||||||
|
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
|
||||||
|
`current_profit < open_relative_stop`.
|
||||||
|
|
||||||
|
### *stoploss_from_absolute()*
|
||||||
|
|
||||||
|
In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price.
|
||||||
|
|
||||||
|
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
||||||
|
|
||||||
|
If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, stoploss_from_open
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def populate_indicators_1h(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,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
candle = dataframe.iloc[-1].squeeze()
|
||||||
|
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### *@informative()*
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def informative(timeframe: str, asset: str = '',
|
||||||
|
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
||||||
|
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||||
|
"""
|
||||||
|
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||||
|
define informative indicators.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
|
current pair.
|
||||||
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
|
specified, defaults to:
|
||||||
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
|
* {column}_{timeframe} if asset is not specified.
|
||||||
|
Format string supports these format variables:
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
|
* {BASE} - same as {base}, except in upper case.
|
||||||
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
* {column} - name of dataframe column.
|
||||||
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
||||||
|
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
||||||
|
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
??? Example "Fast and easy way to define informative pairs"
|
||||||
|
|
||||||
|
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, informative
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# This method is not required.
|
||||||
|
# def informative_pairs(self): ...
|
||||||
|
|
||||||
|
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
||||||
|
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
||||||
|
@informative('30m')
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
||||||
|
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
||||||
|
# instead of hardcoding actual stake currency. Available in populate_indicators and other
|
||||||
|
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
||||||
|
@informative('1h', 'BTC/{stake}')
|
||||||
|
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
||||||
|
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
||||||
|
@informative('1h', 'ETH/BTC')
|
||||||
|
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
||||||
|
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
||||||
|
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
||||||
|
@informative('1h', 'BTC/{stake}', '{column}')
|
||||||
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# Strategy timeframe indicators for current pair.
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
# Informative pairs are available in this method.
|
||||||
|
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
||||||
|
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
stake = self.config['stake_currency']
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
||||||
|
&
|
||||||
|
(dataframe['volume'] > 0)
|
||||||
|
),
|
||||||
|
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
||||||
|
|
||||||
|
!!! Warning "Duplicate method names"
|
||||||
|
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
||||||
|
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
||||||
|
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||||
|
|
||||||
## Additional data (Wallets)
|
## Additional data (Wallets)
|
||||||
|
|
||||||
@@ -781,6 +942,8 @@ Printing more than a few rows is also possible (simply use `print(dataframe)` i
|
|||||||
|
|
||||||
## Common mistakes when developing strategies
|
## Common mistakes when developing strategies
|
||||||
|
|
||||||
|
### Peeking into the future while backtesting
|
||||||
|
|
||||||
Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.
|
Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.
|
||||||
This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions.
|
This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions.
|
||||||
|
|
||||||
|
@@ -228,7 +228,7 @@ graph = generate_candlestick_graph(pair=pair,
|
|||||||
# Show graph inline
|
# Show graph inline
|
||||||
# graph.show()
|
# graph.show()
|
||||||
|
|
||||||
# Render graph in a seperate window
|
# Render graph in a separate window
|
||||||
graph.show(renderer="browser")
|
graph.show(renderer="browser")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@@ -93,7 +93,9 @@ Example configuration showing the different settings:
|
|||||||
"buy_cancel": "silent",
|
"buy_cancel": "silent",
|
||||||
"sell_cancel": "on",
|
"sell_cancel": "on",
|
||||||
"buy_fill": "off",
|
"buy_fill": "off",
|
||||||
"sell_fill": "off"
|
"sell_fill": "off",
|
||||||
|
"protection_trigger": "off",
|
||||||
|
"protection_trigger_global": "on"
|
||||||
},
|
},
|
||||||
"reload": true,
|
"reload": true,
|
||||||
"balance_dust_level": 0.01
|
"balance_dust_level": 0.01
|
||||||
@@ -103,6 +105,7 @@ Example configuration showing the different settings:
|
|||||||
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange.
|
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange.
|
||||||
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange.
|
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange.
|
||||||
`*_fill` notifications are off by default and must be explicitly enabled.
|
`*_fill` notifications are off by default and must be explicitly enabled.
|
||||||
|
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
||||||
|
|
||||||
|
|
||||||
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||||
|
@@ -26,9 +26,7 @@ optional arguments:
|
|||||||
├── data
|
├── data
|
||||||
├── hyperopt_results
|
├── hyperopt_results
|
||||||
├── hyperopts
|
├── hyperopts
|
||||||
│ ├── sample_hyperopt_advanced.py
|
|
||||||
│ ├── sample_hyperopt_loss.py
|
│ ├── sample_hyperopt_loss.py
|
||||||
│ └── sample_hyperopt.py
|
|
||||||
├── notebooks
|
├── notebooks
|
||||||
│ └── strategy_analysis_example.ipynb
|
│ └── strategy_analysis_example.ipynb
|
||||||
├── plot
|
├── plot
|
||||||
@@ -111,46 +109,11 @@ Using the advanced template (populates all optional functions and methods)
|
|||||||
freqtrade new-strategy --strategy AwesomeStrategy --template advanced
|
freqtrade new-strategy --strategy AwesomeStrategy --template advanced
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create new hyperopt
|
## List Strategies
|
||||||
|
|
||||||
Creates a new hyperopt from a template similar to SampleHyperopt.
|
Use the `list-strategies` subcommand to see all strategies in one particular directory.
|
||||||
The file will be named inline with your class name, and will not overwrite existing files.
|
|
||||||
|
|
||||||
Results will be located in `user_data/hyperopts/<classname>.py`.
|
This subcommand is useful for finding problems in your environment with loading strategies: modules with strategies that contain errors and failed to load are printed in red (LOAD FAILED), while strategies with duplicate names are printed in yellow (DUPLICATE NAME).
|
||||||
|
|
||||||
``` output
|
|
||||||
usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME]
|
|
||||||
[--template {full,minimal,advanced}]
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
--userdir PATH, --user-data-dir PATH
|
|
||||||
Path to userdata directory.
|
|
||||||
--hyperopt NAME Specify hyperopt class name which will be used by the
|
|
||||||
bot.
|
|
||||||
--template {full,minimal,advanced}
|
|
||||||
Use a template which is either `minimal`, `full`
|
|
||||||
(containing multiple sample indicators) or `advanced`.
|
|
||||||
Default: `full`.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sample usage of new-hyperopt
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade new-hyperopt --hyperopt AwesomeHyperopt
|
|
||||||
```
|
|
||||||
|
|
||||||
With custom user directory
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt
|
|
||||||
```
|
|
||||||
|
|
||||||
## List Strategies and List Hyperopts
|
|
||||||
|
|
||||||
Use the `list-strategies` subcommand to see all strategies in one particular directory and the `list-hyperopts` subcommand to list custom Hyperopts.
|
|
||||||
|
|
||||||
These subcommands are useful for finding problems in your environment with loading strategies or hyperopt classes: modules with strategies or hyperopt classes that contain errors and failed to load are printed in red (LOAD FAILED), while strategies or hyperopt classes with duplicate names are printed in yellow (DUPLICATE NAME).
|
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
@@ -164,34 +127,6 @@ optional arguments:
|
|||||||
--no-color Disable colorization of hyperopt results. May be
|
--no-color Disable colorization of hyperopt results. May be
|
||||||
useful if you are redirecting output to a file.
|
useful if you are redirecting output to a file.
|
||||||
|
|
||||||
Common arguments:
|
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
|
||||||
--logfile FILE Log to the file specified. Special values are:
|
|
||||||
'syslog', 'journald'. See the documentation for more
|
|
||||||
details.
|
|
||||||
-V, --version show program's version number and exit
|
|
||||||
-c PATH, --config PATH
|
|
||||||
Specify configuration file (default: `config.json`).
|
|
||||||
Multiple --config options may be used. Can be set to
|
|
||||||
`-` to read config from stdin.
|
|
||||||
-d PATH, --datadir PATH
|
|
||||||
Path to directory with historical backtesting data.
|
|
||||||
--userdir PATH, --user-data-dir PATH
|
|
||||||
Path to userdata directory.
|
|
||||||
```
|
|
||||||
```
|
|
||||||
usage: freqtrade list-hyperopts [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|
||||||
[-d PATH] [--userdir PATH]
|
|
||||||
[--hyperopt-path PATH] [-1] [--no-color]
|
|
||||||
|
|
||||||
optional arguments:
|
|
||||||
-h, --help show this help message and exit
|
|
||||||
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
|
|
||||||
Hyperopt Loss functions.
|
|
||||||
-1, --one-column Print output in one column.
|
|
||||||
--no-color Disable colorization of hyperopt results. May be
|
|
||||||
useful if you are redirecting output to a file.
|
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
--logfile FILE Log to the file specified. Special values are:
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
@@ -211,18 +146,16 @@ Common arguments:
|
|||||||
!!! Warning
|
!!! Warning
|
||||||
Using these commands will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed.
|
Using these commands will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed.
|
||||||
|
|
||||||
Example: Search default strategies and hyperopts directories (within the default userdir).
|
Example: Search default strategies directories (within the default userdir).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade list-strategies
|
freqtrade list-strategies
|
||||||
freqtrade list-hyperopts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: Search strategies and hyperopts directory within the userdir.
|
Example: Search strategies directory within the userdir.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade list-strategies --userdir ~/.freqtrade/
|
freqtrade list-strategies --userdir ~/.freqtrade/
|
||||||
freqtrade list-hyperopts --userdir ~/.freqtrade/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: Search dedicated strategy path.
|
Example: Search dedicated strategy path.
|
||||||
@@ -231,12 +164,6 @@ Example: Search dedicated strategy path.
|
|||||||
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: Search dedicated hyperopt path.
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
freqtrade list-hyperopt --hyperopt-path ~/.freqtrade/hyperopts/
|
|
||||||
```
|
|
||||||
|
|
||||||
## List Exchanges
|
## List Exchanges
|
||||||
|
|
||||||
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
||||||
@@ -627,7 +554,7 @@ FreqUI will also show the backtesting results.
|
|||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade webserver [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
usage: freqtrade webserver [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||||
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
[--userdir PATH]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@@ -648,12 +575,6 @@ Common arguments:
|
|||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
|
|
||||||
Strategy arguments:
|
|
||||||
-s NAME, --strategy NAME
|
|
||||||
Specify strategy class name which will be used by the
|
|
||||||
bot.
|
|
||||||
--strategy-path PATH Specify additional strategy lookup path.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## List Hyperopt results
|
## List Hyperopt results
|
||||||
|
@@ -83,6 +83,7 @@ Possible parameters are:
|
|||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
|
* `buy_tag`
|
||||||
|
|
||||||
### Webhookbuycancel
|
### Webhookbuycancel
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ Possible parameters are:
|
|||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
|
* `buy_tag`
|
||||||
|
|
||||||
### Webhookbuyfill
|
### Webhookbuyfill
|
||||||
|
|
||||||
@@ -115,6 +117,7 @@ Possible parameters are:
|
|||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
|
* `buy_tag`
|
||||||
|
|
||||||
### Webhooksell
|
### Webhooksell
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2021.7'
|
__version__ = '2021.9'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if __version__ == 'develop':
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ if __version__ == 'develop':
|
|||||||
# subprocess.check_output(
|
# subprocess.check_output(
|
||||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
# ['git', 'log', '--format="%h"', '-n 1'],
|
||||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||||
except Exception:
|
except Exception: # pragma: no cover
|
||||||
# git not available, ignore
|
# git not available, ignore
|
||||||
try:
|
try:
|
||||||
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
|
||||||
|
@@ -11,11 +11,11 @@ from freqtrade.commands.build_config_commands import start_new_config
|
|||||||
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
||||||
start_list_data)
|
start_list_data)
|
||||||
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
|
||||||
start_new_hyperopt, start_new_strategy)
|
start_new_strategy)
|
||||||
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
||||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts,
|
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
||||||
start_list_markets, start_list_strategies,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_list_timeframes, start_show_trades)
|
start_show_trades)
|
||||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
||||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||||
|
@@ -22,7 +22,7 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
|||||||
"max_open_trades", "stake_amount", "fee", "pairs"]
|
"max_open_trades", "stake_amount", "fee", "pairs"]
|
||||||
|
|
||||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||||
"enable_protections", "dry_run_wallet",
|
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
||||||
"strategy_list", "export", "exportfilename"]
|
"strategy_list", "export", "exportfilename"]
|
||||||
|
|
||||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||||
@@ -55,8 +55,6 @@ ARGS_BUILD_CONFIG = ["config"]
|
|||||||
|
|
||||||
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
|
||||||
|
|
||||||
ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
|
|
||||||
|
|
||||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
||||||
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
|
||||||
|
|
||||||
@@ -92,10 +90,10 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
|||||||
|
|
||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||||
"list-hyperopts", "hyperopt-list", "hyperopt-show",
|
"hyperopt-list", "hyperopt-show",
|
||||||
"plot-dataframe", "plot-profit", "show-trades"]
|
"plot-dataframe", "plot-profit", "show-trades"]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||||
|
|
||||||
|
|
||||||
class Arguments:
|
class Arguments:
|
||||||
@@ -174,12 +172,11 @@ class Arguments:
|
|||||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
||||||
start_download_data, start_edge, start_hyperopt,
|
start_download_data, start_edge, start_hyperopt,
|
||||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
||||||
start_list_data, start_list_exchanges, start_list_hyperopts,
|
start_list_data, start_list_exchanges, start_list_markets,
|
||||||
start_list_markets, start_list_strategies,
|
start_list_strategies, start_list_timeframes,
|
||||||
start_list_timeframes, start_new_config, start_new_hyperopt,
|
start_new_config, start_new_strategy, start_plot_dataframe,
|
||||||
start_new_strategy, start_plot_dataframe, start_plot_profit,
|
start_plot_profit, start_show_trades, start_test_pairlist,
|
||||||
start_show_trades, start_test_pairlist, start_trading,
|
start_trading, start_webserver)
|
||||||
start_webserver)
|
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='command',
|
subparsers = self.parser.add_subparsers(dest='command',
|
||||||
# Use custom message when no subhandler is added
|
# Use custom message when no subhandler is added
|
||||||
@@ -206,12 +203,6 @@ class Arguments:
|
|||||||
build_config_cmd.set_defaults(func=start_new_config)
|
build_config_cmd.set_defaults(func=start_new_config)
|
||||||
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
|
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
|
||||||
|
|
||||||
# add new-hyperopt subcommand
|
|
||||||
build_hyperopt_cmd = subparsers.add_parser('new-hyperopt',
|
|
||||||
help="Create new hyperopt")
|
|
||||||
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
|
|
||||||
self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd)
|
|
||||||
|
|
||||||
# add new-strategy subcommand
|
# add new-strategy subcommand
|
||||||
build_strategy_cmd = subparsers.add_parser('new-strategy',
|
build_strategy_cmd = subparsers.add_parser('new-strategy',
|
||||||
help="Create new strategy")
|
help="Create new strategy")
|
||||||
@@ -300,15 +291,6 @@ class Arguments:
|
|||||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||||
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
||||||
|
|
||||||
# Add list-hyperopts subcommand
|
|
||||||
list_hyperopts_cmd = subparsers.add_parser(
|
|
||||||
'list-hyperopts',
|
|
||||||
help='Print available hyperopt classes.',
|
|
||||||
parents=[_common_parser],
|
|
||||||
)
|
|
||||||
list_hyperopts_cmd.set_defaults(func=start_list_hyperopts)
|
|
||||||
self._build_args(optionlist=ARGS_LIST_HYPEROPTS, parser=list_hyperopts_cmd)
|
|
||||||
|
|
||||||
# Add list-markets subcommand
|
# Add list-markets subcommand
|
||||||
list_markets_cmd = subparsers.add_parser(
|
list_markets_cmd = subparsers.add_parser(
|
||||||
'list-markets',
|
'list-markets',
|
||||||
|
@@ -61,21 +61,27 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "stake_currency",
|
"name": "stake_currency",
|
||||||
"message": "Please insert your stake currency:",
|
"message": "Please insert your stake currency:",
|
||||||
"default": 'BTC',
|
"default": 'USDT',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "stake_amount",
|
"name": "stake_amount",
|
||||||
"message": "Please insert your stake amount:",
|
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||||
"default": "0.01",
|
"default": "100",
|
||||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||||
|
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||||
|
if val == UNLIMITED_STAKE_AMOUNT
|
||||||
|
else val
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"name": "max_open_trades",
|
"name": "max_open_trades",
|
||||||
"message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):",
|
"message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||||
"default": "3",
|
"default": "3",
|
||||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val)
|
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val),
|
||||||
|
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||||
|
if val == UNLIMITED_STAKE_AMOUNT
|
||||||
|
else val
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -99,6 +105,8 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"bittrex",
|
"bittrex",
|
||||||
"kraken",
|
"kraken",
|
||||||
"ftx",
|
"ftx",
|
||||||
|
"kucoin",
|
||||||
|
"gateio",
|
||||||
Separator(),
|
Separator(),
|
||||||
"other",
|
"other",
|
||||||
],
|
],
|
||||||
@@ -122,6 +130,12 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"message": "Insert Exchange Secret",
|
"message": "Insert Exchange Secret",
|
||||||
"when": lambda x: not x['dry_run']
|
"when": lambda x: not x['dry_run']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"name": "exchange_key_password",
|
||||||
|
"message": "Insert Exchange API Key password",
|
||||||
|
"when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "confirm",
|
"type": "confirm",
|
||||||
"name": "telegram",
|
"name": "telegram",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Definition of cli arguments used in arguments.py
|
Definition of cli arguments used in arguments.py
|
||||||
"""
|
"""
|
||||||
from argparse import ArgumentTypeError
|
from argparse import SUPPRESS, ArgumentTypeError
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
||||||
@@ -135,6 +135,10 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help='Override the value of the `stake_amount` configuration setting.',
|
help='Override the value of the `stake_amount` configuration setting.',
|
||||||
),
|
),
|
||||||
# Backtesting
|
# Backtesting
|
||||||
|
"timeframe_detail": Arg(
|
||||||
|
'--timeframe-detail',
|
||||||
|
help='Specify detail timeframe for backtesting (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||||
|
),
|
||||||
"position_stacking": Arg(
|
"position_stacking": Arg(
|
||||||
'--eps', '--enable-position-stacking',
|
'--eps', '--enable-position-stacking',
|
||||||
help='Allow buying the same pair multiple times (position stacking).',
|
help='Allow buying the same pair multiple times (position stacking).',
|
||||||
@@ -162,7 +166,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
'Please note that ticker-interval needs to be set either in config '
|
'Please note that ticker-interval needs to be set either in config '
|
||||||
'or via command line. When using this together with `--export trades`, '
|
'or via command line. When using this together with `--export trades`, '
|
||||||
'the strategy-name is injected into the filename '
|
'the strategy-name is injected into the filename '
|
||||||
'(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`',
|
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
||||||
nargs='+',
|
nargs='+',
|
||||||
),
|
),
|
||||||
"export": Arg(
|
"export": Arg(
|
||||||
@@ -199,13 +203,13 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
# Hyperopt
|
# Hyperopt
|
||||||
"hyperopt": Arg(
|
"hyperopt": Arg(
|
||||||
'--hyperopt',
|
'--hyperopt',
|
||||||
help='Specify hyperopt class name which will be used by the bot.',
|
help=SUPPRESS,
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
required=False,
|
required=False,
|
||||||
),
|
),
|
||||||
"hyperopt_path": Arg(
|
"hyperopt_path": Arg(
|
||||||
'--hyperopt-path',
|
'--hyperopt-path',
|
||||||
help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.',
|
help='Specify additional lookup path for Hyperopt Loss functions.',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
),
|
),
|
||||||
"epochs": Arg(
|
"epochs": Arg(
|
||||||
@@ -218,7 +222,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"spaces": Arg(
|
"spaces": Arg(
|
||||||
'--spaces',
|
'--spaces',
|
||||||
help='Specify which parameters to hyperopt. Space-separated list.',
|
help='Specify which parameters to hyperopt. Space-separated list.',
|
||||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'default'],
|
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
|
||||||
nargs='+',
|
nargs='+',
|
||||||
default='default',
|
default='default',
|
||||||
),
|
),
|
||||||
|
@@ -7,7 +7,7 @@ import requests
|
|||||||
|
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
||||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
from freqtrade.constants import USERPATH_STRATEGIES
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import render_template, render_template_with_fallback
|
from freqtrade.misc import render_template, render_template_with_fallback
|
||||||
@@ -74,8 +74,6 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
if "strategy" in args and args["strategy"]:
|
if "strategy" in args and args["strategy"]:
|
||||||
if args["strategy"] == "DefaultStrategy":
|
|
||||||
raise OperationalException("DefaultStrategy is not allowed as name.")
|
|
||||||
|
|
||||||
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py')
|
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py')
|
||||||
|
|
||||||
@@ -89,58 +87,6 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
|||||||
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
||||||
|
|
||||||
|
|
||||||
def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: str) -> None:
|
|
||||||
"""
|
|
||||||
Deploys a new hyperopt template to hyperopt_path
|
|
||||||
"""
|
|
||||||
fallback = 'full'
|
|
||||||
buy_guards = render_template_with_fallback(
|
|
||||||
templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",
|
|
||||||
templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2",
|
|
||||||
)
|
|
||||||
sell_guards = render_template_with_fallback(
|
|
||||||
templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",
|
|
||||||
templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2",
|
|
||||||
)
|
|
||||||
buy_space = render_template_with_fallback(
|
|
||||||
templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",
|
|
||||||
templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2",
|
|
||||||
)
|
|
||||||
sell_space = render_template_with_fallback(
|
|
||||||
templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",
|
|
||||||
templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2",
|
|
||||||
)
|
|
||||||
|
|
||||||
strategy_text = render_template(templatefile='base_hyperopt.py.j2',
|
|
||||||
arguments={"hyperopt": hyperopt_name,
|
|
||||||
"buy_guards": buy_guards,
|
|
||||||
"sell_guards": sell_guards,
|
|
||||||
"buy_space": buy_space,
|
|
||||||
"sell_space": sell_space,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(f"Writing hyperopt to `{hyperopt_path}`.")
|
|
||||||
hyperopt_path.write_text(strategy_text)
|
|
||||||
|
|
||||||
|
|
||||||
def start_new_hyperopt(args: Dict[str, Any]) -> None:
|
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
|
||||||
|
|
||||||
if 'hyperopt' in args and args['hyperopt']:
|
|
||||||
if args['hyperopt'] == 'DefaultHyperopt':
|
|
||||||
raise OperationalException("DefaultHyperopt is not allowed as name.")
|
|
||||||
|
|
||||||
new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args['hyperopt'] + '.py')
|
|
||||||
|
|
||||||
if new_path.exists():
|
|
||||||
raise OperationalException(f"`{new_path}` already exists. "
|
|
||||||
"Please choose another Hyperopt Name.")
|
|
||||||
deploy_new_hyperopt(args['hyperopt'], new_path, args['template'])
|
|
||||||
else:
|
|
||||||
raise OperationalException("`new-hyperopt` requires --hyperopt to be set.")
|
|
||||||
|
|
||||||
|
|
||||||
def clean_ui_subdir(directory: Path):
|
def clean_ui_subdir(directory: Path):
|
||||||
if directory.is_dir():
|
if directory.is_dir():
|
||||||
logger.info("Removing UI directory content.")
|
logger.info("Removing UI directory content.")
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict
|
||||||
|
|
||||||
from colorama import init as colorama_init
|
from colorama import init as colorama_init
|
||||||
|
|
||||||
@@ -28,30 +28,12 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
no_details = config.get('hyperopt_list_no_details', False)
|
no_details = config.get('hyperopt_list_no_details', False)
|
||||||
no_header = False
|
no_header = False
|
||||||
|
|
||||||
filteroptions = {
|
|
||||||
'only_best': config.get('hyperopt_list_best', False),
|
|
||||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
|
||||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
|
||||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
|
||||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
|
||||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
|
||||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
|
||||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
|
||||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
|
||||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
|
||||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
|
||||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
|
||||||
}
|
|
||||||
|
|
||||||
results_file = get_latest_hyperopt_file(
|
results_file = get_latest_hyperopt_file(
|
||||||
config['user_data_dir'] / 'hyperopt_results',
|
config['user_data_dir'] / 'hyperopt_results',
|
||||||
config.get('hyperoptexportfilename'))
|
config.get('hyperoptexportfilename'))
|
||||||
|
|
||||||
# Previous evaluations
|
# Previous evaluations
|
||||||
epochs = HyperoptTools.load_previous_results(results_file)
|
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
||||||
total_epochs = len(epochs)
|
|
||||||
|
|
||||||
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
|
||||||
|
|
||||||
if print_colorized:
|
if print_colorized:
|
||||||
colorama_init(autoreset=True)
|
colorama_init(autoreset=True)
|
||||||
@@ -59,7 +41,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
if not export_csv:
|
if not export_csv:
|
||||||
try:
|
try:
|
||||||
print(HyperoptTools.get_result_table(config, epochs, total_epochs,
|
print(HyperoptTools.get_result_table(config, epochs, total_epochs,
|
||||||
not filteroptions['only_best'],
|
not config.get('hyperopt_list_best', False),
|
||||||
print_colorized, 0))
|
print_colorized, 0))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print('User interrupted..')
|
print('User interrupted..')
|
||||||
@@ -71,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
if epochs and export_csv:
|
if epochs and export_csv:
|
||||||
HyperoptTools.export_csv_file(
|
HyperoptTools.export_csv_file(
|
||||||
config, epochs, total_epochs, not filteroptions['only_best'], export_csv
|
config, epochs, export_csv
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -91,26 +73,9 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
n = config.get('hyperopt_show_index', -1)
|
n = config.get('hyperopt_show_index', -1)
|
||||||
|
|
||||||
filteroptions = {
|
|
||||||
'only_best': config.get('hyperopt_list_best', False),
|
|
||||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
|
||||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
|
||||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
|
||||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
|
||||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
|
||||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
|
||||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
|
||||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
|
||||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
|
||||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
|
||||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Previous evaluations
|
# Previous evaluations
|
||||||
epochs = HyperoptTools.load_previous_results(results_file)
|
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
||||||
total_epochs = len(epochs)
|
|
||||||
|
|
||||||
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
|
||||||
filtered_epochs = len(epochs)
|
filtered_epochs = len(epochs)
|
||||||
|
|
||||||
if n > filtered_epochs:
|
if n > filtered_epochs:
|
||||||
@@ -137,138 +102,3 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
|
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
|
||||||
header_str="Epoch details")
|
header_str="Epoch details")
|
||||||
|
|
||||||
|
|
||||||
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
|
||||||
"""
|
|
||||||
Filter our items from the list of hyperopt results
|
|
||||||
TODO: after 2021.5 remove all "legacy" mode queries.
|
|
||||||
"""
|
|
||||||
if filteroptions['only_best']:
|
|
||||||
epochs = [x for x in epochs if x['is_best']]
|
|
||||||
if filteroptions['only_profitable']:
|
|
||||||
epochs = [x for x in epochs if x['results_metrics'].get(
|
|
||||||
'profit', x['results_metrics'].get('profit_total', 0)) > 0]
|
|
||||||
|
|
||||||
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
|
|
||||||
|
|
||||||
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
|
|
||||||
|
|
||||||
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
|
|
||||||
|
|
||||||
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
|
|
||||||
|
|
||||||
logger.info(f"{len(epochs)} " +
|
|
||||||
("best " if filteroptions['only_best'] else "") +
|
|
||||||
("profitable " if filteroptions['only_profitable'] else "") +
|
|
||||||
"epochs found.")
|
|
||||||
return epochs
|
|
||||||
|
|
||||||
|
|
||||||
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
|
|
||||||
"""
|
|
||||||
Filter epochs with trade-counts > trades
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
x for x in epochs
|
|
||||||
if x['results_metrics'].get(
|
|
||||||
'trade_count', x['results_metrics'].get('total_trades', 0)
|
|
||||||
) > trade_count
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
|
|
||||||
|
|
||||||
if filteroptions['filter_min_trades'] > 0:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
|
|
||||||
|
|
||||||
if filteroptions['filter_max_trades'] > 0:
|
|
||||||
epochs = [
|
|
||||||
x for x in epochs
|
|
||||||
if x['results_metrics'].get(
|
|
||||||
'trade_count', x['results_metrics'].get('total_trades')
|
|
||||||
) < filteroptions['filter_max_trades']
|
|
||||||
]
|
|
||||||
return epochs
|
|
||||||
|
|
||||||
|
|
||||||
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
|
|
||||||
|
|
||||||
def get_duration_value(x):
|
|
||||||
# Duration in minutes ...
|
|
||||||
if 'duration' in x['results_metrics']:
|
|
||||||
return x['results_metrics']['duration']
|
|
||||||
else:
|
|
||||||
# New mode
|
|
||||||
if 'holding_avg_s' in x['results_metrics']:
|
|
||||||
avg = x['results_metrics']['holding_avg_s']
|
|
||||||
return avg // 60
|
|
||||||
raise OperationalException(
|
|
||||||
"Holding-average not available. Please omit the filter on average time, "
|
|
||||||
"or rerun hyperopt with this version")
|
|
||||||
|
|
||||||
if filteroptions['filter_min_avg_time'] is not None:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
|
||||||
epochs = [
|
|
||||||
x for x in epochs
|
|
||||||
if get_duration_value(x) > filteroptions['filter_min_avg_time']
|
|
||||||
]
|
|
||||||
if filteroptions['filter_max_avg_time'] is not None:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
|
||||||
epochs = [
|
|
||||||
x for x in epochs
|
|
||||||
if get_duration_value(x) < filteroptions['filter_max_avg_time']
|
|
||||||
]
|
|
||||||
|
|
||||||
return epochs
|
|
||||||
|
|
||||||
|
|
||||||
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
|
||||||
|
|
||||||
if filteroptions['filter_min_avg_profit'] is not None:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
|
||||||
epochs = [
|
|
||||||
x for x in epochs
|
|
||||||
if x['results_metrics'].get(
|
|
||||||
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
|
|
||||||
) > filteroptions['filter_min_avg_profit']
|
|
||||||
]
|
|
||||||
if filteroptions['filter_max_avg_profit'] is not None:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
|
||||||
epochs = [
|
|
||||||
x for x in epochs
|
|
||||||
if x['results_metrics'].get(
|
|
||||||
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
|
|
||||||
) < filteroptions['filter_max_avg_profit']
|
|
||||||
]
|
|
||||||
if filteroptions['filter_min_total_profit'] is not None:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
|
||||||
epochs = [
|
|
||||||
x for x in epochs
|
|
||||||
if x['results_metrics'].get(
|
|
||||||
'profit', x['results_metrics'].get('profit_total_abs', 0)
|
|
||||||
) > filteroptions['filter_min_total_profit']
|
|
||||||
]
|
|
||||||
if filteroptions['filter_max_total_profit'] is not None:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
|
||||||
epochs = [
|
|
||||||
x for x in epochs
|
|
||||||
if x['results_metrics'].get(
|
|
||||||
'profit', x['results_metrics'].get('profit_total_abs', 0)
|
|
||||||
) < filteroptions['filter_max_total_profit']
|
|
||||||
]
|
|
||||||
return epochs
|
|
||||||
|
|
||||||
|
|
||||||
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
|
|
||||||
|
|
||||||
if filteroptions['filter_min_objective'] is not None:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
|
||||||
|
|
||||||
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
|
|
||||||
if filteroptions['filter_max_objective'] is not None:
|
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
|
||||||
|
|
||||||
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
|
|
||||||
|
|
||||||
return epochs
|
|
||||||
|
@@ -10,7 +10,7 @@ from colorama import init as colorama_init
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
from freqtrade.constants import USERPATH_STRATEGIES
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import market_is_active, validate_exchanges
|
from freqtrade.exchange import market_is_active, validate_exchanges
|
||||||
@@ -92,25 +92,6 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
|||||||
_print_objs_tabular(strategy_objs, config.get('print_colorized', False))
|
_print_objs_tabular(strategy_objs, config.get('print_colorized', False))
|
||||||
|
|
||||||
|
|
||||||
def start_list_hyperopts(args: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Print files with HyperOpt custom classes available in the directory
|
|
||||||
"""
|
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
|
||||||
|
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
|
||||||
|
|
||||||
directory = Path(config.get('hyperopt_path', config['user_data_dir'] / USERPATH_HYPEROPTS))
|
|
||||||
hyperopt_objs = HyperOptResolver.search_all_objects(directory, not args['print_one_column'])
|
|
||||||
# Sort alphabetically
|
|
||||||
hyperopt_objs = sorted(hyperopt_objs, key=lambda x: x['name'])
|
|
||||||
|
|
||||||
if args['print_one_column']:
|
|
||||||
print('\n'.join([s['name'] for s in hyperopt_objs]))
|
|
||||||
else:
|
|
||||||
_print_objs_tabular(hyperopt_objs, config.get('print_colorized', False))
|
|
||||||
|
|
||||||
|
|
||||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Print timeframes available on Exchange
|
Print timeframes available on Exchange
|
||||||
|
19
freqtrade/configuration/PeriodicCache.py
Normal file
19
freqtrade/configuration/PeriodicCache.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from cachetools.ttl import TTLCache
|
||||||
|
|
||||||
|
|
||||||
|
class PeriodicCache(TTLCache):
|
||||||
|
"""
|
||||||
|
Special cache that expires at "straight" times
|
||||||
|
A timer with ttl of 3600 (1h) will expire at every full hour (:00).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, maxsize, ttl, getsizeof=None):
|
||||||
|
def local_timer():
|
||||||
|
ts = datetime.now(timezone.utc).timestamp()
|
||||||
|
offset = (ts % ttl)
|
||||||
|
return ts - offset
|
||||||
|
|
||||||
|
# Init with smlight offset
|
||||||
|
super().__init__(maxsize=maxsize, ttl=ttl-1e-5, timer=local_timer, getsizeof=getsizeof)
|
@@ -1,7 +1,8 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
|
|
||||||
from freqtrade.configuration.check_exchange import check_exchange, remove_credentials
|
from freqtrade.configuration.check_exchange import check_exchange
|
||||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||||
from freqtrade.configuration.configuration import Configuration
|
from freqtrade.configuration.configuration import Configuration
|
||||||
|
from freqtrade.configuration.PeriodicCache import PeriodicCache
|
||||||
from freqtrade.configuration.timerange import TimeRange
|
from freqtrade.configuration.timerange import TimeRange
|
||||||
|
@@ -10,19 +10,6 @@ from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt,
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def remove_credentials(config: Dict[str, Any]) -> None:
|
|
||||||
"""
|
|
||||||
Removes exchange keys from the configuration and specifies dry-run
|
|
||||||
Used for backtesting / hyperopt / edge and utils.
|
|
||||||
Modifies the input dict!
|
|
||||||
"""
|
|
||||||
config['exchange']['key'] = ''
|
|
||||||
config['exchange']['secret'] = ''
|
|
||||||
config['exchange']['password'] = ''
|
|
||||||
config['exchange']['uid'] = ''
|
|
||||||
config['dry_run'] = True
|
|
||||||
|
|
||||||
|
|
||||||
def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the exchange name in the config file is supported by Freqtrade
|
Check if the exchange name in the config file is supported by Freqtrade
|
||||||
|
@@ -3,7 +3,6 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
|
|
||||||
from .check_exchange import remove_credentials
|
|
||||||
from .config_validation import validate_config_consistency
|
from .config_validation import validate_config_consistency
|
||||||
from .configuration import Configuration
|
from .configuration import Configuration
|
||||||
|
|
||||||
@@ -21,8 +20,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
|
|||||||
configuration = Configuration(args, method)
|
configuration = Configuration(args, method)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
|
|
||||||
# Ensure we do not use Exchange credentials
|
# Ensure these modes are using Dry-run
|
||||||
remove_credentials(config)
|
config['dry_run'] = True
|
||||||
validate_config_consistency(config)
|
validate_config_consistency(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
@@ -11,6 +11,7 @@ from freqtrade import constants
|
|||||||
from freqtrade.configuration.check_exchange import check_exchange
|
from freqtrade.configuration.check_exchange import check_exchange
|
||||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||||
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
|
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
|
||||||
|
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
|
||||||
from freqtrade.configuration.load_config import load_config_file, load_file
|
from freqtrade.configuration.load_config import load_config_file, load_file
|
||||||
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@@ -71,6 +72,11 @@ class Configuration:
|
|||||||
|
|
||||||
# Merge config options, overwriting old values
|
# Merge config options, overwriting old values
|
||||||
config = deep_merge_dicts(load_config_file(path), config)
|
config = deep_merge_dicts(load_config_file(path), config)
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
env_data = enironment_vars_to_dict()
|
||||||
|
config = deep_merge_dicts(env_data, config)
|
||||||
|
|
||||||
config['config_files'] = files
|
config['config_files'] = files
|
||||||
# Normalize config
|
# Normalize config
|
||||||
if 'internals' not in config:
|
if 'internals' not in config:
|
||||||
@@ -236,6 +242,9 @@ class Configuration:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='timeframe_detail',
|
||||||
|
logstring='Parameter --timeframe-detail detected, '
|
||||||
|
'using {} for intra-candle backtesting ...')
|
||||||
self._args_to_config(config, argname='stake_amount',
|
self._args_to_config(config, argname='stake_amount',
|
||||||
logstring='Parameter --stake-amount detected, '
|
logstring='Parameter --stake-amount detected, '
|
||||||
'overriding stake_amount to: {} ...')
|
'overriding stake_amount to: {} ...')
|
||||||
|
@@ -110,3 +110,6 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
|||||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
"Please remove 'ticker_interval' from your configuration to continue operating."
|
||||||
)
|
)
|
||||||
config['timeframe'] = config['ticker_interval']
|
config['timeframe'] = config['ticker_interval']
|
||||||
|
|
||||||
|
if 'protections' in config:
|
||||||
|
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||||
|
54
freqtrade/configuration/environment_vars.py
Normal file
54
freqtrade/configuration/environment_vars.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.constants import ENV_VAR_PREFIX
|
||||||
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_var_typed(val):
|
||||||
|
try:
|
||||||
|
return int(val)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
return float(val)
|
||||||
|
except ValueError:
|
||||||
|
if val.lower() in ('t', 'true'):
|
||||||
|
return True
|
||||||
|
elif val.lower() in ('f', 'false'):
|
||||||
|
return False
|
||||||
|
# keep as string
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Environment variables must be prefixed with FREQTRADE.
|
||||||
|
FREQTRADE__{section}__{key}
|
||||||
|
:param env_dict: Dictionary to validate - usually os.environ
|
||||||
|
:param prefix: Prefix to consider (usually FREQTRADE__)
|
||||||
|
:return: Nested dict based on available and relevant variables.
|
||||||
|
"""
|
||||||
|
relevant_vars: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
for env_var, val in sorted(env_dict.items()):
|
||||||
|
if env_var.startswith(prefix):
|
||||||
|
logger.info(f"Loading variable '{env_var}'")
|
||||||
|
key = env_var.replace(prefix, '')
|
||||||
|
for k in reversed(key.split('__')):
|
||||||
|
val = {k.lower(): get_var_typed(val) if type(val) != dict else val}
|
||||||
|
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||||
|
|
||||||
|
return relevant_vars
|
||||||
|
|
||||||
|
|
||||||
|
def enironment_vars_to_dict() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Read environment variables and return a nested dict for relevant variables
|
||||||
|
Relevant variables must follow the FREQTRADE__{section}__{key} pattern
|
||||||
|
:return: Nested dict based on available and relevant variables.
|
||||||
|
"""
|
||||||
|
return flat_vars_to_nested_dict(os.environ.copy(), ENV_VAR_PREFIX)
|
@@ -47,6 +47,9 @@ USERPATH_STRATEGIES = 'strategies'
|
|||||||
USERPATH_NOTEBOOKS = 'notebooks'
|
USERPATH_NOTEBOOKS = 'notebooks'
|
||||||
|
|
||||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||||
|
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||||
|
|
||||||
|
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
||||||
|
|
||||||
|
|
||||||
# Define decimals per coin for outputs
|
# Define decimals per coin for outputs
|
||||||
@@ -66,9 +69,7 @@ DUST_PER_COIN = {
|
|||||||
# Source files with destination directories within user-directory
|
# Source files with destination directories within user-directory
|
||||||
USER_DATA_FILES = {
|
USER_DATA_FILES = {
|
||||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||||
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
|
|
||||||
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
||||||
'sample_hyperopt.py': USERPATH_HYPEROPTS,
|
|
||||||
'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS,
|
'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'tradable_balance_ratio': {
|
'tradable_balance_ratio': {
|
||||||
'type': 'number',
|
'type': 'number',
|
||||||
'minimum': 0.1,
|
'minimum': 0.0,
|
||||||
'maximum': 1,
|
'maximum': 1,
|
||||||
'default': 0.99
|
'default': 0.99
|
||||||
},
|
},
|
||||||
@@ -190,6 +191,9 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'required': ['price_side']
|
'required': ['price_side']
|
||||||
},
|
},
|
||||||
|
'custom_price_max_distance_ratio': {
|
||||||
|
'type': 'number', 'minimum': 0.0
|
||||||
|
},
|
||||||
'order_types': {
|
'order_types': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
@@ -280,6 +284,15 @@ CONF_SCHEMA = {
|
|||||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
'default': 'off'
|
'default': 'off'
|
||||||
},
|
},
|
||||||
|
'protection_trigger': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
|
'default': 'off'
|
||||||
|
},
|
||||||
|
'protection_trigger_global': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'reload': {'type': 'boolean'},
|
'reload': {'type': 'boolean'},
|
||||||
|
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|||||||
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
|
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
|
||||||
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
|
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||||
|
|
||||||
# Mid-term format, crated by BacktestResult Named Tuple
|
# Mid-term format, created by BacktestResult Named Tuple
|
||||||
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
|
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
|
||||||
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
|
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
|
||||||
'fee_close', 'amount', 'profit_abs', 'profit_ratio']
|
'fee_close', 'amount', 'profit_abs', 'profit_ratio']
|
||||||
@@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
|||||||
'fee_open', 'fee_close', 'trade_duration',
|
'fee_open', 'fee_close', 'trade_duration',
|
||||||
'profit_ratio', 'profit_abs', 'sell_reason',
|
'profit_ratio', 'profit_abs', 'sell_reason',
|
||||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', ]
|
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag']
|
||||||
|
|
||||||
|
|
||||||
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
|
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
|
||||||
|
@@ -242,7 +242,7 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to:
|
|||||||
:param config: Config dictionary
|
:param config: Config dictionary
|
||||||
:param convert_from: Source format
|
:param convert_from: Source format
|
||||||
:param convert_to: Target format
|
:param convert_to: Target format
|
||||||
:param erase: Erase souce data (does not apply if source and target format are identical)
|
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||||
"""
|
"""
|
||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
@@ -267,7 +267,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
|
|||||||
:param config: Config dictionary
|
:param config: Config dictionary
|
||||||
:param convert_from: Source format
|
:param convert_from: Source format
|
||||||
:param convert_to: Target format
|
:param convert_to: Target format
|
||||||
:param erase: Erase souce data (does not apply if source and target format are identical)
|
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||||
"""
|
"""
|
||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
|
@@ -10,11 +10,12 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -31,6 +32,7 @@ class DataProvider:
|
|||||||
self._pairlists = pairlists
|
self._pairlists = pairlists
|
||||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||||
self.__slice_index: Optional[int] = None
|
self.__slice_index: Optional[int] = None
|
||||||
|
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
|
|
||||||
def _set_dataframe_max_index(self, limit_index: int):
|
def _set_dataframe_max_index(self, limit_index: int):
|
||||||
"""
|
"""
|
||||||
@@ -62,11 +64,22 @@ class DataProvider:
|
|||||||
:param pair: pair to get the data for
|
:param pair: pair to get the data for
|
||||||
:param timeframe: timeframe to get data for
|
:param timeframe: timeframe to get data for
|
||||||
"""
|
"""
|
||||||
return load_pair_history(pair=pair,
|
saved_pair = (pair, str(timeframe))
|
||||||
|
if saved_pair not in self.__cached_pairs_backtesting:
|
||||||
|
timerange = TimeRange.parse_timerange(None if self._config.get(
|
||||||
|
'timerange') is None else str(self._config.get('timerange')))
|
||||||
|
# Move informative start time respecting startup_candle_count
|
||||||
|
timerange.subtract_start(
|
||||||
|
timeframe_to_seconds(str(timeframe)) * self._config.get('startup_candle_count', 0)
|
||||||
|
)
|
||||||
|
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
||||||
|
pair=pair,
|
||||||
timeframe=timeframe or self._config['timeframe'],
|
timeframe=timeframe or self._config['timeframe'],
|
||||||
datadir=self._config['datadir'],
|
datadir=self._config['datadir'],
|
||||||
|
timerange=timerange,
|
||||||
data_format=self._config.get('dataformat_ohlcv', 'json')
|
data_format=self._config.get('dataformat_ohlcv', 'json')
|
||||||
)
|
)
|
||||||
|
return self.__cached_pairs_backtesting[saved_pair].copy()
|
||||||
|
|
||||||
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
|
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@@ -136,6 +149,8 @@ class DataProvider:
|
|||||||
Clear pair dataframe cache.
|
Clear pair dataframe cache.
|
||||||
"""
|
"""
|
||||||
self.__cached_pairs = {}
|
self.__cached_pairs = {}
|
||||||
|
self.__cached_pairs_backtesting = {}
|
||||||
|
self.__slice_index = 0
|
||||||
|
|
||||||
# Exchange functions
|
# Exchange functions
|
||||||
|
|
||||||
|
@@ -117,10 +117,11 @@ def refresh_data(datadir: Path,
|
|||||||
:param timerange: Limit data to be loaded to this timerange
|
:param timerange: Limit data to be loaded to this timerange
|
||||||
"""
|
"""
|
||||||
data_handler = get_datahandler(datadir, data_format)
|
data_handler = get_datahandler(datadir, data_format)
|
||||||
for pair in pairs:
|
for idx, pair in enumerate(pairs):
|
||||||
_download_pair_history(pair=pair, timeframe=timeframe,
|
process = f'{idx}/{len(pairs)}'
|
||||||
datadir=datadir, timerange=timerange,
|
_download_pair_history(pair=pair, process=process,
|
||||||
exchange=exchange, data_handler=data_handler)
|
timeframe=timeframe, datadir=datadir,
|
||||||
|
timerange=timerange, exchange=exchange, data_handler=data_handler)
|
||||||
|
|
||||||
|
|
||||||
def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optional[TimeRange],
|
def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optional[TimeRange],
|
||||||
@@ -153,13 +154,14 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona
|
|||||||
return data, start_ms
|
return data, start_ms
|
||||||
|
|
||||||
|
|
||||||
def _download_pair_history(datadir: Path,
|
def _download_pair_history(pair: str, *,
|
||||||
|
datadir: Path,
|
||||||
exchange: Exchange,
|
exchange: Exchange,
|
||||||
pair: str, *,
|
|
||||||
new_pairs_days: int = 30,
|
|
||||||
timeframe: str = '5m',
|
timeframe: str = '5m',
|
||||||
timerange: Optional[TimeRange] = None,
|
process: str = '',
|
||||||
data_handler: IDataHandler = None) -> bool:
|
new_pairs_days: int = 30,
|
||||||
|
data_handler: IDataHandler = None,
|
||||||
|
timerange: Optional[TimeRange] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Download latest candles from the exchange for the pair and timeframe passed in parameters
|
Download latest candles from the exchange for the pair and timeframe passed in parameters
|
||||||
The data is downloaded starting from the last correct data that
|
The data is downloaded starting from the last correct data that
|
||||||
@@ -177,7 +179,7 @@ def _download_pair_history(datadir: Path,
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(
|
logger.info(
|
||||||
f'Download history data for pair: "{pair}", timeframe: {timeframe} '
|
f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe} '
|
||||||
f'and store in {datadir}.'
|
f'and store in {datadir}.'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -195,7 +197,8 @@ def _download_pair_history(datadir: Path,
|
|||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
since_ms=since_ms if since_ms else
|
since_ms=since_ms if since_ms else
|
||||||
arrow.utcnow().shift(
|
arrow.utcnow().shift(
|
||||||
days=-new_pairs_days).int_timestamp * 1000
|
days=-new_pairs_days).int_timestamp * 1000,
|
||||||
|
is_new_pair=data.empty
|
||||||
)
|
)
|
||||||
# TODO: Maybe move parsing to exchange class (?)
|
# TODO: Maybe move parsing to exchange class (?)
|
||||||
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
||||||
@@ -234,7 +237,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
"""
|
"""
|
||||||
pairs_not_available = []
|
pairs_not_available = []
|
||||||
data_handler = get_datahandler(datadir, data_format)
|
data_handler = get_datahandler(datadir, data_format)
|
||||||
for pair in pairs:
|
for idx, pair in enumerate(pairs, start=1):
|
||||||
if pair not in exchange.markets:
|
if pair not in exchange.markets:
|
||||||
pairs_not_available.append(pair)
|
pairs_not_available.append(pair)
|
||||||
logger.info(f"Skipping pair {pair}...")
|
logger.info(f"Skipping pair {pair}...")
|
||||||
@@ -247,10 +250,11 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
||||||
|
|
||||||
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
|
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
|
||||||
_download_pair_history(datadir=datadir, exchange=exchange,
|
process = f'{idx}/{len(pairs)}'
|
||||||
pair=pair, timeframe=str(timeframe),
|
_download_pair_history(pair=pair, process=process,
|
||||||
new_pairs_days=new_pairs_days,
|
datadir=datadir, exchange=exchange,
|
||||||
timerange=timerange, data_handler=data_handler)
|
timerange=timerange, data_handler=data_handler,
|
||||||
|
timeframe=str(timeframe), new_pairs_days=new_pairs_days)
|
||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
|
|
||||||
|
|
||||||
|
@@ -62,7 +62,7 @@ class JsonDataHandler(IDataHandler):
|
|||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||||
_data = data.copy()
|
_data = data.copy()
|
||||||
# Convert date to int
|
# Convert date to int
|
||||||
_data['date'] = _data['date'].astype(np.int64) // 1000 // 1000
|
_data['date'] = _data['date'].view(np.int64) // 1000 // 1000
|
||||||
|
|
||||||
# Reset index, select only appropriate columns and save as json
|
# Reset index, select only appropriate columns and save as json
|
||||||
_data.reset_index(drop=True).loc[:, self._columns].to_json(
|
_data.reset_index(drop=True).loc[:, self._columns].to_json(
|
||||||
|
@@ -119,7 +119,7 @@ class Edge:
|
|||||||
)
|
)
|
||||||
# Download informative pairs too
|
# Download informative pairs too
|
||||||
res = defaultdict(list)
|
res = defaultdict(list)
|
||||||
for p, t in self.strategy.informative_pairs():
|
for p, t in self.strategy.gather_informative_pairs():
|
||||||
res[t].append(p)
|
res[t].append(p)
|
||||||
for timeframe, inf_pairs in res.items():
|
for timeframe, inf_pairs in res.items():
|
||||||
timerange_startup = deepcopy(self._timerange)
|
timerange_startup = deepcopy(self._timerange)
|
||||||
@@ -151,7 +151,7 @@ class Edge:
|
|||||||
# Fake run-mode to Edge
|
# Fake run-mode to Edge
|
||||||
prior_rm = self.config['runmode']
|
prior_rm = self.config['runmode']
|
||||||
self.config['runmode'] = RunMode.EDGE
|
self.config['runmode'] = RunMode.EDGE
|
||||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
preprocessed = self.strategy.advise_all_indicators(data)
|
||||||
self.config['runmode'] = prior_rm
|
self.config['runmode'] = prior_rm
|
||||||
|
|
||||||
# Print timeframe
|
# Print timeframe
|
||||||
|
@@ -3,5 +3,5 @@ from freqtrade.enums.backteststate import BacktestState
|
|||||||
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.enums.selltype import SellType
|
from freqtrade.enums.selltype import SellType
|
||||||
from freqtrade.enums.signaltype import SignalType
|
from freqtrade.enums.signaltype import SignalTagType, SignalType
|
||||||
from freqtrade.enums.state import State
|
from freqtrade.enums.state import State
|
||||||
|
@@ -11,6 +11,8 @@ class RPCMessageType(Enum):
|
|||||||
SELL = 'sell'
|
SELL = 'sell'
|
||||||
SELL_FILL = 'sell_fill'
|
SELL_FILL = 'sell_fill'
|
||||||
SELL_CANCEL = 'sell_cancel'
|
SELL_CANCEL = 'sell_cancel'
|
||||||
|
PROTECTION_TRIGGER = 'protection_trigger'
|
||||||
|
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
@@ -7,3 +7,10 @@ class SignalType(Enum):
|
|||||||
"""
|
"""
|
||||||
BUY = "buy"
|
BUY = "buy"
|
||||||
SELL = "sell"
|
SELL = "sell"
|
||||||
|
|
||||||
|
|
||||||
|
class SignalTagType(Enum):
|
||||||
|
"""
|
||||||
|
Enum for signal columns
|
||||||
|
"""
|
||||||
|
BUY_TAG = "buy_tag"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
# isort: off
|
# isort: off
|
||||||
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS
|
||||||
from freqtrade.exchange.exchange import Exchange
|
from freqtrade.exchange.exchange import Exchange
|
||||||
# isort: on
|
# isort: on
|
||||||
from freqtrade.exchange.bibox import Bibox
|
from freqtrade.exchange.bibox import Bibox
|
||||||
@@ -15,6 +15,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
|||||||
timeframe_to_seconds, validate_exchange,
|
timeframe_to_seconds, validate_exchange,
|
||||||
validate_exchanges)
|
validate_exchanges)
|
||||||
from freqtrade.exchange.ftx import Ftx
|
from freqtrade.exchange.ftx import Ftx
|
||||||
|
from freqtrade.exchange.gateio import Gateio
|
||||||
from freqtrade.exchange.hitbtc import Hitbtc
|
from freqtrade.exchange.hitbtc import Hitbtc
|
||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
from freqtrade.exchange.kucoin import Kucoin
|
from freqtrade.exchange.kucoin import Kucoin
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
""" Binance exchange subclass """
|
""" Binance exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||||
@@ -18,6 +19,7 @@ class Binance(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
|
"time_in_force_parameter": "timeInForce",
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "fromId",
|
"trades_pagination_arg": "fromId",
|
||||||
@@ -89,3 +91,20 @@ class Binance(Exchange):
|
|||||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
|
since_ms: int, is_new_pair: bool
|
||||||
|
) -> List:
|
||||||
|
"""
|
||||||
|
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||||
|
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||||
|
"""
|
||||||
|
if is_new_pair:
|
||||||
|
x = await self._async_get_candle_history(pair, timeframe, 0)
|
||||||
|
if x and x[2] and x[2][0] and x[2][0][0] > since_ms:
|
||||||
|
# Set starting date to first available candle.
|
||||||
|
since_ms = x[2][0][0]
|
||||||
|
logger.info(f"Candle-data for {pair} available starting with "
|
||||||
|
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
||||||
|
return await super()._async_get_historic_ohlcv(
|
||||||
|
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
|
||||||
|
@@ -51,6 +51,19 @@ EXCHANGE_HAS_OPTIONAL = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def remove_credentials(config) -> None:
|
||||||
|
"""
|
||||||
|
Removes exchange keys from the configuration and specifies dry-run
|
||||||
|
Used for backtesting / hyperopt / edge and utils.
|
||||||
|
Modifies the input dict!
|
||||||
|
"""
|
||||||
|
if config.get('dry_run', False):
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
|
config['exchange']['password'] = ''
|
||||||
|
config['exchange']['uid'] = ''
|
||||||
|
|
||||||
|
|
||||||
def calculate_backoff(retrycount, max_retries):
|
def calculate_backoff(retrycount, max_retries):
|
||||||
"""
|
"""
|
||||||
Calculate backoff
|
Calculate backoff
|
||||||
|
@@ -19,15 +19,16 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU
|
|||||||
decimal_to_precision)
|
decimal_to_precision)
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes
|
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
|
||||||
|
ListPairsWithTimeframes)
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, OperationalException, PricingError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
||||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier,
|
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||||
retrier_async)
|
remove_credentials, retrier, retrier_async)
|
||||||
from freqtrade.misc import deep_merge_dicts, safe_value_fallback2
|
from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
|
|
||||||
|
|
||||||
@@ -53,12 +54,16 @@ class Exchange:
|
|||||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||||
_params: Dict = {}
|
_params: Dict = {}
|
||||||
|
|
||||||
|
# Additional headers - added to the ccxt object
|
||||||
|
_headers: Dict = {}
|
||||||
|
|
||||||
# Dict to specify which options each exchange implements
|
# Dict to specify which options each exchange implements
|
||||||
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
||||||
# or by specifying them in the configuration.
|
# or by specifying them in the configuration.
|
||||||
_ft_has_default: Dict = {
|
_ft_has_default: Dict = {
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
"order_time_in_force": ["gtc"],
|
"order_time_in_force": ["gtc"],
|
||||||
|
"time_in_force_parameter": "timeInForce",
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_candle_limit": 500,
|
"ohlcv_candle_limit": 500,
|
||||||
"ohlcv_partial_candle": True,
|
"ohlcv_partial_candle": True,
|
||||||
@@ -99,6 +104,7 @@ class Exchange:
|
|||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
# Holds all open sell orders for dry_run
|
||||||
self._dry_run_open_orders: Dict[str, Any] = {}
|
self._dry_run_open_orders: Dict[str, Any] = {}
|
||||||
|
remove_credentials(config)
|
||||||
|
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
@@ -168,7 +174,7 @@ class Exchange:
|
|||||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||||
|
|
||||||
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||||
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
|
||||||
"""
|
"""
|
||||||
Initialize ccxt with given config and return valid
|
Initialize ccxt with given config and return valid
|
||||||
ccxt instance.
|
ccxt instance.
|
||||||
@@ -187,6 +193,10 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
if ccxt_kwargs:
|
if ccxt_kwargs:
|
||||||
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
||||||
|
if self._headers:
|
||||||
|
# Inject static headers after the above output to not confuse users.
|
||||||
|
ccxt_kwargs = deep_merge_dicts({'headers': self._headers}, ccxt_kwargs)
|
||||||
|
if ccxt_kwargs:
|
||||||
ex_config.update(ccxt_kwargs)
|
ex_config.update(ccxt_kwargs)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
@@ -351,9 +361,16 @@ class Exchange:
|
|||||||
def validate_stakecurrency(self, stake_currency: str) -> None:
|
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||||
"""
|
"""
|
||||||
Checks stake-currency against available currencies on the exchange.
|
Checks stake-currency against available currencies on the exchange.
|
||||||
|
Only runs on startup. If markets have not been loaded, there's been a problem with
|
||||||
|
the connection to the exchange.
|
||||||
:param stake_currency: Stake-currency to validate
|
:param stake_currency: Stake-currency to validate
|
||||||
:raise: OperationalException if stake-currency is not available.
|
:raise: OperationalException if stake-currency is not available.
|
||||||
"""
|
"""
|
||||||
|
if not self._markets:
|
||||||
|
raise OperationalException(
|
||||||
|
'Could not load markets, therefore cannot start. '
|
||||||
|
'Please investigate the above error for more details.'
|
||||||
|
)
|
||||||
quote_currencies = self.get_quote_currencies()
|
quote_currencies = self.get_quote_currencies()
|
||||||
if stake_currency not in quote_currencies:
|
if stake_currency not in quote_currencies:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
@@ -618,6 +635,8 @@ class Exchange:
|
|||||||
if self.exchange_has('fetchL2OrderBook'):
|
if self.exchange_has('fetchL2OrderBook'):
|
||||||
ob = self.fetch_l2_order_book(pair, 20)
|
ob = self.fetch_l2_order_book(pair, 20)
|
||||||
ob_type = 'asks' if side == 'buy' else 'bids'
|
ob_type = 'asks' if side == 'buy' else 'bids'
|
||||||
|
slippage = 0.05
|
||||||
|
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
|
||||||
|
|
||||||
remaining_amount = amount
|
remaining_amount = amount
|
||||||
filled_amount = 0
|
filled_amount = 0
|
||||||
@@ -626,7 +645,9 @@ class Exchange:
|
|||||||
book_entry_coin_volume = book_entry[1]
|
book_entry_coin_volume = book_entry[1]
|
||||||
if remaining_amount > 0:
|
if remaining_amount > 0:
|
||||||
if remaining_amount < book_entry_coin_volume:
|
if remaining_amount < book_entry_coin_volume:
|
||||||
|
# Orderbook at this slot bigger than remaining amount
|
||||||
filled_amount += remaining_amount * book_entry_price
|
filled_amount += remaining_amount * book_entry_price
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
filled_amount += book_entry_coin_volume * book_entry_price
|
filled_amount += book_entry_coin_volume * book_entry_price
|
||||||
remaining_amount -= book_entry_coin_volume
|
remaining_amount -= book_entry_coin_volume
|
||||||
@@ -635,7 +656,14 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
# If remaining_amount wasn't consumed completely (break was not called)
|
# If remaining_amount wasn't consumed completely (break was not called)
|
||||||
filled_amount += remaining_amount * book_entry_price
|
filled_amount += remaining_amount * book_entry_price
|
||||||
forecast_avg_filled_price = filled_amount / amount
|
forecast_avg_filled_price = max(filled_amount, 0) / amount
|
||||||
|
# Limit max. slippage to specified value
|
||||||
|
if side == 'buy':
|
||||||
|
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
|
||||||
|
|
||||||
|
else:
|
||||||
|
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
|
||||||
|
|
||||||
return self.price_to_precision(pair, forecast_avg_filled_price)
|
return self.price_to_precision(pair, forecast_avg_filled_price)
|
||||||
|
|
||||||
return rate
|
return rate
|
||||||
@@ -689,7 +717,17 @@ class Exchange:
|
|||||||
# Order handling
|
# Order handling
|
||||||
|
|
||||||
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
rate: float, params: Dict = {}) -> Dict:
|
rate: float, time_in_force: str = 'gtc') -> Dict:
|
||||||
|
|
||||||
|
if self._config['dry_run']:
|
||||||
|
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate)
|
||||||
|
return dry_order
|
||||||
|
|
||||||
|
params = self._params.copy()
|
||||||
|
if time_in_force != 'gtc' and ordertype != 'market':
|
||||||
|
param = self._ft_has.get('time_in_force_parameter', '')
|
||||||
|
params.update({param: time_in_force})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
amount = self.amount_to_precision(pair, amount)
|
amount = self.amount_to_precision(pair, amount)
|
||||||
@@ -720,32 +758,6 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def buy(self, pair: str, ordertype: str, amount: float,
|
|
||||||
rate: float, time_in_force: str) -> Dict:
|
|
||||||
|
|
||||||
if self._config['dry_run']:
|
|
||||||
dry_order = self.create_dry_run_order(pair, ordertype, "buy", amount, rate)
|
|
||||||
return dry_order
|
|
||||||
|
|
||||||
params = self._params.copy()
|
|
||||||
if time_in_force != 'gtc' and ordertype != 'market':
|
|
||||||
params.update({'timeInForce': time_in_force})
|
|
||||||
|
|
||||||
return self.create_order(pair, ordertype, 'buy', amount, rate, params)
|
|
||||||
|
|
||||||
def sell(self, pair: str, ordertype: str, amount: float,
|
|
||||||
rate: float, time_in_force: str = 'gtc') -> Dict:
|
|
||||||
|
|
||||||
if self._config['dry_run']:
|
|
||||||
dry_order = self.create_dry_run_order(pair, ordertype, "sell", amount, rate)
|
|
||||||
return dry_order
|
|
||||||
|
|
||||||
params = self._params.copy()
|
|
||||||
if time_in_force != 'gtc' and ordertype != 'market':
|
|
||||||
params.update({'timeInForce': time_in_force})
|
|
||||||
|
|
||||||
return self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
|
||||||
|
|
||||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
@@ -810,7 +822,7 @@ class Exchange:
|
|||||||
:param order: Order dict as returned from fetch_order()
|
:param order: Order dict as returned from fetch_order()
|
||||||
:return: True if order has been cancelled without being filled, False otherwise.
|
:return: True if order has been cancelled without being filled, False otherwise.
|
||||||
"""
|
"""
|
||||||
return (order.get('status') in ('closed', 'canceled', 'cancelled')
|
return (order.get('status') in NON_OPEN_EXCHANGE_STATES
|
||||||
and order.get('filled') == 0.0)
|
and order.get('filled') == 0.0)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
@@ -1044,7 +1056,7 @@ class Exchange:
|
|||||||
logger.debug(f"Using Last {conf_strategy['price_side'].capitalize()} / Last Price")
|
logger.debug(f"Using Last {conf_strategy['price_side'].capitalize()} / Last Price")
|
||||||
ticker = self.fetch_ticker(pair)
|
ticker = self.fetch_ticker(pair)
|
||||||
ticker_rate = ticker[conf_strategy['price_side']]
|
ticker_rate = ticker[conf_strategy['price_side']]
|
||||||
if ticker['last']:
|
if ticker['last'] and ticker_rate:
|
||||||
if side == 'buy' and ticker_rate > ticker['last']:
|
if side == 'buy' and ticker_rate > ticker['last']:
|
||||||
balance = conf_strategy['ask_last_balance']
|
balance = conf_strategy['ask_last_balance']
|
||||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||||
@@ -1183,7 +1195,7 @@ class Exchange:
|
|||||||
# Historic data
|
# Historic data
|
||||||
|
|
||||||
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int) -> List:
|
since_ms: int, is_new_pair: bool = False) -> List:
|
||||||
"""
|
"""
|
||||||
Get candle history using asyncio and returns the list of candles.
|
Get candle history using asyncio and returns the list of candles.
|
||||||
Handles all async work for this.
|
Handles all async work for this.
|
||||||
@@ -1195,7 +1207,7 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return asyncio.get_event_loop().run_until_complete(
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||||
since_ms=since_ms))
|
since_ms=since_ms, is_new_pair=is_new_pair))
|
||||||
|
|
||||||
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
||||||
since_ms: int) -> DataFrame:
|
since_ms: int) -> DataFrame:
|
||||||
@@ -1210,11 +1222,12 @@ class Exchange:
|
|||||||
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||||
drop_incomplete=self._ohlcv_partial_candle)
|
drop_incomplete=self._ohlcv_partial_candle)
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
timeframe: str,
|
since_ms: int, is_new_pair: bool
|
||||||
since_ms: int) -> List:
|
) -> List:
|
||||||
"""
|
"""
|
||||||
Download historic ohlcv
|
Download historic ohlcv
|
||||||
|
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||||
"""
|
"""
|
||||||
|
|
||||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
||||||
@@ -1227,10 +1240,11 @@ class Exchange:
|
|||||||
pair, timeframe, since) for since in
|
pair, timeframe, since) for since in
|
||||||
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
||||||
|
|
||||||
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
|
||||||
|
|
||||||
# Combine gathered results
|
|
||||||
data: List = []
|
data: List = []
|
||||||
|
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||||
|
for input_coro in chunks(input_coroutines, 100):
|
||||||
|
|
||||||
|
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||||
for res in results:
|
for res in results:
|
||||||
if isinstance(res, Exception):
|
if isinstance(res, Exception):
|
||||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||||
@@ -1241,7 +1255,7 @@ class Exchange:
|
|||||||
data.extend(new_data)
|
data.extend(new_data)
|
||||||
# Sort data again after extending the result - above calls return in "async order"
|
# Sort data again after extending the result - above calls return in "async order"
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
logger.info("Downloaded data for %s with length %s.", pair, len(data))
|
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||||
@@ -1259,7 +1273,7 @@ class Exchange:
|
|||||||
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
||||||
|
|
||||||
input_coroutines = []
|
input_coroutines = []
|
||||||
|
cached_pairs = []
|
||||||
# Gather coroutines to run
|
# Gather coroutines to run
|
||||||
for pair, timeframe in set(pair_list):
|
for pair, timeframe in set(pair_list):
|
||||||
if (((pair, timeframe) not in self._klines)
|
if (((pair, timeframe) not in self._klines)
|
||||||
@@ -1271,6 +1285,7 @@ class Exchange:
|
|||||||
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
||||||
pair, timeframe
|
pair, timeframe
|
||||||
)
|
)
|
||||||
|
cached_pairs.append((pair, timeframe))
|
||||||
|
|
||||||
results = asyncio.get_event_loop().run_until_complete(
|
results = asyncio.get_event_loop().run_until_complete(
|
||||||
asyncio.gather(*input_coroutines, return_exceptions=True))
|
asyncio.gather(*input_coroutines, return_exceptions=True))
|
||||||
@@ -1293,6 +1308,10 @@ class Exchange:
|
|||||||
results_df[(pair, timeframe)] = ohlcv_df
|
results_df[(pair, timeframe)] = ohlcv_df
|
||||||
if cache:
|
if cache:
|
||||||
self._klines[(pair, timeframe)] = ohlcv_df
|
self._klines[(pair, timeframe)] = ohlcv_df
|
||||||
|
# Return cached klines
|
||||||
|
for pair, timeframe in cached_pairs:
|
||||||
|
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)
|
||||||
|
|
||||||
return results_df
|
return results_df
|
||||||
|
|
||||||
def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:
|
def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:
|
||||||
@@ -1503,7 +1522,7 @@ class Exchange:
|
|||||||
:returns List of trade data
|
:returns List of trade data
|
||||||
"""
|
"""
|
||||||
if not self.exchange_has("fetchTrades"):
|
if not self.exchange_has("fetchTrades"):
|
||||||
raise OperationalException("This exchange does not suport downloading Trades.")
|
raise OperationalException("This exchange does not support downloading Trades.")
|
||||||
|
|
||||||
return asyncio.get_event_loop().run_until_complete(
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
self._async_get_trade_history(pair=pair, since=since,
|
self._async_get_trade_history(pair=pair, since=since,
|
||||||
|
25
freqtrade/exchange/gateio.py
Normal file
25
freqtrade/exchange/gateio.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
""" Gate.io exchange subclass """
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Gateio(Exchange):
|
||||||
|
"""
|
||||||
|
Gate.io exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
|
with this exchange.
|
||||||
|
|
||||||
|
Please note that this exchange is not included in the list of exchanges
|
||||||
|
officially supported by the Freqtrade development team. So some features
|
||||||
|
may still not work as expected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ft_has: Dict = {
|
||||||
|
"ohlcv_candle_limit": 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
|
@@ -21,4 +21,6 @@ class Kucoin(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"l2_limit_range": [20, 100],
|
"l2_limit_range": [20, 100],
|
||||||
"l2_limit_range_required": False,
|
"l2_limit_range_required": False,
|
||||||
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
|
"time_in_force_parameter": "timeInForce",
|
||||||
}
|
}
|
||||||
|
@@ -83,10 +83,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
||||||
|
|
||||||
# Attach Dataprovider to Strategy baseclass
|
# Attach Dataprovider to strategy instance
|
||||||
IStrategy.dp = self.dataprovider
|
self.strategy.dp = self.dataprovider
|
||||||
# Attach Wallets to Strategy baseclass
|
# Attach Wallets to strategy instance
|
||||||
IStrategy.wallets = self.wallets
|
self.strategy.wallets = self.wallets
|
||||||
|
|
||||||
# Initializing Edge only if enabled
|
# Initializing Edge only if enabled
|
||||||
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
||||||
@@ -99,7 +99,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||||
|
|
||||||
# Protect sell-logic from forcesell and vice versa
|
# Protect sell-logic from forcesell and vice versa
|
||||||
self._sell_lock = Lock()
|
self._exit_lock = Lock()
|
||||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||||
|
|
||||||
def notify_status(self, msg: str) -> None:
|
def notify_status(self, msg: str) -> None:
|
||||||
@@ -160,20 +160,20 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# Refreshing candles
|
# Refreshing candles
|
||||||
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
||||||
self.strategy.informative_pairs())
|
self.strategy.gather_informative_pairs())
|
||||||
|
|
||||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||||
|
|
||||||
self.strategy.analyze(self.active_pair_whitelist)
|
self.strategy.analyze(self.active_pair_whitelist)
|
||||||
|
|
||||||
with self._sell_lock:
|
with self._exit_lock:
|
||||||
# Check and handle any timed out open orders
|
# Check and handle any timed out open orders
|
||||||
self.check_handle_timedout()
|
self.check_handle_timedout()
|
||||||
|
|
||||||
# Protect from collisions with forcesell.
|
# Protect from collisions with forcesell.
|
||||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||||
# while selling is in process, since telegram messages arrive in an different thread.
|
# while selling is in process, since telegram messages arrive in an different thread.
|
||||||
with self._sell_lock:
|
with self._exit_lock:
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
# First process current opened trades (positions)
|
# First process current opened trades (positions)
|
||||||
self.exit_positions(trades)
|
self.exit_positions(trades)
|
||||||
@@ -296,9 +296,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if sell_order:
|
if sell_order:
|
||||||
self.refind_lost_order(trade)
|
self.refind_lost_order(trade)
|
||||||
else:
|
else:
|
||||||
self.reupdate_buy_order_fees(trade)
|
self.reupdate_enter_order_fees(trade)
|
||||||
|
|
||||||
def reupdate_buy_order_fees(self, trade: Trade):
|
def reupdate_enter_order_fees(self, trade: Trade):
|
||||||
"""
|
"""
|
||||||
Get buy order from database, and try to reupdate.
|
Get buy order from database, and try to reupdate.
|
||||||
Handles trades where the initial fee-update did not work.
|
Handles trades where the initial fee-update did not work.
|
||||||
@@ -420,7 +420,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# running get_signal on historical data fetched
|
# running get_signal on historical data fetched
|
||||||
(buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
|
(buy, sell, buy_tag) = self.strategy.get_signal(
|
||||||
|
pair,
|
||||||
|
self.strategy.timeframe,
|
||||||
|
analyzed_df
|
||||||
|
)
|
||||||
|
|
||||||
if buy and not sell:
|
if buy and not sell:
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
|
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
|
||||||
@@ -429,11 +433,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if ((bid_check_dom.get('enabled', False)) and
|
if ((bid_check_dom.get('enabled', False)) and
|
||||||
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
||||||
if self._check_depth_of_market_buy(pair, bid_check_dom):
|
if self._check_depth_of_market_buy(pair, bid_check_dom):
|
||||||
return self.execute_buy(pair, stake_amount)
|
return self.execute_entry(pair, stake_amount, buy_tag=buy_tag)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self.execute_buy(pair, stake_amount)
|
return self.execute_entry(pair, stake_amount, buy_tag=buy_tag)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -461,8 +465,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
||||||
forcebuy: bool = False) -> bool:
|
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a limit buy for the given pair
|
Executes a limit buy for the given pair
|
||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
@@ -472,15 +476,21 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
time_in_force = self.strategy.order_time_in_force['buy']
|
time_in_force = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
buy_limit_requested = price
|
enter_limit_requested = price
|
||||||
else:
|
else:
|
||||||
# Calculate price
|
# Calculate price
|
||||||
buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy")
|
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||||
|
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||||
|
default_retval=proposed_enter_rate)(
|
||||||
|
pair=pair, current_time=datetime.now(timezone.utc),
|
||||||
|
proposed_rate=proposed_enter_rate)
|
||||||
|
|
||||||
if not buy_limit_requested:
|
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||||
|
|
||||||
|
if not enter_limit_requested:
|
||||||
raise PricingError('Could not determine buy price.')
|
raise PricingError('Could not determine buy price.')
|
||||||
|
|
||||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
|
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested,
|
||||||
self.strategy.stoploss)
|
self.strategy.stoploss)
|
||||||
|
|
||||||
if not self.edge:
|
if not self.edge:
|
||||||
@@ -488,7 +498,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||||
default_retval=stake_amount)(
|
default_retval=stake_amount)(
|
||||||
pair=pair, current_time=datetime.now(timezone.utc),
|
pair=pair, current_time=datetime.now(timezone.utc),
|
||||||
current_rate=buy_limit_requested, proposed_stake=stake_amount,
|
current_rate=enter_limit_requested, proposed_stake=stake_amount,
|
||||||
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||||
|
|
||||||
@@ -498,27 +508,27 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||||
f"{stake_amount} ...")
|
f"{stake_amount} ...")
|
||||||
|
|
||||||
amount = stake_amount / buy_limit_requested
|
amount = stake_amount / enter_limit_requested
|
||||||
order_type = self.strategy.order_types['buy']
|
order_type = self.strategy.order_types['buy']
|
||||||
if forcebuy:
|
if forcebuy:
|
||||||
# Forcebuy can define a different ordertype
|
# Forcebuy can define a different ordertype
|
||||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
|
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
|
time_in_force=time_in_force, current_time=datetime.now(timezone.utc)):
|
||||||
logger.info(f"User requested abortion of buying {pair}")
|
logger.info(f"User requested abortion of buying {pair}")
|
||||||
return False
|
return False
|
||||||
amount = self.exchange.amount_to_precision(pair, amount)
|
amount = self.exchange.amount_to_precision(pair, amount)
|
||||||
order = self.exchange.buy(pair=pair, ordertype=order_type,
|
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||||
amount=amount, rate=buy_limit_requested,
|
amount=amount, rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force)
|
time_in_force=time_in_force)
|
||||||
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
||||||
order_id = order['id']
|
order_id = order['id']
|
||||||
order_status = order.get('status', None)
|
order_status = order.get('status', None)
|
||||||
|
|
||||||
# we assume the order is executed at the price requested
|
# we assume the order is executed at the price requested
|
||||||
buy_limit_filled_price = buy_limit_requested
|
enter_limit_filled_price = enter_limit_requested
|
||||||
amount_requested = amount
|
amount_requested = amount
|
||||||
|
|
||||||
if order_status == 'expired' or order_status == 'rejected':
|
if order_status == 'expired' or order_status == 'rejected':
|
||||||
@@ -541,13 +551,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
stake_amount = order['cost']
|
stake_amount = order['cost']
|
||||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||||
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||||
|
|
||||||
# in case of FOK the order may be filled immediately and fully
|
# in case of FOK the order may be filled immediately and fully
|
||||||
elif order_status == 'closed':
|
elif order_status == 'closed':
|
||||||
stake_amount = order['cost']
|
stake_amount = order['cost']
|
||||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||||
buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||||
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
@@ -559,12 +569,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount_requested=amount_requested,
|
amount_requested=amount_requested,
|
||||||
fee_open=fee,
|
fee_open=fee,
|
||||||
fee_close=fee,
|
fee_close=fee,
|
||||||
open_rate=buy_limit_filled_price,
|
open_rate=enter_limit_filled_price,
|
||||||
open_rate_requested=buy_limit_requested,
|
open_rate_requested=enter_limit_requested,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
exchange=self.exchange.id,
|
exchange=self.exchange.id,
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
strategy=self.strategy.get_strategy_name(),
|
strategy=self.strategy.get_strategy_name(),
|
||||||
|
buy_tag=buy_tag,
|
||||||
timeframe=timeframe_to_minutes(self.config['timeframe'])
|
timeframe=timeframe_to_minutes(self.config['timeframe'])
|
||||||
)
|
)
|
||||||
trade.orders.append(order_obj)
|
trade.orders.append(order_obj)
|
||||||
@@ -579,17 +590,18 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Updating wallets
|
# Updating wallets
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
self._notify_buy(trade, order_type)
|
self._notify_enter(trade, order_type)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
def _notify_enter(self, trade: Trade, order_type: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy occurred.
|
Sends rpc notification when a buy occurred.
|
||||||
"""
|
"""
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY,
|
'type': RPCMessageType.BUY,
|
||||||
|
'buy_tag': trade.buy_tag,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'limit': trade.open_rate,
|
'limit': trade.open_rate,
|
||||||
@@ -605,7 +617,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Send the message
|
# Send the message
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy cancel occurred.
|
Sends rpc notification when a buy cancel occurred.
|
||||||
"""
|
"""
|
||||||
@@ -614,6 +626,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_CANCEL,
|
'type': RPCMessageType.BUY_CANCEL,
|
||||||
|
'buy_tag': trade.buy_tag,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'limit': trade.open_rate,
|
'limit': trade.open_rate,
|
||||||
@@ -630,10 +643,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Send the message
|
# Send the message
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_buy_fill(self, trade: Trade) -> None:
|
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_FILL,
|
'type': RPCMessageType.BUY_FILL,
|
||||||
|
'buy_tag': trade.buy_tag,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'open_rate': trade.open_rate,
|
'open_rate': trade.open_rate,
|
||||||
@@ -692,11 +706,15 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||||
self.strategy.timeframe)
|
self.strategy.timeframe)
|
||||||
|
|
||||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
|
(buy, sell, _) = self.strategy.get_signal(
|
||||||
|
trade.pair,
|
||||||
|
self.strategy.timeframe,
|
||||||
|
analyzed_df
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug('checking sell')
|
logger.debug('checking sell')
|
||||||
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
if self._check_and_execute_exit(trade, exit_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('Found no sell signal for %s.', trade)
|
logger.debug('Found no sell signal for %s.', trade)
|
||||||
@@ -726,8 +744,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
except InvalidOrderException as e:
|
except InvalidOrderException as e:
|
||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||||
logger.warning('Selling the trade forcefully')
|
logger.warning('Exiting the trade forcefully')
|
||||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||||
sell_type=SellType.EMERGENCY_SELL))
|
sell_type=SellType.EMERGENCY_SELL))
|
||||||
|
|
||||||
except ExchangeError:
|
except ExchangeError:
|
||||||
@@ -764,7 +782,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||||
reason='Auto lock')
|
reason='Auto lock')
|
||||||
self._notify_sell(trade, "stoploss")
|
self._notify_exit(trade, "stoploss")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if trade.open_order_id or not trade.is_open:
|
if trade.open_order_id or not trade.is_open:
|
||||||
@@ -833,19 +851,19 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.warning(f"Could not create trailing stoploss order "
|
logger.warning(f"Could not create trailing stoploss order "
|
||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||||
buy: bool, sell: bool) -> bool:
|
buy: bool, sell: bool) -> bool:
|
||||||
"""
|
"""
|
||||||
Check and execute sell
|
Check and execute exit
|
||||||
"""
|
"""
|
||||||
should_sell = self.strategy.should_sell(
|
should_sell = self.strategy.should_sell(
|
||||||
trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
|
||||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if should_sell.sell_flag:
|
if should_sell.sell_flag:
|
||||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||||
self.execute_sell(trade, sell_rate, should_sell)
|
self.execute_trade_exit(trade, exit_rate, should_sell)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -888,7 +906,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
default_retval=False)(pair=trade.pair,
|
default_retval=False)(pair=trade.pair,
|
||||||
trade=trade,
|
trade=trade,
|
||||||
order=order))):
|
order=order))):
|
||||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
|
|
||||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||||
fully_cancelled
|
fully_cancelled
|
||||||
@@ -897,7 +915,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
default_retval=False)(pair=trade.pair,
|
default_retval=False)(pair=trade.pair,
|
||||||
trade=trade,
|
trade=trade,
|
||||||
order=order))):
|
order=order))):
|
||||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
|
|
||||||
def cancel_all_open_orders(self) -> None:
|
def cancel_all_open_orders(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -913,13 +931,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if order['side'] == 'buy':
|
if order['side'] == 'buy':
|
||||||
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||||
|
|
||||||
elif order['side'] == 'sell':
|
elif order['side'] == 'sell':
|
||||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool:
|
def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Buy cancel - cancel order
|
Buy cancel - cancel order
|
||||||
:return: True if order was fully cancelled
|
:return: True if order was fully cancelled
|
||||||
@@ -927,7 +945,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
was_trade_fully_canceled = False
|
was_trade_fully_canceled = False
|
||||||
|
|
||||||
# Cancelled orders may have the status of 'canceled' or 'closed'
|
# Cancelled orders may have the status of 'canceled' or 'closed'
|
||||||
if order['status'] not in ('cancelled', 'canceled', 'closed'):
|
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
filled_val = order.get('filled', 0.0) or 0.0
|
filled_val = order.get('filled', 0.0) or 0.0
|
||||||
filled_stake = filled_val * trade.open_rate
|
filled_stake = filled_val * trade.open_rate
|
||||||
minstake = self.exchange.get_min_pair_stake_amount(
|
minstake = self.exchange.get_min_pair_stake_amount(
|
||||||
@@ -943,7 +961,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Avoid race condition where the order could not be cancelled coz its already filled.
|
# Avoid race condition where the order could not be cancelled coz its already filled.
|
||||||
# Simply bailing here is the only safe way - as this order will then be
|
# Simply bailing here is the only safe way - as this order will then be
|
||||||
# handled in the next iteration.
|
# handled in the next iteration.
|
||||||
if corder.get('status') not in ('cancelled', 'canceled', 'closed'):
|
if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
|
logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@@ -965,7 +983,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# if trade is partially complete, edit the stake details for the trade
|
# if trade is partially complete, edit the stake details for the trade
|
||||||
# and close the order
|
# and close the order
|
||||||
# cancel_order may not contain the full order dict, so we need to fallback
|
# cancel_order may not contain the full order dict, so we need to fallback
|
||||||
# to the order dict aquired before cancelling.
|
# to the order dict acquired before cancelling.
|
||||||
# we need to fall back to the values from order if corder does not contain these keys.
|
# we need to fall back to the values from order if corder does not contain these keys.
|
||||||
trade.amount = filled_amount
|
trade.amount = filled_amount
|
||||||
trade.stake_amount = trade.amount * trade.open_rate
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
@@ -976,11 +994,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||||
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'],
|
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'],
|
||||||
reason=reason)
|
reason=reason)
|
||||||
return was_trade_fully_canceled
|
return was_trade_fully_canceled
|
||||||
|
|
||||||
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str:
|
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
|
||||||
"""
|
"""
|
||||||
Sell cancel - cancel order and update trade
|
Sell cancel - cancel order and update trade
|
||||||
:return: Reason for cancel
|
:return: Reason for cancel
|
||||||
@@ -1014,14 +1032,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
self._notify_sell_cancel(
|
self._notify_exit_cancel(
|
||||||
trade,
|
trade,
|
||||||
order_type=self.strategy.order_types['sell'],
|
order_type=self.strategy.order_types['sell'],
|
||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
return reason
|
return reason
|
||||||
|
|
||||||
def _safe_sell_amount(self, pair: str, amount: float) -> float:
|
def _safe_exit_amount(self, pair: str, amount: float) -> float:
|
||||||
"""
|
"""
|
||||||
Get sellable amount.
|
Get sellable amount.
|
||||||
Should be trade.amount - but will fall back to the available amount if necessary.
|
Should be trade.amount - but will fall back to the available amount if necessary.
|
||||||
@@ -1046,9 +1064,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||||
|
|
||||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a limit sell for the given trade and limit
|
Executes a trade exit for the given trade and limit
|
||||||
:param trade: Trade instance
|
:param trade: Trade instance
|
||||||
:param limit: limit rate for the sell order
|
:param limit: limit rate for the sell order
|
||||||
:param sell_reason: Reason the sell was triggered
|
:param sell_reason: Reason the sell was triggered
|
||||||
@@ -1064,6 +1082,17 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
and self.strategy.order_types['stoploss_on_exchange']:
|
and self.strategy.order_types['stoploss_on_exchange']:
|
||||||
limit = trade.stop_loss
|
limit = trade.stop_loss
|
||||||
|
|
||||||
|
# set custom_exit_price if available
|
||||||
|
proposed_limit_rate = limit
|
||||||
|
current_profit = trade.calc_profit_ratio(limit)
|
||||||
|
custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||||
|
default_retval=proposed_limit_rate)(
|
||||||
|
pair=trade.pair, trade=trade,
|
||||||
|
current_time=datetime.now(timezone.utc),
|
||||||
|
proposed_rate=proposed_limit_rate, current_profit=current_profit)
|
||||||
|
|
||||||
|
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
||||||
|
|
||||||
# First cancelling stoploss on exchange ...
|
# First cancelling stoploss on exchange ...
|
||||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||||
try:
|
try:
|
||||||
@@ -1082,7 +1111,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# but we allow this value to be changed)
|
# but we allow this value to be changed)
|
||||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||||
|
|
||||||
amount = self._safe_sell_amount(trade.pair, trade.amount)
|
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||||
@@ -1094,8 +1123,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order = self.exchange.sell(pair=trade.pair,
|
order = self.exchange.create_order(pair=trade.pair,
|
||||||
ordertype=order_type,
|
ordertype=order_type, side="sell",
|
||||||
amount=amount, rate=limit,
|
amount=amount, rate=limit,
|
||||||
time_in_force=time_in_force
|
time_in_force=time_in_force
|
||||||
)
|
)
|
||||||
@@ -1113,7 +1142,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
trade.sell_reason = sell_reason.sell_reason
|
trade.sell_reason = sell_reason.sell_reason
|
||||||
# In case of market sell orders the order can be closed immediately
|
# In case of market sell orders the order can be closed immediately
|
||||||
if order.get('status', 'unknown') == 'closed':
|
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||||
self.update_trade_state(trade, trade.open_order_id, order)
|
self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
@@ -1121,11 +1150,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||||
reason='Auto lock')
|
reason='Auto lock')
|
||||||
|
|
||||||
self._notify_sell(trade, order_type)
|
self._notify_exit(trade, order_type)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a sell occurred.
|
Sends rpc notification when a sell occurred.
|
||||||
"""
|
"""
|
||||||
@@ -1167,7 +1196,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Send the message
|
# Send the message
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a sell cancel occurred.
|
Sends rpc notification when a sell cancel occurred.
|
||||||
"""
|
"""
|
||||||
@@ -1188,7 +1217,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'gain': gain,
|
'gain': gain,
|
||||||
'limit': profit_rate,
|
'limit': profit_rate or 0,
|
||||||
'order_type': order_type,
|
'order_type': order_type,
|
||||||
'amount': trade.amount,
|
'amount': trade.amount,
|
||||||
'open_rate': trade.open_rate,
|
'open_rate': trade.open_rate,
|
||||||
@@ -1197,7 +1226,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date,
|
'close_date': trade.close_date or datetime.now(timezone.utc),
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
'reason': reason,
|
'reason': reason,
|
||||||
@@ -1262,16 +1291,28 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Updating wallets when order is closed
|
# Updating wallets when order is closed
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
if not stoploss_order and not trade.open_order_id:
|
if not stoploss_order and not trade.open_order_id:
|
||||||
self._notify_sell(trade, '', True)
|
self._notify_exit(trade, '', True)
|
||||||
self.protections.stop_per_pair(trade.pair)
|
self.handle_protections(trade.pair)
|
||||||
self.protections.global_stop()
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
elif not trade.open_order_id:
|
elif not trade.open_order_id:
|
||||||
# Buy fill
|
# Buy fill
|
||||||
self._notify_buy_fill(trade)
|
self._notify_enter_fill(trade)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def handle_protections(self, pair: str) -> None:
|
||||||
|
prot_trig = self.protections.stop_per_pair(pair)
|
||||||
|
if prot_trig:
|
||||||
|
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
|
||||||
|
msg.update(prot_trig.to_json())
|
||||||
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
|
prot_trig_glb = self.protections.global_stop()
|
||||||
|
if prot_trig_glb:
|
||||||
|
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
|
||||||
|
msg.update(prot_trig_glb.to_json())
|
||||||
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
|
||||||
amount: float, fee_abs: float) -> float:
|
amount: float, fee_abs: float) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -1352,6 +1393,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if fee_currency:
|
if fee_currency:
|
||||||
# fee_rate should use mean
|
# fee_rate should use mean
|
||||||
fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
|
fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
|
||||||
|
if fee_rate is not None and fee_rate < 0.02:
|
||||||
|
# Only update if fee-rate is < 2%
|
||||||
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
||||||
|
|
||||||
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
||||||
@@ -1363,3 +1406,26 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount=amount, fee_abs=fee_abs)
|
amount=amount, fee_abs=fee_abs)
|
||||||
else:
|
else:
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
|
||||||
|
"""
|
||||||
|
Return the valid price.
|
||||||
|
Check if the custom price is of the good type if not return proposed_price
|
||||||
|
:return: valid price for the order
|
||||||
|
"""
|
||||||
|
if custom_price:
|
||||||
|
try:
|
||||||
|
valid_custom_price = float(custom_price)
|
||||||
|
except ValueError:
|
||||||
|
valid_custom_price = proposed_price
|
||||||
|
else:
|
||||||
|
valid_custom_price = proposed_price
|
||||||
|
|
||||||
|
cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02)
|
||||||
|
min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r)
|
||||||
|
max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r)
|
||||||
|
|
||||||
|
# Bracket between min_custom_price_allowed and max_custom_price_allowed
|
||||||
|
return max(
|
||||||
|
min(valid_custom_price, max_custom_price_allowed),
|
||||||
|
min_custom_price_allowed)
|
||||||
|
@@ -87,7 +87,7 @@ def setup_logging(config: Dict[str, Any]) -> None:
|
|||||||
# syslog config. The messages should be equal for this.
|
# syslog config. The messages should be equal for this.
|
||||||
handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
||||||
logging.root.addHandler(handler_sl)
|
logging.root.addHandler(handler_sl)
|
||||||
elif s[0] == 'journald':
|
elif s[0] == 'journald': # pragma: no cover
|
||||||
try:
|
try:
|
||||||
from systemd.journal import JournaldLogHandler
|
from systemd.journal import JournaldLogHandler
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@@ -9,7 +9,7 @@ from typing import Any, List
|
|||||||
|
|
||||||
|
|
||||||
# check min. python version
|
# check min. python version
|
||||||
if sys.version_info < (3, 7):
|
if sys.version_info < (3, 7): # pragma: no cover
|
||||||
sys.exit("Freqtrade requires Python version >= 3.7")
|
sys.exit("Freqtrade requires Python version >= 3.7")
|
||||||
|
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
@@ -46,7 +46,7 @@ def main(sysargv: List[str] = None) -> None:
|
|||||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||||
)
|
)
|
||||||
|
|
||||||
except SystemExit as e:
|
except SystemExit as e: # pragma: no cover
|
||||||
return_code = e
|
return_code = e
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('SIGINT received, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
@@ -60,5 +60,5 @@ def main(sysargv: List[str] = None) -> None:
|
|||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__': # pragma: no cover
|
||||||
main()
|
main()
|
||||||
|
@@ -11,11 +11,11 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
||||||
from freqtrade.data.converter import trim_dataframes
|
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.enums import BacktestState, SellType
|
from freqtrade.enums import BacktestState, SellType
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
@@ -43,6 +43,7 @@ CLOSE_IDX = 3
|
|||||||
SELL_IDX = 4
|
SELL_IDX = 4
|
||||||
LOW_IDX = 5
|
LOW_IDX = 5
|
||||||
HIGH_IDX = 6
|
HIGH_IDX = 6
|
||||||
|
BUY_TAG_IDX = 7
|
||||||
|
|
||||||
|
|
||||||
class Backtesting:
|
class Backtesting:
|
||||||
@@ -60,8 +61,7 @@ class Backtesting:
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.results: Optional[Dict[str, Any]] = None
|
self.results: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
# Reset keys for backtesting
|
config['dry_run'] = True
|
||||||
remove_credentials(self.config)
|
|
||||||
self.strategylist: List[IStrategy] = []
|
self.strategylist: List[IStrategy] = []
|
||||||
self.all_results: Dict[str, Dict] = {}
|
self.all_results: Dict[str, Dict] = {}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class Backtesting:
|
|||||||
"configuration or as cli argument `--timeframe 5m`")
|
"configuration or as cli argument `--timeframe 5m`")
|
||||||
self.timeframe = str(self.config.get('timeframe'))
|
self.timeframe = str(self.config.get('timeframe'))
|
||||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||||
|
self.init_backtest_detail()
|
||||||
self.pairlists = PairListManager(self.exchange, self.config)
|
self.pairlists = PairListManager(self.exchange, self.config)
|
||||||
if 'VolumePairList' in self.pairlists.name_list:
|
if 'VolumePairList' in self.pairlists.name_list:
|
||||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||||
@@ -108,26 +108,46 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||||
|
|
||||||
Trade.use_db = False
|
self.timerange = TimeRange.parse_timerange(
|
||||||
Trade.reset_trades()
|
None if self.config.get('timerange') is None else str(self.config.get('timerange')))
|
||||||
PairLocks.timeframe = self.config['timeframe']
|
|
||||||
PairLocks.use_db = False
|
|
||||||
PairLocks.reset_locks()
|
|
||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange, log=False)
|
|
||||||
|
|
||||||
# Get maximum required startup period
|
# Get maximum required startup period
|
||||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||||
|
# Add maximum startup candle count to configuration for informative pairs support
|
||||||
|
self.config['startup_candle_count'] = self.required_startup
|
||||||
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
||||||
|
self.init_backtest()
|
||||||
self.progress = BTProgress()
|
|
||||||
self.abort = False
|
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
LoggingMixin.show_output = True
|
LoggingMixin.show_output = True
|
||||||
PairLocks.use_db = True
|
PairLocks.use_db = True
|
||||||
Trade.use_db = True
|
Trade.use_db = True
|
||||||
|
|
||||||
|
def init_backtest_detail(self):
|
||||||
|
# Load detail timeframe if specified
|
||||||
|
self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
|
||||||
|
if self.timeframe_detail:
|
||||||
|
self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail)
|
||||||
|
if self.timeframe_min <= self.timeframe_detail_min:
|
||||||
|
raise OperationalException(
|
||||||
|
"Detail timeframe must be smaller than strategy timeframe.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.timeframe_detail_min = 0
|
||||||
|
self.detail_data: Dict[str, DataFrame] = {}
|
||||||
|
|
||||||
|
def init_backtest(self):
|
||||||
|
|
||||||
|
self.prepare_backtest(False)
|
||||||
|
|
||||||
|
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||||
|
|
||||||
|
self.progress = BTProgress()
|
||||||
|
self.abort = False
|
||||||
|
|
||||||
def _set_strategy(self, strategy: IStrategy):
|
def _set_strategy(self, strategy: IStrategy):
|
||||||
"""
|
"""
|
||||||
Load strategy into backtesting
|
Load strategy into backtesting
|
||||||
@@ -135,11 +155,13 @@ class Backtesting:
|
|||||||
self.strategy: IStrategy = strategy
|
self.strategy: IStrategy = strategy
|
||||||
strategy.dp = self.dataprovider
|
strategy.dp = self.dataprovider
|
||||||
# Attach Wallets to Strategy baseclass
|
# Attach Wallets to Strategy baseclass
|
||||||
IStrategy.wallets = self.wallets
|
strategy.wallets = self.wallets
|
||||||
# Set stoploss_on_exchange to false for backtesting,
|
# Set stoploss_on_exchange to false for backtesting,
|
||||||
# since a "perfect" stoploss-sell is assumed anyway
|
# since a "perfect" stoploss-sell is assumed anyway
|
||||||
# And the regular "stoploss" function would not apply to that case
|
# And the regular "stoploss" function would not apply to that case
|
||||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||||
|
|
||||||
|
def _load_protections(self, strategy: IStrategy):
|
||||||
if self.config.get('enable_protections', False):
|
if self.config.get('enable_protections', False):
|
||||||
conf = self.config
|
conf = self.config
|
||||||
if hasattr(strategy, 'protections'):
|
if hasattr(strategy, 'protections'):
|
||||||
@@ -154,14 +176,11 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
self.progress.init_step(BacktestState.DATALOAD, 1)
|
self.progress.init_step(BacktestState.DATALOAD, 1)
|
||||||
|
|
||||||
timerange = TimeRange.parse_timerange(None if self.config.get(
|
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
|
||||||
|
|
||||||
data = history.load_data(
|
data = history.load_data(
|
||||||
datadir=self.config['datadir'],
|
datadir=self.config['datadir'],
|
||||||
pairs=self.pairlists.whitelist,
|
pairs=self.pairlists.whitelist,
|
||||||
timeframe=self.timeframe,
|
timeframe=self.timeframe,
|
||||||
timerange=timerange,
|
timerange=self.timerange,
|
||||||
startup_candles=self.required_startup,
|
startup_candles=self.required_startup,
|
||||||
fail_without_data=True,
|
fail_without_data=True,
|
||||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
@@ -174,11 +193,28 @@ class Backtesting:
|
|||||||
f'({(max_date - min_date).days} days).')
|
f'({(max_date - min_date).days} days).')
|
||||||
|
|
||||||
# Adjust startts forward if not enough data is available
|
# Adjust startts forward if not enough data is available
|
||||||
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
|
self.timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
|
||||||
self.required_startup, min_date)
|
self.required_startup, min_date)
|
||||||
|
|
||||||
self.progress.set_new_value(1)
|
self.progress.set_new_value(1)
|
||||||
return data, timerange
|
return data, self.timerange
|
||||||
|
|
||||||
|
def load_bt_data_detail(self) -> None:
|
||||||
|
"""
|
||||||
|
Loads backtest detail data (smaller timeframe) if necessary.
|
||||||
|
"""
|
||||||
|
if self.timeframe_detail:
|
||||||
|
self.detail_data = history.load_data(
|
||||||
|
datadir=self.config['datadir'],
|
||||||
|
pairs=self.pairlists.whitelist,
|
||||||
|
timeframe=self.timeframe_detail,
|
||||||
|
timerange=self.timerange,
|
||||||
|
startup_candles=0,
|
||||||
|
fail_without_data=True,
|
||||||
|
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.detail_data = {}
|
||||||
|
|
||||||
def prepare_backtest(self, enable_protections):
|
def prepare_backtest(self, enable_protections):
|
||||||
"""
|
"""
|
||||||
@@ -191,6 +227,8 @@ class Backtesting:
|
|||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
self.rejected_trades = 0
|
self.rejected_trades = 0
|
||||||
self.dataprovider.clear_cache()
|
self.dataprovider.clear_cache()
|
||||||
|
if enable_protections:
|
||||||
|
self._load_protections(self.strategy)
|
||||||
|
|
||||||
def check_abort(self):
|
def check_abort(self):
|
||||||
"""
|
"""
|
||||||
@@ -209,7 +247,7 @@ class Backtesting:
|
|||||||
"""
|
"""
|
||||||
# Every change to this headers list must evaluate further usages of the resulting tuple
|
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||||
# and eventually change the constants for indexes at the top
|
# and eventually change the constants for indexes at the top
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag']
|
||||||
data: Dict = {}
|
data: Dict = {}
|
||||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||||
|
|
||||||
@@ -220,20 +258,27 @@ class Backtesting:
|
|||||||
if not pair_data.empty:
|
if not pair_data.empty:
|
||||||
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
|
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
|
||||||
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
|
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
|
||||||
|
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
|
||||||
|
|
||||||
df_analyzed = self.strategy.advise_sell(
|
df_analyzed = self.strategy.advise_sell(
|
||||||
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
|
||||||
|
# Trim startup period from analyzed dataframe
|
||||||
|
df_analyzed = trim_dataframe(df_analyzed, self.timerange,
|
||||||
|
startup_candles=self.required_startup)
|
||||||
# To avoid using data from future, we use buy/sell signals shifted
|
# To avoid using data from future, we use buy/sell signals shifted
|
||||||
# from the previous candle
|
# from the previous candle
|
||||||
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
|
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
|
||||||
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
|
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
|
||||||
|
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
|
||||||
|
|
||||||
df_analyzed.drop(df_analyzed.head(1).index, inplace=True)
|
# Update dataprovider cache
|
||||||
|
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
|
||||||
|
|
||||||
|
df_analyzed = df_analyzed.drop(df_analyzed.head(1).index)
|
||||||
|
|
||||||
# Convert from Pandas to list for performance reasons
|
# Convert from Pandas to list for performance reasons
|
||||||
# (Looping Pandas is slow.)
|
# (Looping Pandas is slow.)
|
||||||
data[pair] = df_analyzed.values.tolist()
|
data[pair] = df_analyzed[headers].values.tolist()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
|
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
|
||||||
@@ -302,15 +347,16 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
return sell_row[OPEN_IDX]
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
||||||
|
sell_row: Tuple) -> Optional[LocalTrade]:
|
||||||
|
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
||||||
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX],
|
sell_candle_time, sell_row[BUY_IDX],
|
||||||
sell_row[SELL_IDX],
|
sell_row[SELL_IDX],
|
||||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||||
|
|
||||||
if sell.sell_flag:
|
if sell.sell_flag:
|
||||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
trade.close_date = sell_candle_time
|
||||||
trade.sell_reason = sell.sell_reason
|
trade.sell_reason = sell.sell_reason
|
||||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||||
@@ -322,7 +368,7 @@ class Backtesting:
|
|||||||
rate=closerate,
|
rate=closerate,
|
||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
sell_reason=sell.sell_reason,
|
sell_reason=sell.sell_reason,
|
||||||
current_time=sell_row[DATE_IDX].to_pydatetime()):
|
current_time=sell_candle_time):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
trade.close(closerate, show_msg=False)
|
trade.close(closerate, show_msg=False)
|
||||||
@@ -330,6 +376,32 @@ class Backtesting:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
||||||
|
if self.timeframe_detail and trade.pair in self.detail_data:
|
||||||
|
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
|
||||||
|
sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min)
|
||||||
|
|
||||||
|
detail_data = self.detail_data[trade.pair]
|
||||||
|
detail_data = detail_data.loc[
|
||||||
|
(detail_data['date'] >= sell_candle_time) &
|
||||||
|
(detail_data['date'] < sell_candle_end)
|
||||||
|
].copy()
|
||||||
|
if len(detail_data) == 0:
|
||||||
|
# Fall back to "regular" data if no detail data was found for this candle
|
||||||
|
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||||
|
detail_data.loc[:, 'buy'] = sell_row[BUY_IDX]
|
||||||
|
detail_data.loc[:, 'sell'] = sell_row[SELL_IDX]
|
||||||
|
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||||
|
for det_row in detail_data[headers].values.tolist():
|
||||||
|
res = self._get_sell_trade_entry_for_candle(trade, det_row)
|
||||||
|
if res:
|
||||||
|
return res
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
else:
|
||||||
|
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||||
|
|
||||||
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
|
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
|
||||||
try:
|
try:
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
||||||
@@ -358,6 +430,7 @@ class Backtesting:
|
|||||||
|
|
||||||
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||||
# Enter trade
|
# Enter trade
|
||||||
|
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
|
||||||
trade = LocalTrade(
|
trade = LocalTrade(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
open_rate=row[OPEN_IDX],
|
open_rate=row[OPEN_IDX],
|
||||||
@@ -367,6 +440,7 @@ class Backtesting:
|
|||||||
fee_open=self.fee,
|
fee_open=self.fee,
|
||||||
fee_close=self.fee,
|
fee_close=self.fee,
|
||||||
is_open=True,
|
is_open=True,
|
||||||
|
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
|
||||||
exchange='backtesting',
|
exchange='backtesting',
|
||||||
)
|
)
|
||||||
return trade
|
return trade
|
||||||
@@ -423,10 +497,6 @@ class Backtesting:
|
|||||||
trades: List[LocalTrade] = []
|
trades: List[LocalTrade] = []
|
||||||
self.prepare_backtest(enable_protections)
|
self.prepare_backtest(enable_protections)
|
||||||
|
|
||||||
# Update dataprovider cache
|
|
||||||
for pair, dataframe in processed.items():
|
|
||||||
self.dataprovider._set_cached_df(pair, self.timeframe, dataframe)
|
|
||||||
|
|
||||||
# Use dict of lists with data for performance
|
# Use dict of lists with data for performance
|
||||||
# (looping lists is a lot faster than pandas DataFrames)
|
# (looping lists is a lot faster than pandas DataFrames)
|
||||||
data: Dict = self._get_ohlcv_as_lists(processed)
|
data: Dict = self._get_ohlcv_as_lists(processed)
|
||||||
@@ -448,6 +518,8 @@ class Backtesting:
|
|||||||
for i, pair in enumerate(data):
|
for i, pair in enumerate(data):
|
||||||
row_index = indexes[pair]
|
row_index = indexes[pair]
|
||||||
try:
|
try:
|
||||||
|
# Row is treated as "current incomplete candle".
|
||||||
|
# Buy / sell signals are shifted by 1 to compensate for this.
|
||||||
row = data[pair][row_index]
|
row = data[pair][row_index]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# missing Data for one pair at the end.
|
# missing Data for one pair at the end.
|
||||||
@@ -459,8 +531,8 @@ class Backtesting:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
row_index += 1
|
row_index += 1
|
||||||
self.dataprovider._set_dataframe_max_index(row_index)
|
|
||||||
indexes[pair] = row_index
|
indexes[pair] = row_index
|
||||||
|
self.dataprovider._set_dataframe_max_index(row_index)
|
||||||
|
|
||||||
# without positionstacking, we can only have one open trade per pair.
|
# without positionstacking, we can only have one open trade per pair.
|
||||||
# max_open_trades must be respected
|
# max_open_trades must be respected
|
||||||
@@ -484,7 +556,7 @@ class Backtesting:
|
|||||||
open_trades[pair].append(trade)
|
open_trades[pair].append(trade)
|
||||||
LocalTrade.add_bt_trade(trade)
|
LocalTrade.add_bt_trade(trade)
|
||||||
|
|
||||||
for trade in open_trades[pair]:
|
for trade in list(open_trades[pair]):
|
||||||
# also check the buying candle for sell conditions.
|
# also check the buying candle for sell conditions.
|
||||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||||
# Sell occurred
|
# Sell occurred
|
||||||
@@ -515,7 +587,8 @@ class Backtesting:
|
|||||||
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
|
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame],
|
||||||
|
timerange: TimeRange):
|
||||||
self.progress.init_step(BacktestState.ANALYZE, 0)
|
self.progress.init_step(BacktestState.ANALYZE, 0)
|
||||||
|
|
||||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||||
@@ -534,17 +607,18 @@ class Backtesting:
|
|||||||
max_open_trades = 0
|
max_open_trades = 0
|
||||||
|
|
||||||
# need to reprocess data every time to populate signals
|
# need to reprocess data every time to populate signals
|
||||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
preprocessed = self.strategy.advise_all_indicators(data)
|
||||||
|
|
||||||
# Trim startup period from analyzed dataframe
|
# Trim startup period from analyzed dataframe
|
||||||
preprocessed = trim_dataframes(preprocessed, timerange, self.required_startup)
|
preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup)
|
||||||
|
|
||||||
if not preprocessed:
|
if not preprocessed_tmp:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"No data left after adjusting for startup candles.")
|
"No data left after adjusting for startup candles.")
|
||||||
|
|
||||||
min_date, max_date = history.get_timerange(preprocessed)
|
# Use preprocessed_tmp for date generation (the trimmed dataframe).
|
||||||
|
# Backtesting will re-trim the dataframes after buy/sell signal generation.
|
||||||
|
min_date, max_date = history.get_timerange(preprocessed_tmp)
|
||||||
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||||
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||||
f'({(max_date - min_date).days} days).')
|
f'({(max_date - min_date).days} days).')
|
||||||
@@ -574,6 +648,7 @@ class Backtesting:
|
|||||||
data: Dict[str, Any] = {}
|
data: Dict[str, Any] = {}
|
||||||
|
|
||||||
data, timerange = self.load_bt_data()
|
data, timerange = self.load_bt_data()
|
||||||
|
self.load_bt_data_detail()
|
||||||
logger.info("Dataload complete. Calculating indicators")
|
logger.info("Dataload complete. Calculating indicators")
|
||||||
|
|
||||||
for strat in self.strategylist:
|
for strat in self.strategylist:
|
||||||
|
@@ -7,7 +7,8 @@ import logging
|
|||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
@@ -28,11 +29,12 @@ class EdgeCli:
|
|||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
# Reset keys for edge
|
# Ensure using dry-run
|
||||||
remove_credentials(self.config)
|
self.config['dry_run'] = True
|
||||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||||
|
self.strategy.dp = DataProvider(config, None)
|
||||||
|
|
||||||
validate_config_consistency(self.config)
|
validate_config_consistency(self.config)
|
||||||
|
|
||||||
|
@@ -22,6 +22,7 @@ from pandas import DataFrame
|
|||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
||||||
from freqtrade.data.converter import trim_dataframes
|
from freqtrade.data.converter import trim_dataframes
|
||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||||
@@ -30,7 +31,7 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
|
|||||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
||||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
|
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
|
||||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||||
|
|
||||||
|
|
||||||
# Suppress scikit-learn FutureWarnings from skopt
|
# Suppress scikit-learn FutureWarnings from skopt
|
||||||
@@ -44,7 +45,7 @@ progressbar.streams.wrap_stdout()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
INITIAL_POINTS = 30
|
INITIAL_POINTS = 5
|
||||||
|
|
||||||
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||||
# in the skopt model queue, to optimize memory consumption
|
# in the skopt model queue, to optimize memory consumption
|
||||||
@@ -66,6 +67,7 @@ class Hyperopt:
|
|||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.buy_space: List[Dimension] = []
|
self.buy_space: List[Dimension] = []
|
||||||
self.sell_space: List[Dimension] = []
|
self.sell_space: List[Dimension] = []
|
||||||
|
self.protection_space: List[Dimension] = []
|
||||||
self.roi_space: List[Dimension] = []
|
self.roi_space: List[Dimension] = []
|
||||||
self.stoploss_space: List[Dimension] = []
|
self.stoploss_space: List[Dimension] = []
|
||||||
self.trailing_space: List[Dimension] = []
|
self.trailing_space: List[Dimension] = []
|
||||||
@@ -77,10 +79,10 @@ class Hyperopt:
|
|||||||
|
|
||||||
if not self.config.get('hyperopt'):
|
if not self.config.get('hyperopt'):
|
||||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||||
self.auto_hyperopt = True
|
|
||||||
else:
|
else:
|
||||||
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
|
raise OperationalException(
|
||||||
self.auto_hyperopt = False
|
"Using separate Hyperopt files has been removed in 2021.9. Please convert "
|
||||||
|
"your existing Hyperopt file to the new Hyperoptable strategy interface")
|
||||||
|
|
||||||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||||
@@ -102,17 +104,6 @@ class Hyperopt:
|
|||||||
self.num_epochs_saved = 0
|
self.num_epochs_saved = 0
|
||||||
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
self.current_best_epoch: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
|
||||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
|
||||||
self.backtesting.strategy.advise_indicators = ( # type: ignore
|
|
||||||
self.custom_hyperopt.populate_indicators) # type: ignore
|
|
||||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
|
||||||
self.backtesting.strategy.advise_buy = ( # type: ignore
|
|
||||||
self.custom_hyperopt.populate_buy_trend) # type: ignore
|
|
||||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
|
||||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
|
||||||
self.custom_hyperopt.populate_sell_trend) # type: ignore
|
|
||||||
|
|
||||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||||
if self.config.get('use_max_market_positions', True):
|
if self.config.get('use_max_market_positions', True):
|
||||||
self.max_open_trades = self.config['max_open_trades']
|
self.max_open_trades = self.config['max_open_trades']
|
||||||
@@ -189,6 +180,8 @@ class Hyperopt:
|
|||||||
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space}
|
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space}
|
||||||
if HyperoptTools.has_space(self.config, 'sell'):
|
if HyperoptTools.has_space(self.config, 'sell'):
|
||||||
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space}
|
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space}
|
||||||
|
if HyperoptTools.has_space(self.config, 'protection'):
|
||||||
|
result['protection'] = {p.name: params.get(p.name) for p in self.protection_space}
|
||||||
if HyperoptTools.has_space(self.config, 'roi'):
|
if HyperoptTools.has_space(self.config, 'roi'):
|
||||||
result['roi'] = {str(k): v for k, v in
|
result['roi'] = {str(k): v for k, v in
|
||||||
self.custom_hyperopt.generate_roi_table(params).items()}
|
self.custom_hyperopt.generate_roi_table(params).items()}
|
||||||
@@ -239,10 +232,16 @@ class Hyperopt:
|
|||||||
"""
|
"""
|
||||||
Assign the dimensions in the hyperoptimization space.
|
Assign the dimensions in the hyperoptimization space.
|
||||||
"""
|
"""
|
||||||
|
if HyperoptTools.has_space(self.config, 'protection'):
|
||||||
|
# Protections can only be optimized when using the Parameter interface
|
||||||
|
logger.debug("Hyperopt has 'protection' space")
|
||||||
|
# Enable Protections if protection space is selected.
|
||||||
|
self.config['enable_protections'] = True
|
||||||
|
self.protection_space = self.custom_hyperopt.protection_space()
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'buy'):
|
if HyperoptTools.has_space(self.config, 'buy'):
|
||||||
logger.debug("Hyperopt has 'buy' space")
|
logger.debug("Hyperopt has 'buy' space")
|
||||||
self.buy_space = self.custom_hyperopt.indicator_space()
|
self.buy_space = self.custom_hyperopt.buy_indicator_space()
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'sell'):
|
if HyperoptTools.has_space(self.config, 'sell'):
|
||||||
logger.debug("Hyperopt has 'sell' space")
|
logger.debug("Hyperopt has 'sell' space")
|
||||||
@@ -259,30 +258,41 @@ class Hyperopt:
|
|||||||
if HyperoptTools.has_space(self.config, 'trailing'):
|
if HyperoptTools.has_space(self.config, 'trailing'):
|
||||||
logger.debug("Hyperopt has 'trailing' space")
|
logger.debug("Hyperopt has 'trailing' space")
|
||||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||||
self.dimensions = (self.buy_space + self.sell_space + self.roi_space +
|
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||||
self.stoploss_space + self.trailing_space)
|
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
||||||
|
|
||||||
|
def assign_params(self, params_dict: Dict, category: str) -> None:
|
||||||
|
"""
|
||||||
|
Assign hyperoptable parameters
|
||||||
|
"""
|
||||||
|
for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category):
|
||||||
|
if attr.optimize:
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
attr.value = params_dict[attr_name]
|
||||||
|
|
||||||
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Used Optimize function. Called once per epoch to optimize whatever is configured.
|
Used Optimize function.
|
||||||
|
Called once per epoch to optimize whatever is configured.
|
||||||
Keep this function as optimized as possible!
|
Keep this function as optimized as possible!
|
||||||
"""
|
"""
|
||||||
backtest_start_time = datetime.now(timezone.utc)
|
backtest_start_time = datetime.now(timezone.utc)
|
||||||
params_dict = self._get_params_dict(self.dimensions, raw_params)
|
params_dict = self._get_params_dict(self.dimensions, raw_params)
|
||||||
|
|
||||||
# Apply parameters
|
# Apply parameters
|
||||||
|
if HyperoptTools.has_space(self.config, 'buy'):
|
||||||
|
self.assign_params(params_dict, 'buy')
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, 'sell'):
|
||||||
|
self.assign_params(params_dict, 'sell')
|
||||||
|
|
||||||
|
if HyperoptTools.has_space(self.config, 'protection'):
|
||||||
|
self.assign_params(params_dict, 'protection')
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'roi'):
|
if HyperoptTools.has_space(self.config, 'roi'):
|
||||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||||
self.custom_hyperopt.generate_roi_table(params_dict))
|
self.custom_hyperopt.generate_roi_table(params_dict))
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'buy'):
|
|
||||||
self.backtesting.strategy.advise_buy = ( # type: ignore
|
|
||||||
self.custom_hyperopt.buy_strategy_generator(params_dict))
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'sell'):
|
|
||||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
|
||||||
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'stoploss'):
|
if HyperoptTools.has_space(self.config, 'stoploss'):
|
||||||
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
||||||
|
|
||||||
@@ -355,10 +365,20 @@ class Hyperopt:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
|
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
|
||||||
|
estimator = self.custom_hyperopt.generate_estimator()
|
||||||
|
|
||||||
|
acq_optimizer = "sampling"
|
||||||
|
if isinstance(estimator, str):
|
||||||
|
if estimator not in ("GP", "RF", "ET", "GBRT"):
|
||||||
|
raise OperationalException(f"Estimator {estimator} not supported.")
|
||||||
|
else:
|
||||||
|
acq_optimizer = "auto"
|
||||||
|
|
||||||
|
logger.info(f"Using estimator {estimator}.")
|
||||||
return Optimizer(
|
return Optimizer(
|
||||||
dimensions,
|
dimensions,
|
||||||
base_estimator="ET",
|
base_estimator=estimator,
|
||||||
acq_optimizer="auto",
|
acq_optimizer=acq_optimizer,
|
||||||
n_initial_points=INITIAL_POINTS,
|
n_initial_points=INITIAL_POINTS,
|
||||||
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
||||||
random_state=self.random_state,
|
random_state=self.random_state,
|
||||||
@@ -376,18 +396,17 @@ class Hyperopt:
|
|||||||
data, timerange = self.backtesting.load_bt_data()
|
data, timerange = self.backtesting.load_bt_data()
|
||||||
logger.info("Dataload complete. Calculating indicators")
|
logger.info("Dataload complete. Calculating indicators")
|
||||||
|
|
||||||
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
|
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
||||||
|
|
||||||
# Trim startup period from analyzed dataframe
|
# Trim startup period from analyzed dataframe to get correct dates for output.
|
||||||
processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup)
|
processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup)
|
||||||
|
|
||||||
self.min_date, self.max_date = get_timerange(processed)
|
self.min_date, self.max_date = get_timerange(processed)
|
||||||
|
|
||||||
logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||||
f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||||
f'({(self.max_date - self.min_date).days} days)..')
|
f'({(self.max_date - self.min_date).days} days)..')
|
||||||
|
# Store non-trimmed data - will be trimmed after signal generation.
|
||||||
dump(processed, self.data_pickle_file)
|
dump(preprocessed, self.data_pickle_file)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
||||||
@@ -488,7 +507,6 @@ class Hyperopt:
|
|||||||
f"saved to '{self.results_file}'.")
|
f"saved to '{self.results_file}'.")
|
||||||
|
|
||||||
if self.current_best_epoch:
|
if self.current_best_epoch:
|
||||||
if self.auto_hyperopt:
|
|
||||||
HyperoptTools.try_export_params(
|
HyperoptTools.try_export_params(
|
||||||
self.config,
|
self.config,
|
||||||
self.backtesting.strategy.get_strategy_name(),
|
self.backtesting.strategy.get_strategy_name(),
|
||||||
|
@@ -4,15 +4,23 @@ This module implements a convenience auto-hyperopt class, which can be used toge
|
|||||||
that implement IHyperStrategy interface.
|
that implement IHyperStrategy interface.
|
||||||
"""
|
"""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Callable, Dict, List
|
||||||
|
|
||||||
from pandas import DataFrame
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
with suppress(ImportError):
|
with suppress(ImportError):
|
||||||
from skopt.space import Dimension
|
from skopt.space import Dimension
|
||||||
|
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
|
||||||
|
|
||||||
|
|
||||||
|
def _format_exception_message(space: str) -> str:
|
||||||
|
raise OperationalException(
|
||||||
|
f"The '{space}' space is included into the hyperoptimization "
|
||||||
|
f"but no parameter for this space was not found in your Strategy. "
|
||||||
|
f"Please make sure to have parameters for this space enabled for optimization "
|
||||||
|
f"or remove the '{space}' space from hyperoptimization.")
|
||||||
|
|
||||||
|
|
||||||
class HyperOptAuto(IHyperOpt):
|
class HyperOptAuto(IHyperOpt):
|
||||||
@@ -22,26 +30,6 @@ class HyperOptAuto(IHyperOpt):
|
|||||||
sell_indicator_space methods, but other hyperopt methods can be overridden as well.
|
sell_indicator_space methods, but other hyperopt methods can be overridden as well.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict):
|
|
||||||
for attr_name, attr in self.strategy.enumerate_parameters('buy'):
|
|
||||||
if attr.optimize:
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
attr.value = params[attr_name]
|
|
||||||
return self.strategy.populate_buy_trend(dataframe, metadata)
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
|
|
||||||
def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
|
||||||
def populate_sell_trend(dataframe: DataFrame, metadata: dict):
|
|
||||||
for attr_name, attr in self.strategy.enumerate_parameters('sell'):
|
|
||||||
if attr.optimize:
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
attr.value = params[attr_name]
|
|
||||||
return self.strategy.populate_sell_trend(dataframe, metadata)
|
|
||||||
|
|
||||||
return populate_sell_trend
|
|
||||||
|
|
||||||
def _get_func(self, name) -> Callable:
|
def _get_func(self, name) -> Callable:
|
||||||
"""
|
"""
|
||||||
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
|
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
|
||||||
@@ -60,18 +48,22 @@ class HyperOptAuto(IHyperOpt):
|
|||||||
if attr.optimize:
|
if attr.optimize:
|
||||||
yield attr.get_space(attr_name)
|
yield attr.get_space(attr_name)
|
||||||
|
|
||||||
def _get_indicator_space(self, category, fallback_method_name):
|
def _get_indicator_space(self, category):
|
||||||
|
# TODO: is this necessary, or can we call "generate_space" directly?
|
||||||
indicator_space = list(self._generate_indicator_space(category))
|
indicator_space = list(self._generate_indicator_space(category))
|
||||||
if len(indicator_space) > 0:
|
if len(indicator_space) > 0:
|
||||||
return indicator_space
|
return indicator_space
|
||||||
else:
|
else:
|
||||||
return self._get_func(fallback_method_name)()
|
_format_exception_message(category)
|
||||||
|
|
||||||
def indicator_space(self) -> List['Dimension']:
|
def buy_indicator_space(self) -> List['Dimension']:
|
||||||
return self._get_indicator_space('buy', 'indicator_space')
|
return self._get_indicator_space('buy')
|
||||||
|
|
||||||
def sell_indicator_space(self) -> List['Dimension']:
|
def sell_indicator_space(self) -> List['Dimension']:
|
||||||
return self._get_indicator_space('sell', 'sell_indicator_space')
|
return self._get_indicator_space('sell')
|
||||||
|
|
||||||
|
def protection_space(self) -> List['Dimension']:
|
||||||
|
return self._get_indicator_space('protection')
|
||||||
|
|
||||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||||
return self._get_func('generate_roi_table')(params)
|
return self._get_func('generate_roi_table')(params)
|
||||||
@@ -87,3 +79,6 @@ class HyperOptAuto(IHyperOpt):
|
|||||||
|
|
||||||
def trailing_space(self) -> List['Dimension']:
|
def trailing_space(self) -> List['Dimension']:
|
||||||
return self._get_func('trailing_space')()
|
return self._get_func('trailing_space')()
|
||||||
|
|
||||||
|
def generate_estimator(self) -> EstimatorType:
|
||||||
|
return self._get_func('generate_estimator')()
|
||||||
|
128
freqtrade/optimize/hyperopt_epoch_filters.py
Normal file
128
freqtrade/optimize/hyperopt_epoch_filters.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def hyperopt_filter_epochs(epochs: List, filteroptions: dict, log: bool = True) -> List:
|
||||||
|
"""
|
||||||
|
Filter our items from the list of hyperopt results
|
||||||
|
"""
|
||||||
|
if filteroptions['only_best']:
|
||||||
|
epochs = [x for x in epochs if x['is_best']]
|
||||||
|
if filteroptions['only_profitable']:
|
||||||
|
epochs = [x for x in epochs
|
||||||
|
if x['results_metrics'].get('profit_total', 0) > 0]
|
||||||
|
|
||||||
|
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
|
||||||
|
|
||||||
|
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
|
||||||
|
|
||||||
|
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
|
||||||
|
|
||||||
|
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
|
||||||
|
if log:
|
||||||
|
logger.info(f"{len(epochs)} " +
|
||||||
|
("best " if filteroptions['only_best'] else "") +
|
||||||
|
("profitable " if filteroptions['only_profitable'] else "") +
|
||||||
|
"epochs found.")
|
||||||
|
return epochs
|
||||||
|
|
||||||
|
|
||||||
|
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
|
||||||
|
"""
|
||||||
|
Filter epochs with trade-counts > trades
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
x for x in epochs if x['results_metrics'].get('total_trades', 0) > trade_count
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
|
||||||
|
|
||||||
|
if filteroptions['filter_min_trades'] > 0:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
|
||||||
|
|
||||||
|
if filteroptions['filter_max_trades'] > 0:
|
||||||
|
epochs = [
|
||||||
|
x for x in epochs
|
||||||
|
if x['results_metrics'].get('total_trades') < filteroptions['filter_max_trades']
|
||||||
|
]
|
||||||
|
return epochs
|
||||||
|
|
||||||
|
|
||||||
|
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
|
||||||
|
|
||||||
|
def get_duration_value(x):
|
||||||
|
# Duration in minutes ...
|
||||||
|
if 'holding_avg_s' in x['results_metrics']:
|
||||||
|
avg = x['results_metrics']['holding_avg_s']
|
||||||
|
return avg // 60
|
||||||
|
raise OperationalException(
|
||||||
|
"Holding-average not available. Please omit the filter on average time, "
|
||||||
|
"or rerun hyperopt with this version")
|
||||||
|
|
||||||
|
if filteroptions['filter_min_avg_time'] is not None:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
|
epochs = [
|
||||||
|
x for x in epochs
|
||||||
|
if get_duration_value(x) > filteroptions['filter_min_avg_time']
|
||||||
|
]
|
||||||
|
if filteroptions['filter_max_avg_time'] is not None:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
|
epochs = [
|
||||||
|
x for x in epochs
|
||||||
|
if get_duration_value(x) < filteroptions['filter_max_avg_time']
|
||||||
|
]
|
||||||
|
|
||||||
|
return epochs
|
||||||
|
|
||||||
|
|
||||||
|
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
||||||
|
|
||||||
|
if filteroptions['filter_min_avg_profit'] is not None:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
|
epochs = [
|
||||||
|
x for x in epochs
|
||||||
|
if x['results_metrics'].get('profit_mean', 0) * 100
|
||||||
|
> filteroptions['filter_min_avg_profit']
|
||||||
|
]
|
||||||
|
if filteroptions['filter_max_avg_profit'] is not None:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
|
epochs = [
|
||||||
|
x for x in epochs
|
||||||
|
if x['results_metrics'].get('profit_mean', 0) * 100
|
||||||
|
< filteroptions['filter_max_avg_profit']
|
||||||
|
]
|
||||||
|
if filteroptions['filter_min_total_profit'] is not None:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
|
epochs = [
|
||||||
|
x for x in epochs
|
||||||
|
if x['results_metrics'].get('profit_total_abs', 0)
|
||||||
|
> filteroptions['filter_min_total_profit']
|
||||||
|
]
|
||||||
|
if filteroptions['filter_max_total_profit'] is not None:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
|
epochs = [
|
||||||
|
x for x in epochs
|
||||||
|
if x['results_metrics'].get('profit_total_abs', 0)
|
||||||
|
< filteroptions['filter_max_total_profit']
|
||||||
|
]
|
||||||
|
return epochs
|
||||||
|
|
||||||
|
|
||||||
|
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
|
||||||
|
|
||||||
|
if filteroptions['filter_min_objective'] is not None:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
|
|
||||||
|
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
|
||||||
|
if filteroptions['filter_max_objective'] is not None:
|
||||||
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
|
|
||||||
|
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
|
||||||
|
|
||||||
|
return epochs
|
@@ -5,11 +5,11 @@ This module defines the interface to apply for hyperopt
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
|
from sklearn.base import RegressorMixin
|
||||||
from skopt.space import Categorical, Dimension, Integer
|
from skopt.space import Categorical, Dimension, Integer
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.misc import round_dict
|
from freqtrade.misc import round_dict
|
||||||
from freqtrade.optimize.space import SKDecimal
|
from freqtrade.optimize.space import SKDecimal
|
||||||
@@ -18,12 +18,7 @@ from freqtrade.strategy import IStrategy
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
EstimatorType = Union[RegressorMixin, str]
|
||||||
def _format_exception_message(method: str, space: str) -> str:
|
|
||||||
return (f"The '{space}' space is included into the hyperoptimization "
|
|
||||||
f"but {method}() method is not found in your "
|
|
||||||
f"custom Hyperopt class. You should either implement this "
|
|
||||||
f"method or remove the '{space}' space from hyperoptimization.")
|
|
||||||
|
|
||||||
|
|
||||||
class IHyperOpt(ABC):
|
class IHyperOpt(ABC):
|
||||||
@@ -45,29 +40,13 @@ class IHyperOpt(ABC):
|
|||||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||||
IHyperOpt.timeframe = str(config['timeframe'])
|
IHyperOpt.timeframe = str(config['timeframe'])
|
||||||
|
|
||||||
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
def generate_estimator(self) -> EstimatorType:
|
||||||
"""
|
"""
|
||||||
Create a buy strategy generator.
|
Return base_estimator.
|
||||||
|
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
|
||||||
|
inheriting from RegressorMixin (from sklearn).
|
||||||
"""
|
"""
|
||||||
raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy'))
|
return 'ET'
|
||||||
|
|
||||||
def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Create a sell strategy generator.
|
|
||||||
"""
|
|
||||||
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
|
|
||||||
|
|
||||||
def indicator_space(self) -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Create an indicator space.
|
|
||||||
"""
|
|
||||||
raise OperationalException(_format_exception_message('indicator_space', 'buy'))
|
|
||||||
|
|
||||||
def sell_indicator_space(self) -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Create a sell indicator space.
|
|
||||||
"""
|
|
||||||
raise OperationalException(_format_exception_message('sell_indicator_space', 'sell'))
|
|
||||||
|
|
||||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||||
"""
|
"""
|
||||||
|
@@ -4,9 +4,10 @@ import logging
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
import rapidjson
|
import rapidjson
|
||||||
import tabulate
|
import tabulate
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
@@ -15,6 +16,7 @@ from pandas import isna, json_normalize
|
|||||||
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
|
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
||||||
|
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -82,53 +84,77 @@ class HyperoptTools():
|
|||||||
"""
|
"""
|
||||||
Tell if the space value is contained in the configuration
|
Tell if the space value is contained in the configuration
|
||||||
"""
|
"""
|
||||||
# The 'trailing' space is not included in the 'default' set of spaces
|
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
||||||
if space == 'trailing':
|
if space in ('trailing', 'protection'):
|
||||||
return any(s in config['spaces'] for s in [space, 'all'])
|
return any(s in config['spaces'] for s in [space, 'all'])
|
||||||
else:
|
else:
|
||||||
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _read_results_pickle(results_file: Path) -> List:
|
def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]:
|
||||||
"""
|
"""
|
||||||
Read hyperopt results from pickle file
|
Stream hyperopt results from file
|
||||||
LEGACY method - new files are written as json and cannot be read with this method.
|
|
||||||
"""
|
|
||||||
from joblib import load
|
|
||||||
|
|
||||||
logger.info(f"Reading pickled epochs from '{results_file}'")
|
|
||||||
data = load(results_file)
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _read_results(results_file: Path) -> List:
|
|
||||||
"""
|
|
||||||
Read hyperopt results from file
|
|
||||||
"""
|
"""
|
||||||
import rapidjson
|
import rapidjson
|
||||||
logger.info(f"Reading epochs from '{results_file}'")
|
logger.info(f"Reading epochs from '{results_file}'")
|
||||||
with results_file.open('r') as f:
|
with results_file.open('r') as f:
|
||||||
data = [rapidjson.loads(line) for line in f]
|
data = []
|
||||||
return data
|
for line in f:
|
||||||
|
data += [rapidjson.loads(line)]
|
||||||
|
if len(data) >= batch_size:
|
||||||
|
yield data
|
||||||
|
data = []
|
||||||
|
yield data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_previous_results(results_file: Path) -> List:
|
def _test_hyperopt_results_exist(results_file) -> bool:
|
||||||
"""
|
|
||||||
Load data for epochs from the file if we have one
|
|
||||||
"""
|
|
||||||
epochs: List = []
|
|
||||||
if results_file.is_file() and results_file.stat().st_size > 0:
|
if results_file.is_file() and results_file.stat().st_size > 0:
|
||||||
if results_file.suffix == '.pickle':
|
if results_file.suffix == '.pickle':
|
||||||
epochs = HyperoptTools._read_results_pickle(results_file)
|
raise OperationalException(
|
||||||
|
"Legacy hyperopt results are no longer supported."
|
||||||
|
"Please rerun hyperopt or use an older version to load this file."
|
||||||
|
)
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
epochs = HyperoptTools._read_results(results_file)
|
# No file found.
|
||||||
# Detection of some old format, without 'is_best' field saved
|
return False
|
||||||
if epochs[0].get('is_best') is None:
|
|
||||||
|
@staticmethod
|
||||||
|
def load_filtered_results(results_file: Path, config: Dict[str, Any]) -> Tuple[List, int]:
|
||||||
|
filteroptions = {
|
||||||
|
'only_best': config.get('hyperopt_list_best', False),
|
||||||
|
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||||
|
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||||
|
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||||
|
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
||||||
|
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
||||||
|
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||||
|
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||||
|
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||||
|
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||||
|
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||||
|
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
||||||
|
}
|
||||||
|
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
||||||
|
# No file found.
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
epochs = []
|
||||||
|
total_epochs = 0
|
||||||
|
for epochs_tmp in HyperoptTools._read_results(results_file):
|
||||||
|
if total_epochs == 0 and epochs_tmp[0].get('is_best') is None:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"The file with HyperoptTools results is incompatible with this version "
|
"The file with HyperoptTools results is incompatible with this version "
|
||||||
"of Freqtrade and cannot be loaded.")
|
"of Freqtrade and cannot be loaded.")
|
||||||
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
|
total_epochs += len(epochs_tmp)
|
||||||
return epochs
|
epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False)
|
||||||
|
|
||||||
|
logger.info(f"Loaded {total_epochs} previous evaluations from disk.")
|
||||||
|
|
||||||
|
# Final filter run ...
|
||||||
|
epochs = hyperopt_filter_epochs(epochs, filteroptions, log=True)
|
||||||
|
|
||||||
|
return epochs, total_epochs
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_epoch_details(results, total_epochs: int, print_json: bool,
|
def show_epoch_details(results, total_epochs: int, print_json: bool,
|
||||||
@@ -149,7 +175,7 @@ class HyperoptTools():
|
|||||||
|
|
||||||
if print_json:
|
if print_json:
|
||||||
result_dict: Dict = {}
|
result_dict: Dict = {}
|
||||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
|
for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
|
||||||
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
||||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||||
|
|
||||||
@@ -158,6 +184,8 @@ class HyperoptTools():
|
|||||||
non_optimized)
|
non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
||||||
non_optimized)
|
non_optimized)
|
||||||
|
HyperoptTools._params_pretty_print(params, 'protection',
|
||||||
|
"Protection hyperspace params:", non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
||||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
||||||
@@ -271,8 +299,8 @@ class HyperoptTools():
|
|||||||
f"Objective: {results['loss']:.5f}")
|
f"Objective: {results['loss']:.5f}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str:
|
def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool,
|
||||||
|
has_drawdown: bool) -> pd.DataFrame:
|
||||||
trials['Best'] = ''
|
trials['Best'] = ''
|
||||||
|
|
||||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||||
@@ -408,8 +436,7 @@ class HyperoptTools():
|
|||||||
return table
|
return table
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
|
def export_csv_file(config: dict, results: list, csv_file: str) -> None:
|
||||||
csv_file: str) -> None:
|
|
||||||
"""
|
"""
|
||||||
Log result to csv-file
|
Log result to csv-file
|
||||||
"""
|
"""
|
||||||
@@ -431,7 +458,6 @@ class HyperoptTools():
|
|||||||
trials['Best'] = ''
|
trials['Best'] = ''
|
||||||
trials['Stake currency'] = config['stake_currency']
|
trials['Stake currency'] = config['stake_currency']
|
||||||
|
|
||||||
if 'results_metrics.total_trades' in trials:
|
|
||||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||||
'results_metrics.profit_total',
|
'results_metrics.profit_total',
|
||||||
@@ -439,13 +465,7 @@ class HyperoptTools():
|
|||||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||||
'loss', 'is_initial_point', 'is_best']
|
'loss', 'is_initial_point', 'is_best']
|
||||||
perc_multi = 100
|
perc_multi = 100
|
||||||
else:
|
|
||||||
perc_multi = 1
|
|
||||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
|
|
||||||
'results_metrics.avg_profit', 'results_metrics.median_profit',
|
|
||||||
'results_metrics.total_profit',
|
|
||||||
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
|
|
||||||
'loss', 'is_initial_point', 'is_best']
|
|
||||||
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
|
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
|
||||||
trials = trials[base_metrics + param_metrics]
|
trials = trials[base_metrics + param_metrics]
|
||||||
|
|
||||||
@@ -473,11 +493,6 @@ class HyperoptTools():
|
|||||||
trials['Avg profit'] = trials['Avg profit'].apply(
|
trials['Avg profit'] = trials['Avg profit'].apply(
|
||||||
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
|
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
|
||||||
)
|
)
|
||||||
if perc_multi == 1:
|
|
||||||
trials['Avg duration'] = trials['Avg duration'].apply(
|
|
||||||
lambda x: f'{x:,.1f} m' if isinstance(
|
|
||||||
x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else ""
|
|
||||||
)
|
|
||||||
trials['Objective'] = trials['Objective'].apply(
|
trials['Objective'] = trials['Objective'].apply(
|
||||||
lambda x: f'{x:,.5f}' if x != 100000 else ""
|
lambda x: f'{x:,.5f}' if x != 100000 else ""
|
||||||
)
|
)
|
||||||
|
@@ -368,6 +368,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
|||||||
'max_open_trades_setting': (config['max_open_trades']
|
'max_open_trades_setting': (config['max_open_trades']
|
||||||
if config['max_open_trades'] != float('inf') else -1),
|
if config['max_open_trades'] != float('inf') else -1),
|
||||||
'timeframe': config['timeframe'],
|
'timeframe': config['timeframe'],
|
||||||
|
'timeframe_detail': config.get('timeframe_detail', ''),
|
||||||
'timerange': config.get('timerange', ''),
|
'timerange': config.get('timerange', ''),
|
||||||
'enable_protections': config.get('enable_protections', False),
|
'enable_protections': config.get('enable_protections', False),
|
||||||
'strategy_name': strategy,
|
'strategy_name': strategy,
|
||||||
|
@@ -47,6 +47,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
min_rate = get_column_def(cols, 'min_rate', 'null')
|
min_rate = get_column_def(cols, 'min_rate', 'null')
|
||||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||||
strategy = get_column_def(cols, 'strategy', 'null')
|
strategy = get_column_def(cols, 'strategy', 'null')
|
||||||
|
buy_tag = get_column_def(cols, 'buy_tag', 'null')
|
||||||
# If ticker-interval existed use that, else null.
|
# If ticker-interval existed use that, else null.
|
||||||
if has_column(cols, 'ticker_interval'):
|
if has_column(cols, 'ticker_interval'):
|
||||||
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
|
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
|
||||||
@@ -64,7 +65,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
# Schema migration necessary
|
# Schema migration necessary
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(text(f"alter table trades rename to {table_back_name}"))
|
connection.execute(text(f"alter table trades rename to {table_back_name}"))
|
||||||
# drop indexes on backup table
|
with engine.begin() as connection:
|
||||||
|
# drop indexes on backup table in new session
|
||||||
for index in inspector.get_indexes(table_back_name):
|
for index in inspector.get_indexes(table_back_name):
|
||||||
connection.execute(text(f"drop index {index['name']}"))
|
connection.execute(text(f"drop index {index['name']}"))
|
||||||
# let SQLAlchemy create the schema as required
|
# let SQLAlchemy create the schema as required
|
||||||
@@ -75,22 +77,15 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
connection.execute(text(f"""insert into trades
|
connection.execute(text(f"""insert into trades
|
||||||
(id, exchange, pair, is_open,
|
(id, exchange, pair, is_open,
|
||||||
fee_open, fee_open_cost, fee_open_currency,
|
fee_open, fee_open_cost, fee_open_currency,
|
||||||
fee_close, fee_close_cost, fee_open_currency, open_rate,
|
fee_close, fee_close_cost, fee_close_currency, open_rate,
|
||||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||||
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
||||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||||
stoploss_order_id, stoploss_last_update,
|
stoploss_order_id, stoploss_last_update,
|
||||||
max_rate, min_rate, sell_reason, sell_order_status, strategy,
|
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
|
||||||
timeframe, open_trade_value, close_profit_abs
|
timeframe, open_trade_value, close_profit_abs
|
||||||
)
|
)
|
||||||
select id, lower(exchange),
|
select id, lower(exchange), pair,
|
||||||
case
|
|
||||||
when instr(pair, '_') != 0 then
|
|
||||||
substr(pair, instr(pair, '_') + 1) || '/' ||
|
|
||||||
substr(pair, 1, instr(pair, '_') - 1)
|
|
||||||
else pair
|
|
||||||
end
|
|
||||||
pair,
|
|
||||||
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
|
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
|
||||||
{fee_open_currency} fee_open_currency, {fee_close} fee_close,
|
{fee_open_currency} fee_open_currency, {fee_close} fee_close,
|
||||||
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
|
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
|
||||||
@@ -103,7 +98,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||||
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||||
{sell_order_status} sell_order_status,
|
{sell_order_status} sell_order_status,
|
||||||
{strategy} strategy, {timeframe} timeframe,
|
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
|
||||||
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
|
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
"""))
|
"""))
|
||||||
@@ -131,7 +126,9 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col
|
|||||||
|
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(text(f"alter table orders rename to {table_back_name}"))
|
connection.execute(text(f"alter table orders rename to {table_back_name}"))
|
||||||
# drop indexes on backup table
|
|
||||||
|
with engine.begin() as connection:
|
||||||
|
# drop indexes on backup table in new session
|
||||||
for index in inspector.get_indexes(table_back_name):
|
for index in inspector.get_indexes(table_back_name):
|
||||||
connection.execute(text(f"drop index {index['name']}"))
|
connection.execute(text(f"drop index {index['name']}"))
|
||||||
|
|
||||||
@@ -160,7 +157,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
|||||||
table_back_name = get_backup_name(tabs, 'trades_bak')
|
table_back_name = get_backup_name(tabs, 'trades_bak')
|
||||||
|
|
||||||
# Check for latest column
|
# Check for latest column
|
||||||
if not has_column(cols, 'open_trade_value'):
|
if not has_column(cols, 'buy_tag'):
|
||||||
logger.info(f'Running database migration for trades - backup: {table_back_name}')
|
logger.info(f'Running database migration for trades - backup: {table_back_name}')
|
||||||
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
||||||
# Reread columns - the above recreated the table!
|
# Reread columns - the above recreated the table!
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
This module contains the class to persist trades into SQLite
|
This module contains the class to persist trades into SQLite
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session
|
|||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
from sqlalchemy.sql.schema import UniqueConstraint
|
from sqlalchemy.sql.schema import UniqueConstraint
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
|
||||||
from freqtrade.enums import SellType
|
from freqtrade.enums import SellType
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.misc import safe_value_fallback
|
from freqtrade.misc import safe_value_fallback
|
||||||
@@ -159,9 +159,9 @@ class Order(_DECL_BASE):
|
|||||||
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
|
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
|
||||||
|
|
||||||
self.ft_is_open = True
|
self.ft_is_open = True
|
||||||
if self.status in ('closed', 'canceled', 'cancelled'):
|
if self.status in NON_OPEN_EXCHANGE_STATES:
|
||||||
self.ft_is_open = False
|
self.ft_is_open = False
|
||||||
if order.get('filled', 0) > 0:
|
if (order.get('filled', 0.0) or 0.0) > 0:
|
||||||
self.order_filled_date = datetime.now(timezone.utc)
|
self.order_filled_date = datetime.now(timezone.utc)
|
||||||
self.order_update_date = datetime.now(timezone.utc)
|
self.order_update_date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
@@ -257,6 +257,7 @@ class LocalTrade():
|
|||||||
sell_reason: str = ''
|
sell_reason: str = ''
|
||||||
sell_order_status: str = ''
|
sell_order_status: str = ''
|
||||||
strategy: str = ''
|
strategy: str = ''
|
||||||
|
buy_tag: Optional[str] = None
|
||||||
timeframe: Optional[int] = None
|
timeframe: Optional[int] = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -288,6 +289,7 @@ class LocalTrade():
|
|||||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||||
'stake_amount': round(self.stake_amount, 8),
|
'stake_amount': round(self.stake_amount, 8),
|
||||||
'strategy': self.strategy,
|
'strategy': self.strategy,
|
||||||
|
'buy_tag': self.buy_tag,
|
||||||
'timeframe': self.timeframe,
|
'timeframe': self.timeframe,
|
||||||
|
|
||||||
'fee_open': self.fee_open,
|
'fee_open': self.fee_open,
|
||||||
@@ -352,12 +354,12 @@ class LocalTrade():
|
|||||||
LocalTrade.trades_open = []
|
LocalTrade.trades_open = []
|
||||||
LocalTrade.total_profit = 0
|
LocalTrade.total_profit = 0
|
||||||
|
|
||||||
def adjust_min_max_rates(self, current_price: float) -> None:
|
def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
|
||||||
"""
|
"""
|
||||||
Adjust the max_rate and min_rate.
|
Adjust the max_rate and min_rate.
|
||||||
"""
|
"""
|
||||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||||
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
|
||||||
|
|
||||||
def _set_new_stoploss(self, new_loss: float, stoploss: float):
|
def _set_new_stoploss(self, new_loss: float, stoploss: float):
|
||||||
"""Assign new stop value"""
|
"""Assign new stop value"""
|
||||||
@@ -703,6 +705,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
sell_reason = Column(String(100), nullable=True)
|
sell_reason = Column(String(100), nullable=True)
|
||||||
sell_order_status = Column(String(100), nullable=True)
|
sell_order_status = Column(String(100), nullable=True)
|
||||||
strategy = Column(String(100), nullable=True)
|
strategy = Column(String(100), nullable=True)
|
||||||
|
buy_tag = Column(String(100), nullable=True)
|
||||||
timeframe = Column(Integer, nullable=True)
|
timeframe = Column(Integer, nullable=True)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -829,17 +832,21 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
return total_open_stake_amount or 0
|
return total_open_stake_amount or 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_overall_performance() -> List[Dict[str, Any]]:
|
def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns List of dicts containing all Trades, including profit and trade count
|
Returns List of dicts containing all Trades, including profit and trade count
|
||||||
NOTE: Not supported in Backtesting.
|
NOTE: Not supported in Backtesting.
|
||||||
"""
|
"""
|
||||||
|
filters = [Trade.is_open.is_(False)]
|
||||||
|
if minutes:
|
||||||
|
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
|
||||||
|
filters.append(Trade.close_date >= start_date)
|
||||||
pair_rates = Trade.query.with_entities(
|
pair_rates = Trade.query.with_entities(
|
||||||
Trade.pair,
|
Trade.pair,
|
||||||
func.sum(Trade.close_profit).label('profit_sum'),
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||||
func.count(Trade.pair).label('count')
|
func.count(Trade.pair).label('count')
|
||||||
).filter(Trade.is_open.is_(False))\
|
).filter(*filters)\
|
||||||
.group_by(Trade.pair) \
|
.group_by(Trade.pair) \
|
||||||
.order_by(desc('profit_sum_abs')) \
|
.order_by(desc('profit_sum_abs')) \
|
||||||
.all()
|
.all()
|
||||||
|
@@ -30,7 +30,8 @@ class PairLocks():
|
|||||||
PairLocks.locks = []
|
PairLocks.locks = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def lock_pair(pair: str, until: datetime, reason: str = None, *, now: datetime = None) -> None:
|
def lock_pair(pair: str, until: datetime, reason: str = None, *,
|
||||||
|
now: datetime = None) -> PairLock:
|
||||||
"""
|
"""
|
||||||
Create PairLock from now to "until".
|
Create PairLock from now to "until".
|
||||||
Uses database by default, unless PairLocks.use_db is set to False,
|
Uses database by default, unless PairLocks.use_db is set to False,
|
||||||
@@ -52,6 +53,7 @@ class PairLocks():
|
|||||||
PairLock.query.session.commit()
|
PairLock.query.session.commit()
|
||||||
else:
|
else:
|
||||||
PairLocks.locks.append(lock)
|
PairLocks.locks.append(lock)
|
||||||
|
return lock
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
|
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
|
||||||
|
@@ -538,7 +538,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||||||
- Initializes plot-script
|
- Initializes plot-script
|
||||||
- Get candle (OHLCV) data
|
- Get candle (OHLCV) data
|
||||||
- Generate Dafaframes populated with indicators and signals based on configured strategy
|
- Generate Dafaframes populated with indicators and signals based on configured strategy
|
||||||
- Load trades excecuted during the selected period
|
- Load trades executed during the selected period
|
||||||
- Generate Plotly plot objects
|
- Generate Plotly plot objects
|
||||||
- Generate plot files
|
- Generate plot files
|
||||||
:return: None
|
:return: None
|
||||||
|
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.configuration import PeriodicCache
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
@@ -18,14 +19,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class AgeFilter(IPairList):
|
class AgeFilter(IPairList):
|
||||||
|
|
||||||
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
|
||||||
_symbolsChecked: Dict[str, int] = {}
|
|
||||||
|
|
||||||
def __init__(self, exchange, pairlistmanager,
|
def __init__(self, exchange, pairlistmanager,
|
||||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
|
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||||
|
self._symbolsChecked: Dict[str, int] = {}
|
||||||
|
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
||||||
|
|
||||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||||
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
||||||
|
|
||||||
@@ -69,9 +71,12 @@ class AgeFilter(IPairList):
|
|||||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||||
:return: new allowlist
|
:return: new allowlist
|
||||||
"""
|
"""
|
||||||
needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked]
|
needed_pairs = [
|
||||||
|
(p, '1d') for p in pairlist
|
||||||
|
if p not in self._symbolsChecked and p not in self._symbolsCheckFailed]
|
||||||
if not needed_pairs:
|
if not needed_pairs:
|
||||||
return pairlist
|
# Remove pairs that have been removed before
|
||||||
|
return [p for p in pairlist if p not in self._symbolsCheckFailed]
|
||||||
|
|
||||||
since_days = -(
|
since_days = -(
|
||||||
self._max_days_listed if self._max_days_listed else self._min_days_listed
|
self._max_days_listed if self._max_days_listed else self._min_days_listed
|
||||||
@@ -118,5 +123,6 @@ class AgeFilter(IPairList):
|
|||||||
" or more than "
|
" or more than "
|
||||||
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
||||||
) if self._max_days_listed else ''), logger.info)
|
) if self._max_days_listed else ''), logger.info)
|
||||||
|
self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000
|
||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
|
@@ -150,18 +150,20 @@ class IPairList(LoggingMixin, ABC):
|
|||||||
for pair in pairlist:
|
for pair in pairlist:
|
||||||
# pair is not in the generated dynamic market or has the wrong stake currency
|
# pair is not in the generated dynamic market or has the wrong stake currency
|
||||||
if pair not in markets:
|
if pair not in markets:
|
||||||
logger.warning(f"Pair {pair} is not compatible with exchange "
|
self.log_once(f"Pair {pair} is not compatible with exchange "
|
||||||
f"{self._exchange.name}. Removing it from whitelist..")
|
f"{self._exchange.name}. Removing it from whitelist..",
|
||||||
|
logger.warning)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not self._exchange.market_is_tradable(markets[pair]):
|
if not self._exchange.market_is_tradable(markets[pair]):
|
||||||
logger.warning(f"Pair {pair} is not tradable with Freqtrade."
|
self.log_once(f"Pair {pair} is not tradable with Freqtrade."
|
||||||
"Removing it from whitelist..")
|
"Removing it from whitelist..", logger.warning)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']:
|
if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']:
|
||||||
logger.warning(f"Pair {pair} is not compatible with your stake currency "
|
self.log_once(f"Pair {pair} is not compatible with your stake currency "
|
||||||
f"{self._config['stake_currency']}. Removing it from whitelist..")
|
f"{self._config['stake_currency']}. Removing it from whitelist..",
|
||||||
|
logger.warning)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if market is active
|
# Check if market is active
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
Performance pair list filter
|
Performance pair list filter
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@@ -15,6 +15,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class PerformanceFilter(IPairList):
|
class PerformanceFilter(IPairList):
|
||||||
|
|
||||||
|
def __init__(self, exchange, pairlistmanager,
|
||||||
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
|
pairlist_pos: int) -> None:
|
||||||
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
|
self._minutes = pairlistconfig.get('minutes', 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -40,7 +47,7 @@ class PerformanceFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
# Get the trading performance for pairs from database
|
# Get the trading performance for pairs from database
|
||||||
try:
|
try:
|
||||||
performance = pd.DataFrame(Trade.get_overall_performance())
|
performance = pd.DataFrame(Trade.get_overall_performance(self._minutes))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Performancefilter does not work in backtesting.
|
# Performancefilter does not work in backtesting.
|
||||||
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
|
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
|
||||||
|
@@ -4,6 +4,7 @@ Volume PairList provider
|
|||||||
Provides dynamic pair list based on trade volumes
|
Provides dynamic pair list based on trade volumes
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from functools import partial
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
@@ -115,18 +116,18 @@ class VolumePairList(IPairList):
|
|||||||
pairlist = self._pair_cache.get('pairlist')
|
pairlist = self._pair_cache.get('pairlist')
|
||||||
if pairlist:
|
if pairlist:
|
||||||
# Item found - no refresh necessary
|
# Item found - no refresh necessary
|
||||||
return pairlist
|
return pairlist.copy()
|
||||||
else:
|
else:
|
||||||
# Use fresh pairlist
|
# Use fresh pairlist
|
||||||
# Check if pair quote currency equals to the stake currency.
|
# Check if pair quote currency equals to the stake currency.
|
||||||
filtered_tickers = [
|
filtered_tickers = [
|
||||||
v for k, v in tickers.items()
|
v for k, v in tickers.items()
|
||||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||||
and v[self._sort_key] is not None)]
|
and (self._use_range or v[self._sort_key] is not None))]
|
||||||
pairlist = [s['symbol'] for s in filtered_tickers]
|
pairlist = [s['symbol'] for s in filtered_tickers]
|
||||||
|
|
||||||
pairlist = self.filter_pairlist(pairlist, tickers)
|
pairlist = self.filter_pairlist(pairlist, tickers)
|
||||||
self._pair_cache['pairlist'] = pairlist
|
self._pair_cache['pairlist'] = pairlist.copy()
|
||||||
|
|
||||||
return pairlist
|
return pairlist
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ class VolumePairList(IPairList):
|
|||||||
|
|
||||||
# Validate whitelist to only have active market pairs
|
# Validate whitelist to only have active market pairs
|
||||||
pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
|
pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
|
||||||
pairs = self.verify_blacklist(pairs, logger.info)
|
pairs = self.verify_blacklist(pairs, partial(self.log_once, logmethod=logger.info))
|
||||||
# Limit pairlist to the requested number of pairs
|
# Limit pairlist to the requested number of pairs
|
||||||
pairs = pairs[:self._number_pairs]
|
pairs = pairs[:self._number_pairs]
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
|||||||
if keep_invalid:
|
if keep_invalid:
|
||||||
for pair_wc in wildcardpl:
|
for pair_wc in wildcardpl:
|
||||||
try:
|
try:
|
||||||
comp = re.compile(pair_wc)
|
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||||
result_partial = [
|
result_partial = [
|
||||||
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
||||||
]
|
]
|
||||||
@@ -33,7 +33,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
|||||||
else:
|
else:
|
||||||
for pair_wc in wildcardpl:
|
for pair_wc in wildcardpl:
|
||||||
try:
|
try:
|
||||||
comp = re.compile(pair_wc)
|
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||||
result += [
|
result += [
|
||||||
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
pair for pair in available_pairs if re.fullmatch(comp, pair)
|
||||||
]
|
]
|
||||||
|
@@ -26,6 +26,7 @@ class RangeStabilityFilter(IPairList):
|
|||||||
|
|
||||||
self._days = pairlistconfig.get('lookback_days', 10)
|
self._days = pairlistconfig.get('lookback_days', 10)
|
||||||
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
||||||
|
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', None)
|
||||||
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
|
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
|
||||||
|
|
||||||
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
||||||
@@ -50,8 +51,12 @@ class RangeStabilityFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
"""
|
"""
|
||||||
|
max_rate_desc = ""
|
||||||
|
if self._max_rate_of_change:
|
||||||
|
max_rate_desc = (f" and above {self._max_rate_of_change}")
|
||||||
return (f"{self.name} - Filtering pairs with rate of change below "
|
return (f"{self.name} - Filtering pairs with rate of change below "
|
||||||
f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.")
|
f"{self._min_rate_of_change}{max_rate_desc} over the "
|
||||||
|
f"last {plural(self._days, 'day')}.")
|
||||||
|
|
||||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||||
"""
|
"""
|
||||||
@@ -104,6 +109,17 @@ class RangeStabilityFilter(IPairList):
|
|||||||
f"which is below the threshold of {self._min_rate_of_change}.",
|
f"which is below the threshold of {self._min_rate_of_change}.",
|
||||||
logger.info)
|
logger.info)
|
||||||
result = False
|
result = False
|
||||||
|
if self._max_rate_of_change:
|
||||||
|
if pct_change <= self._max_rate_of_change:
|
||||||
|
result = True
|
||||||
|
else:
|
||||||
|
self.log_once(
|
||||||
|
f"Removed {pair} from whitelist, because rate of change "
|
||||||
|
f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, "
|
||||||
|
f"which is above the threshold of {self._max_rate_of_change}.",
|
||||||
|
logger.info)
|
||||||
|
result = False
|
||||||
self._pair_cache[pair] = result
|
self._pair_cache[pair] = result
|
||||||
|
else:
|
||||||
|
self.log_once(f"Removed {pair} from whitelist, no candles found.", logger.info)
|
||||||
return result
|
return result
|
||||||
|
@@ -6,6 +6,7 @@ from datetime import datetime, timezone
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade.persistence import PairLocks
|
from freqtrade.persistence import PairLocks
|
||||||
|
from freqtrade.persistence.models import PairLock
|
||||||
from freqtrade.plugins.protections import IProtection
|
from freqtrade.plugins.protections import IProtection
|
||||||
from freqtrade.resolvers import ProtectionResolver
|
from freqtrade.resolvers import ProtectionResolver
|
||||||
|
|
||||||
@@ -43,30 +44,28 @@ class ProtectionManager():
|
|||||||
"""
|
"""
|
||||||
return [{p.name: p.short_desc()} for p in self._protection_handlers]
|
return [{p.name: p.short_desc()} for p in self._protection_handlers]
|
||||||
|
|
||||||
def global_stop(self, now: Optional[datetime] = None) -> bool:
|
def global_stop(self, now: Optional[datetime] = None) -> Optional[PairLock]:
|
||||||
if not now:
|
if not now:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
result = False
|
result = None
|
||||||
for protection_handler in self._protection_handlers:
|
for protection_handler in self._protection_handlers:
|
||||||
if protection_handler.has_global_stop:
|
if protection_handler.has_global_stop:
|
||||||
result, until, reason = protection_handler.global_stop(now)
|
lock, until, reason = protection_handler.global_stop(now)
|
||||||
|
|
||||||
# Early stopping - first positive result blocks further trades
|
# Early stopping - first positive result blocks further trades
|
||||||
if result and until:
|
if lock and until:
|
||||||
if not PairLocks.is_global_lock(until):
|
if not PairLocks.is_global_lock(until):
|
||||||
PairLocks.lock_pair('*', until, reason, now=now)
|
result = PairLocks.lock_pair('*', until, reason, now=now)
|
||||||
result = True
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def stop_per_pair(self, pair, now: Optional[datetime] = None) -> bool:
|
def stop_per_pair(self, pair, now: Optional[datetime] = None) -> Optional[PairLock]:
|
||||||
if not now:
|
if not now:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
result = False
|
result = None
|
||||||
for protection_handler in self._protection_handlers:
|
for protection_handler in self._protection_handlers:
|
||||||
if protection_handler.has_local_stop:
|
if protection_handler.has_local_stop:
|
||||||
result, until, reason = protection_handler.stop_per_pair(pair, now)
|
lock, until, reason = protection_handler.stop_per_pair(pair, now)
|
||||||
if result and until:
|
if lock and until:
|
||||||
if not PairLocks.is_pair_locked(pair, until):
|
if not PairLocks.is_pair_locked(pair, until):
|
||||||
PairLocks.lock_pair(pair, until, reason, now=now)
|
result = PairLocks.lock_pair(pair, until, reason, now=now)
|
||||||
result = True
|
|
||||||
return result
|
return result
|
||||||
|
@@ -25,19 +25,22 @@ class IProtection(LoggingMixin, ABC):
|
|||||||
def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
self._protection_config = protection_config
|
self._protection_config = protection_config
|
||||||
|
self._stop_duration_candles: Optional[int] = None
|
||||||
|
self._lookback_period_candles: Optional[int] = None
|
||||||
|
|
||||||
tf_in_min = timeframe_to_minutes(config['timeframe'])
|
tf_in_min = timeframe_to_minutes(config['timeframe'])
|
||||||
if 'stop_duration_candles' in protection_config:
|
if 'stop_duration_candles' in protection_config:
|
||||||
self._stop_duration_candles = protection_config.get('stop_duration_candles', 1)
|
self._stop_duration_candles = int(protection_config.get('stop_duration_candles', 1))
|
||||||
self._stop_duration = (tf_in_min * self._stop_duration_candles)
|
self._stop_duration = (tf_in_min * self._stop_duration_candles)
|
||||||
else:
|
else:
|
||||||
self._stop_duration_candles = None
|
self._stop_duration_candles = None
|
||||||
self._stop_duration = protection_config.get('stop_duration', 60)
|
self._stop_duration = protection_config.get('stop_duration', 60)
|
||||||
if 'lookback_period_candles' in protection_config:
|
if 'lookback_period_candles' in protection_config:
|
||||||
self._lookback_period_candles = protection_config.get('lookback_period_candles', 1)
|
self._lookback_period_candles = int(protection_config.get('lookback_period_candles', 1))
|
||||||
self._lookback_period = tf_in_min * self._lookback_period_candles
|
self._lookback_period = tf_in_min * self._lookback_period_candles
|
||||||
else:
|
else:
|
||||||
self._lookback_period_candles = None
|
self._lookback_period_candles = None
|
||||||
self._lookback_period = protection_config.get('lookback_period', 60)
|
self._lookback_period = int(protection_config.get('lookback_period', 60))
|
||||||
|
|
||||||
LoggingMixin.__init__(self, logger)
|
LoggingMixin.__init__(self, logger)
|
||||||
|
|
||||||
|
@@ -8,6 +8,3 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
|||||||
from freqtrade.resolvers.pairlist_resolver import PairListResolver
|
from freqtrade.resolvers.pairlist_resolver import PairListResolver
|
||||||
from freqtrade.resolvers.protection_resolver import ProtectionResolver
|
from freqtrade.resolvers.protection_resolver import ProtectionResolver
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -9,7 +9,6 @@ from typing import Dict
|
|||||||
|
|
||||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS
|
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
|
||||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
||||||
from freqtrade.resolvers import IResolver
|
from freqtrade.resolvers import IResolver
|
||||||
|
|
||||||
@@ -17,43 +16,6 @@ from freqtrade.resolvers import IResolver
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HyperOptResolver(IResolver):
|
|
||||||
"""
|
|
||||||
This class contains all the logic to load custom hyperopt class
|
|
||||||
"""
|
|
||||||
object_type = IHyperOpt
|
|
||||||
object_type_str = "Hyperopt"
|
|
||||||
user_subdir = USERPATH_HYPEROPTS
|
|
||||||
initial_search_path = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_hyperopt(config: Dict) -> IHyperOpt:
|
|
||||||
"""
|
|
||||||
Load the custom hyperopt class from config parameter
|
|
||||||
:param config: configuration dictionary
|
|
||||||
"""
|
|
||||||
if not config.get('hyperopt'):
|
|
||||||
raise OperationalException("No Hyperopt set. Please use `--hyperopt` to specify "
|
|
||||||
"the Hyperopt class to use.")
|
|
||||||
|
|
||||||
hyperopt_name = config['hyperopt']
|
|
||||||
|
|
||||||
hyperopt = HyperOptResolver.load_object(hyperopt_name, config,
|
|
||||||
kwargs={'config': config},
|
|
||||||
extra_dir=config.get('hyperopt_path'))
|
|
||||||
|
|
||||||
if not hasattr(hyperopt, 'populate_indicators'):
|
|
||||||
logger.info("Hyperopt class does not provide populate_indicators() method. "
|
|
||||||
"Using populate_indicators from the strategy.")
|
|
||||||
if not hasattr(hyperopt, 'populate_buy_trend'):
|
|
||||||
logger.info("Hyperopt class does not provide populate_buy_trend() method. "
|
|
||||||
"Using populate_buy_trend from the strategy.")
|
|
||||||
if not hasattr(hyperopt, 'populate_sell_trend'):
|
|
||||||
logger.info("Hyperopt class does not provide populate_sell_trend() method. "
|
|
||||||
"Using populate_sell_trend from the strategy.")
|
|
||||||
return hyperopt
|
|
||||||
|
|
||||||
|
|
||||||
class HyperOptLossResolver(IResolver):
|
class HyperOptLossResolver(IResolver):
|
||||||
"""
|
"""
|
||||||
This class contains all the logic to load custom hyperopt loss class
|
This class contains all the logic to load custom hyperopt loss class
|
||||||
|
@@ -119,7 +119,7 @@ class StrategyResolver(IResolver):
|
|||||||
- default (if not None)
|
- default (if not None)
|
||||||
"""
|
"""
|
||||||
if (attribute in config
|
if (attribute in config
|
||||||
and not isinstance(getattr(type(strategy), 'my_property', None), property)):
|
and not isinstance(getattr(type(strategy), attribute, None), property)):
|
||||||
# Ensure Properties are not overwritten
|
# Ensure Properties are not overwritten
|
||||||
setattr(strategy, attribute, config[attribute])
|
setattr(strategy, attribute, config[attribute])
|
||||||
logger.info("Override strategy '%s' with value in config file: %s.",
|
logger.info("Override strategy '%s' with value in config file: %s.",
|
||||||
|
@@ -4,6 +4,7 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||||
|
|
||||||
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||||
from freqtrade.enums import BacktestState
|
from freqtrade.enums import BacktestState
|
||||||
from freqtrade.exceptions import DependencyException
|
from freqtrade.exceptions import DependencyException
|
||||||
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
||||||
@@ -42,35 +43,40 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
|||||||
# Reload strategy
|
# Reload strategy
|
||||||
lastconfig = ApiServer._bt_last_config
|
lastconfig = ApiServer._bt_last_config
|
||||||
strat = StrategyResolver.load_strategy(btconfig)
|
strat = StrategyResolver.load_strategy(btconfig)
|
||||||
|
validate_config_consistency(btconfig)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not ApiServer._bt
|
not ApiServer._bt
|
||||||
or lastconfig.get('timeframe') != strat.timeframe
|
or lastconfig.get('timeframe') != strat.timeframe
|
||||||
or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)
|
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
|
||||||
|
or lastconfig.get('timerange') != btconfig['timerange']
|
||||||
):
|
):
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
ApiServer._bt = Backtesting(btconfig)
|
ApiServer._bt = Backtesting(btconfig)
|
||||||
|
if ApiServer._bt.timeframe_detail:
|
||||||
# Only reload data if timeframe or timerange changed.
|
ApiServer._bt.load_bt_data_detail()
|
||||||
|
else:
|
||||||
|
ApiServer._bt.config = btconfig
|
||||||
|
ApiServer._bt.init_backtest()
|
||||||
|
# Only reload data if timeframe changed.
|
||||||
if (
|
if (
|
||||||
not ApiServer._bt_data
|
not ApiServer._bt_data
|
||||||
or not ApiServer._bt_timerange
|
or not ApiServer._bt_timerange
|
||||||
or lastconfig.get('timerange') != btconfig['timerange']
|
|
||||||
or lastconfig.get('stake_amount') != btconfig.get('stake_amount')
|
|
||||||
or lastconfig.get('enable_protections') != btconfig.get('enable_protections')
|
|
||||||
or lastconfig.get('protections') != btconfig.get('protections', [])
|
|
||||||
or lastconfig.get('timeframe') != strat.timeframe
|
or lastconfig.get('timeframe') != strat.timeframe
|
||||||
|
or lastconfig.get('timerange') != btconfig['timerange']
|
||||||
):
|
):
|
||||||
|
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
|
||||||
|
|
||||||
lastconfig['timerange'] = btconfig['timerange']
|
lastconfig['timerange'] = btconfig['timerange']
|
||||||
|
lastconfig['timeframe'] = strat.timeframe
|
||||||
lastconfig['protections'] = btconfig.get('protections', [])
|
lastconfig['protections'] = btconfig.get('protections', [])
|
||||||
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
||||||
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
||||||
lastconfig['timeframe'] = strat.timeframe
|
|
||||||
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
|
|
||||||
|
|
||||||
ApiServer._bt.abort = False
|
ApiServer._bt.abort = False
|
||||||
min_date, max_date = ApiServer._bt.backtest_one_strategy(
|
min_date, max_date = ApiServer._bt.backtest_one_strategy(
|
||||||
strat, ApiServer._bt_data, ApiServer._bt_timerange)
|
strat, ApiServer._bt_data, ApiServer._bt_timerange)
|
||||||
|
|
||||||
ApiServer._bt.results = generate_backtest_stats(
|
ApiServer._bt.results = generate_backtest_stats(
|
||||||
ApiServer._bt_data, ApiServer._bt.all_results,
|
ApiServer._bt_data, ApiServer._bt.all_results,
|
||||||
min_date=min_date, max_date=max_date)
|
min_date=min_date, max_date=max_date)
|
||||||
|
@@ -46,6 +46,12 @@ class Balances(BaseModel):
|
|||||||
value: float
|
value: float
|
||||||
stake: str
|
stake: str
|
||||||
note: str
|
note: str
|
||||||
|
starting_capital: float
|
||||||
|
starting_capital_ratio: float
|
||||||
|
starting_capital_pct: float
|
||||||
|
starting_capital_fiat: float
|
||||||
|
starting_capital_fiat_ratio: float
|
||||||
|
starting_capital_fiat_pct: float
|
||||||
|
|
||||||
|
|
||||||
class Count(BaseModel):
|
class Count(BaseModel):
|
||||||
@@ -151,6 +157,7 @@ class TradeSchema(BaseModel):
|
|||||||
amount_requested: float
|
amount_requested: float
|
||||||
stake_amount: float
|
stake_amount: float
|
||||||
strategy: str
|
strategy: str
|
||||||
|
buy_tag: Optional[str]
|
||||||
timeframe: int
|
timeframe: int
|
||||||
fee_open: Optional[float]
|
fee_open: Optional[float]
|
||||||
fee_open_cost: Optional[float]
|
fee_open_cost: Optional[float]
|
||||||
@@ -323,6 +330,7 @@ class PairHistory(BaseModel):
|
|||||||
class BacktestRequest(BaseModel):
|
class BacktestRequest(BaseModel):
|
||||||
strategy: str
|
strategy: str
|
||||||
timeframe: Optional[str]
|
timeframe: Optional[str]
|
||||||
|
timeframe_detail: Optional[str]
|
||||||
timerange: Optional[str]
|
timerange: Optional[str]
|
||||||
max_open_trades: Optional[int]
|
max_open_trades: Optional[int]
|
||||||
stake_amount: Optional[Union[float, str]]
|
stake_amount: Optional[Union[float, str]]
|
||||||
|
@@ -223,11 +223,11 @@ def list_strategies(config=Depends(get_config)):
|
|||||||
@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy'])
|
@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy'])
|
||||||
def get_strategy(strategy: str, config=Depends(get_config)):
|
def get_strategy(strategy: str, config=Depends(get_config)):
|
||||||
|
|
||||||
config = deepcopy(config)
|
config_ = deepcopy(config)
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
try:
|
try:
|
||||||
strategy_obj = StrategyResolver._load_strategy(strategy, config,
|
strategy_obj = StrategyResolver._load_strategy(strategy, config_,
|
||||||
extra_dir=config.get('strategy_path'))
|
extra_dir=config_.get('strategy_path'))
|
||||||
except OperationalException:
|
except OperationalException:
|
||||||
raise HTTPException(status_code=404, detail='Strategy not found')
|
raise HTTPException(status_code=404, detail='Strategy not found')
|
||||||
|
|
||||||
|
@@ -5,6 +5,20 @@ import time
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|
||||||
|
def asyncio_setup() -> None: # pragma: no cover
|
||||||
|
# Set eventloop for win32 setups
|
||||||
|
# Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop
|
||||||
|
# via policy.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8) and sys.platform == "win32":
|
||||||
|
import asyncio
|
||||||
|
import selectors
|
||||||
|
selector = selectors.SelectSelector()
|
||||||
|
loop = asyncio.SelectorEventLoop(selector)
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
|
||||||
class UvicornServer(uvicorn.Server):
|
class UvicornServer(uvicorn.Server):
|
||||||
"""
|
"""
|
||||||
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
|
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
|
||||||
@@ -28,12 +42,15 @@ class UvicornServer(uvicorn.Server):
|
|||||||
try:
|
try:
|
||||||
import uvloop # noqa
|
import uvloop # noqa
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
from uvicorn.loops.asyncio import asyncio_setup
|
|
||||||
asyncio_setup()
|
asyncio_setup()
|
||||||
else:
|
else:
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||||
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
# When running in a thread, we'll not have an eventloop yet.
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
loop.run_until_complete(self.serve(sockets=sockets))
|
loop.run_until_complete(self.serve(sockets=sockets))
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
|
@@ -29,6 +29,16 @@ async def ui_version():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_relative_to(path, base) -> bool:
|
||||||
|
# Helper function simulating behaviour of is_relative_to, which was only added in python 3.9
|
||||||
|
try:
|
||||||
|
path.relative_to(base)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@router_ui.get('/{rest_of_path:path}', include_in_schema=False)
|
@router_ui.get('/{rest_of_path:path}', include_in_schema=False)
|
||||||
async def index_html(rest_of_path: str):
|
async def index_html(rest_of_path: str):
|
||||||
"""
|
"""
|
||||||
@@ -37,8 +47,11 @@ async def index_html(rest_of_path: str):
|
|||||||
if rest_of_path.startswith('api') or rest_of_path.startswith('.'):
|
if rest_of_path.startswith('api') or rest_of_path.startswith('.'):
|
||||||
raise HTTPException(status_code=404, detail="Not Found")
|
raise HTTPException(status_code=404, detail="Not Found")
|
||||||
uibase = Path(__file__).parent / 'ui/installed/'
|
uibase = Path(__file__).parent / 'ui/installed/'
|
||||||
if (uibase / rest_of_path).is_file():
|
filename = uibase / rest_of_path
|
||||||
return FileResponse(str(uibase / rest_of_path))
|
# It's security relevant to check "relative_to".
|
||||||
|
# Without this, Directory-traversal is possible.
|
||||||
|
if filename.is_file() and is_relative_to(filename, uibase):
|
||||||
|
return FileResponse(str(filename))
|
||||||
|
|
||||||
index_file = uibase / 'index.html'
|
index_file = uibase / 'index.html'
|
||||||
if not index_file.is_file():
|
if not index_file.is_file():
|
||||||
|
@@ -5,7 +5,7 @@ e.g BTC to USD
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
from cachetools.ttl import TTLCache
|
from cachetools.ttl import TTLCache
|
||||||
from pycoingecko import CoinGeckoAPI
|
from pycoingecko import CoinGeckoAPI
|
||||||
@@ -25,8 +25,7 @@ class CryptoToFiatConverter:
|
|||||||
"""
|
"""
|
||||||
__instance = None
|
__instance = None
|
||||||
_coingekko: CoinGeckoAPI = None
|
_coingekko: CoinGeckoAPI = None
|
||||||
|
_coinlistings: List[Dict] = []
|
||||||
_cryptomap: Dict = {}
|
|
||||||
_backoff: float = 0.0
|
_backoff: float = 0.0
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
@@ -49,9 +48,8 @@ class CryptoToFiatConverter:
|
|||||||
|
|
||||||
def _load_cryptomap(self) -> None:
|
def _load_cryptomap(self) -> None:
|
||||||
try:
|
try:
|
||||||
coinlistings = self._coingekko.get_coins_list()
|
# Use list-comprehension to ensure we get a list.
|
||||||
# Create mapping table from symbol to coingekko_id
|
self._coinlistings = [x for x in self._coingekko.get_coins_list()]
|
||||||
self._cryptomap = {x['symbol']: x['id'] for x in coinlistings}
|
|
||||||
except RequestException as request_exception:
|
except RequestException as request_exception:
|
||||||
if "429" in str(request_exception):
|
if "429" in str(request_exception):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -69,6 +67,24 @@ class CryptoToFiatConverter:
|
|||||||
logger.error(
|
logger.error(
|
||||||
f"Could not load FIAT Cryptocurrency map for the following problem: {exception}")
|
f"Could not load FIAT Cryptocurrency map for the following problem: {exception}")
|
||||||
|
|
||||||
|
def _get_gekko_id(self, crypto_symbol):
|
||||||
|
if not self._coinlistings:
|
||||||
|
if self._backoff <= datetime.datetime.now().timestamp():
|
||||||
|
self._load_cryptomap()
|
||||||
|
# Still not loaded.
|
||||||
|
if not self._coinlistings:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol]
|
||||||
|
if len(found) == 1:
|
||||||
|
return found[0]['id']
|
||||||
|
|
||||||
|
if len(found) > 0:
|
||||||
|
# Wrong!
|
||||||
|
logger.warning(f"Found multiple mappings in goingekko for {crypto_symbol}.")
|
||||||
|
return None
|
||||||
|
|
||||||
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
"""
|
"""
|
||||||
Convert an amount of crypto-currency to fiat
|
Convert an amount of crypto-currency to fiat
|
||||||
@@ -143,22 +159,14 @@ class CryptoToFiatConverter:
|
|||||||
if crypto_symbol == fiat_symbol:
|
if crypto_symbol == fiat_symbol:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
if self._cryptomap == {}:
|
_gekko_id = self._get_gekko_id(crypto_symbol)
|
||||||
if self._backoff <= datetime.datetime.now().timestamp():
|
|
||||||
self._load_cryptomap()
|
|
||||||
# return 0.0 if we still don't have data to check, no reason to proceed
|
|
||||||
if self._cryptomap == {}:
|
|
||||||
return 0.0
|
|
||||||
else:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
if crypto_symbol not in self._cryptomap:
|
if not _gekko_id:
|
||||||
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
||||||
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_gekko_id = self._cryptomap[crypto_symbol]
|
|
||||||
return float(
|
return float(
|
||||||
self._coingekko.get_price(
|
self._coingekko.get_price(
|
||||||
ids=_gekko_id,
|
ids=_gekko_id,
|
||||||
|
@@ -403,6 +403,9 @@ class RPC:
|
|||||||
# Doing the sum is not right - overall profit needs to be based on initial capital
|
# Doing the sum is not right - overall profit needs to be based on initial capital
|
||||||
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
|
||||||
starting_balance = self._freqtrade.wallets.get_starting_balance()
|
starting_balance = self._freqtrade.wallets.get_starting_balance()
|
||||||
|
profit_closed_ratio_fromstart = 0
|
||||||
|
profit_all_ratio_fromstart = 0
|
||||||
|
if starting_balance:
|
||||||
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
||||||
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
||||||
|
|
||||||
@@ -455,6 +458,9 @@ class RPC:
|
|||||||
raise RPCException('Error getting current tickers.')
|
raise RPCException('Error getting current tickers.')
|
||||||
|
|
||||||
self._freqtrade.wallets.update(require_update=False)
|
self._freqtrade.wallets.update(require_update=False)
|
||||||
|
starting_capital = self._freqtrade.wallets.get_starting_balance()
|
||||||
|
starting_cap_fiat = self._fiat_converter.convert_amount(
|
||||||
|
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||||
|
|
||||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||||
if not balance.total:
|
if not balance.total:
|
||||||
@@ -490,15 +496,25 @@ class RPC:
|
|||||||
else:
|
else:
|
||||||
raise RPCException('All balances are zero.')
|
raise RPCException('All balances are zero.')
|
||||||
|
|
||||||
symbol = fiat_display_currency
|
value = self._fiat_converter.convert_amount(
|
||||||
value = self._fiat_converter.convert_amount(total, stake_currency,
|
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||||
symbol) if self._fiat_converter else 0
|
|
||||||
|
starting_capital_ratio = 0.0
|
||||||
|
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
||||||
|
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'currencies': output,
|
'currencies': output,
|
||||||
'total': total,
|
'total': total,
|
||||||
'symbol': symbol,
|
'symbol': fiat_display_currency,
|
||||||
'value': value,
|
'value': value,
|
||||||
'stake': stake_currency,
|
'stake': stake_currency,
|
||||||
|
'starting_capital': starting_capital,
|
||||||
|
'starting_capital_ratio': starting_capital_ratio,
|
||||||
|
'starting_capital_pct': round(starting_capital_ratio * 100, 2),
|
||||||
|
'starting_capital_fiat': starting_cap_fiat,
|
||||||
|
'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
|
||||||
|
'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
|
||||||
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,25 +561,25 @@ class RPC:
|
|||||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||||
|
|
||||||
if order['side'] == 'buy':
|
if order['side'] == 'buy':
|
||||||
fully_canceled = self._freqtrade.handle_cancel_buy(
|
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||||
trade, order, CANCEL_REASON['FORCE_SELL'])
|
trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||||
|
|
||||||
if order['side'] == 'sell':
|
if order['side'] == 'sell':
|
||||||
# Cancel order - so it is placed anew with a fresh price.
|
# Cancel order - so it is placed anew with a fresh price.
|
||||||
self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL'])
|
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||||
|
|
||||||
if not fully_canceled:
|
if not fully_canceled:
|
||||||
# Get current rate and execute sell
|
# Get current rate and execute sell
|
||||||
current_rate = self._freqtrade.exchange.get_rate(
|
current_rate = self._freqtrade.exchange.get_rate(
|
||||||
trade.pair, refresh=False, side="sell")
|
trade.pair, refresh=False, side="sell")
|
||||||
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||||
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
|
self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
raise RPCException('trader is not running')
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
with self._freqtrade._sell_lock:
|
with self._freqtrade._exit_lock:
|
||||||
if trade_id == 'all':
|
if trade_id == 'all':
|
||||||
# Execute sell for all open orders
|
# Execute sell for all open orders
|
||||||
for trade in Trade.get_open_trades():
|
for trade in Trade.get_open_trades():
|
||||||
@@ -613,7 +629,7 @@ class RPC:
|
|||||||
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
||||||
|
|
||||||
# execute buy
|
# execute buy
|
||||||
if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True):
|
if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True):
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
return trade
|
return trade
|
||||||
@@ -625,7 +641,7 @@ class RPC:
|
|||||||
Handler for delete <id>.
|
Handler for delete <id>.
|
||||||
Delete the given trade and close eventually existing open orders.
|
Delete the given trade and close eventually existing open orders.
|
||||||
"""
|
"""
|
||||||
with self._freqtrade._sell_lock:
|
with self._freqtrade._exit_lock:
|
||||||
c_count = 0
|
c_count = 0
|
||||||
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
||||||
if not trade:
|
if not trade:
|
||||||
@@ -776,7 +792,7 @@ class RPC:
|
|||||||
if has_content:
|
if has_content:
|
||||||
|
|
||||||
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
|
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
|
||||||
# Move open to seperate column when signal for easy plotting
|
# Move open to separate column when signal for easy plotting
|
||||||
if 'buy' in dataframe.columns:
|
if 'buy' in dataframe.columns:
|
||||||
buy_mask = (dataframe['buy'] == 1)
|
buy_mask = (dataframe['buy'] == 1)
|
||||||
buy_signals = int(buy_mask.sum())
|
buy_signals = int(buy_mask.sum())
|
||||||
|
@@ -15,6 +15,7 @@ class RPCManager:
|
|||||||
"""
|
"""
|
||||||
Class to manage RPC objects (Telegram, API, ...)
|
Class to manage RPC objects (Telegram, API, ...)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, freqtrade) -> None:
|
def __init__(self, freqtrade) -> None:
|
||||||
""" Initializes all enabled rpc modules """
|
""" Initializes all enabled rpc modules """
|
||||||
self.registered_modules: List[RPCHandler] = []
|
self.registered_modules: List[RPCHandler] = []
|
||||||
|
@@ -77,7 +77,6 @@ class Telegram(RPCHandler):
|
|||||||
""" This class handles all telegram communication """
|
""" This class handles all telegram communication """
|
||||||
|
|
||||||
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
|
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Init the Telegram call, and init the super class RPCHandler
|
Init the Telegram call, and init the super class RPCHandler
|
||||||
:param rpc: instance of RPC Helper class
|
:param rpc: instance of RPC Helper class
|
||||||
@@ -208,15 +207,25 @@ class Telegram(RPCHandler):
|
|||||||
else:
|
else:
|
||||||
msg['stake_amount_fiat'] = 0
|
msg['stake_amount_fiat'] = 0
|
||||||
|
|
||||||
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
|
content = []
|
||||||
|
content.append(
|
||||||
|
f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
|
||||||
f" (#{msg['trade_id']})\n"
|
f" (#{msg['trade_id']})\n"
|
||||||
f"*Amount:* `{msg['amount']:.8f}`\n"
|
)
|
||||||
f"*Open Rate:* `{msg['limit']:.8f}`\n"
|
if msg.get('buy_tag', None):
|
||||||
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n")
|
||||||
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
|
content.append(f"*Amount:* `{msg['amount']:.8f}`\n")
|
||||||
|
content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n")
|
||||||
|
content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n")
|
||||||
|
content.append(
|
||||||
|
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
||||||
|
)
|
||||||
if msg.get('fiat_currency', None):
|
if msg.get('fiat_currency', None):
|
||||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
content.append(
|
||||||
|
f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
message = ''.join(content)
|
||||||
message += ")`"
|
message += ")`"
|
||||||
return message
|
return message
|
||||||
|
|
||||||
@@ -251,6 +260,50 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
||||||
|
|
||||||
|
if msg_type == RPCMessageType.BUY:
|
||||||
|
message = self._format_buy_msg(msg)
|
||||||
|
|
||||||
|
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
||||||
|
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
||||||
|
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||||
|
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
||||||
|
"Reason: {reason}.".format(**msg))
|
||||||
|
|
||||||
|
elif msg_type == RPCMessageType.BUY_FILL:
|
||||||
|
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||||
|
"Buy order for {pair} (#{trade_id}) filled "
|
||||||
|
"for {open_rate}.".format(**msg))
|
||||||
|
elif msg_type == RPCMessageType.SELL_FILL:
|
||||||
|
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||||
|
"Sell order for {pair} (#{trade_id}) filled "
|
||||||
|
"for {close_rate}.".format(**msg))
|
||||||
|
elif msg_type == RPCMessageType.SELL:
|
||||||
|
message = self._format_sell_msg(msg)
|
||||||
|
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
||||||
|
message = (
|
||||||
|
"*Protection* triggered due to {reason}. "
|
||||||
|
"`{pair}` will be locked until `{lock_end_time}`."
|
||||||
|
).format(**msg)
|
||||||
|
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
||||||
|
message = (
|
||||||
|
"*Protection* triggered due to {reason}. "
|
||||||
|
"*All pairs* will be locked until `{lock_end_time}`."
|
||||||
|
).format(**msg)
|
||||||
|
elif msg_type == RPCMessageType.STATUS:
|
||||||
|
message = '*Status:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
|
elif msg_type == RPCMessageType.WARNING:
|
||||||
|
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
|
elif msg_type == RPCMessageType.STARTUP:
|
||||||
|
message = '{status}'.format(**msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
|
||||||
|
return message
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
""" Send a message to telegram channel """
|
""" Send a message to telegram channel """
|
||||||
|
|
||||||
@@ -275,37 +328,7 @@ class Telegram(RPCHandler):
|
|||||||
# Notification disabled
|
# Notification disabled
|
||||||
return
|
return
|
||||||
|
|
||||||
if msg_type == RPCMessageType.BUY:
|
message = self.compose_message(msg, msg_type)
|
||||||
message = self._format_buy_msg(msg)
|
|
||||||
|
|
||||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
|
||||||
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
|
||||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
|
||||||
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
|
||||||
"Reason: {reason}.".format(**msg))
|
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.BUY_FILL:
|
|
||||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
|
||||||
"Buy order for {pair} (#{trade_id}) filled "
|
|
||||||
"for {open_rate}.".format(**msg))
|
|
||||||
elif msg_type == RPCMessageType.SELL_FILL:
|
|
||||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
|
||||||
"Sell order for {pair} (#{trade_id}) filled "
|
|
||||||
"for {close_rate}.".format(**msg))
|
|
||||||
elif msg_type == RPCMessageType.SELL:
|
|
||||||
message = self._format_sell_msg(msg)
|
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STATUS:
|
|
||||||
message = '*Status:* `{status}`'.format(**msg)
|
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.WARNING:
|
|
||||||
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STARTUP:
|
|
||||||
message = '{status}'.format(**msg)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
|
|
||||||
|
|
||||||
self._send_msg(message, disable_notification=(noti == 'silent'))
|
self._send_msg(message, disable_notification=(noti == 'silent'))
|
||||||
|
|
||||||
@@ -354,6 +377,7 @@ class Telegram(RPCHandler):
|
|||||||
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
|
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
|
||||||
"*Current Pair:* {pair}",
|
"*Current Pair:* {pair}",
|
||||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
||||||
|
"*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "",
|
||||||
"*Open Rate:* `{open_rate:.8f}`",
|
"*Open Rate:* `{open_rate:.8f}`",
|
||||||
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
||||||
"*Current Rate:* `{current_rate:.8f}`",
|
"*Current Rate:* `{current_rate:.8f}`",
|
||||||
@@ -567,7 +591,8 @@ class Telegram(RPCHandler):
|
|||||||
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
||||||
)
|
)
|
||||||
durations = stats['durations']
|
durations = stats['durations']
|
||||||
duration_msg = tabulate([
|
duration_msg = tabulate(
|
||||||
|
[
|
||||||
['Wins', str(timedelta(seconds=durations['wins']))
|
['Wins', str(timedelta(seconds=durations['wins']))
|
||||||
if durations['wins'] != 'N/A' else 'N/A'],
|
if durations['wins'] != 'N/A' else 'N/A'],
|
||||||
['Losses', str(timedelta(seconds=durations['losses']))
|
['Losses', str(timedelta(seconds=durations['losses']))
|
||||||
@@ -592,12 +617,15 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
output = ''
|
output = ''
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
output += (
|
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
||||||
f"*Warning:* Simulated balances in Dry Mode.\n"
|
|
||||||
"This mode is still experimental!\n"
|
output += ("Starting capital: "
|
||||||
"Starting capital: "
|
f"`{result['starting_capital']}` {self._config['stake_currency']}"
|
||||||
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
|
|
||||||
)
|
)
|
||||||
|
output += (f" `{result['starting_capital_fiat']}` "
|
||||||
|
f"{self._config['fiat_display_currency']}.\n"
|
||||||
|
) if result['starting_capital_fiat'] > 0 else '.\n'
|
||||||
|
|
||||||
total_dust_balance = 0
|
total_dust_balance = 0
|
||||||
total_dust_currencies = 0
|
total_dust_currencies = 0
|
||||||
for curr in result['currencies']:
|
for curr in result['currencies']:
|
||||||
@@ -630,9 +658,12 @@ class Telegram(RPCHandler):
|
|||||||
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
||||||
|
|
||||||
output += ("\n*Estimated Value*:\n"
|
output += ("\n*Estimated Value*:\n"
|
||||||
f"\t`{result['stake']}: {result['total']: .8f}`\n"
|
f"\t`{result['stake']}: "
|
||||||
|
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
||||||
|
f" `({result['starting_capital_pct']}%)`\n"
|
||||||
f"\t`{result['symbol']}: "
|
f"\t`{result['symbol']}: "
|
||||||
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
|
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
||||||
|
f" `({result['starting_capital_fiat_pct']}%)`\n")
|
||||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user