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)
|
||||
|
||||
## Summary
|
||||
|
||||
Explain in one sentence the goal of this PR
|
||||
|
||||
Solve the issue: #___
|
||||
|
||||
## Quick changelog
|
||||
|
||||
- <change log #1>
|
||||
- <change log #2>
|
||||
- <change log 1>
|
||||
- <change log 1>
|
||||
|
||||
## What's new?
|
||||
|
||||
*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: |
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
|
||||
- name: Flake8
|
||||
run: |
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
run: |
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
|
||||
- name: Flake8
|
||||
run: |
|
||||
@@ -247,7 +247,7 @@ jobs:
|
||||
run: |
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
|
||||
- name: Flake8
|
||||
run: |
|
||||
|
@@ -33,7 +33,7 @@ jobs:
|
||||
- script:
|
||||
- cp config_examples/config_bittrex.example.json config.json
|
||||
- freqtrade create-userdir --userdir user_data
|
||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily
|
||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily
|
||||
name: hyperopt
|
||||
- script: 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
|
||||
ENV LANG C.UTF-8
|
||||
@@ -13,7 +13,7 @@ RUN mkdir /freqtrade \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \
|
||||
&& 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 \
|
||||
# Allow 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.
|
||||
|
||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||
- [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] [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)_
|
||||
|
||||
### 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:
|
||||
|
||||
- [X] [Bitvavo](https://bitvavo.com/)
|
||||
- [X] [Kukoin](https://www.kucoin.com/)
|
||||
- [X] [Kucoin](https://www.kucoin.com/)
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -78,22 +79,22 @@ For any other type of installation please refer to [Installation doc](https://ww
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
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.
|
||||
create-userdir Create user-data directory.
|
||||
new-config Create new config
|
||||
new-hyperopt Create new hyperopt
|
||||
new-strategy Create new strategy
|
||||
download-data Download backtesting data.
|
||||
convert-data Convert candle (OHLCV) data from one format to
|
||||
another.
|
||||
convert-trade-data Convert trade data from one format to another.
|
||||
list-data List downloaded data.
|
||||
backtesting Backtesting module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
@@ -107,8 +108,10 @@ positional arguments:
|
||||
list-timeframes Print available timeframes for the exchange.
|
||||
show-trades Show trades.
|
||||
test-pairlist Test your pairlist configuration.
|
||||
install-ui Install FreqUI
|
||||
plot-dataframe Plot candles with indicators.
|
||||
plot-profit Generate plot showing profits.
|
||||
webserver Webserver module.
|
||||
|
||||
optional arguments:
|
||||
-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 \
|
||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||
&& make -j$(nproc) \
|
||||
&& which sudo && sudo make install || make install \
|
||||
&& cd ..
|
||||
&& which sudo && sudo make install || make install
|
||||
if [ -x "$(command -v apt-get)" ]; then
|
||||
echo "Updating library path using ldconfig"
|
||||
sudo ldconfig
|
||||
fi
|
||||
cd .. && rm -rf ./ta-lib/
|
||||
else
|
||||
echo "TA-lib already installed, skipping installation"
|
||||
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
|
||||
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
|
||||
|
||||
# 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
|
||||
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 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}
|
||||
|
||||
Tag as latest for develop builds
|
||||
# Tag as latest for develop builds
|
||||
if [ "${TAG}" = "develop" ]; then
|
||||
docker tag ${IMAGE_NAME}:develop ${IMAGE_NAME}:latest
|
||||
docker push ${IMAGE_NAME}:latest
|
||||
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
|
||||
docker manifest push -p ${IMAGE_NAME}:latest
|
||||
fi
|
||||
|
||||
docker images
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed building image"
|
||||
return 1
|
||||
fi
|
||||
# Cleanup old images from arm64 node.
|
||||
docker image prune -a --force --filter "until=24h"
|
||||
|
@@ -48,12 +48,12 @@ fi
|
||||
# Tag image for upload and next build step
|
||||
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
|
||||
|
||||
# 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
|
||||
echo "failed running backtest"
|
||||
|
@@ -78,33 +78,6 @@
|
||||
"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": {
|
||||
"name": "binance",
|
||||
"sandbox": false,
|
||||
@@ -176,7 +149,9 @@
|
||||
},
|
||||
"sell_fill": "on",
|
||||
"buy_cancel": "on",
|
||||
"sell_cancel": "on"
|
||||
"sell_cancel": "on",
|
||||
"protection_trigger": "off",
|
||||
"protection_trigger_global": "on"
|
||||
},
|
||||
"reload": true,
|
||||
"balance_dust_level": 0.01
|
||||
@@ -201,7 +176,7 @@
|
||||
"heartbeat_interval": 60
|
||||
},
|
||||
"disable_dataframe_checks": false,
|
||||
"strategy": "DefaultStrategy",
|
||||
"strategy": "SampleStrategy",
|
||||
"strategy_path": "user_data/strategies/",
|
||||
"dataformat_ohlcv": "json",
|
||||
"dataformat_trades": "jsongz"
|
||||
|
@@ -1,5 +1,6 @@
|
||||
ARG sourceimage=develop
|
||||
FROM freqtradeorg/freqtrade:${sourceimage}
|
||||
ARG sourceimage=freqtradeorg/freqtrade
|
||||
ARG sourcetag=develop
|
||||
FROM ${sourceimage}:${sourcetag}
|
||||
|
||||
# Install dependencies
|
||||
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.
|
||||
|
||||
!!! 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
|
||||
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later.
|
||||
!!! Note "`*args` and `**kwargs`"
|
||||
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface in the future.
|
||||
|
||||
## 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 HyperOpt:
|
||||
# Define a custom stoploss space.
|
||||
def stoploss_space(self):
|
||||
def stoploss_space():
|
||||
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
|
||||
|
||||
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]`).
|
||||
|
||||
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]
|
||||
[--enable-protections]
|
||||
[--dry-run-wallet DRY_RUN_WALLET]
|
||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export {none,trades}] [--export-filename PATH]
|
||||
|
||||
@@ -55,6 +56,9 @@ optional arguments:
|
||||
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
|
||||
Starting balance, used for backtesting / hyperopt and
|
||||
dry-runs.
|
||||
--timeframe-detail TIMEFRAME_DETAIL
|
||||
Specify detail timeframe for backtesting (`1m`, `5m`,
|
||||
`30m`, `1h`, `1d`).
|
||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||
Provide a space-separated list of strategies to
|
||||
backtest. Please note that ticker-interval needs to be
|
||||
@@ -62,7 +66,7 @@ optional arguments:
|
||||
this together with `--export trades`, the strategy-
|
||||
name is injected into the filename (so `backtest-
|
||||
data.json` becomes `backtest-data-
|
||||
DefaultStrategy.json`
|
||||
SampleStrategy.json`
|
||||
--export {none,trades}
|
||||
Export backtest results (default: trades).
|
||||
--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).
|
||||
- `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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
### Further backtest-result analysis
|
||||
### Improved backtest accuracy
|
||||
|
||||
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.
|
||||
One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?).
|
||||
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
|
||||
|
||||
|
@@ -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.
|
||||
* **Trade**: Open position.
|
||||
* **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"`, ...).
|
||||
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
||||
* **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_sell_timeout()` strategy callback for open sell orders.
|
||||
* Verifies existing positions and eventually places sell orders.
|
||||
* Considers stoploss, ROI and sell-signal.
|
||||
* Determine sell-price based on `ask_strategy` configuration setting.
|
||||
* Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
|
||||
* Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
|
||||
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||
* Verifies buy signal trying to enter new positions.
|
||||
* Determine buy-price based on `bid_strategy` configuration setting.
|
||||
* 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.
|
||||
|
||||
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.
|
||||
* Calls `bot_loop_start()` once.
|
||||
* Calculate indicators (calls `populate_indicators()` 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)
|
||||
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
|
||||
* Loops per candle simulating entry and exit points.
|
||||
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
||||
* Generate backtest report output
|
||||
|
||||
!!! Note
|
||||
|
@@ -12,22 +12,22 @@ This page explains the different parameters of the bot and how to run it.
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
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.
|
||||
create-userdir Create user-data directory.
|
||||
new-config Create new config
|
||||
new-hyperopt Create new hyperopt
|
||||
new-strategy Create new strategy
|
||||
download-data Download backtesting data.
|
||||
convert-data Convert candle (OHLCV) data from one format to
|
||||
another.
|
||||
convert-trade-data Convert trade data from one format to another.
|
||||
list-data List downloaded data.
|
||||
backtesting Backtesting module.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
@@ -41,8 +41,10 @@ positional arguments:
|
||||
list-timeframes Print available timeframes for the exchange.
|
||||
show-trades Show trades.
|
||||
test-pairlist Test your pairlist configuration.
|
||||
install-ui Install FreqUI
|
||||
plot-dataframe Plot candles with indicators.
|
||||
plot-profit Generate plot showing profits.
|
||||
webserver Webserver module.
|
||||
|
||||
optional arguments:
|
||||
-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.
|
||||
|
||||
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.
|
||||
|
||||
!!! 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.
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
- CLI arguments override any other option
|
||||
- [Environment Variables](#environment-variables)
|
||||
- Configuration files are used in sequence (the last file wins) and override Strategy configurations.
|
||||
- Strategy configurations are only used if they are not set via configuration or command-line arguments. These options are marked with [Strategy Override](#parameters-in-the-strategy) in the below table.
|
||||
|
||||
@@ -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
|
||||
| `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_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_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_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.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
|
||||
@@ -422,8 +444,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
This is ongoing work. For now, it is supported only for binance.
|
||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values.
|
||||
This is ongoing work. For now, it is supported only for binance and kucoin.
|
||||
Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange.
|
||||
|
||||
### Exchange configuration
|
||||
|
||||
@@ -526,9 +548,10 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
|
||||
|
||||
## Switch to production mode
|
||||
|
||||
In production mode, the bot will engage your money. Be careful, since a wrong
|
||||
strategy can lose all your money. Be aware of what you are doing when
|
||||
you run it in production mode.
|
||||
In production mode, the bot will engage your money. Be careful, since a wrong strategy can lose all your money.
|
||||
Be aware of what you are doing when 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
|
||||
|
||||
|
@@ -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.
|
||||
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
|
||||
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.
|
||||
|
||||
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
|
||||
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
|
||||
|
||||
Also try to use `freqtrade download-data` for an extended timerange and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
|
||||
|
||||
### Stoploss On Exchange
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
!!! 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
|
||||
`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 supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
|
||||
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 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
|
||||
"exchange": {
|
||||
@@ -113,8 +121,12 @@ Kucoin requries a passphrase for each api key, you will therefore need to add th
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"password": "your_exchange_api_key_password",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
### Kucoin Blacklists
|
||||
|
||||
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"],
|
||||
"ohlcv_candle_limit": 200
|
||||
}
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
!!! 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.
|
||||
|
||||
```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?
|
||||
|
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}]
|
||||
[--max-open-trades INT]
|
||||
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
|
||||
[-p PAIRS [PAIRS ...]] [--hyperopt NAME]
|
||||
[--hyperopt-path PATH] [--eps] [--dmmp]
|
||||
[--enable-protections]
|
||||
[-p PAIRS [PAIRS ...]] [--hyperopt-path PATH]
|
||||
[--eps] [--dmmp] [--enable-protections]
|
||||
[--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]
|
||||
[--random-state INT] [--min-trades INT]
|
||||
[--hyperopt-loss NAME] [--disable-param-export]
|
||||
@@ -73,10 +72,8 @@ optional arguments:
|
||||
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
|
||||
Limit command to these pairs. Pairs are space-
|
||||
separated.
|
||||
--hyperopt NAME Specify hyperopt class name which will be used by the
|
||||
bot.
|
||||
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
|
||||
Hyperopt Loss functions.
|
||||
--hyperopt-path PATH Specify additional lookup path for Hyperopt Loss
|
||||
functions.
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
@@ -92,7 +89,7 @@ optional arguments:
|
||||
Starting balance, used for backtesting / hyperopt and
|
||||
dry-runs.
|
||||
-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
|
||||
list.
|
||||
--print-all Print all results, not only the best ones.
|
||||
@@ -253,7 +250,7 @@ We continue to define hyperoptable parameters:
|
||||
class MyAwesomeStrategy(IStrategy):
|
||||
buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, 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_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.
|
||||
* `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.
|
||||
* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters.
|
||||
|
||||
!!! Tip "Disabling parameter optimization"
|
||||
Each parameter takes two boolean parameters:
|
||||
@@ -326,7 +324,7 @@ There are four parameter types each suited for different purposes.
|
||||
!!! 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.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -336,8 +334,8 @@ from functools import reduce
|
||||
|
||||
import talib.abstract as ta
|
||||
|
||||
from freqtrade.strategy import IStrategy
|
||||
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter
|
||||
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IStrategy, IntParameter)
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
|
||||
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).
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
freqtrade hyperopt --hyperopt <hyperoptname> --strategy <strategyname> --timerange 20210101-20210201
|
||||
freqtrade hyperopt --strategy <strategyname> --timerange 20210101-20210201
|
||||
```
|
||||
|
||||
### Running Hyperopt with Smaller Search Space
|
||||
@@ -483,7 +573,8 @@ Legal values are:
|
||||
* `roi`: just optimize the minimal profit table for your strategy
|
||||
* `stoploss`: search for the best stoploss value
|
||||
* `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`
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
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"
|
||||
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.
|
||||
|
||||
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"
|
||||
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.
|
||||
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
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 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 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",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"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.
|
||||
|
||||
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",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 86400,
|
||||
"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"
|
||||
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:
|
||||
|
||||
```json
|
||||
@@ -109,6 +133,7 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"min_value": 0,
|
||||
"refresh_period": 3600,
|
||||
"lookback_timeframe": "1h",
|
||||
"lookback_period": 72
|
||||
@@ -140,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist:
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
// ...
|
||||
{
|
||||
"method": "OffsetFilter",
|
||||
"offset": 10
|
||||
@@ -165,6 +191,19 @@ Sorts pairs by past trade performance, as follows:
|
||||
|
||||
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
|
||||
`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
|
||||
|
||||
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:
|
||||
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
|
||||
"pairlists": [
|
||||
@@ -232,6 +271,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit
|
||||
"method": "RangeStabilityFilter",
|
||||
"lookback_days": 10,
|
||||
"min_rate_of_change": 0.01,
|
||||
"max_rate_of_change": 0.99,
|
||||
"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
|
||||
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
|
||||
|
||||
|
@@ -15,6 +15,10 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
||||
!!! Note "Backtesting"
|
||||
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
|
||||
|
||||
!!! Warning "Setting protections from the configuration"
|
||||
Setting protections from the configuration via `"protections": [],` key should be considered deprecated and will be removed in a future version.
|
||||
It is also no longer guaranteed that your protections apply to the strategy in cases where the strategy defines [protections as property](hyperopt.md#optimizing-protections).
|
||||
|
||||
### Available Protections
|
||||
|
||||
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
|
||||
@@ -47,15 +51,17 @@ 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.
|
||||
|
||||
``` python
|
||||
protections = [
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period_candles": 24,
|
||||
"trade_limit": 4,
|
||||
"stop_duration_candles": 4,
|
||||
"only_per_pair": False
|
||||
}
|
||||
]
|
||||
@property
|
||||
def protections(self):
|
||||
return [
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period_candles": 24,
|
||||
"trade_limit": 4,
|
||||
"stop_duration_candles": 4,
|
||||
"only_per_pair": False
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
!!! Note
|
||||
@@ -69,15 +75,17 @@ 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.
|
||||
|
||||
``` python
|
||||
protections = [
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"lookback_period_candles": 48,
|
||||
"trade_limit": 20,
|
||||
"stop_duration_candles": 12,
|
||||
"max_allowed_drawdown": 0.2
|
||||
},
|
||||
]
|
||||
@property
|
||||
def protections(self):
|
||||
return [
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"lookback_period_candles": 48,
|
||||
"trade_limit": 20,
|
||||
"stop_duration_candles": 12,
|
||||
"max_allowed_drawdown": 0.2
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
#### Low Profit Pairs
|
||||
@@ -88,15 +96,17 @@ 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.
|
||||
|
||||
``` python
|
||||
protections = [
|
||||
{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period_candles": 6,
|
||||
"trade_limit": 2,
|
||||
"stop_duration": 60,
|
||||
"required_profit": 0.02
|
||||
}
|
||||
]
|
||||
@property
|
||||
def protections(self):
|
||||
return [
|
||||
{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period_candles": 6,
|
||||
"trade_limit": 2,
|
||||
"stop_duration": 60,
|
||||
"required_profit": 0.02
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 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".
|
||||
|
||||
``` python
|
||||
protections = [
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"stop_duration_candles": 2
|
||||
}
|
||||
]
|
||||
@property
|
||||
def protections(self):
|
||||
return [
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"stop_duration_candles": 2
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
!!! Note
|
||||
@@ -136,39 +148,42 @@ from freqtrade.strategy import IStrategy
|
||||
|
||||
class AwesomeStrategy(IStrategy)
|
||||
timeframe = '1h'
|
||||
protections = [
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"stop_duration_candles": 5
|
||||
},
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"lookback_period_candles": 48,
|
||||
"trade_limit": 20,
|
||||
"stop_duration_candles": 4,
|
||||
"max_allowed_drawdown": 0.2
|
||||
},
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period_candles": 24,
|
||||
"trade_limit": 4,
|
||||
"stop_duration_candles": 2,
|
||||
"only_per_pair": False
|
||||
},
|
||||
{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period_candles": 6,
|
||||
"trade_limit": 2,
|
||||
"stop_duration_candles": 60,
|
||||
"required_profit": 0.02
|
||||
},
|
||||
{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period_candles": 24,
|
||||
"trade_limit": 4,
|
||||
"stop_duration_candles": 2,
|
||||
"required_profit": 0.01
|
||||
}
|
||||
]
|
||||
|
||||
@property
|
||||
def protections(self):
|
||||
return [
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"stop_duration_candles": 5
|
||||
},
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"lookback_period_candles": 48,
|
||||
"trade_limit": 20,
|
||||
"stop_duration_candles": 4,
|
||||
"max_allowed_drawdown": 0.2
|
||||
},
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period_candles": 24,
|
||||
"trade_limit": 4,
|
||||
"stop_duration_candles": 2,
|
||||
"only_per_pair": False
|
||||
},
|
||||
{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period_candles": 6,
|
||||
"trade_limit": 2,
|
||||
"stop_duration_candles": 60,
|
||||
"required_profit": 0.02
|
||||
},
|
||||
{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period_candles": 24,
|
||||
"trade_limit": 4,
|
||||
"stop_duration_candles": 2,
|
||||
"required_profit": 0.01
|
||||
}
|
||||
]
|
||||
# ...
|
||||
```
|
||||
|
@@ -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.
|
||||
|
||||
- [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] [FTX](https://ftx.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)_
|
||||
|
||||
### Community tested
|
||||
@@ -47,7 +48,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
||||
Exchanges confirmed working by the community:
|
||||
|
||||
- [X] [Bitvavo](https://bitvavo.com/)
|
||||
- [X] [Kukoin](https://www.kucoin.com/)
|
||||
- [X] [Kucoin](https://www.kucoin.com/)
|
||||
|
||||
## Requirements
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
mkdocs==1.2.2
|
||||
mkdocs-material==7.2.1
|
||||
mkdocs-material==7.3.0
|
||||
mdx_truly_sane_lists==1.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.
|
||||
|
||||
Installation:
|
||||
`pip install psycopg2`
|
||||
`pip install psycopg2-binary`
|
||||
|
||||
Usage:
|
||||
`... --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.
|
||||
|
||||
## 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
|
||||
|
||||
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()`.
|
||||
|
||||
### Calculating stoploss percentage from absolute price
|
||||
|
||||
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||
|
||||
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||
|
||||
#### Stepped stoploss
|
||||
|
||||
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
||||
@@ -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
|
||||
|
||||
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!
|
||||
|
||||
## 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.
|
||||
|
||||
!!! 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)
|
||||
|
||||
@@ -781,6 +942,8 @@ Printing more than a few rows is also possible (simply use `print(dataframe)` i
|
||||
|
||||
## 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.
|
||||
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
|
||||
# graph.show()
|
||||
|
||||
# Render graph in a seperate window
|
||||
# Render graph in a separate window
|
||||
graph.show(renderer="browser")
|
||||
|
||||
```
|
||||
|
@@ -93,7 +93,9 @@ Example configuration showing the different settings:
|
||||
"buy_cancel": "silent",
|
||||
"sell_cancel": "on",
|
||||
"buy_fill": "off",
|
||||
"sell_fill": "off"
|
||||
"sell_fill": "off",
|
||||
"protection_trigger": "off",
|
||||
"protection_trigger_global": "on"
|
||||
},
|
||||
"reload": true,
|
||||
"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.
|
||||
`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.
|
||||
`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.
|
||||
|
@@ -26,9 +26,7 @@ optional arguments:
|
||||
├── data
|
||||
├── hyperopt_results
|
||||
├── hyperopts
|
||||
│ ├── sample_hyperopt_advanced.py
|
||||
│ ├── sample_hyperopt_loss.py
|
||||
│ └── sample_hyperopt.py
|
||||
├── notebooks
|
||||
│ └── strategy_analysis_example.ipynb
|
||||
├── plot
|
||||
@@ -111,46 +109,11 @@ Using the advanced template (populates all optional functions and methods)
|
||||
freqtrade new-strategy --strategy AwesomeStrategy --template advanced
|
||||
```
|
||||
|
||||
## Create new hyperopt
|
||||
## List Strategies
|
||||
|
||||
Creates a new hyperopt from a template similar to SampleHyperopt.
|
||||
The file will be named inline with your class name, and will not overwrite existing files.
|
||||
Use the `list-strategies` subcommand to see all strategies in one particular directory.
|
||||
|
||||
Results will be located in `user_data/hyperopts/<classname>.py`.
|
||||
|
||||
``` 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).
|
||||
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).
|
||||
|
||||
```
|
||||
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
|
||||
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:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
@@ -211,18 +146,16 @@ Common arguments:
|
||||
!!! 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.
|
||||
|
||||
Example: Search default strategies and hyperopts directories (within the default userdir).
|
||||
Example: Search default strategies directories (within the default userdir).
|
||||
|
||||
``` bash
|
||||
freqtrade list-strategies
|
||||
freqtrade list-hyperopts
|
||||
```
|
||||
|
||||
Example: Search strategies and hyperopts directory within the userdir.
|
||||
Example: Search strategies directory within the userdir.
|
||||
|
||||
``` bash
|
||||
freqtrade list-strategies --userdir ~/.freqtrade/
|
||||
freqtrade list-hyperopts --userdir ~/.freqtrade/
|
||||
```
|
||||
|
||||
Example: Search dedicated strategy path.
|
||||
@@ -231,12 +164,6 @@ Example: Search dedicated strategy path.
|
||||
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
||||
```
|
||||
|
||||
Example: Search dedicated hyperopt path.
|
||||
|
||||
``` bash
|
||||
freqtrade list-hyperopt --hyperopt-path ~/.freqtrade/hyperopts/
|
||||
```
|
||||
|
||||
## List Exchanges
|
||||
|
||||
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]
|
||||
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
||||
[--userdir PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -648,12 +575,6 @@ Common arguments:
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
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
|
||||
|
@@ -83,6 +83,7 @@ Possible parameters are:
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
* `buy_tag`
|
||||
|
||||
### Webhookbuycancel
|
||||
|
||||
@@ -100,6 +101,7 @@ Possible parameters are:
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
* `buy_tag`
|
||||
|
||||
### Webhookbuyfill
|
||||
|
||||
@@ -115,6 +117,7 @@ Possible parameters are:
|
||||
* `stake_amount`
|
||||
* `stake_currency`
|
||||
* `fiat_currency`
|
||||
* `buy_tag`
|
||||
|
||||
### Webhooksell
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2021.7'
|
||||
__version__ = '2021.9'
|
||||
|
||||
if __version__ == 'develop':
|
||||
|
||||
@@ -22,7 +22,7 @@ if __version__ == 'develop':
|
||||
# subprocess.check_output(
|
||||
# ['git', 'log', '--format="%h"', '-n 1'],
|
||||
# stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
|
||||
except Exception:
|
||||
except Exception: # pragma: no cover
|
||||
# git not available, ignore
|
||||
try:
|
||||
# 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,
|
||||
start_list_data)
|
||||
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.list_commands import (start_list_exchanges, start_list_hyperopts,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_show_trades)
|
||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_show_trades)
|
||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||
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"]
|
||||
|
||||
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"]
|
||||
|
||||
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_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
|
||||
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
||||
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",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||
"list-hyperopts", "hyperopt-list", "hyperopt-show",
|
||||
"hyperopt-list", "hyperopt-show",
|
||||
"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:
|
||||
@@ -174,12 +172,11 @@ class Arguments:
|
||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
||||
start_download_data, start_edge, start_hyperopt,
|
||||
start_hyperopt_list, start_hyperopt_show, start_install_ui,
|
||||
start_list_data, start_list_exchanges, start_list_hyperopts,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_new_config, start_new_hyperopt,
|
||||
start_new_strategy, start_plot_dataframe, start_plot_profit,
|
||||
start_show_trades, start_test_pairlist, start_trading,
|
||||
start_webserver)
|
||||
start_list_data, start_list_exchanges, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_new_config, start_new_strategy, start_plot_dataframe,
|
||||
start_plot_profit, start_show_trades, start_test_pairlist,
|
||||
start_trading, start_webserver)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
# Use custom message when no subhandler is added
|
||||
@@ -206,12 +203,6 @@ class Arguments:
|
||||
build_config_cmd.set_defaults(func=start_new_config)
|
||||
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
|
||||
build_strategy_cmd = subparsers.add_parser('new-strategy',
|
||||
help="Create new strategy")
|
||||
@@ -300,15 +291,6 @@ class Arguments:
|
||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||
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
|
||||
list_markets_cmd = subparsers.add_parser(
|
||||
'list-markets',
|
||||
|
@@ -61,21 +61,27 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"type": "text",
|
||||
"name": "stake_currency",
|
||||
"message": "Please insert your stake currency:",
|
||||
"default": 'BTC',
|
||||
"default": 'USDT',
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "stake_amount",
|
||||
"message": "Please insert your stake amount:",
|
||||
"default": "0.01",
|
||||
"message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"default": "100",
|
||||
"validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val),
|
||||
"filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"'
|
||||
if val == UNLIMITED_STAKE_AMOUNT
|
||||
else val
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"name": "max_open_trades",
|
||||
"message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):",
|
||||
"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",
|
||||
@@ -99,6 +105,8 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"bittrex",
|
||||
"kraken",
|
||||
"ftx",
|
||||
"kucoin",
|
||||
"gateio",
|
||||
Separator(),
|
||||
"other",
|
||||
],
|
||||
@@ -122,6 +130,12 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"message": "Insert Exchange Secret",
|
||||
"when": lambda x: not x['dry_run']
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"name": "exchange_key_password",
|
||||
"message": "Insert Exchange API Key password",
|
||||
"when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin'
|
||||
},
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "telegram",
|
||||
@@ -193,7 +207,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
|
||||
selections['exchange'] = render_template(
|
||||
templatefile=f"subtemplates/exchange_{exchange_template}.j2",
|
||||
arguments=selections
|
||||
)
|
||||
)
|
||||
except TemplateNotFound:
|
||||
selections['exchange'] = render_template(
|
||||
templatefile="subtemplates/exchange_generic.j2",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Definition of cli arguments used in arguments.py
|
||||
"""
|
||||
from argparse import ArgumentTypeError
|
||||
from argparse import SUPPRESS, ArgumentTypeError
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
||||
@@ -135,6 +135,10 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help='Override the value of the `stake_amount` configuration setting.',
|
||||
),
|
||||
# Backtesting
|
||||
"timeframe_detail": Arg(
|
||||
'--timeframe-detail',
|
||||
help='Specify detail timeframe for backtesting (`1m`, `5m`, `30m`, `1h`, `1d`).',
|
||||
),
|
||||
"position_stacking": Arg(
|
||||
'--eps', '--enable-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 '
|
||||
'or via command line. When using this together with `--export trades`, '
|
||||
'the strategy-name is injected into the filename '
|
||||
'(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`',
|
||||
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',
|
||||
nargs='+',
|
||||
),
|
||||
"export": Arg(
|
||||
@@ -199,13 +203,13 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
# Hyperopt
|
||||
"hyperopt": Arg(
|
||||
'--hyperopt',
|
||||
help='Specify hyperopt class name which will be used by the bot.',
|
||||
help=SUPPRESS,
|
||||
metavar='NAME',
|
||||
required=False,
|
||||
),
|
||||
"hyperopt_path": Arg(
|
||||
'--hyperopt-path',
|
||||
help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.',
|
||||
help='Specify additional lookup path for Hyperopt Loss functions.',
|
||||
metavar='PATH',
|
||||
),
|
||||
"epochs": Arg(
|
||||
@@ -218,7 +222,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"spaces": Arg(
|
||||
'--spaces',
|
||||
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='+',
|
||||
default='default',
|
||||
),
|
||||
|
@@ -7,7 +7,7 @@ import requests
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||
from freqtrade.constants import USERPATH_STRATEGIES
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import render_template, render_template_with_fallback
|
||||
@@ -38,15 +38,15 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
||||
indicators = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/indicators_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/indicators_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
buy_trend = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
sell_trend = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2",
|
||||
)
|
||||
)
|
||||
plot_config = render_template_with_fallback(
|
||||
templatefile=f"subtemplates/plot_config_{subtemplate}.j2",
|
||||
templatefallbackfile=f"subtemplates/plot_config_{fallback}.j2",
|
||||
@@ -74,8 +74,6 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
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')
|
||||
|
||||
@@ -89,58 +87,6 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
||||
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):
|
||||
if directory.is_dir():
|
||||
logger.info("Removing UI directory content.")
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict
|
||||
|
||||
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_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(
|
||||
config['user_data_dir'] / 'hyperopt_results',
|
||||
config.get('hyperoptexportfilename'))
|
||||
|
||||
# Previous evaluations
|
||||
epochs = HyperoptTools.load_previous_results(results_file)
|
||||
total_epochs = len(epochs)
|
||||
|
||||
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
||||
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
||||
|
||||
if print_colorized:
|
||||
colorama_init(autoreset=True)
|
||||
@@ -59,7 +41,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
if not export_csv:
|
||||
try:
|
||||
print(HyperoptTools.get_result_table(config, epochs, total_epochs,
|
||||
not filteroptions['only_best'],
|
||||
not config.get('hyperopt_list_best', False),
|
||||
print_colorized, 0))
|
||||
except KeyboardInterrupt:
|
||||
print('User interrupted..')
|
||||
@@ -71,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
|
||||
if epochs and export_csv:
|
||||
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)
|
||||
|
||||
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
|
||||
epochs = HyperoptTools.load_previous_results(results_file)
|
||||
total_epochs = len(epochs)
|
||||
epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
|
||||
|
||||
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
||||
filtered_epochs = len(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,
|
||||
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 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.exceptions import OperationalException
|
||||
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))
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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
|
||||
|
||||
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_validation import validate_config_consistency
|
||||
from freqtrade.configuration.configuration import Configuration
|
||||
from freqtrade.configuration.PeriodicCache import PeriodicCache
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
|
@@ -10,19 +10,6 @@ from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt,
|
||||
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:
|
||||
"""
|
||||
Check if the exchange name in the config file is supported by Freqtrade
|
||||
@@ -51,10 +38,10 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
||||
|
||||
if not is_exchange_known_ccxt(exchange):
|
||||
raise OperationalException(
|
||||
f'Exchange "{exchange}" is not known to the ccxt library '
|
||||
f'and therefore not available for the bot.\n'
|
||||
f'The following exchanges are available for Freqtrade: '
|
||||
f'{", ".join(available_exchanges())}'
|
||||
f'Exchange "{exchange}" is not known to the ccxt library '
|
||||
f'and therefore not available for the bot.\n'
|
||||
f'The following exchanges are available for Freqtrade: '
|
||||
f'{", ".join(available_exchanges())}'
|
||||
)
|
||||
|
||||
valid, reason = validate_exchange(exchange)
|
||||
|
@@ -3,7 +3,6 @@ from typing import Any, Dict
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
|
||||
from .check_exchange import remove_credentials
|
||||
from .config_validation import validate_config_consistency
|
||||
from .configuration import Configuration
|
||||
|
||||
@@ -21,8 +20,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
|
||||
configuration = Configuration(args, method)
|
||||
config = configuration.get_config()
|
||||
|
||||
# Ensure we do not use Exchange credentials
|
||||
remove_credentials(config)
|
||||
# Ensure these modes are using Dry-run
|
||||
config['dry_run'] = True
|
||||
validate_config_consistency(config)
|
||||
|
||||
return config
|
||||
|
@@ -115,7 +115,7 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
|
||||
if conf.get('stoploss') == 0.0:
|
||||
raise OperationalException(
|
||||
'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
|
||||
)
|
||||
)
|
||||
# Skip if trailing stoploss is not activated
|
||||
if not conf.get('trailing_stop', False):
|
||||
return
|
||||
@@ -180,7 +180,7 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
|
||||
raise OperationalException(
|
||||
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
|
||||
f"Please fix the protection {prot.get('method')}"
|
||||
)
|
||||
)
|
||||
|
||||
if ('lookback_period' in prot and 'lookback_period_candles' in prot):
|
||||
raise OperationalException(
|
||||
|
@@ -11,6 +11,7 @@ from freqtrade import constants
|
||||
from freqtrade.configuration.check_exchange import check_exchange
|
||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||
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.enums import NON_UTIL_MODES, TRADING_MODES, RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
@@ -71,6 +72,11 @@ class Configuration:
|
||||
|
||||
# Merge config options, overwriting old values
|
||||
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
|
||||
# Normalize config
|
||||
if 'internals' not in config:
|
||||
@@ -236,6 +242,9 @@ class Configuration:
|
||||
except ValueError:
|
||||
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',
|
||||
logstring='Parameter --stake-amount detected, '
|
||||
'overriding stake_amount to: {} ...')
|
||||
|
@@ -108,5 +108,8 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
raise OperationalException(
|
||||
"Both 'timeframe' and 'ticker_interval' detected."
|
||||
"Please remove 'ticker_interval' from your configuration to continue operating."
|
||||
)
|
||||
)
|
||||
config['timeframe'] = config['ticker_interval']
|
||||
|
||||
if 'protections' in config:
|
||||
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")
|
||||
|
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'
|
||||
|
||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||
|
||||
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
||||
|
||||
|
||||
# Define decimals per coin for outputs
|
||||
@@ -66,9 +69,7 @@ DUST_PER_COIN = {
|
||||
# Source files with destination directories within user-directory
|
||||
USER_DATA_FILES = {
|
||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
|
||||
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
||||
'sample_hyperopt.py': USERPATH_HYPEROPTS,
|
||||
'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS,
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'tradable_balance_ratio': {
|
||||
'type': 'number',
|
||||
'minimum': 0.1,
|
||||
'minimum': 0.0,
|
||||
'maximum': 1,
|
||||
'default': 0.99
|
||||
},
|
||||
@@ -190,6 +191,9 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'required': ['price_side']
|
||||
},
|
||||
'custom_price_max_distance_ratio': {
|
||||
'type': 'number', 'minimum': 0.0
|
||||
},
|
||||
'order_types': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
@@ -279,7 +283,16 @@ CONF_SCHEMA = {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
},
|
||||
},
|
||||
'protection_trigger': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
},
|
||||
'protection_trigger_global': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
},
|
||||
}
|
||||
},
|
||||
'reload': {'type': 'boolean'},
|
||||
|
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
||||
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
|
||||
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||
|
||||
# Mid-term format, 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',
|
||||
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
|
||||
'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',
|
||||
'profit_ratio', 'profit_abs', 'sell_reason',
|
||||
'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:
|
||||
|
@@ -242,7 +242,7 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to:
|
||||
:param config: Config dictionary
|
||||
:param convert_from: Source 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
|
||||
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 convert_from: Source 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
|
||||
src = get_datahandler(config['datadir'], convert_from)
|
||||
|
@@ -10,11 +10,12 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||
from freqtrade.data.history import load_pair_history
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -31,6 +32,7 @@ class DataProvider:
|
||||
self._pairlists = pairlists
|
||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||
self.__slice_index: Optional[int] = None
|
||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||
|
||||
def _set_dataframe_max_index(self, limit_index: int):
|
||||
"""
|
||||
@@ -62,11 +64,22 @@ class DataProvider:
|
||||
:param pair: pair to get the data for
|
||||
:param timeframe: timeframe to get data for
|
||||
"""
|
||||
return load_pair_history(pair=pair,
|
||||
timeframe=timeframe or self._config['timeframe'],
|
||||
datadir=self._config['datadir'],
|
||||
data_format=self._config.get('dataformat_ohlcv', 'json')
|
||||
)
|
||||
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'],
|
||||
datadir=self._config['datadir'],
|
||||
timerange=timerange,
|
||||
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:
|
||||
"""
|
||||
@@ -136,6 +149,8 @@ class DataProvider:
|
||||
Clear pair dataframe cache.
|
||||
"""
|
||||
self.__cached_pairs = {}
|
||||
self.__cached_pairs_backtesting = {}
|
||||
self.__slice_index = 0
|
||||
|
||||
# Exchange functions
|
||||
|
||||
|
@@ -117,10 +117,11 @@ def refresh_data(datadir: Path,
|
||||
:param timerange: Limit data to be loaded to this timerange
|
||||
"""
|
||||
data_handler = get_datahandler(datadir, data_format)
|
||||
for pair in pairs:
|
||||
_download_pair_history(pair=pair, timeframe=timeframe,
|
||||
datadir=datadir, timerange=timerange,
|
||||
exchange=exchange, data_handler=data_handler)
|
||||
for idx, pair in enumerate(pairs):
|
||||
process = f'{idx}/{len(pairs)}'
|
||||
_download_pair_history(pair=pair, process=process,
|
||||
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],
|
||||
@@ -153,13 +154,14 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona
|
||||
return data, start_ms
|
||||
|
||||
|
||||
def _download_pair_history(datadir: Path,
|
||||
def _download_pair_history(pair: str, *,
|
||||
datadir: Path,
|
||||
exchange: Exchange,
|
||||
pair: str, *,
|
||||
new_pairs_days: int = 30,
|
||||
timeframe: str = '5m',
|
||||
timerange: Optional[TimeRange] = None,
|
||||
data_handler: IDataHandler = None) -> bool:
|
||||
process: str = '',
|
||||
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
|
||||
The data is downloaded starting from the last correct data that
|
||||
@@ -177,7 +179,7 @@ def _download_pair_history(datadir: Path,
|
||||
|
||||
try:
|
||||
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}.'
|
||||
)
|
||||
|
||||
@@ -195,7 +197,8 @@ def _download_pair_history(datadir: Path,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms if since_ms else
|
||||
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 (?)
|
||||
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 = []
|
||||
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:
|
||||
pairs_not_available.append(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}.')
|
||||
|
||||
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
|
||||
_download_pair_history(datadir=datadir, exchange=exchange,
|
||||
pair=pair, timeframe=str(timeframe),
|
||||
new_pairs_days=new_pairs_days,
|
||||
timerange=timerange, data_handler=data_handler)
|
||||
process = f'{idx}/{len(pairs)}'
|
||||
_download_pair_history(pair=pair, process=process,
|
||||
datadir=datadir, exchange=exchange,
|
||||
timerange=timerange, data_handler=data_handler,
|
||||
timeframe=str(timeframe), new_pairs_days=new_pairs_days)
|
||||
return pairs_not_available
|
||||
|
||||
|
||||
|
@@ -62,7 +62,7 @@ class JsonDataHandler(IDataHandler):
|
||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||
_data = data.copy()
|
||||
# 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
|
||||
_data.reset_index(drop=True).loc[:, self._columns].to_json(
|
||||
|
@@ -119,7 +119,7 @@ class Edge:
|
||||
)
|
||||
# Download informative pairs too
|
||||
res = defaultdict(list)
|
||||
for p, t in self.strategy.informative_pairs():
|
||||
for p, t in self.strategy.gather_informative_pairs():
|
||||
res[t].append(p)
|
||||
for timeframe, inf_pairs in res.items():
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
@@ -151,7 +151,7 @@ class Edge:
|
||||
# Fake run-mode to Edge
|
||||
prior_rm = self.config['runmode']
|
||||
self.config['runmode'] = RunMode.EDGE
|
||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
||||
preprocessed = self.strategy.advise_all_indicators(data)
|
||||
self.config['runmode'] = prior_rm
|
||||
|
||||
# Print timeframe
|
||||
@@ -231,12 +231,12 @@ class Edge:
|
||||
'Minimum expectancy and minimum winrate are met only for %s,'
|
||||
' so other pairs are filtered out.',
|
||||
self._final_pairs
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
'Edge removed all pairs as no pair with minimum expectancy '
|
||||
'and minimum winrate was found !'
|
||||
)
|
||||
)
|
||||
|
||||
return self._final_pairs
|
||||
|
||||
@@ -247,7 +247,7 @@ class Edge:
|
||||
final = []
|
||||
for pair, info in self._cached_pairs.items():
|
||||
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
|
||||
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
|
||||
final.append({
|
||||
'Pair': pair,
|
||||
'Winrate': info.winrate,
|
||||
|
@@ -3,5 +3,5 @@ from freqtrade.enums.backteststate import BacktestState
|
||||
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||
from freqtrade.enums.selltype import SellType
|
||||
from freqtrade.enums.signaltype import SignalType
|
||||
from freqtrade.enums.signaltype import SignalTagType, SignalType
|
||||
from freqtrade.enums.state import State
|
||||
|
@@ -11,6 +11,8 @@ class RPCMessageType(Enum):
|
||||
SELL = 'sell'
|
||||
SELL_FILL = 'sell_fill'
|
||||
SELL_CANCEL = 'sell_cancel'
|
||||
PROTECTION_TRIGGER = 'protection_trigger'
|
||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
|
@@ -7,3 +7,10 @@ class SignalType(Enum):
|
||||
"""
|
||||
BUY = "buy"
|
||||
SELL = "sell"
|
||||
|
||||
|
||||
class SignalTagType(Enum):
|
||||
"""
|
||||
Enum for signal columns
|
||||
"""
|
||||
BUY_TAG = "buy_tag"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# flake8: noqa: F401
|
||||
# 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
|
||||
# isort: on
|
||||
from freqtrade.exchange.bibox import Bibox
|
||||
@@ -15,6 +15,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
||||
timeframe_to_seconds, validate_exchange,
|
||||
validate_exchanges)
|
||||
from freqtrade.exchange.ftx import Ftx
|
||||
from freqtrade.exchange.gateio import Gateio
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
|
@@ -1,7 +1,8 @@
|
||||
""" Binance exchange subclass """
|
||||
import logging
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
@@ -18,6 +19,7 @@ class Binance(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"trades_pagination": "id",
|
||||
"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
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool
|
||||
) -> List:
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Calculate backoff
|
||||
|
@@ -19,15 +19,16 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU
|
||||
decimal_to_precision)
|
||||
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.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException, PricingError,
|
||||
RetryableOrderError, TemporaryError)
|
||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES,
|
||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier,
|
||||
retrier_async)
|
||||
from freqtrade.misc import deep_merge_dicts, safe_value_fallback2
|
||||
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||
remove_credentials, retrier, retrier_async)
|
||||
from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2
|
||||
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)
|
||||
_params: Dict = {}
|
||||
|
||||
# Additional headers - added to the ccxt object
|
||||
_headers: Dict = {}
|
||||
|
||||
# Dict to specify which options each exchange implements
|
||||
# This defines defaults, which can be selectively overridden by subclasses using _ft_has
|
||||
# or by specifying them in the configuration.
|
||||
_ft_has_default: Dict = {
|
||||
"stoploss_on_exchange": False,
|
||||
"order_time_in_force": ["gtc"],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_params": {},
|
||||
"ohlcv_candle_limit": 500,
|
||||
"ohlcv_partial_candle": True,
|
||||
@@ -99,6 +104,7 @@ class Exchange:
|
||||
|
||||
# Holds all open sell orders for dry_run
|
||||
self._dry_run_open_orders: Dict[str, Any] = {}
|
||||
remove_credentials(config)
|
||||
|
||||
if config['dry_run']:
|
||||
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())
|
||||
|
||||
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
|
||||
ccxt instance.
|
||||
@@ -187,6 +193,10 @@ class Exchange:
|
||||
}
|
||||
if 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)
|
||||
try:
|
||||
|
||||
@@ -351,9 +361,16 @@ class Exchange:
|
||||
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||
"""
|
||||
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
|
||||
: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()
|
||||
if stake_currency not in quote_currencies:
|
||||
raise OperationalException(
|
||||
@@ -618,6 +635,8 @@ class Exchange:
|
||||
if self.exchange_has('fetchL2OrderBook'):
|
||||
ob = self.fetch_l2_order_book(pair, 20)
|
||||
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
|
||||
filled_amount = 0
|
||||
@@ -626,7 +645,9 @@ class Exchange:
|
||||
book_entry_coin_volume = book_entry[1]
|
||||
if remaining_amount > 0:
|
||||
if remaining_amount < book_entry_coin_volume:
|
||||
# Orderbook at this slot bigger than remaining amount
|
||||
filled_amount += remaining_amount * book_entry_price
|
||||
break
|
||||
else:
|
||||
filled_amount += book_entry_coin_volume * book_entry_price
|
||||
remaining_amount -= book_entry_coin_volume
|
||||
@@ -635,7 +656,14 @@ class Exchange:
|
||||
else:
|
||||
# If remaining_amount wasn't consumed completely (break was not called)
|
||||
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 rate
|
||||
@@ -689,7 +717,17 @@ class Exchange:
|
||||
# Order handling
|
||||
|
||||
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:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
@@ -720,32 +758,6 @@ class Exchange:
|
||||
except ccxt.BaseError as 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:
|
||||
"""
|
||||
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()
|
||||
: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)
|
||||
|
||||
@retrier
|
||||
@@ -1044,7 +1056,7 @@ class Exchange:
|
||||
logger.debug(f"Using Last {conf_strategy['price_side'].capitalize()} / Last Price")
|
||||
ticker = self.fetch_ticker(pair)
|
||||
ticker_rate = ticker[conf_strategy['price_side']]
|
||||
if ticker['last']:
|
||||
if ticker['last'] and ticker_rate:
|
||||
if side == 'buy' and ticker_rate > ticker['last']:
|
||||
balance = conf_strategy['ask_last_balance']
|
||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||
@@ -1183,7 +1195,7 @@ class Exchange:
|
||||
# Historic data
|
||||
|
||||
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.
|
||||
Handles all async work for this.
|
||||
@@ -1195,7 +1207,7 @@ class Exchange:
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
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,
|
||||
since_ms: int) -> DataFrame:
|
||||
@@ -1210,11 +1222,12 @@ class Exchange:
|
||||
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str,
|
||||
timeframe: str,
|
||||
since_ms: int) -> List:
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool
|
||||
) -> List:
|
||||
"""
|
||||
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)
|
||||
@@ -1227,21 +1240,22 @@ class Exchange:
|
||||
pair, timeframe, since) for since in
|
||||
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
||||
|
||||
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
|
||||
# Combine gathered results
|
||||
data: List = []
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(new_data)
|
||||
# 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:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(new_data)
|
||||
# Sort data again after extending the result - above calls return in "async order"
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
logger.info("Downloaded data for %s with length %s.", pair, len(data))
|
||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||
return data
|
||||
|
||||
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))
|
||||
|
||||
input_coroutines = []
|
||||
|
||||
cached_pairs = []
|
||||
# Gather coroutines to run
|
||||
for pair, timeframe in set(pair_list):
|
||||
if (((pair, timeframe) not in self._klines)
|
||||
@@ -1271,6 +1285,7 @@ class Exchange:
|
||||
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
||||
pair, timeframe
|
||||
)
|
||||
cached_pairs.append((pair, timeframe))
|
||||
|
||||
results = asyncio.get_event_loop().run_until_complete(
|
||||
asyncio.gather(*input_coroutines, return_exceptions=True))
|
||||
@@ -1293,6 +1308,10 @@ class Exchange:
|
||||
results_df[(pair, timeframe)] = ohlcv_df
|
||||
if cache:
|
||||
self._klines[(pair, timeframe)] = ohlcv_df
|
||||
# Return cached klines
|
||||
for pair, timeframe in cached_pairs:
|
||||
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)
|
||||
|
||||
return results_df
|
||||
|
||||
def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:
|
||||
@@ -1503,7 +1522,7 @@ class Exchange:
|
||||
:returns List of trade data
|
||||
"""
|
||||
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(
|
||||
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 = {
|
||||
"l2_limit_range": [20, 100],
|
||||
"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)
|
||||
|
||||
# Attach Dataprovider to Strategy baseclass
|
||||
IStrategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.wallets = self.wallets
|
||||
# Attach Dataprovider to strategy instance
|
||||
self.strategy.dp = self.dataprovider
|
||||
# Attach Wallets to strategy instance
|
||||
self.strategy.wallets = self.wallets
|
||||
|
||||
# Initializing Edge only if enabled
|
||||
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
|
||||
|
||||
# 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))
|
||||
|
||||
def notify_status(self, msg: str) -> None:
|
||||
@@ -160,20 +160,20 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# Refreshing candles
|
||||
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)()
|
||||
|
||||
self.strategy.analyze(self.active_pair_whitelist)
|
||||
|
||||
with self._sell_lock:
|
||||
with self._exit_lock:
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
|
||||
# Protect from collisions with forcesell.
|
||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||
# 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()
|
||||
# First process current opened trades (positions)
|
||||
self.exit_positions(trades)
|
||||
@@ -296,9 +296,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
if sell_order:
|
||||
self.refind_lost_order(trade)
|
||||
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.
|
||||
Handles trades where the initial fee-update did not work.
|
||||
@@ -420,20 +420,24 @@ class FreqtradeBot(LoggingMixin):
|
||||
return False
|
||||
|
||||
# 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:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
|
||||
|
||||
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
|
||||
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):
|
||||
return self.execute_buy(pair, stake_amount)
|
||||
return self.execute_entry(pair, stake_amount, buy_tag=buy_tag)
|
||||
else:
|
||||
return False
|
||||
|
||||
return self.execute_buy(pair, stake_amount)
|
||||
return self.execute_entry(pair, stake_amount, buy_tag=buy_tag)
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -461,8 +465,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||
return False
|
||||
|
||||
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
||||
forcebuy: bool = False) -> bool:
|
||||
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
||||
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Executes a limit buy for the given pair
|
||||
: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']
|
||||
|
||||
if price:
|
||||
buy_limit_requested = price
|
||||
enter_limit_requested = price
|
||||
else:
|
||||
# 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.')
|
||||
|
||||
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)
|
||||
|
||||
if not self.edge:
|
||||
@@ -488,7 +498,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
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)
|
||||
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: "
|
||||
f"{stake_amount} ...")
|
||||
|
||||
amount = stake_amount / buy_limit_requested
|
||||
amount = stake_amount / enter_limit_requested
|
||||
order_type = self.strategy.order_types['buy']
|
||||
if forcebuy:
|
||||
# Forcebuy can define a different ordertype
|
||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
||||
|
||||
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)):
|
||||
logger.info(f"User requested abortion of buying {pair}")
|
||||
return False
|
||||
amount = self.exchange.amount_to_precision(pair, amount)
|
||||
order = self.exchange.buy(pair=pair, ordertype=order_type,
|
||||
amount=amount, rate=buy_limit_requested,
|
||||
time_in_force=time_in_force)
|
||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||
amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force)
|
||||
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
||||
order_id = order['id']
|
||||
order_status = order.get('status', None)
|
||||
|
||||
# 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
|
||||
|
||||
if order_status == 'expired' or order_status == 'rejected':
|
||||
@@ -541,13 +551,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
)
|
||||
stake_amount = order['cost']
|
||||
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
|
||||
elif order_status == 'closed':
|
||||
stake_amount = order['cost']
|
||||
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 = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||
@@ -559,12 +569,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount_requested=amount_requested,
|
||||
fee_open=fee,
|
||||
fee_close=fee,
|
||||
open_rate=buy_limit_filled_price,
|
||||
open_rate_requested=buy_limit_requested,
|
||||
open_rate=enter_limit_filled_price,
|
||||
open_rate_requested=enter_limit_requested,
|
||||
open_date=datetime.utcnow(),
|
||||
exchange=self.exchange.id,
|
||||
open_order_id=order_id,
|
||||
strategy=self.strategy.get_strategy_name(),
|
||||
buy_tag=buy_tag,
|
||||
timeframe=timeframe_to_minutes(self.config['timeframe'])
|
||||
)
|
||||
trade.orders.append(order_obj)
|
||||
@@ -579,17 +590,18 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Updating wallets
|
||||
self.wallets.update()
|
||||
|
||||
self._notify_buy(trade, order_type)
|
||||
self._notify_enter(trade, order_type)
|
||||
|
||||
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.
|
||||
"""
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'limit': trade.open_rate,
|
||||
@@ -605,7 +617,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
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.
|
||||
"""
|
||||
@@ -614,6 +626,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_CANCEL,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'limit': trade.open_rate,
|
||||
@@ -630,10 +643,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_buy_fill(self, trade: Trade) -> None:
|
||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_FILL,
|
||||
'buy_tag': trade.buy_tag,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'open_rate': trade.open_rate,
|
||||
@@ -692,11 +706,15 @@ class FreqtradeBot(LoggingMixin):
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||
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')
|
||||
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
||||
if self._check_and_execute_exit(trade, exit_rate, buy, sell):
|
||||
return True
|
||||
|
||||
logger.debug('Found no sell signal for %s.', trade)
|
||||
@@ -726,8 +744,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
except InvalidOrderException as e:
|
||||
trade.stoploss_order_id = None
|
||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||
logger.warning('Selling the trade forcefully')
|
||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||
logger.warning('Exiting the trade forcefully')
|
||||
self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||
sell_type=SellType.EMERGENCY_SELL))
|
||||
|
||||
except ExchangeError:
|
||||
@@ -764,7 +782,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
self._notify_sell(trade, "stoploss")
|
||||
self._notify_exit(trade, "stoploss")
|
||||
return True
|
||||
|
||||
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 "
|
||||
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:
|
||||
"""
|
||||
Check and execute sell
|
||||
Check and execute exit
|
||||
"""
|
||||
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
|
||||
)
|
||||
|
||||
if should_sell.sell_flag:
|
||||
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 False
|
||||
|
||||
@@ -888,7 +906,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
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 (
|
||||
fully_cancelled
|
||||
@@ -897,7 +915,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
default_retval=False)(pair=trade.pair,
|
||||
trade=trade,
|
||||
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:
|
||||
"""
|
||||
@@ -913,13 +931,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
continue
|
||||
|
||||
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':
|
||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
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
|
||||
:return: True if order was fully cancelled
|
||||
@@ -927,7 +945,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
was_trade_fully_canceled = False
|
||||
|
||||
# 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_stake = filled_val * trade.open_rate
|
||||
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.
|
||||
# Simply bailing here is the only safe way - as this order will then be
|
||||
# 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.")
|
||||
return False
|
||||
else:
|
||||
@@ -965,7 +983,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
# and close the order
|
||||
# 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.
|
||||
trade.amount = filled_amount
|
||||
trade.stake_amount = trade.amount * trade.open_rate
|
||||
@@ -976,11 +994,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'],
|
||||
reason=reason)
|
||||
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'],
|
||||
reason=reason)
|
||||
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
|
||||
:return: Reason for cancel
|
||||
@@ -1014,14 +1032,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_sell_cancel(
|
||||
self._notify_exit_cancel(
|
||||
trade,
|
||||
order_type=self.strategy.order_types['sell'],
|
||||
reason=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.
|
||||
Should be trade.amount - but will fall back to the available amount if necessary.
|
||||
@@ -1046,9 +1064,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
raise DependencyException(
|
||||
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 limit: limit rate for the sell order
|
||||
:param sell_reason: Reason the sell was triggered
|
||||
@@ -1064,6 +1082,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
and self.strategy.order_types['stoploss_on_exchange']:
|
||||
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 ...
|
||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||
try:
|
||||
@@ -1082,7 +1111,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# but we allow this value to be changed)
|
||||
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']
|
||||
|
||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||
@@ -1094,11 +1123,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
try:
|
||||
# Execute sell and update trade record
|
||||
order = self.exchange.sell(pair=trade.pair,
|
||||
ordertype=order_type,
|
||||
amount=amount, rate=limit,
|
||||
time_in_force=time_in_force
|
||||
)
|
||||
order = self.exchange.create_order(pair=trade.pair,
|
||||
ordertype=order_type, side="sell",
|
||||
amount=amount, rate=limit,
|
||||
time_in_force=time_in_force
|
||||
)
|
||||
except InsufficientFundsError as e:
|
||||
logger.warning(f"Unable to place order {e}.")
|
||||
# Try to figure out what went wrong
|
||||
@@ -1113,7 +1142,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.close_rate_requested = limit
|
||||
trade.sell_reason = sell_reason.sell_reason
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') == 'closed':
|
||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
Trade.commit()
|
||||
|
||||
@@ -1121,11 +1150,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
self._notify_exit(trade, order_type)
|
||||
|
||||
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.
|
||||
"""
|
||||
@@ -1167,7 +1196,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Send the message
|
||||
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.
|
||||
"""
|
||||
@@ -1188,7 +1217,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'limit': profit_rate,
|
||||
'limit': profit_rate or 0,
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
@@ -1197,7 +1226,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
'profit_ratio': profit_ratio,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'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'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'reason': reason,
|
||||
@@ -1262,16 +1291,28 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Updating wallets when order is closed
|
||||
if not trade.is_open:
|
||||
if not stoploss_order and not trade.open_order_id:
|
||||
self._notify_sell(trade, '', True)
|
||||
self.protections.stop_per_pair(trade.pair)
|
||||
self.protections.global_stop()
|
||||
self._notify_exit(trade, '', True)
|
||||
self.handle_protections(trade.pair)
|
||||
self.wallets.update()
|
||||
elif not trade.open_order_id:
|
||||
# Buy fill
|
||||
self._notify_buy_fill(trade)
|
||||
self._notify_enter_fill(trade)
|
||||
|
||||
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,
|
||||
amount: float, fee_abs: float) -> float:
|
||||
"""
|
||||
@@ -1352,7 +1393,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
if fee_currency:
|
||||
# fee_rate should use mean
|
||||
fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
|
||||
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
|
||||
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', ''))
|
||||
|
||||
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
|
||||
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
|
||||
@@ -1363,3 +1406,26 @@ class FreqtradeBot(LoggingMixin):
|
||||
amount=amount, fee_abs=fee_abs)
|
||||
else:
|
||||
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.
|
||||
handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
||||
logging.root.addHandler(handler_sl)
|
||||
elif s[0] == 'journald':
|
||||
elif s[0] == 'journald': # pragma: no cover
|
||||
try:
|
||||
from systemd.journal import JournaldLogHandler
|
||||
except ImportError:
|
||||
|
@@ -9,7 +9,7 @@ from typing import Any, List
|
||||
|
||||
|
||||
# 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")
|
||||
|
||||
from freqtrade.commands import Arguments
|
||||
@@ -44,9 +44,9 @@ def main(sysargv: List[str] = None) -> None:
|
||||
"as `freqtrade trade [options...]`.\n"
|
||||
"To see the full list of options available, please use "
|
||||
"`freqtrade --help` or `freqtrade <command> --help`."
|
||||
)
|
||||
)
|
||||
|
||||
except SystemExit as e:
|
||||
except SystemExit as e: # pragma: no cover
|
||||
return_code = e
|
||||
except KeyboardInterrupt:
|
||||
logger.info('SIGINT received, aborting ...')
|
||||
@@ -60,5 +60,5 @@ def main(sysargv: List[str] = None) -> None:
|
||||
sys.exit(return_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
main()
|
||||
|
@@ -11,11 +11,11 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
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.data import history
|
||||
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.enums import BacktestState, SellType
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
@@ -43,6 +43,7 @@ CLOSE_IDX = 3
|
||||
SELL_IDX = 4
|
||||
LOW_IDX = 5
|
||||
HIGH_IDX = 6
|
||||
BUY_TAG_IDX = 7
|
||||
|
||||
|
||||
class Backtesting:
|
||||
@@ -60,8 +61,7 @@ class Backtesting:
|
||||
self.config = config
|
||||
self.results: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Reset keys for backtesting
|
||||
remove_credentials(self.config)
|
||||
config['dry_run'] = True
|
||||
self.strategylist: List[IStrategy] = []
|
||||
self.all_results: Dict[str, Dict] = {}
|
||||
|
||||
@@ -85,7 +85,7 @@ class Backtesting:
|
||||
"configuration or as cli argument `--timeframe 5m`")
|
||||
self.timeframe = str(self.config.get('timeframe'))
|
||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||
|
||||
self.init_backtest_detail()
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||
@@ -108,26 +108,46 @@ class Backtesting:
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||
|
||||
Trade.use_db = False
|
||||
Trade.reset_trades()
|
||||
PairLocks.timeframe = self.config['timeframe']
|
||||
PairLocks.use_db = False
|
||||
PairLocks.reset_locks()
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange, log=False)
|
||||
self.timerange = TimeRange.parse_timerange(
|
||||
None if self.config.get('timerange') is None else str(self.config.get('timerange')))
|
||||
|
||||
# Get maximum required startup period
|
||||
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.progress = BTProgress()
|
||||
self.abort = False
|
||||
self.init_backtest()
|
||||
|
||||
def __del__(self):
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
LoggingMixin.show_output = True
|
||||
PairLocks.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):
|
||||
"""
|
||||
Load strategy into backtesting
|
||||
@@ -135,11 +155,13 @@ class Backtesting:
|
||||
self.strategy: IStrategy = strategy
|
||||
strategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.wallets = self.wallets
|
||||
strategy.wallets = self.wallets
|
||||
# Set stoploss_on_exchange to false for backtesting,
|
||||
# since a "perfect" stoploss-sell is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||
|
||||
def _load_protections(self, strategy: IStrategy):
|
||||
if self.config.get('enable_protections', False):
|
||||
conf = self.config
|
||||
if hasattr(strategy, 'protections'):
|
||||
@@ -154,14 +176,11 @@ class Backtesting:
|
||||
"""
|
||||
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(
|
||||
datadir=self.config['datadir'],
|
||||
pairs=self.pairlists.whitelist,
|
||||
timeframe=self.timeframe,
|
||||
timerange=timerange,
|
||||
timerange=self.timerange,
|
||||
startup_candles=self.required_startup,
|
||||
fail_without_data=True,
|
||||
data_format=self.config.get('dataformat_ohlcv', 'json'),
|
||||
@@ -174,11 +193,28 @@ class Backtesting:
|
||||
f'({(max_date - min_date).days} days).')
|
||||
|
||||
# Adjust startts forward if not enough data is available
|
||||
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
|
||||
self.required_startup, min_date)
|
||||
self.timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
|
||||
self.required_startup, min_date)
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -191,6 +227,8 @@ class Backtesting:
|
||||
Trade.reset_trades()
|
||||
self.rejected_trades = 0
|
||||
self.dataprovider.clear_cache()
|
||||
if enable_protections:
|
||||
self._load_protections(self.strategy)
|
||||
|
||||
def check_abort(self):
|
||||
"""
|
||||
@@ -209,7 +247,7 @@ class Backtesting:
|
||||
"""
|
||||
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||
# 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 = {}
|
||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||
|
||||
@@ -220,20 +258,27 @@ class Backtesting:
|
||||
if not pair_data.empty:
|
||||
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[:, 'buy_tag'] = None # cleanup if buy_tag is exist
|
||||
|
||||
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
|
||||
# from the previous candle
|
||||
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].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
|
||||
# (Looping Pandas is slow.)
|
||||
data[pair] = df_analyzed.values.tolist()
|
||||
data[pair] = df_analyzed[headers].values.tolist()
|
||||
return data
|
||||
|
||||
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
|
||||
@@ -262,7 +307,7 @@ class Backtesting:
|
||||
# Worst case: price reaches stop_positive_offset and dives down.
|
||||
stop_rate = (sell_row[OPEN_IDX] *
|
||||
(1 + abs(self.strategy.trailing_stop_positive_offset) -
|
||||
abs(self.strategy.trailing_stop_positive)))
|
||||
abs(self.strategy.trailing_stop_positive)))
|
||||
else:
|
||||
# Worst case: price ticks tiny bit above open and dives down.
|
||||
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
|
||||
@@ -302,15 +347,16 @@ class Backtesting:
|
||||
else:
|
||||
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_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX],
|
||||
sell_candle_time, sell_row[BUY_IDX],
|
||||
sell_row[SELL_IDX],
|
||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||
|
||||
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_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
@@ -322,7 +368,7 @@ class Backtesting:
|
||||
rate=closerate,
|
||||
time_in_force=time_in_force,
|
||||
sell_reason=sell.sell_reason,
|
||||
current_time=sell_row[DATE_IDX].to_pydatetime()):
|
||||
current_time=sell_candle_time):
|
||||
return None
|
||||
|
||||
trade.close(closerate, show_msg=False)
|
||||
@@ -330,6 +376,32 @@ class Backtesting:
|
||||
|
||||
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]:
|
||||
try:
|
||||
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):
|
||||
# Enter trade
|
||||
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
|
||||
trade = LocalTrade(
|
||||
pair=pair,
|
||||
open_rate=row[OPEN_IDX],
|
||||
@@ -367,6 +440,7 @@ class Backtesting:
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
|
||||
exchange='backtesting',
|
||||
)
|
||||
return trade
|
||||
@@ -423,10 +497,6 @@ class Backtesting:
|
||||
trades: List[LocalTrade] = []
|
||||
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
|
||||
# (looping lists is a lot faster than pandas DataFrames)
|
||||
data: Dict = self._get_ohlcv_as_lists(processed)
|
||||
@@ -448,6 +518,8 @@ class Backtesting:
|
||||
for i, pair in enumerate(data):
|
||||
row_index = indexes[pair]
|
||||
try:
|
||||
# Row is treated as "current incomplete candle".
|
||||
# Buy / sell signals are shifted by 1 to compensate for this.
|
||||
row = data[pair][row_index]
|
||||
except IndexError:
|
||||
# missing Data for one pair at the end.
|
||||
@@ -459,8 +531,8 @@ class Backtesting:
|
||||
continue
|
||||
|
||||
row_index += 1
|
||||
self.dataprovider._set_dataframe_max_index(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.
|
||||
# max_open_trades must be respected
|
||||
@@ -484,7 +556,7 @@ class Backtesting:
|
||||
open_trades[pair].append(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.
|
||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||
# Sell occurred
|
||||
@@ -515,7 +587,8 @@ class Backtesting:
|
||||
'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)
|
||||
|
||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||
@@ -534,17 +607,18 @@ class Backtesting:
|
||||
max_open_trades = 0
|
||||
|
||||
# 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
|
||||
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(
|
||||
"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)} '
|
||||
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||
f'({(max_date - min_date).days} days).')
|
||||
@@ -574,6 +648,7 @@ class Backtesting:
|
||||
data: Dict[str, Any] = {}
|
||||
|
||||
data, timerange = self.load_bt_data()
|
||||
self.load_bt_data_detail()
|
||||
logger.info("Dataload complete. Calculating indicators")
|
||||
|
||||
for strat in self.strategylist:
|
||||
|
@@ -7,7 +7,8 @@ import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
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.optimize.optimize_reports import generate_edge_table
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
@@ -28,11 +29,12 @@ class EdgeCli:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.config = config
|
||||
|
||||
# Reset keys for edge
|
||||
remove_credentials(self.config)
|
||||
# Ensure using dry-run
|
||||
self.config['dry_run'] = True
|
||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||
self.strategy.dp = DataProvider(config, None)
|
||||
|
||||
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.data.converter import trim_dataframes
|
||||
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.optimize.backtesting import Backtesting
|
||||
# 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_tools import HyperoptTools, hyperopt_serializer
|
||||
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
|
||||
@@ -44,7 +45,7 @@ progressbar.streams.wrap_stdout()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INITIAL_POINTS = 30
|
||||
INITIAL_POINTS = 5
|
||||
|
||||
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||
# in the skopt model queue, to optimize memory consumption
|
||||
@@ -66,6 +67,7 @@ class Hyperopt:
|
||||
def __init__(self, config: Dict[str, Any]) -> None:
|
||||
self.buy_space: List[Dimension] = []
|
||||
self.sell_space: List[Dimension] = []
|
||||
self.protection_space: List[Dimension] = []
|
||||
self.roi_space: List[Dimension] = []
|
||||
self.stoploss_space: List[Dimension] = []
|
||||
self.trailing_space: List[Dimension] = []
|
||||
@@ -77,10 +79,10 @@ class Hyperopt:
|
||||
|
||||
if not self.config.get('hyperopt'):
|
||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||
self.auto_hyperopt = True
|
||||
else:
|
||||
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
|
||||
self.auto_hyperopt = False
|
||||
raise OperationalException(
|
||||
"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.custom_hyperopt.strategy = self.backtesting.strategy
|
||||
@@ -102,17 +104,6 @@ class Hyperopt:
|
||||
self.num_epochs_saved = 0
|
||||
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
|
||||
if self.config.get('use_max_market_positions', True):
|
||||
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}
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
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'):
|
||||
result['roi'] = {str(k): v for k, v in
|
||||
self.custom_hyperopt.generate_roi_table(params).items()}
|
||||
@@ -239,10 +232,16 @@ class Hyperopt:
|
||||
"""
|
||||
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'):
|
||||
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'):
|
||||
logger.debug("Hyperopt has 'sell' space")
|
||||
@@ -259,30 +258,41 @@ class Hyperopt:
|
||||
if HyperoptTools.has_space(self.config, 'trailing'):
|
||||
logger.debug("Hyperopt has 'trailing' space")
|
||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||
self.dimensions = (self.buy_space + self.sell_space + self.roi_space +
|
||||
self.stoploss_space + self.trailing_space)
|
||||
self.dimensions = (self.buy_space + self.sell_space + self.protection_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:
|
||||
"""
|
||||
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!
|
||||
"""
|
||||
backtest_start_time = datetime.now(timezone.utc)
|
||||
params_dict = self._get_params_dict(self.dimensions, raw_params)
|
||||
|
||||
# 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'):
|
||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||
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'):
|
||||
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
||||
|
||||
@@ -355,10 +365,20 @@ class Hyperopt:
|
||||
}
|
||||
|
||||
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(
|
||||
dimensions,
|
||||
base_estimator="ET",
|
||||
acq_optimizer="auto",
|
||||
base_estimator=estimator,
|
||||
acq_optimizer=acq_optimizer,
|
||||
n_initial_points=INITIAL_POINTS,
|
||||
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
||||
random_state=self.random_state,
|
||||
@@ -376,18 +396,17 @@ class Hyperopt:
|
||||
data, timerange = self.backtesting.load_bt_data()
|
||||
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)
|
||||
|
||||
self.min_date, self.max_date = get_timerange(processed)
|
||||
|
||||
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'({(self.max_date - self.min_date).days} days)..')
|
||||
|
||||
dump(processed, self.data_pickle_file)
|
||||
# Store non-trimmed data - will be trimmed after signal generation.
|
||||
dump(preprocessed, self.data_pickle_file)
|
||||
|
||||
def start(self) -> None:
|
||||
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
||||
@@ -442,9 +461,9 @@ class Hyperopt:
|
||||
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
|
||||
]
|
||||
with progressbar.ProgressBar(
|
||||
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
|
||||
widgets=widgets
|
||||
) as pbar:
|
||||
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
|
||||
widgets=widgets
|
||||
) as pbar:
|
||||
EVALS = ceil(self.total_epochs / jobs)
|
||||
for i in range(EVALS):
|
||||
# Correct the number of epochs to be processed for the last
|
||||
@@ -488,11 +507,10 @@ class Hyperopt:
|
||||
f"saved to '{self.results_file}'.")
|
||||
|
||||
if self.current_best_epoch:
|
||||
if self.auto_hyperopt:
|
||||
HyperoptTools.try_export_params(
|
||||
self.config,
|
||||
self.backtesting.strategy.get_strategy_name(),
|
||||
self.current_best_epoch)
|
||||
HyperoptTools.try_export_params(
|
||||
self.config,
|
||||
self.backtesting.strategy.get_strategy_name(),
|
||||
self.current_best_epoch)
|
||||
|
||||
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
|
||||
self.print_json)
|
||||
|
@@ -4,15 +4,23 @@ This module implements a convenience auto-hyperopt class, which can be used toge
|
||||
that implement IHyperStrategy interface.
|
||||
"""
|
||||
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):
|
||||
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):
|
||||
@@ -22,26 +30,6 @@ class HyperOptAuto(IHyperOpt):
|
||||
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:
|
||||
"""
|
||||
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
|
||||
@@ -60,18 +48,22 @@ class HyperOptAuto(IHyperOpt):
|
||||
if attr.optimize:
|
||||
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))
|
||||
if len(indicator_space) > 0:
|
||||
return indicator_space
|
||||
else:
|
||||
return self._get_func(fallback_method_name)()
|
||||
_format_exception_message(category)
|
||||
|
||||
def indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('buy', 'indicator_space')
|
||||
def buy_indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('buy')
|
||||
|
||||
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]:
|
||||
return self._get_func('generate_roi_table')(params)
|
||||
@@ -87,3 +79,6 @@ class HyperOptAuto(IHyperOpt):
|
||||
|
||||
def trailing_space(self) -> List['Dimension']:
|
||||
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 math
|
||||
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 freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import round_dict
|
||||
from freqtrade.optimize.space import SKDecimal
|
||||
@@ -18,12 +18,7 @@ from freqtrade.strategy import IStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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.")
|
||||
EstimatorType = Union[RegressorMixin, str]
|
||||
|
||||
|
||||
class IHyperOpt(ABC):
|
||||
@@ -45,29 +40,13 @@ class IHyperOpt(ABC):
|
||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||
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'))
|
||||
|
||||
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'))
|
||||
return 'ET'
|
||||
|
||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||
"""
|
||||
|
@@ -4,9 +4,10 @@ import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
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 pandas as pd
|
||||
import rapidjson
|
||||
import tabulate
|
||||
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.exceptions import OperationalException
|
||||
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__)
|
||||
@@ -82,53 +84,77 @@ class HyperoptTools():
|
||||
"""
|
||||
Tell if the space value is contained in the configuration
|
||||
"""
|
||||
# The 'trailing' space is not included in the 'default' set of spaces
|
||||
if space == 'trailing':
|
||||
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
|
||||
if space in ('trailing', 'protection'):
|
||||
return any(s in config['spaces'] for s in [space, 'all'])
|
||||
else:
|
||||
return any(s in config['spaces'] for s in [space, 'all', 'default'])
|
||||
|
||||
@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
|
||||
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
|
||||
Stream hyperopt results from file
|
||||
"""
|
||||
import rapidjson
|
||||
logger.info(f"Reading epochs from '{results_file}'")
|
||||
with results_file.open('r') as f:
|
||||
data = [rapidjson.loads(line) for line in f]
|
||||
return data
|
||||
data = []
|
||||
for line in f:
|
||||
data += [rapidjson.loads(line)]
|
||||
if len(data) >= batch_size:
|
||||
yield data
|
||||
data = []
|
||||
yield data
|
||||
|
||||
@staticmethod
|
||||
def load_previous_results(results_file: Path) -> List:
|
||||
"""
|
||||
Load data for epochs from the file if we have one
|
||||
"""
|
||||
epochs: List = []
|
||||
def _test_hyperopt_results_exist(results_file) -> bool:
|
||||
if results_file.is_file() and results_file.stat().st_size > 0:
|
||||
if results_file.suffix == '.pickle':
|
||||
epochs = HyperoptTools._read_results_pickle(results_file)
|
||||
else:
|
||||
epochs = HyperoptTools._read_results(results_file)
|
||||
# Detection of some old format, without 'is_best' field saved
|
||||
if epochs[0].get('is_best') is None:
|
||||
raise OperationalException(
|
||||
"Legacy hyperopt results are no longer supported."
|
||||
"Please rerun hyperopt or use an older version to load this file."
|
||||
)
|
||||
return True
|
||||
else:
|
||||
# No file found.
|
||||
return False
|
||||
|
||||
@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(
|
||||
"The file with HyperoptTools results is incompatible with this version "
|
||||
"of Freqtrade and cannot be loaded.")
|
||||
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
|
||||
return epochs
|
||||
total_epochs += len(epochs_tmp)
|
||||
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
|
||||
def show_epoch_details(results, total_epochs: int, print_json: bool,
|
||||
@@ -149,7 +175,7 @@ class HyperoptTools():
|
||||
|
||||
if print_json:
|
||||
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)
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||
|
||||
@@ -158,6 +184,8 @@ class HyperoptTools():
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
||||
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, 'stoploss', "Stoploss:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
||||
@@ -203,7 +231,7 @@ class HyperoptTools():
|
||||
elif space == "roi":
|
||||
result = result[:-1] + f'{appendix}\n'
|
||||
minimal_roi_result = rapidjson.dumps({
|
||||
str(k): v for k, v in (space_params or no_params).items()
|
||||
str(k): v for k, v in (space_params or no_params).items()
|
||||
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||
result += f"minimal_roi = {minimal_roi_result}"
|
||||
elif space == "trailing":
|
||||
@@ -271,8 +299,8 @@ class HyperoptTools():
|
||||
f"Objective: {results['loss']:.5f}")
|
||||
|
||||
@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'] = ''
|
||||
|
||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||
@@ -408,8 +436,7 @@ class HyperoptTools():
|
||||
return table
|
||||
|
||||
@staticmethod
|
||||
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
|
||||
csv_file: str) -> None:
|
||||
def export_csv_file(config: dict, results: list, csv_file: str) -> None:
|
||||
"""
|
||||
Log result to csv-file
|
||||
"""
|
||||
@@ -431,21 +458,14 @@ class HyperoptTools():
|
||||
trials['Best'] = ''
|
||||
trials['Stake currency'] = config['stake_currency']
|
||||
|
||||
if 'results_metrics.total_trades' in trials:
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||
'results_metrics.profit_total',
|
||||
'Stake currency',
|
||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
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']
|
||||
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_median',
|
||||
'results_metrics.profit_total',
|
||||
'Stake currency',
|
||||
'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
perc_multi = 100
|
||||
|
||||
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
|
||||
trials = trials[base_metrics + param_metrics]
|
||||
|
||||
@@ -473,11 +493,6 @@ class HyperoptTools():
|
||||
trials['Avg profit'] = trials['Avg profit'].apply(
|
||||
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(
|
||||
lambda x: f'{x:,.5f}' if x != 100000 else ""
|
||||
)
|
||||
|
@@ -31,7 +31,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
|
||||
filename = Path.joinpath(
|
||||
recordfilename.parent,
|
||||
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
|
||||
).with_suffix(recordfilename.suffix)
|
||||
).with_suffix(recordfilename.suffix)
|
||||
file_dump_json(filename, stats)
|
||||
|
||||
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
|
||||
@@ -173,7 +173,7 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
|
||||
for strategy, results in all_results.items():
|
||||
tabular_data.append(_generate_result_line(
|
||||
results['results'], results['config']['dry_run_wallet'], strategy)
|
||||
)
|
||||
)
|
||||
try:
|
||||
max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'],
|
||||
value_col='profit_ratio')
|
||||
@@ -368,6 +368,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
'max_open_trades_setting': (config['max_open_trades']
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
'timeframe': config['timeframe'],
|
||||
'timeframe_detail': config.get('timeframe_detail', ''),
|
||||
'timerange': config.get('timerange', ''),
|
||||
'enable_protections': config.get('enable_protections', False),
|
||||
'strategy_name': strategy,
|
||||
@@ -604,7 +605,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
strat_results['stake_currency'])
|
||||
stake_amount = round_coin_value(
|
||||
strat_results['stake_amount'], strat_results['stake_currency']
|
||||
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
|
||||
|
||||
message = ("No trades made. "
|
||||
f"Your starting balance was {start_balance}, "
|
||||
|
@@ -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')
|
||||
sell_reason = get_column_def(cols, 'sell_reason', '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 has_column(cols, '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
|
||||
with engine.begin() as connection:
|
||||
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):
|
||||
connection.execute(text(f"drop index {index['name']}"))
|
||||
# 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
|
||||
(id, exchange, pair, is_open,
|
||||
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,
|
||||
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||
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
|
||||
)
|
||||
select id, lower(exchange),
|
||||
case
|
||||
when instr(pair, '_') != 0 then
|
||||
substr(pair, instr(pair, '_') + 1) || '/' ||
|
||||
substr(pair, 1, instr(pair, '_') - 1)
|
||||
else pair
|
||||
end
|
||||
pair,
|
||||
select id, lower(exchange), pair,
|
||||
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
|
||||
{fee_open_currency} fee_open_currency, {fee_close} fee_close,
|
||||
{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,
|
||||
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
|
||||
{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
|
||||
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:
|
||||
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):
|
||||
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')
|
||||
|
||||
# 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}')
|
||||
migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
|
||||
# Reread columns - the above recreated the table!
|
||||
|
@@ -2,7 +2,7 @@
|
||||
This module contains the class to persist trades into SQLite
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
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.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.exceptions import DependencyException, OperationalException
|
||||
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.ft_is_open = True
|
||||
if self.status in ('closed', 'canceled', 'cancelled'):
|
||||
if self.status in NON_OPEN_EXCHANGE_STATES:
|
||||
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_update_date = datetime.now(timezone.utc)
|
||||
|
||||
@@ -257,6 +257,7 @@ class LocalTrade():
|
||||
sell_reason: str = ''
|
||||
sell_order_status: str = ''
|
||||
strategy: str = ''
|
||||
buy_tag: Optional[str] = None
|
||||
timeframe: Optional[int] = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -288,6 +289,7 @@ class LocalTrade():
|
||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||
'stake_amount': round(self.stake_amount, 8),
|
||||
'strategy': self.strategy,
|
||||
'buy_tag': self.buy_tag,
|
||||
'timeframe': self.timeframe,
|
||||
|
||||
'fee_open': self.fee_open,
|
||||
@@ -352,12 +354,12 @@ class LocalTrade():
|
||||
LocalTrade.trades_open = []
|
||||
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.
|
||||
"""
|
||||
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):
|
||||
"""Assign new stop value"""
|
||||
@@ -636,7 +638,7 @@ class LocalTrade():
|
||||
|
||||
# skip case if trailing-stop changed the stoploss already.
|
||||
if (trade.stop_loss == trade.initial_stop_loss
|
||||
and trade.initial_stop_loss_pct != desired_stoploss):
|
||||
and trade.initial_stop_loss_pct != desired_stoploss):
|
||||
# Stoploss value got changed
|
||||
|
||||
logger.info(f"Stoploss for {trade} needs adjustment...")
|
||||
@@ -703,6 +705,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
sell_reason = Column(String(100), nullable=True)
|
||||
sell_order_status = Column(String(100), nullable=True)
|
||||
strategy = Column(String(100), nullable=True)
|
||||
buy_tag = Column(String(100), nullable=True)
|
||||
timeframe = Column(Integer, nullable=True)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -829,17 +832,21 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
return total_open_stake_amount or 0
|
||||
|
||||
@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
|
||||
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(
|
||||
Trade.pair,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(Trade.is_open.is_(False))\
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.pair) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
@@ -30,7 +30,8 @@ class PairLocks():
|
||||
PairLocks.locks = []
|
||||
|
||||
@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".
|
||||
Uses database by default, unless PairLocks.use_db is set to False,
|
||||
@@ -52,6 +53,7 @@ class PairLocks():
|
||||
PairLock.query.session.commit()
|
||||
else:
|
||||
PairLocks.locks.append(lock)
|
||||
return lock
|
||||
|
||||
@staticmethod
|
||||
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
|
||||
|
@@ -334,8 +334,8 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots:
|
||||
)
|
||||
elif indicator_b not in data:
|
||||
logger.info(
|
||||
'fill_to: "%s" ignored. Reason: This indicator is not '
|
||||
'in your strategy.', indicator_b
|
||||
'fill_to: "%s" ignored. Reason: This indicator is not '
|
||||
'in your strategy.', indicator_b
|
||||
)
|
||||
return fig
|
||||
|
||||
@@ -538,7 +538,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
- Initializes plot-script
|
||||
- Get candle (OHLCV) data
|
||||
- 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 plot files
|
||||
:return: None
|
||||
|
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import PeriodicCache
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
@@ -18,14 +19,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class AgeFilter(IPairList):
|
||||
|
||||
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||
_symbolsChecked: Dict[str, int] = {}
|
||||
|
||||
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)
|
||||
|
||||
# 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._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.
|
||||
: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:
|
||||
return pairlist
|
||||
# Remove pairs that have been removed before
|
||||
return [p for p in pairlist if p not in self._symbolsCheckFailed]
|
||||
|
||||
since_days = -(
|
||||
self._max_days_listed if self._max_days_listed else self._min_days_listed
|
||||
@@ -118,5 +123,6 @@ class AgeFilter(IPairList):
|
||||
" or more than "
|
||||
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
||||
) if self._max_days_listed else ''), logger.info)
|
||||
self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000
|
||||
return False
|
||||
return False
|
||||
|
@@ -144,24 +144,26 @@ class IPairList(LoggingMixin, ABC):
|
||||
markets = self._exchange.markets
|
||||
if not markets:
|
||||
raise OperationalException(
|
||||
'Markets not loaded. Make sure that exchange is initialized correctly.')
|
||||
'Markets not loaded. Make sure that exchange is initialized correctly.')
|
||||
|
||||
sanitized_whitelist: List[str] = []
|
||||
for pair in pairlist:
|
||||
# pair is not in the generated dynamic market or has the wrong stake currency
|
||||
if pair not in markets:
|
||||
logger.warning(f"Pair {pair} is not compatible with exchange "
|
||||
f"{self._exchange.name}. Removing it from whitelist..")
|
||||
self.log_once(f"Pair {pair} is not compatible with exchange "
|
||||
f"{self._exchange.name}. Removing it from whitelist..",
|
||||
logger.warning)
|
||||
continue
|
||||
|
||||
if not self._exchange.market_is_tradable(markets[pair]):
|
||||
logger.warning(f"Pair {pair} is not tradable with Freqtrade."
|
||||
"Removing it from whitelist..")
|
||||
self.log_once(f"Pair {pair} is not tradable with Freqtrade."
|
||||
"Removing it from whitelist..", logger.warning)
|
||||
continue
|
||||
|
||||
if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']:
|
||||
logger.warning(f"Pair {pair} is not compatible with your stake currency "
|
||||
f"{self._config['stake_currency']}. Removing it from whitelist..")
|
||||
self.log_once(f"Pair {pair} is not compatible with your stake currency "
|
||||
f"{self._config['stake_currency']}. Removing it from whitelist..",
|
||||
logger.warning)
|
||||
continue
|
||||
|
||||
# Check if market is active
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Performance pair list filter
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pandas as pd
|
||||
|
||||
@@ -15,6 +15,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
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
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
@@ -40,7 +47,7 @@ class PerformanceFilter(IPairList):
|
||||
"""
|
||||
# Get the trading performance for pairs from database
|
||||
try:
|
||||
performance = pd.DataFrame(Trade.get_overall_performance())
|
||||
performance = pd.DataFrame(Trade.get_overall_performance(self._minutes))
|
||||
except AttributeError:
|
||||
# Performancefilter does not work in backtesting.
|
||||
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
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import arrow
|
||||
@@ -115,18 +116,18 @@ class VolumePairList(IPairList):
|
||||
pairlist = self._pair_cache.get('pairlist')
|
||||
if pairlist:
|
||||
# Item found - no refresh necessary
|
||||
return pairlist
|
||||
return pairlist.copy()
|
||||
else:
|
||||
# Use fresh pairlist
|
||||
# Check if pair quote currency equals to the stake currency.
|
||||
filtered_tickers = [
|
||||
v for k, v in tickers.items()
|
||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||
and v[self._sort_key] is not None)]
|
||||
v for k, v in tickers.items()
|
||||
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
|
||||
and (self._use_range or v[self._sort_key] is not None))]
|
||||
pairlist = [s['symbol'] for s in filtered_tickers]
|
||||
|
||||
pairlist = self.filter_pairlist(pairlist, tickers)
|
||||
self._pair_cache['pairlist'] = pairlist
|
||||
self._pair_cache['pairlist'] = pairlist.copy()
|
||||
|
||||
return pairlist
|
||||
|
||||
@@ -197,13 +198,13 @@ class VolumePairList(IPairList):
|
||||
|
||||
if self._min_value > 0:
|
||||
filtered_tickers = [
|
||||
v for v in filtered_tickers if v[self._sort_key] > self._min_value]
|
||||
v for v in filtered_tickers if v[self._sort_key] > self._min_value]
|
||||
|
||||
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key])
|
||||
|
||||
# Validate whitelist to only have active market pairs
|
||||
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
|
||||
pairs = pairs[:self._number_pairs]
|
||||
|
||||
|
@@ -17,7 +17,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
|
||||
if keep_invalid:
|
||||
for pair_wc in wildcardpl:
|
||||
try:
|
||||
comp = re.compile(pair_wc)
|
||||
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||
result_partial = [
|
||||
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:
|
||||
for pair_wc in wildcardpl:
|
||||
try:
|
||||
comp = re.compile(pair_wc)
|
||||
comp = re.compile(pair_wc, re.IGNORECASE)
|
||||
result += [
|
||||
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._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._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
|
||||
"""
|
||||
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 "
|
||||
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]:
|
||||
"""
|
||||
@@ -104,6 +109,17 @@ class RangeStabilityFilter(IPairList):
|
||||
f"which is below the threshold of {self._min_rate_of_change}.",
|
||||
logger.info)
|
||||
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
|
||||
|
||||
else:
|
||||
self.log_once(f"Removed {pair} from whitelist, no candles found.", logger.info)
|
||||
return result
|
||||
|
@@ -28,13 +28,13 @@ class PairListManager():
|
||||
self._tickers_needed = False
|
||||
for pairlist_handler_config in self._config.get('pairlists', None):
|
||||
pairlist_handler = PairListResolver.load_pairlist(
|
||||
pairlist_handler_config['method'],
|
||||
exchange=exchange,
|
||||
pairlistmanager=self,
|
||||
config=config,
|
||||
pairlistconfig=pairlist_handler_config,
|
||||
pairlist_pos=len(self._pairlist_handlers)
|
||||
)
|
||||
pairlist_handler_config['method'],
|
||||
exchange=exchange,
|
||||
pairlistmanager=self,
|
||||
config=config,
|
||||
pairlistconfig=pairlist_handler_config,
|
||||
pairlist_pos=len(self._pairlist_handlers)
|
||||
)
|
||||
self._tickers_needed |= pairlist_handler.needstickers
|
||||
self._pairlist_handlers.append(pairlist_handler)
|
||||
|
||||
|
@@ -6,6 +6,7 @@ from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from freqtrade.persistence import PairLocks
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.plugins.protections import IProtection
|
||||
from freqtrade.resolvers import ProtectionResolver
|
||||
|
||||
@@ -43,30 +44,28 @@ class ProtectionManager():
|
||||
"""
|
||||
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:
|
||||
now = datetime.now(timezone.utc)
|
||||
result = False
|
||||
result = None
|
||||
for protection_handler in self._protection_handlers:
|
||||
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
|
||||
if result and until:
|
||||
if lock and until:
|
||||
if not PairLocks.is_global_lock(until):
|
||||
PairLocks.lock_pair('*', until, reason, now=now)
|
||||
result = True
|
||||
result = PairLocks.lock_pair('*', until, reason, now=now)
|
||||
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:
|
||||
now = datetime.now(timezone.utc)
|
||||
result = False
|
||||
result = None
|
||||
for protection_handler in self._protection_handlers:
|
||||
if protection_handler.has_local_stop:
|
||||
result, until, reason = protection_handler.stop_per_pair(pair, now)
|
||||
if result and until:
|
||||
lock, until, reason = protection_handler.stop_per_pair(pair, now)
|
||||
if lock and until:
|
||||
if not PairLocks.is_pair_locked(pair, until):
|
||||
PairLocks.lock_pair(pair, until, reason, now=now)
|
||||
result = True
|
||||
result = PairLocks.lock_pair(pair, until, reason, now=now)
|
||||
return result
|
||||
|
@@ -25,19 +25,22 @@ class IProtection(LoggingMixin, ABC):
|
||||
def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None:
|
||||
self._config = 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'])
|
||||
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)
|
||||
else:
|
||||
self._stop_duration_candles = None
|
||||
self._stop_duration = protection_config.get('stop_duration', 60)
|
||||
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
|
||||
else:
|
||||
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)
|
||||
|
||||
|
@@ -54,9 +54,9 @@ class StoplossGuard(IProtection):
|
||||
|
||||
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
||||
trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
|
||||
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
|
||||
SellType.STOPLOSS_ON_EXCHANGE.value)
|
||||
and trade.close_profit and trade.close_profit < 0)]
|
||||
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
|
||||
SellType.STOPLOSS_ON_EXCHANGE.value)
|
||||
and trade.close_profit and trade.close_profit < 0)]
|
||||
|
||||
if len(trades) < self._trade_limit:
|
||||
return False, None, None
|
||||
|
@@ -8,6 +8,3 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
from freqtrade.resolvers.pairlist_resolver import PairListResolver
|
||||
from freqtrade.resolvers.protection_resolver import ProtectionResolver
|
||||
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.exceptions import OperationalException
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
|
||||
from freqtrade.resolvers import IResolver
|
||||
|
||||
@@ -17,43 +16,6 @@ from freqtrade.resolvers import IResolver
|
||||
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):
|
||||
"""
|
||||
This class contains all the logic to load custom hyperopt loss class
|
||||
|
@@ -50,7 +50,7 @@ class StrategyResolver(IResolver):
|
||||
if 'timeframe' not in config:
|
||||
logger.warning(
|
||||
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
|
||||
)
|
||||
)
|
||||
strategy.timeframe = strategy.ticker_interval
|
||||
|
||||
if strategy._ft_params_from_file:
|
||||
@@ -119,7 +119,7 @@ class StrategyResolver(IResolver):
|
||||
- default (if not None)
|
||||
"""
|
||||
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
|
||||
setattr(strategy, attribute, config[attribute])
|
||||
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 freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.enums import BacktestState
|
||||
from freqtrade.exceptions import DependencyException
|
||||
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
|
||||
lastconfig = ApiServer._bt_last_config
|
||||
strat = StrategyResolver.load_strategy(btconfig)
|
||||
validate_config_consistency(btconfig)
|
||||
|
||||
if (
|
||||
not ApiServer._bt
|
||||
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
|
||||
ApiServer._bt = Backtesting(btconfig)
|
||||
|
||||
# Only reload data if timeframe or timerange changed.
|
||||
if ApiServer._bt.timeframe_detail:
|
||||
ApiServer._bt.load_bt_data_detail()
|
||||
else:
|
||||
ApiServer._bt.config = btconfig
|
||||
ApiServer._bt.init_backtest()
|
||||
# Only reload data if timeframe changed.
|
||||
if (
|
||||
not ApiServer._bt_data
|
||||
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('timerange') != btconfig['timerange']
|
||||
):
|
||||
lastconfig['timerange'] = btconfig['timerange']
|
||||
lastconfig['protections'] = btconfig.get('protections', [])
|
||||
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
||||
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
||||
lastconfig['timeframe'] = strat.timeframe
|
||||
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
|
||||
|
||||
lastconfig['timerange'] = btconfig['timerange']
|
||||
lastconfig['timeframe'] = strat.timeframe
|
||||
lastconfig['protections'] = btconfig.get('protections', [])
|
||||
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
||||
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
||||
|
||||
ApiServer._bt.abort = False
|
||||
min_date, max_date = ApiServer._bt.backtest_one_strategy(
|
||||
strat, ApiServer._bt_data, ApiServer._bt_timerange)
|
||||
|
||||
ApiServer._bt.results = generate_backtest_stats(
|
||||
ApiServer._bt_data, ApiServer._bt.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
@@ -46,6 +46,12 @@ class Balances(BaseModel):
|
||||
value: float
|
||||
stake: 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):
|
||||
@@ -151,6 +157,7 @@ class TradeSchema(BaseModel):
|
||||
amount_requested: float
|
||||
stake_amount: float
|
||||
strategy: str
|
||||
buy_tag: Optional[str]
|
||||
timeframe: int
|
||||
fee_open: Optional[float]
|
||||
fee_open_cost: Optional[float]
|
||||
@@ -323,6 +330,7 @@ class PairHistory(BaseModel):
|
||||
class BacktestRequest(BaseModel):
|
||||
strategy: str
|
||||
timeframe: Optional[str]
|
||||
timeframe_detail: Optional[str]
|
||||
timerange: Optional[str]
|
||||
max_open_trades: Optional[int]
|
||||
stake_amount: Optional[Union[float, str]]
|
||||
|
@@ -199,8 +199,8 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
|
||||
config=Depends(get_config)):
|
||||
config = deepcopy(config)
|
||||
config.update({
|
||||
'strategy': strategy,
|
||||
})
|
||||
'strategy': strategy,
|
||||
})
|
||||
return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)
|
||||
|
||||
|
||||
@@ -223,11 +223,11 @@ def list_strategies(config=Depends(get_config)):
|
||||
@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy'])
|
||||
def get_strategy(strategy: str, config=Depends(get_config)):
|
||||
|
||||
config = deepcopy(config)
|
||||
config_ = deepcopy(config)
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
try:
|
||||
strategy_obj = StrategyResolver._load_strategy(strategy, config,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
strategy_obj = StrategyResolver._load_strategy(strategy, config_,
|
||||
extra_dir=config_.get('strategy_path'))
|
||||
except OperationalException:
|
||||
raise HTTPException(status_code=404, detail='Strategy not found')
|
||||
|
||||
|
@@ -5,6 +5,20 @@ import time
|
||||
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):
|
||||
"""
|
||||
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
|
||||
@@ -28,12 +42,15 @@ class UvicornServer(uvicorn.Server):
|
||||
try:
|
||||
import uvloop # noqa
|
||||
except ImportError: # pragma: no cover
|
||||
from uvicorn.loops.asyncio import asyncio_setup
|
||||
|
||||
asyncio_setup()
|
||||
else:
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
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))
|
||||
|
||||
@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)
|
||||
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('.'):
|
||||
raise HTTPException(status_code=404, detail="Not Found")
|
||||
uibase = Path(__file__).parent / 'ui/installed/'
|
||||
if (uibase / rest_of_path).is_file():
|
||||
return FileResponse(str(uibase / rest_of_path))
|
||||
filename = 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'
|
||||
if not index_file.is_file():
|
||||
|
@@ -5,7 +5,7 @@ e.g BTC to USD
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
from cachetools.ttl import TTLCache
|
||||
from pycoingecko import CoinGeckoAPI
|
||||
@@ -25,8 +25,7 @@ class CryptoToFiatConverter:
|
||||
"""
|
||||
__instance = None
|
||||
_coingekko: CoinGeckoAPI = None
|
||||
|
||||
_cryptomap: Dict = {}
|
||||
_coinlistings: List[Dict] = []
|
||||
_backoff: float = 0.0
|
||||
|
||||
def __new__(cls):
|
||||
@@ -49,9 +48,8 @@ class CryptoToFiatConverter:
|
||||
|
||||
def _load_cryptomap(self) -> None:
|
||||
try:
|
||||
coinlistings = self._coingekko.get_coins_list()
|
||||
# Create mapping table from symbol to coingekko_id
|
||||
self._cryptomap = {x['symbol']: x['id'] for x in coinlistings}
|
||||
# Use list-comprehension to ensure we get a list.
|
||||
self._coinlistings = [x for x in self._coingekko.get_coins_list()]
|
||||
except RequestException as request_exception:
|
||||
if "429" in str(request_exception):
|
||||
logger.warning(
|
||||
@@ -62,13 +60,31 @@ class CryptoToFiatConverter:
|
||||
# If the request is not a 429 error we want to raise the normal error
|
||||
logger.error(
|
||||
"Could not load FIAT Cryptocurrency map for the following problem: {}".format(
|
||||
request_exception
|
||||
request_exception
|
||||
)
|
||||
)
|
||||
except (Exception) as exception:
|
||||
logger.error(
|
||||
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:
|
||||
"""
|
||||
Convert an amount of crypto-currency to fiat
|
||||
@@ -143,22 +159,14 @@ class CryptoToFiatConverter:
|
||||
if crypto_symbol == fiat_symbol:
|
||||
return 1.0
|
||||
|
||||
if self._cryptomap == {}:
|
||||
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
|
||||
_gekko_id = self._get_gekko_id(crypto_symbol)
|
||||
|
||||
if crypto_symbol not in self._cryptomap:
|
||||
if not _gekko_id:
|
||||
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
||||
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
_gekko_id = self._cryptomap[crypto_symbol]
|
||||
return float(
|
||||
self._coingekko.get_price(
|
||||
ids=_gekko_id,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user