Compare commits
529 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a599645b03 | ||
|
03064731ac | ||
|
d0528a6213 | ||
|
6490b82ad6 | ||
|
8768df647a | ||
|
cf4d1875dd | ||
|
a7b8de92a3 | ||
|
d8298a295b | ||
|
b1feabc816 | ||
|
7480b5cd0f | ||
|
ce3e81ae5f | ||
|
a451a97274 | ||
|
a0b1157a10 | ||
|
27fe6e0a1b | ||
|
d2d21baa04 | ||
|
5c011cba73 | ||
|
0fac9c9cf2 | ||
|
6a3838ea4b | ||
|
0f82174c52 | ||
|
7d428f9cb9 | ||
|
11937fd1bf | ||
|
05f74bdf53 | ||
|
1e32a3ca09 | ||
|
b42afb9dae | ||
|
06e7f379b3 | ||
|
b84a1d0c92 | ||
|
986aafcdf9 | ||
|
f870c0099b | ||
|
e4b42b2b5b | ||
|
550a9de097 | ||
|
1f5504975c | ||
|
b0bfbb6558 | ||
|
d13524f7c1 | ||
|
59916d0e8b | ||
|
cd9341a116 | ||
|
fe8de98832 | ||
|
55f3877ee6 | ||
|
c434d99b4f | ||
|
31b19b9a58 | ||
|
bff353a299 | ||
|
56c3e0c3ba | ||
|
3a3ef4f35d | ||
|
b7c951eacc | ||
|
7efad98e47 | ||
|
4a26889743 | ||
|
31b3b49999 | ||
|
c1f1dfb36e | ||
|
e9d9668e8a | ||
|
ffeff9c0f7 | ||
|
adef5d89f3 | ||
|
7b7d9c02d7 | ||
|
0282d13221 | ||
|
51cc903248 | ||
|
44df5eeacf | ||
|
7c27525bd8 | ||
|
03a4ae4674 | ||
|
53a8c693b8 | ||
|
d652e6fcc4 | ||
|
4899b06b31 | ||
|
3bc36cd650 | ||
|
a1ab8066f2 | ||
|
804bc8134f | ||
|
b7dc2989e7 | ||
|
2e95df4d8d | ||
|
2928ee22ce | ||
|
708d5691b0 | ||
|
07e3f82400 | ||
|
65ce7c9838 | ||
|
74d7497a47 | ||
|
cde041f702 | ||
|
697bf92f6f | ||
|
02d716a8be | ||
|
c9c7f84e8c | ||
|
f5c47767cb | ||
|
288c92301f | ||
|
3451687135 | ||
|
362436f7d2 | ||
|
29e2b858ca | ||
|
2bf7705f2c | ||
|
a261b188da | ||
|
08a4da6f51 | ||
|
91e5562ae0 | ||
|
3c001dfcf6 | ||
|
8def18b002 | ||
|
313cf6a013 | ||
|
c78b2075d8 | ||
|
6a53e2c764 | ||
|
f94dbcd085 | ||
|
40db424363 | ||
|
6a8e8875a2 | ||
|
7863746904 | ||
|
b41c234440 | ||
|
ed77889d6b | ||
|
c583452a5a | ||
|
5f94c3f81b | ||
|
81c50aca01 | ||
|
21ef08d9a8 | ||
|
f4caf9b93c | ||
|
8b78a3bde2 | ||
|
38296e8689 | ||
|
7ea0a74c53 | ||
|
0e4466ca1e | ||
|
f658cfa349 | ||
|
52ae95b2a5 | ||
|
ad26b0dad0 | ||
|
72a103f32d | ||
|
e4e2340f91 | ||
|
cf6f706078 | ||
|
6129c5ca9e | ||
|
2f33b97b95 | ||
|
fb25130588 | ||
|
d96d6024f4 | ||
|
03861945a3 | ||
|
2a4a980855 | ||
|
863391122f | ||
|
225522762b | ||
|
8be0241573 | ||
|
e5da7ff6db | ||
|
76e51cddba | ||
|
7dc826d6b3 | ||
|
5a2bc192d4 | ||
|
4d4ed82db8 | ||
|
62f2597f7a | ||
|
682f880630 | ||
|
8248d1acd1 | ||
|
00a1931f40 | ||
|
c44e87cd30 | ||
|
6cea0ef2d7 | ||
|
f30e300f18 | ||
|
3c3772703b | ||
|
d1104bd434 | ||
|
b7a9853d9a | ||
|
a4bd862323 | ||
|
36d4a15d24 | ||
|
005da97183 | ||
|
5474d5ee64 | ||
|
e5b1657ab3 | ||
|
2ec22f1d97 | ||
|
830b2548bc | ||
|
129c7b02d0 | ||
|
17b3cc2097 | ||
|
b44d215b90 | ||
|
804d99cce9 | ||
|
8566306010 | ||
|
134c61126e | ||
|
03140a0ecb | ||
|
37b15e830a | ||
|
048008756f | ||
|
06b6726029 | ||
|
f96d7dfe6d | ||
|
edb8c4f0e5 | ||
|
5c18c8726d | ||
|
df55259737 | ||
|
02b84bd018 | ||
|
800e314bfd | ||
|
97e8ec91f0 | ||
|
ef137546fe | ||
|
0f3d34eaf4 | ||
|
502c69dce3 | ||
|
dec523eef0 | ||
|
1e87225e91 | ||
|
baf6bca34e | ||
|
10998eb0fa | ||
|
1682578a39 | ||
|
346d66748b | ||
|
5626ca5a06 | ||
|
70a41a0f67 | ||
|
eb3ead4930 | ||
|
ac7598ff14 | ||
|
0c8afea382 | ||
|
94ec9d2366 | ||
|
6c789e4130 | ||
|
1e696c4a20 | ||
|
d146697297 | ||
|
d1555a1095 | ||
|
7ae5f47242 | ||
|
2f97846bd8 | ||
|
0d787fde58 | ||
|
7ac55e5415 | ||
|
85c7b55750 | ||
|
d758b0ccab | ||
|
c5489d530a | ||
|
c3cf71bba8 | ||
|
2d5ced7801 | ||
|
9e548657e0 | ||
|
558bcc7959 | ||
|
4aa2ae37bd | ||
|
791dfd9ba3 | ||
|
898bef1837 | ||
|
9919061c78 | ||
|
348dbeff3f | ||
|
77293b1f1e | ||
|
a4096318e0 | ||
|
7efa228d73 | ||
|
dbdd7f38a8 | ||
|
b722e12350 | ||
|
f6511c3e3f | ||
|
b72bbebccb | ||
|
3d9f3eeb07 | ||
|
e9dbd57da4 | ||
|
dc8abd77df | ||
|
3686efa08a | ||
|
53f963dd73 | ||
|
62da4b452c | ||
|
055229a44a | ||
|
9d6860337f | ||
|
3503fdb4ec | ||
|
fbd91cd3f8 | ||
|
b25ad68c44 | ||
|
849f01e6b7 | ||
|
7acbc9a554 | ||
|
99bc6bbb8f | ||
|
e034f11dcc | ||
|
b8de3270fa | ||
|
60b7f6edff | ||
|
15e36a20e1 | ||
|
bc0742ae67 | ||
|
0809225a0a | ||
|
645da51b5f | ||
|
dcf53ac3ff | ||
|
ff61b8a2e7 | ||
|
84703080b8 | ||
|
55f032b18e | ||
|
62cdbdc26a | ||
|
af04c8e2da | ||
|
a8117c6e0b | ||
|
a2ccc1526e | ||
|
8ca0076332 | ||
|
d4514f5f16 | ||
|
a7e9e362b7 | ||
|
8b7010fc9a | ||
|
aa5181ca81 | ||
|
ef14359d31 | ||
|
e97de4643f | ||
|
34e6ce431f | ||
|
2310deec53 | ||
|
8cdd1e3aef | ||
|
2bf17f71e7 | ||
|
750c780293 | ||
|
eb5cee4934 | ||
|
d54de72471 | ||
|
65d7e74888 | ||
|
0907a572df | ||
|
534083c665 | ||
|
a0f28f4a15 | ||
|
5af04c1a66 | ||
|
8a0523885e | ||
|
a92b332550 | ||
|
d1b0964c03 | ||
|
2e5b719de8 | ||
|
c99ae3b419 | ||
|
3215232691 | ||
|
f3684e0051 | ||
|
0f586bec2d | ||
|
91bb378207 | ||
|
6dbebe2634 | ||
|
86e8ab6bf3 | ||
|
82677f4235 | ||
|
06829c8400 | ||
|
694f30d0f8 | ||
|
22c6d18cd8 | ||
|
6a769c9d59 | ||
|
157ff82197 | ||
|
6824e64dcd | ||
|
9e09b271e7 | ||
|
30557c28bb | ||
|
281c18badc | ||
|
6ce58601f7 | ||
|
3026c340ca | ||
|
d41218c97e | ||
|
738fe45b4b | ||
|
d810c262e4 | ||
|
d09b712458 | ||
|
ab07fb5b3f | ||
|
34448fb87c | ||
|
00a7097b9e | ||
|
3f669147f1 | ||
|
158cb415a9 | ||
|
ce69abc06e | ||
|
b7f01a08f3 | ||
|
0235868c66 | ||
|
1067a9f356 | ||
|
60c7308126 | ||
|
fa72ed10b6 | ||
|
f9ef30bc02 | ||
|
1cb057bda7 | ||
|
7fe42852a8 | ||
|
c62fad0088 | ||
|
0ecf456d7f | ||
|
ea89af30c7 | ||
|
59a33d0fa9 | ||
|
8c542e4028 | ||
|
ae6a5c908e | ||
|
d59a38665c | ||
|
d294ef10d7 | ||
|
1440b2f7fe | ||
|
a46f60bd94 | ||
|
3d9336459f | ||
|
40545e62af | ||
|
1a82685dd8 | ||
|
fef73b1b6a | ||
|
ec2c4dd883 | ||
|
91231a6073 | ||
|
ea236abf18 | ||
|
69a3aee01e | ||
|
9e91240283 | ||
|
2ade3ec7b9 | ||
|
f585ffa264 | ||
|
538a1acdb5 | ||
|
e0d3ca6c6d | ||
|
c938edc01b | ||
|
f7c09ba63a | ||
|
18c00a4222 | ||
|
3c70768e18 | ||
|
dce01b0542 | ||
|
74b9be82a2 | ||
|
10e94350e9 | ||
|
e97c82c514 | ||
|
0605cbb06e | ||
|
b8d6e68916 | ||
|
8c1484ed5e | ||
|
dda0a48dfa | ||
|
b7a5f9d138 | ||
|
2d05a8bea1 | ||
|
147ecb2063 | ||
|
fdc04e27a4 | ||
|
9f18decd92 | ||
|
b4bb88cad3 | ||
|
b85fdf11b4 | ||
|
bb0ee837bc | ||
|
a6628fc65f | ||
|
eab6399490 | ||
|
fc7b372ce4 | ||
|
17f8936f42 | ||
|
97351c95c0 | ||
|
7f434c0413 | ||
|
347eceeda5 | ||
|
204758834d | ||
|
122943d835 | ||
|
96fbb226c5 | ||
|
a7f8342171 | ||
|
6e99e3fbbb | ||
|
39b876e37a | ||
|
e40d481d09 | ||
|
656bebd4da | ||
|
6e89fbd146 | ||
|
e1010ff592 | ||
|
0a1e15988f | ||
|
1567804509 | ||
|
546ca01071 | ||
|
96cd76998b | ||
|
90d37f5ec6 | ||
|
8562e19776 | ||
|
a9f111dca0 | ||
|
7ff794cb87 | ||
|
8bb464bd64 | ||
|
c4bc47e6e7 | ||
|
a49ca9cbf7 | ||
|
b38ab84a13 | ||
|
1c9def2fdb | ||
|
1bb04bb0c2 | ||
|
38ed49cef5 | ||
|
6d5fc96714 | ||
|
0af9bcef60 | ||
|
cf7394d01c | ||
|
4ba7a2bbd2 | ||
|
9c789856bd | ||
|
63802aa7f6 | ||
|
61845f9706 | ||
|
cb10f8cd4f | ||
|
9c64fe466d | ||
|
fe933e78bd | ||
|
3f1d6d453c | ||
|
4530ae28cd | ||
|
6dc4259c6e | ||
|
1d0a178eb5 | ||
|
cd6620a044 | ||
|
e226252921 | ||
|
a95f760ff7 | ||
|
03eff69829 | ||
|
d32508aa75 | ||
|
7b372fbcaa | ||
|
eaf0aac77e | ||
|
fb4dd6c2ac | ||
|
d54ee0eb04 | ||
|
c65b4e5d3b | ||
|
d35b2e3b8f | ||
|
a05e38dbd3 | ||
|
e2bbc0aa04 | ||
|
c215b24a19 | ||
|
ef208012c4 | ||
|
c292926086 | ||
|
d4dfdf04fc | ||
|
f484ec216e | ||
|
40f1ede775 | ||
|
756904f985 | ||
|
9c34304cb9 | ||
|
3c149b9b59 | ||
|
89b9915c12 | ||
|
d16a619489 | ||
|
b9cf950bbf | ||
|
e71d965e32 | ||
|
3310a45029 | ||
|
3cce668353 | ||
|
816bb531b3 | ||
|
4595db39aa | ||
|
c513c9685d | ||
|
5c3a418e65 | ||
|
35d6140068 | ||
|
4512ece17d | ||
|
97a12ddab7 | ||
|
dff8490daa | ||
|
ad16dbc50a | ||
|
57cd8888e2 | ||
|
38e28dbf4e | ||
|
9a87765e61 | ||
|
b5bd695f2b | ||
|
2878cca52c | ||
|
bda7af08fa | ||
|
bf5796744b | ||
|
14119d7366 | ||
|
77a2feeb9f | ||
|
2468ae35cd | ||
|
69d74544aa | ||
|
9073a05328 | ||
|
c8accd314a | ||
|
be6d6b7d74 | ||
|
6479217cb4 | ||
|
c76848e089 | ||
|
c389d44e9a | ||
|
db03a24109 | ||
|
1e988c97ad | ||
|
a0893b291a | ||
|
42b6d28b3c | ||
|
8e44de7f83 | ||
|
8f4700e690 | ||
|
812eb229df | ||
|
80af6e43e4 | ||
|
3dab58e6db | ||
|
cabab44b75 | ||
|
387f3bbc5d | ||
|
bd1984386e | ||
|
12916243ec | ||
|
4e1425023e | ||
|
4c277b3039 | ||
|
67beda6c92 | ||
|
10cd89a99d | ||
|
a257137993 | ||
|
9edcb393b6 | ||
|
1594402312 | ||
|
79552a93fe | ||
|
53b1f38952 | ||
|
f920c26802 | ||
|
2f816dff9b | ||
|
b5e3fe3b8e | ||
|
f9541d301f | ||
|
1829da669c | ||
|
1dc2af78ce | ||
|
3d54ab78b2 | ||
|
a92865ce8a | ||
|
5d4e182336 | ||
|
b4319b5ad8 | ||
|
eb166147c3 | ||
|
cd300c52ee | ||
|
2d7ccaeb3d | ||
|
06b59551b0 | ||
|
f9bcf19f9a | ||
|
e3d5c9cb10 | ||
|
e17e35f0ef | ||
|
d3e255935a | ||
|
901d984ee3 | ||
|
4ac3e2978b | ||
|
806838c3af | ||
|
b54da430b9 | ||
|
08f96df3ac | ||
|
d7fdc2114a | ||
|
a81a672ffe | ||
|
f6b1abe23f | ||
|
9cf2c2201b | ||
|
1e052bde90 | ||
|
8658be004e | ||
|
313567d07d | ||
|
9d5ffce732 | ||
|
6418f2eedb | ||
|
4617967e14 | ||
|
14df243661 | ||
|
012309a06a | ||
|
36b68d3702 | ||
|
4b5a9d8c49 | ||
|
59366208b0 | ||
|
27bd3cea4f | ||
|
a965436cd6 | ||
|
6224a656c3 | ||
|
8a56af9192 | ||
|
a42effd4fc | ||
|
b740ed8064 | ||
|
7bfe935e37 | ||
|
377352fced | ||
|
6235a4d92e | ||
|
a89364aa98 | ||
|
5d96107496 | ||
|
3014bc3467 | ||
|
9fbc5c0537 | ||
|
cf39dd2163 | ||
|
e0083bc58e | ||
|
66de5df1d1 | ||
|
b82f7a2dfd | ||
|
17f74f7da8 | ||
|
a01d05997e | ||
|
6fb32c3594 | ||
|
eaa47ff335 | ||
|
c31cb67118 | ||
|
2f79958acb | ||
|
c5c323ca88 | ||
|
8bef7217ec | ||
|
a6cd353655 | ||
|
bd44deea0d | ||
|
336f4aa6a7 | ||
|
935ed36433 | ||
|
e9841910e9 | ||
|
1cb430f59b | ||
|
e0ca3c014c | ||
|
30da307d13 | ||
|
cf03daa0fd | ||
|
af98e025d1 | ||
|
ba32708ed4 | ||
|
54d0ac9d20 | ||
|
21d3635e8d | ||
|
69d62ef383 |
@@ -1,11 +1,20 @@
|
||||
{
|
||||
"name": "freqtrade Develop",
|
||||
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": ".."
|
||||
},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [
|
||||
8080
|
||||
],
|
||||
"mounts": [
|
||||
"source=freqtrade-bashhistory,target=/home/ftuser/commandhistory,type=volume"
|
||||
],
|
||||
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "ftuser",
|
||||
|
||||
"service": "ft_vscode",
|
||||
"postCreateCommand": "freqtrade create-userdir --userdir user_data/",
|
||||
|
||||
"workspaceFolder": "/freqtrade/",
|
||||
|
||||
@@ -25,20 +34,6 @@
|
||||
"ms-python.vscode-pylance",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Uncomment the next line if you want start specific services in your Docker Compose config.
|
||||
// "runServices": [],
|
||||
|
||||
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
|
||||
// "shutdownAction": "none",
|
||||
|
||||
// Uncomment the next line to run commands after the container is created - for example installing curl.
|
||||
// "postCreateCommand": "sudo apt-get update && apt-get install -y git",
|
||||
|
||||
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "ftuser"
|
||||
}
|
||||
|
@@ -1,24 +0,0 @@
|
||||
---
|
||||
version: '3'
|
||||
services:
|
||||
ft_vscode:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ".devcontainer/Dockerfile"
|
||||
volumes:
|
||||
# Allow git usage within container
|
||||
- "${HOME}/.ssh:/home/ftuser/.ssh:ro"
|
||||
- "${HOME}/.gitconfig:/home/ftuser/.gitconfig:ro"
|
||||
- ..:/freqtrade:cached
|
||||
# Persist bash-history
|
||||
- freqtrade-vscode-server:/home/ftuser/.vscode-server
|
||||
- freqtrade-bashhistory:/home/ftuser/commandhistory
|
||||
# Expose API port
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
|
||||
|
||||
volumes:
|
||||
freqtrade-vscode-server:
|
||||
freqtrade-bashhistory:
|
@@ -3,6 +3,7 @@
|
||||
Dockerfile
|
||||
Dockerfile.armhf
|
||||
.dockerignore
|
||||
docker/
|
||||
.coveragerc
|
||||
.eggs
|
||||
.github
|
||||
|
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -1,3 +1,3 @@
|
||||
*.py eol=lf
|
||||
*.sh eol=lf
|
||||
*.ps1 eol=crlf
|
||||
*.py eol=lf
|
||||
*.sh eol=lf
|
||||
*.ps1 eol=crlf
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,5 +2,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord Server
|
||||
url: https://discord.gg/MA9v74M
|
||||
url: https://discord.gg/p7nuUNVfP7
|
||||
about: Ask a question or get community support from our Discord server
|
||||
|
56
.github/workflows/ci.yml
vendored
56
.github/workflows/ci.yml
vendored
@@ -75,17 +75,17 @@ jobs:
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
run: |
|
||||
# Allow failure for coveralls
|
||||
coveralls -v || true
|
||||
coveralls || true
|
||||
|
||||
- name: Backtesting
|
||||
run: |
|
||||
cp config_bittrex.json.example config.json
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||
|
||||
- name: Hyperopt
|
||||
run: |
|
||||
cp config_bittrex.json.example config.json
|
||||
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
|
||||
|
||||
@@ -172,13 +172,13 @@ jobs:
|
||||
|
||||
- name: Backtesting
|
||||
run: |
|
||||
cp config_bittrex.json.example config.json
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||
|
||||
- name: Hyperopt
|
||||
run: |
|
||||
cp config_bittrex.json.example config.json
|
||||
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
|
||||
|
||||
@@ -239,13 +239,13 @@ jobs:
|
||||
|
||||
- name: Backtesting
|
||||
run: |
|
||||
cp config_bittrex.json.example config.json
|
||||
cp config_examples/config_bittrex.example.json config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||
|
||||
- name: Hyperopt
|
||||
run: |
|
||||
cp config_bittrex.json.example config.json
|
||||
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
|
||||
|
||||
@@ -334,6 +334,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -374,13 +375,6 @@ jobs:
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Build and test and push docker image
|
||||
env:
|
||||
IMAGE_NAME: freqtradeorg/freqtrade
|
||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
||||
run: |
|
||||
build_helpers/publish_docker.sh
|
||||
|
||||
# We need docker experimental to pull the ARM image.
|
||||
- name: Switch docker to experimental
|
||||
run: |
|
||||
@@ -399,12 +393,12 @@ jobs:
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Build Raspberry docker image
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
IMAGE_NAME: freqtradeorg/freqtrade
|
||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}_pi
|
||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
||||
run: |
|
||||
build_helpers/publish_docker_pi.sh
|
||||
build_helpers/publish_docker_multi.sh
|
||||
|
||||
|
||||
- name: Slack Notification
|
||||
@@ -418,3 +412,31 @@ jobs:
|
||||
channel: '#notifications'
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
|
||||
deploy_arm:
|
||||
needs: [ deploy ]
|
||||
# Only run on 64bit machines
|
||||
runs-on: [self-hosted, linux, ARM64]
|
||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})"
|
||||
id: extract_branch
|
||||
|
||||
- name: Dockerhub login
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
|
||||
|
||||
- name: Build and test and push docker images
|
||||
env:
|
||||
IMAGE_NAME: freqtradeorg/freqtrade
|
||||
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
|
||||
run: |
|
||||
build_helpers/publish_docker_arm64.sh
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -95,3 +95,8 @@ target/
|
||||
|
||||
#exceptions
|
||||
!*.gitkeep
|
||||
!config_examples/config_binance.example.json
|
||||
!config_examples/config_bittrex.example.json
|
||||
!config_examples/config_ftx.example.json
|
||||
!config_examples/config_full.example.json
|
||||
!config_examples/config_kraken.example.json
|
||||
|
10
.travis.yml
10
.travis.yml
@@ -26,12 +26,12 @@ jobs:
|
||||
# - coveralls || true
|
||||
name: pytest
|
||||
- script:
|
||||
- cp config_bittrex.json.example config.json
|
||||
- cp config_examples/config_bittrex.example.json config.json
|
||||
- freqtrade create-userdir --userdir user_data
|
||||
- freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
||||
name: backtest
|
||||
- script:
|
||||
- cp config_bittrex.json.example config.json
|
||||
- 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
|
||||
name: hyperopt
|
||||
@@ -46,12 +46,6 @@ jobs:
|
||||
- script: mypy freqtrade scripts
|
||||
name: mypy
|
||||
|
||||
# - stage: docker
|
||||
# if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron))
|
||||
# script:
|
||||
# - build_helpers/publish_docker.sh
|
||||
# name: "Build and test and push docker image"
|
||||
|
||||
notifications:
|
||||
slack:
|
||||
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
||||
|
@@ -12,7 +12,7 @@ Few pointers for contributions:
|
||||
- New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR.
|
||||
- PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished).
|
||||
|
||||
If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
||||
If you are unsure, discuss the feature on our [discord server](https://discord.gg/p7nuUNVfP7) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a Pull Request.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
16
Dockerfile
16
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM python:3.9.5-slim-buster as base
|
||||
FROM python:3.9.6-slim-buster as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
@@ -10,8 +10,8 @@ ENV FT_APP_ENV="docker"
|
||||
|
||||
# Prepare environment
|
||||
RUN mkdir /freqtrade \
|
||||
&& apt update \
|
||||
&& apt install -y sudo \
|
||||
&& 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 \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
@@ -22,10 +22,10 @@ WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
FROM base as python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install curl build-essential libssl-dev git \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
@@ -49,7 +49,7 @@ USER ftuser
|
||||
# Install and execute
|
||||
COPY --chown=ftuser:ftuser . /freqtrade/
|
||||
|
||||
RUN pip install -e . --user --no-cache-dir \
|
||||
RUN pip install -e . --user --no-cache-dir --no-build-isolation \
|
||||
&& mkdir /freqtrade/user_data/ \
|
||||
&& freqtrade install-ui
|
||||
|
||||
|
13
README.md
13
README.md
@@ -37,6 +37,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/)
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -123,7 +124,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
|
||||
- `/stop`: Stops the trader.
|
||||
- `/stopbuy`: Stop entering new trades.
|
||||
- `/status <trade_id>|[table]`: Lists all or specific open trades.
|
||||
- `/profit`: Lists cumulative profit from all finished trades
|
||||
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
|
||||
- `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
- `/performance`: Show performance of each finished trade grouped by pair
|
||||
- `/balance`: Show account balance per currency.
|
||||
@@ -141,13 +142,9 @@ The project is currently setup in two main branches:
|
||||
|
||||
## Support
|
||||
|
||||
### Help / Discord / Slack
|
||||
### Help / Discord
|
||||
|
||||
For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel.
|
||||
|
||||
Please check out our [discord server](https://discord.gg/MA9v74M).
|
||||
|
||||
You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw).
|
||||
For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join the Freqtrade [discord server](https://discord.gg/p7nuUNVfP7).
|
||||
|
||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||
|
||||
@@ -178,7 +175,7 @@ to understand the requirements before sending your pull-requests.
|
||||
Coding is not a necessity to contribute - maybe start with improving our documentation?
|
||||
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
||||
|
||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) (please use the #dev channel for this). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||
|
||||
**Important:** Always create your PR against the `develop` branch, not `stable`.
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.21-cp37-cp37m-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.21-cp37-cp37m-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.21-cp38-cp38-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.21-cp38-cp38-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.21-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.21-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
@@ -6,10 +6,13 @@ python -m pip install --upgrade pip
|
||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||
|
||||
if ($pyv -eq '3.7') {
|
||||
pip install build_helpers\TA_Lib-0.4.20-cp37-cp37m-win_amd64.whl
|
||||
pip install build_helpers\TA_Lib-0.4.21-cp37-cp37m-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.8') {
|
||||
pip install build_helpers\TA_Lib-0.4.20-cp38-cp38-win_amd64.whl
|
||||
pip install build_helpers\TA_Lib-0.4.21-cp38-cp38-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.9') {
|
||||
pip install build_helpers\TA_Lib-0.4.21-cp39-cp39-win_amd64.whl
|
||||
}
|
||||
|
||||
pip install -r requirements-dev.txt
|
||||
|
@@ -1,59 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Replace / with _ to create a valid tag
|
||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||
TAG_PLOT=${TAG}_plot
|
||||
echo "Running for ${TAG}"
|
||||
|
||||
# Add commit and commit_message to docker container
|
||||
echo "${GITHUB_SHA}" > freqtrade_commit
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
|
||||
echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache"
|
||||
docker build -t freqtrade:${TAG} .
|
||||
else
|
||||
echo "event ${GITHUB_EVENT_NAME}: building with cache"
|
||||
# Pull last build to avoid rebuilding the whole image
|
||||
docker pull ${IMAGE_NAME}:${TAG}
|
||||
docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} .
|
||||
fi
|
||||
# Tag image for upload and next build step
|
||||
docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG
|
||||
|
||||
docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot .
|
||||
|
||||
docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed building image"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Run backtest
|
||||
docker run --rm -v $(pwd)/config_bittrex.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed running backtest"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed tagging image"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Tag as latest for develop builds
|
||||
if [ "${TAG}" = "develop" ]; then
|
||||
docker tag freqtrade:$TAG ${IMAGE_NAME}:latest
|
||||
fi
|
||||
|
||||
# Show all available images
|
||||
docker images
|
||||
|
||||
docker push ${IMAGE_NAME}
|
||||
docker push ${IMAGE_NAME}:$TAG_PLOT
|
||||
docker push ${IMAGE_NAME}:$TAG
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed pushing repo"
|
||||
return 1
|
||||
fi
|
80
build_helpers/publish_docker_arm64.sh
Executable file
80
build_helpers/publish_docker_arm64.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Use BuildKit, otherwise building on ARM fails
|
||||
export DOCKER_BUILDKIT=1
|
||||
|
||||
# Replace / with _ to create a valid tag
|
||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||
TAG_PLOT=${TAG}_plot
|
||||
TAG_PI="${TAG}_pi"
|
||||
|
||||
TAG_ARM=${TAG}_arm
|
||||
TAG_PLOT_ARM=${TAG_PLOT}_arm
|
||||
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
||||
|
||||
echo "Running for ${TAG}"
|
||||
|
||||
# Add commit and commit_message to docker container
|
||||
echo "${GITHUB_SHA}" > freqtrade_commit
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
|
||||
echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache"
|
||||
# Build regular image
|
||||
docker build -t freqtrade:${TAG_ARM} .
|
||||
|
||||
else
|
||||
echo "event ${GITHUB_EVENT_NAME}: building with cache"
|
||||
# Build regular image
|
||||
docker pull ${IMAGE_NAME}:${TAG_ARM}
|
||||
docker build --cache-from ${IMAGE_NAME}:${TAG_ARM} -t freqtrade:${TAG_ARM} .
|
||||
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed building multiarch images"
|
||||
return 1
|
||||
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 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
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed running backtest"
|
||||
return 1
|
||||
fi
|
||||
|
||||
docker images
|
||||
|
||||
# docker push ${IMAGE_NAME}
|
||||
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
|
||||
docker push ${CACHE_IMAGE}:$TAG_ARM
|
||||
|
||||
# Create multi-arch image
|
||||
# Make sure that all images contained here are pushed to github first.
|
||||
# Otherwise installation might fail.
|
||||
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 push -p ${IMAGE_NAME}:${TAG_PLOT}
|
||||
|
||||
Tag as latest for develop builds
|
||||
if [ "${TAG}" = "develop" ]; then
|
||||
docker tag ${IMAGE_NAME}:develop ${IMAGE_NAME}:latest
|
||||
docker push ${IMAGE_NAME}:latest
|
||||
fi
|
||||
|
||||
docker images
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed building image"
|
||||
return 1
|
||||
fi
|
75
build_helpers/publish_docker_multi.sh
Executable file
75
build_helpers/publish_docker_multi.sh
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
|
||||
# The below assumes a correctly setup docker buildx environment
|
||||
|
||||
# Replace / with _ to create a valid tag
|
||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||
TAG_PLOT=${TAG}_plot
|
||||
TAG_PI="${TAG}_pi"
|
||||
|
||||
PI_PLATFORM="linux/arm/v7"
|
||||
echo "Running for ${TAG}"
|
||||
CACHE_IMAGE=freqtradeorg/freqtrade_cache
|
||||
CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache
|
||||
|
||||
# Add commit and commit_message to docker container
|
||||
echo "${GITHUB_SHA}" > freqtrade_commit
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
|
||||
echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache"
|
||||
# Build regular image
|
||||
docker build -t freqtrade:${TAG} .
|
||||
# Build PI image
|
||||
docker buildx build \
|
||||
--cache-to=type=registry,ref=${CACHE_TAG} \
|
||||
-f docker/Dockerfile.armhf \
|
||||
--platform ${PI_PLATFORM} \
|
||||
-t ${IMAGE_NAME}:${TAG_PI} --push .
|
||||
else
|
||||
echo "event ${GITHUB_EVENT_NAME}: building with cache"
|
||||
# Build regular image
|
||||
docker pull ${IMAGE_NAME}:${TAG}
|
||||
docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} .
|
||||
|
||||
# Pull last build to avoid rebuilding the whole image
|
||||
# docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG}
|
||||
docker buildx build \
|
||||
--cache-from=type=registry,ref=${CACHE_TAG} \
|
||||
--cache-to=type=registry,ref=${CACHE_TAG} \
|
||||
-f docker/Dockerfile.armhf \
|
||||
--platform ${PI_PLATFORM} \
|
||||
-t ${IMAGE_NAME}:${TAG_PI} --push .
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed building multiarch images"
|
||||
return 1
|
||||
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 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
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed running backtest"
|
||||
return 1
|
||||
fi
|
||||
|
||||
docker images
|
||||
|
||||
docker push ${CACHE_IMAGE}
|
||||
docker push ${CACHE_IMAGE}:$TAG_PLOT
|
||||
docker push ${CACHE_IMAGE}:$TAG
|
||||
|
||||
|
||||
docker images
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed building image"
|
||||
return 1
|
||||
fi
|
@@ -1,36 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# The below assumes a correctly setup docker buildx environment
|
||||
|
||||
# Replace / with _ to create a valid tag
|
||||
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
|
||||
PI_PLATFORM="linux/arm/v7"
|
||||
echo "Running for ${TAG}"
|
||||
CACHE_TAG=freqtradeorg/freqtrade_cache:${TAG}_cache
|
||||
|
||||
# Add commit and commit_message to docker container
|
||||
echo "${GITHUB_SHA}" > freqtrade_commit
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
|
||||
echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache"
|
||||
docker buildx build \
|
||||
--cache-to=type=registry,ref=${CACHE_TAG} \
|
||||
-f Dockerfile.armhf \
|
||||
--platform ${PI_PLATFORM} \
|
||||
-t ${IMAGE_NAME}:${TAG} --push .
|
||||
else
|
||||
echo "event ${GITHUB_EVENT_NAME}: building with cache"
|
||||
# Pull last build to avoid rebuilding the whole image
|
||||
# docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG}
|
||||
docker buildx build \
|
||||
--cache-from=type=registry,ref=${CACHE_TAG} \
|
||||
--cache-to=type=registry,ref=${CACHE_TAG} \
|
||||
-f Dockerfile.armhf \
|
||||
--platform ${PI_PLATFORM} \
|
||||
-t ${IMAGE_NAME}:${TAG} --push .
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed building image"
|
||||
return 1
|
||||
fi
|
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0,
|
||||
"use_order_book": false,
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
@@ -21,12 +21,8 @@
|
||||
}
|
||||
},
|
||||
"ask_strategy": {
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 1,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "binance",
|
@@ -12,7 +12,7 @@
|
||||
"sell": 30
|
||||
},
|
||||
"bid_strategy": {
|
||||
"use_order_book": false,
|
||||
"use_order_book": true,
|
||||
"ask_last_balance": 0.0,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
@@ -21,12 +21,8 @@
|
||||
}
|
||||
},
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 1,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0,
|
||||
"use_order_book": false,
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
@@ -21,12 +21,8 @@
|
||||
}
|
||||
},
|
||||
"ask_strategy": {
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 1,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "ftx",
|
@@ -14,6 +14,10 @@
|
||||
"trailing_stop_positive": 0.005,
|
||||
"trailing_stop_positive_offset": 0.0051,
|
||||
"trailing_only_offset_is_reached": false,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"sell_profit_offset": 0.0,
|
||||
"ignore_roi_if_buy_signal": false,
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
@@ -28,7 +32,7 @@
|
||||
},
|
||||
"bid_strategy": {
|
||||
"price_side": "bid",
|
||||
"use_order_book": false,
|
||||
"use_order_book": true,
|
||||
"ask_last_balance": 0.0,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
@@ -38,13 +42,8 @@
|
||||
},
|
||||
"ask_strategy":{
|
||||
"price_side": "ask",
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 1,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"sell_profit_offset": 0.0,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"order_types": {
|
||||
"buy": "limit",
|
||||
@@ -165,11 +164,22 @@
|
||||
"startup": "on",
|
||||
"buy": "on",
|
||||
"buy_fill": "on",
|
||||
"sell": "on",
|
||||
"sell": {
|
||||
"roi": "off",
|
||||
"emergency_sell": "off",
|
||||
"force_sell": "off",
|
||||
"sell_signal": "off",
|
||||
"trailing_stop_loss": "off",
|
||||
"stop_loss": "off",
|
||||
"stoploss_on_exchange": "off",
|
||||
"custom_sell": "off"
|
||||
},
|
||||
"sell_fill": "on",
|
||||
"buy_cancel": "on",
|
||||
"sell_cancel": "on"
|
||||
}
|
||||
},
|
||||
"reload": true,
|
||||
"balance_dust_level": 0.01
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": false,
|
@@ -12,7 +12,7 @@
|
||||
"sell": 30
|
||||
},
|
||||
"bid_strategy": {
|
||||
"use_order_book": false,
|
||||
"use_order_book": true,
|
||||
"ask_last_balance": 0.0,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
@@ -21,12 +21,8 @@
|
||||
}
|
||||
},
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 1,
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
"use_order_book": true,
|
||||
"order_book_top": 1
|
||||
},
|
||||
"exchange": {
|
||||
"name": "kraken",
|
@@ -1,58 +0,0 @@
|
||||
FROM --platform=linux/arm64/v8 python:3.9.4-slim-buster as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONFAULTHANDLER 1
|
||||
ENV PATH=/home/ftuser/.local/bin:$PATH
|
||||
ENV FT_APP_ENV="docker"
|
||||
|
||||
# Prepare environment
|
||||
RUN mkdir /freqtrade \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \
|
||||
&& apt-get clean \
|
||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
# Allow sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
|
||||
|
||||
WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
FROM base as python-deps
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install curl build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
|
||||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
|
||||
USER ftuser
|
||||
RUN pip install --user --no-cache-dir numpy \
|
||||
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt
|
||||
|
||||
# Copy dependencies to runtime-image
|
||||
FROM base as runtime-image
|
||||
COPY --from=python-deps /usr/local/lib /usr/local/lib
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
|
||||
COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
|
||||
|
||||
USER ftuser
|
||||
# Install and execute
|
||||
COPY --chown=ftuser:ftuser . /freqtrade/
|
||||
|
||||
RUN pip install -e . --user --no-cache-dir \
|
||||
&& mkdir /freqtrade/user_data/ \
|
||||
&& freqtrade install-ui
|
||||
|
||||
ENTRYPOINT ["freqtrade"]
|
||||
# Default to trade mode
|
||||
CMD [ "trade" ]
|
@@ -1,4 +1,4 @@
|
||||
FROM --platform=linux/arm/v7 python:3.7.10-slim-buster as base
|
||||
FROM python:3.7.10-slim-buster as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
@@ -11,7 +11,7 @@ ENV FT_APP_ENV="docker"
|
||||
# Prepare environment
|
||||
RUN mkdir /freqtrade \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install libatlas3-base curl sqlite3 libhdf5-serial-dev sudo \
|
||||
&& apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-dev \
|
||||
&& apt-get clean \
|
||||
&& useradd -u 1000 -G sudo -U -m ftuser \
|
||||
&& chown ftuser:ftuser /freqtrade \
|
||||
@@ -22,7 +22,8 @@ WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies
|
||||
FROM base as python-deps
|
||||
RUN apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 \
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip \
|
||||
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
||||
@@ -49,7 +50,7 @@ USER ftuser
|
||||
# Install and execute
|
||||
COPY --chown=ftuser:ftuser . /freqtrade/
|
||||
|
||||
RUN pip install -e . --user --no-cache-dir \
|
||||
RUN pip install -e . --user --no-cache-dir --no-build-isolation\
|
||||
&& mkdir /freqtrade/user_data/ \
|
||||
&& freqtrade install-ui
|
||||
|
@@ -32,6 +32,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
config: Dict, processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
@@ -53,7 +54,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
|
||||
|
||||
Currently, the arguments are:
|
||||
|
||||
* `results`: DataFrame containing the result
|
||||
* `results`: DataFrame containing the resulting trades.
|
||||
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
|
||||
`pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs`
|
||||
* `trade_count`: Amount of trades (identical to `len(results)`)
|
||||
@@ -61,6 +62,7 @@ Currently, the arguments are:
|
||||
* `min_date`: End date of the timerange used
|
||||
* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
|
||||
* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting.
|
||||
* `backtest_stats`: Backtesting statistics using the same format as the backtesting file "strategy" substructure. Available fields can be seen in `generate_strategy_stats()` in `optimize_reports.py`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -289,7 +291,7 @@ 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
|
||||
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,
|
||||
|
@@ -19,7 +19,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[--enable-protections]
|
||||
[--dry-run-wallet DRY_RUN_WALLET]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
[--export EXPORT] [--export-filename PATH]
|
||||
[--export {none,trades}] [--export-filename PATH]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -63,8 +63,8 @@ optional arguments:
|
||||
name is injected into the filename (so `backtest-
|
||||
data.json` becomes `backtest-data-
|
||||
DefaultStrategy.json`
|
||||
--export EXPORT Export backtest results, argument are: trades.
|
||||
Example: `--export=trades`
|
||||
--export {none,trades}
|
||||
Export backtest results (default: trades).
|
||||
--export-filename PATH
|
||||
Save backtest results to the file with this filename.
|
||||
Requires `--export` to be set as well. Example:
|
||||
@@ -100,7 +100,7 @@ Strategy arguments:
|
||||
Now you have good Buy and Sell strategies and some historic data, you want to test it against
|
||||
real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
||||
|
||||
Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default.
|
||||
Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHLCV) data from `user_data/data/<exchange>` by default.
|
||||
If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`.
|
||||
For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation.
|
||||
|
||||
@@ -110,11 +110,16 @@ All profit calculations include fees, and freqtrade will use the exchange's defa
|
||||
|
||||
!!! Warning "Using dynamic pairlists for backtesting"
|
||||
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
||||
Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed.
|
||||
Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed.
|
||||
Please read the [pairlists documentation](plugins.md#pairlists) for more information.
|
||||
|
||||
To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist.
|
||||
|
||||
!!! Note
|
||||
By default, Freqtrade will export backtesting results to `user_data/backtest_results`.
|
||||
The exported trades can be used for [further analysis](#further-backtest-result-analysis) or can be used by the [plotting sub-command](plotting.md#plot-price-and-indicators) (`freqtrade plot-dataframe`) in the scripts directory.
|
||||
|
||||
|
||||
### Starting balance
|
||||
|
||||
Backtesting will require a starting balance, which can be provided as `--dry-run-wallet <balance>` or `--starting-balance <balance>` command line argument, or via `dry_run_wallet` configuration setting.
|
||||
@@ -174,13 +179,13 @@ Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies
|
||||
|
||||
---
|
||||
|
||||
Exporting trades to file
|
||||
Prevent exporting trades to file
|
||||
|
||||
```bash
|
||||
freqtrade backtesting --strategy backtesting --export trades --config config.json
|
||||
freqtrade backtesting --strategy backtesting --export none --config config.json
|
||||
```
|
||||
|
||||
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory.
|
||||
Only use this if you're sure you'll not want to plot or analyze your results further.
|
||||
|
||||
---
|
||||
|
||||
@@ -279,7 +284,7 @@ A backtesting result will look like that:
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Max open trades | 3 |
|
||||
| | |
|
||||
| Total trades | 429 |
|
||||
| Total/Daily Avg Trades| 429 / 3.575 |
|
||||
| Starting balance | 0.01000000 BTC |
|
||||
| Final balance | 0.01762792 BTC |
|
||||
| Absolute profit | 0.00762792 BTC |
|
||||
@@ -297,7 +302,6 @@ A backtesting result will look like that:
|
||||
| Days win/draw/lose | 12 / 82 / 25 |
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Zero Duration Trades | 4.6% (20) |
|
||||
| Rejected Buy signals | 3089 |
|
||||
| | |
|
||||
| Min balance | 0.00945123 BTC |
|
||||
@@ -368,12 +372,11 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Max open trades | 3 |
|
||||
| | |
|
||||
| Total trades | 429 |
|
||||
| Total/Daily Avg Trades| 429 / 3.575 |
|
||||
| Starting balance | 0.01000000 BTC |
|
||||
| Final balance | 0.01762792 BTC |
|
||||
| Absolute profit | 0.00762792 BTC |
|
||||
| Total profit % | 76.2% |
|
||||
| Trades per day | 3.575 |
|
||||
| Avg. stake amount | 0.001 BTC |
|
||||
| Total trade volume | 0.429 BTC |
|
||||
| | |
|
||||
@@ -386,7 +389,6 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
| Days win/draw/lose | 12 / 82 / 25 |
|
||||
| Avg. Duration Winners | 4:23:00 |
|
||||
| Avg. Duration Loser | 6:55:00 |
|
||||
| Zero Duration Trades | 4.6% (20) |
|
||||
| Rejected Buy signals | 3089 |
|
||||
| | |
|
||||
| Min balance | 0.00945123 BTC |
|
||||
@@ -404,12 +406,11 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
|
||||
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
||||
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
|
||||
- `Total trades`: Identical to the total trades of the backtest output table.
|
||||
- `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
|
||||
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
|
||||
- `Final balance`: Final balance - starting balance + absolute profit.
|
||||
- `Absolute profit`: Profit made in stake currency.
|
||||
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
||||
- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
|
||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
|
||||
@@ -417,7 +418,6 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||
- `Zero Duration Trades`: A number of trades that completed within same candle as they opened and had `trailing_stop_loss` sell reason. A significant amount of such trades may indicate that strategy is exploiting trailing stoploss behavior in backtesting and produces unrealistic results.
|
||||
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
|
||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
|
||||
@@ -441,6 +441,7 @@ Since backtesting lacks some detailed information about what happens within a ca
|
||||
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
|
||||
- Low happens before high for stoploss, protecting capital first
|
||||
- Trailing stoploss
|
||||
- Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
|
||||
- High happens first - adjusting stoploss
|
||||
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
|
||||
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
|
||||
|
@@ -5,11 +5,11 @@ By default, these settings are configured via the configuration file (see below)
|
||||
|
||||
## The Freqtrade configuration file
|
||||
|
||||
The bot uses a set of configuration parameters during its operation that all together conform the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file).
|
||||
The bot uses a set of configuration parameters during its operation that all together conform to the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file).
|
||||
|
||||
Per default, the bot loads the configuration from the `config.json` file, located in the current working directory.
|
||||
|
||||
You can specify a different configuration file used by the bot with the `-c/--config` command line option.
|
||||
You can specify a different configuration file used by the bot with the `-c/--config` command-line option.
|
||||
|
||||
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.
|
||||
|
||||
@@ -25,33 +25,34 @@ Multiple configuration files can be specified and used by the bot or the bot can
|
||||
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 default configuration file is not created we recommend you to use `freqtrade new-config --config config.json` to generate a basic configuration file.
|
||||
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 the JSON format.
|
||||
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 syntax of the configuration file at startup and will warn you if you made any errors editing it, pointing out problematic lines.
|
||||
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.
|
||||
|
||||
Freqtrade can also load many options via command line (CLI) arguments (check out the commands `--help` output for details).
|
||||
The prevelance for all Options is as follows:
|
||||
The prevalence for all Options is as follows:
|
||||
|
||||
- CLI arguments override any other option
|
||||
- Configuration files are used in sequence (last file wins), and override Strategy configurations.
|
||||
- Strategy configurations are only used if they are not set via configuration or via command line arguments. These options are marked with [Strategy Override](#parameters-in-the-strategy) in the below table.
|
||||
- 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.
|
||||
|
||||
Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways.
|
||||
|
||||
| Parameter | Description |
|
||||
|------------|-------------|
|
||||
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
|
||||
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
|
||||
| `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
|
||||
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
|
||||
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
|
||||
| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float.
|
||||
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
|
||||
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
|
||||
@@ -74,19 +75,18 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
|
||||
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
|
||||
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
||||
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||
| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
|
||||
| `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).<br> *Defaults to `ask`.* <br> **Datatype:** String (either `ask` or `bid`).
|
||||
| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled).
|
||||
| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean
|
||||
| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||
| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||
| `ask_strategy.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
|
||||
| `ask_strategy.sell_profit_only` | Wait until the bot reaches `ask_strategy.sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `ask_strategy.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)
|
||||
| `ask_strategy.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
|
||||
| `ask_strategy.ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
||||
| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||
| `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `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)
|
||||
| `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
|
||||
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
||||
@@ -102,10 +102,11 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
||||
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** List of Dicts
|
||||
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). <br> **Datatype:** List of Dicts
|
||||
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
||||
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
@@ -140,7 +141,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
|
||||
### Parameters in the strategy
|
||||
|
||||
The following parameters can be set in either configuration file or strategy.
|
||||
The following parameters can be set in the configuration file or strategy.
|
||||
Values set in the configuration file always overwrite values set in the strategy.
|
||||
|
||||
* `minimal_roi`
|
||||
@@ -156,52 +157,67 @@ Values set in the configuration file always overwrite values set in the strategy
|
||||
* `order_time_in_force`
|
||||
* `unfilledtimeout`
|
||||
* `disable_dataframe_checks`
|
||||
* `protections`
|
||||
* `use_sell_signal` (ask_strategy)
|
||||
* `sell_profit_only` (ask_strategy)
|
||||
* `sell_profit_offset` (ask_strategy)
|
||||
* `ignore_roi_if_buy_signal` (ask_strategy)
|
||||
* `ignore_buying_expired_candle_after` (ask_strategy)
|
||||
* `use_sell_signal`
|
||||
* `sell_profit_only`
|
||||
* `sell_profit_offset`
|
||||
* `ignore_roi_if_buy_signal`
|
||||
* `ignore_buying_expired_candle_after`
|
||||
|
||||
### Configuring amount per trade
|
||||
|
||||
There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below.
|
||||
There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#tradable-balance) as explained below.
|
||||
|
||||
#### Minimum trade stake
|
||||
|
||||
The minimum stake amount will depend by exchange and pair, and is usually listed in the exchange support pages.
|
||||
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.4$.
|
||||
The minimum stake amount will depend on exchange and pair and is usually listed in the exchange support pages.
|
||||
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$.
|
||||
|
||||
The minimum stake amount to buy this pair is therefore `20 * 0.6 ~= 12`.
|
||||
The minimum stake amount to buy this pair is, therefore, `20 * 0.6 ~= 12`.
|
||||
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
|
||||
|
||||
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
|
||||
|
||||
With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)`). If we take in account a stoploss of 10% on top of that - we'd end up with a value of ~14$ (`12.6 / (1 - 0.1)`).
|
||||
With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)`). If we take into account a stoploss of 10% on top of that - we'd end up with a value of ~14$ (`12.6 / (1 - 0.1)`).
|
||||
|
||||
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
|
||||
|
||||
!!! Warning
|
||||
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange.
|
||||
|
||||
#### Available balance
|
||||
#### Tradable balance
|
||||
|
||||
By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade.
|
||||
Freqtrade will reserve 1% for eventual fees when entering a trade and will therefore not touch that by default.
|
||||
|
||||
You can configure the "untouched" amount by using the `tradable_balance_ratio` setting.
|
||||
|
||||
For example, if you have 10 ETH available in your wallet on the exchange and `tradable_balance_ratio=0.5` (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers this as available balance. The rest of the wallet is untouched by the trades.
|
||||
For example, if you have 10 ETH available in your wallet on the exchange and `tradable_balance_ratio=0.5` (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers this as an available balance. The rest of the wallet is untouched by the trades.
|
||||
|
||||
!!! Danger
|
||||
This setting should **not** be used when running multiple bots on the same account. Please look at [Available Capital to the bot](#assign-available-capital) instead.
|
||||
|
||||
!!! Warning
|
||||
The `tradable_balance_ratio` setting applies to the current balance (free balance + tied up in trades). Therefore, assuming the starting balance of 1000, a configuration with `tradable_balance_ratio=0.99` will not guarantee that 10 currency units will always remain available on the exchange. For example, the free amount may reduce to 5 units if the total balance is reduced to 500 (either by a losing streak, or by withdrawing balance).
|
||||
The `tradable_balance_ratio` setting applies to the current balance (free balance + tied up in trades). Therefore, assuming the starting balance of 1000, a configuration with `tradable_balance_ratio=0.99` will not guarantee that 10 currency units will always remain available on the exchange. For example, the free amount may reduce to 5 units if the total balance is reduced to 500 (either by a losing streak or by withdrawing balance).
|
||||
|
||||
#### Assign available Capital
|
||||
|
||||
To fully utilize compounding profits when using multiple bots on the same exchange account, you'll want to limit each bot to a certain starting balance.
|
||||
This can be accomplished by setting `available_capital` to the desired starting balance.
|
||||
|
||||
Assuming your account has 10.000 USDT and you want to run 2 different strategies on this exchange.
|
||||
You'd set `available_capital=5000` - granting each bot an initial capital of 5000 USDT.
|
||||
The bot will then split this starting balance equally into `max_open_trades` buckets.
|
||||
Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot.
|
||||
|
||||
!!! Warning "Incompatible with `tradable_balance_ratio`"
|
||||
Setting this option will replace any configuration of `tradable_balance_ratio`.
|
||||
|
||||
#### Amend last stake amount
|
||||
|
||||
Assuming we have the tradable balance of 1000 USDT, `stake_amount=400`, and `max_open_trades=3`.
|
||||
The bot would open 2 trades, and will be unable to fill the last trading slot, since the requested 400 USDT are no longer available, since 800 USDT are already tied in other trades.
|
||||
The bot would open 2 trades and will be unable to fill the last trading slot, since the requested 400 USDT are no longer available since 800 USDT are already tied in other trades.
|
||||
|
||||
To overcome this, the option `amend_last_stake_amount` can be set to `True`, which will enable the bot to reduce stake_amount to the available balance in order to fill the last trade slot.
|
||||
To overcome this, the option `amend_last_stake_amount` can be set to `True`, which will enable the bot to reduce stake_amount to the available balance to fill the last trade slot.
|
||||
|
||||
In the example above this would mean:
|
||||
|
||||
@@ -229,7 +245,7 @@ For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a conf
|
||||
|
||||
#### Dynamic stake amount
|
||||
|
||||
Alternatively, you can use a dynamic stake amount, which will use the available balance on the exchange, and divide that equally by the amount of allowed trades (`max_open_trades`).
|
||||
Alternatively, you can use a dynamic stake amount, which will use the available balance on the exchange, and divide that equally by the number of allowed trades (`max_open_trades`).
|
||||
|
||||
To configure this, set `stake_amount="unlimited"`. We also recommend to set `tradable_balance_ratio=0.99` (99%) - to keep a minimum balance for eventual fees.
|
||||
|
||||
@@ -247,18 +263,18 @@ To allow the bot to trade all the available `stake_currency` in your account (mi
|
||||
```
|
||||
|
||||
!!! Tip "Compounding profits"
|
||||
This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available), and will result in profit compounding.
|
||||
This configuration will allow increasing/decreasing stakes depending on the performance of the bot (lower stake if the bot is losing, higher stakes if the bot has a winning record since higher balances are available), and will result in profit compounding.
|
||||
|
||||
!!! Note "When using Dry-Run Mode"
|
||||
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time.
|
||||
It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
||||
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve.
|
||||
It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise, it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
|
||||
|
||||
--8<-- "includes/pricing.md"
|
||||
|
||||
### Understand minimal_roi
|
||||
|
||||
The `minimal_roi` configuration parameter is a JSON object where the key is a duration
|
||||
in minutes and the value is the minimum ROI as ratio.
|
||||
in minutes and the value is the minimum ROI as a ratio.
|
||||
See the example below:
|
||||
|
||||
```json
|
||||
@@ -273,7 +289,7 @@ See the example below:
|
||||
Most of the strategy files already include the optimal `minimal_roi` value.
|
||||
This parameter can be set in either Strategy or Configuration file. If you use it in the configuration file, it will override the
|
||||
`minimal_roi` value from the strategy file.
|
||||
If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal roi is disabled unless your trade generates 1000% profit.
|
||||
If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal ROI is disabled unless your trade generates 1000% profit.
|
||||
|
||||
!!! Note "Special case to forcesell after a specific time"
|
||||
A special case presents using `"<N>": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell.
|
||||
@@ -292,18 +308,21 @@ See [the telegram documentation](telegram-usage.md) for details on usage.
|
||||
|
||||
When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it.
|
||||
|
||||
In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired.
|
||||
In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired.
|
||||
|
||||
For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy:
|
||||
|
||||
``` json
|
||||
"ask_strategy":{
|
||||
{
|
||||
//...
|
||||
"ignore_buying_expired_candle_after": 300,
|
||||
"price_side": "bid",
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
This setting resets with each new candle, so it will not prevent sticking-signals from executing on the 2nd or 3rd candle they're active. Best use a "trigger" selector for buy signals, which are only active for one candle.
|
||||
|
||||
### Understand order_types
|
||||
|
||||
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`, `forcebuy`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
|
||||
@@ -316,7 +335,7 @@ the buy order is fulfilled.
|
||||
`order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place.
|
||||
|
||||
If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
||||
`stoploss_on_exchange`) need to be present, otherwise the bot will fail to start.
|
||||
`stoploss_on_exchange`) need to be present, otherwise, the bot will fail to start.
|
||||
|
||||
For information on (`emergencysell`,`forcesell`, `forcebuy`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
|
||||
|
||||
@@ -367,7 +386,7 @@ Configuration:
|
||||
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
|
||||
|
||||
!!! Warning "Warning: stoploss_on_exchange failures"
|
||||
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
|
||||
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however, this is not advised.
|
||||
|
||||
### Understand order_time_in_force
|
||||
|
||||
@@ -377,12 +396,12 @@ is executed on the exchange. Three commonly used time in force are:
|
||||
**GTC (Good Till Canceled):**
|
||||
|
||||
This is most of the time the default time in force. It means the order will remain
|
||||
on exchange till it is canceled by user. It can be fully or partially fulfilled.
|
||||
on exchange till it is cancelled by the user. It can be fully or partially fulfilled.
|
||||
If partially fulfilled, the remaining will stay on the exchange till cancelled.
|
||||
|
||||
**FOK (Fill Or Kill):**
|
||||
|
||||
It means if the order is not executed immediately AND fully then it is canceled by the exchange.
|
||||
It means if the order is not executed immediately AND fully then it is cancelled by the exchange.
|
||||
|
||||
**IOC (Immediate Or Canceled):**
|
||||
|
||||
@@ -403,8 +422,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
This is an ongoing work. For now it is supported only for binance and only for buy orders.
|
||||
Please don't change the default value unless you know what you are doing.
|
||||
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.
|
||||
|
||||
### Exchange configuration
|
||||
|
||||
@@ -412,7 +431,7 @@ Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports
|
||||
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
||||
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python).
|
||||
However, the bot was tested by the development team with only Bittrex, Binance and Kraken,
|
||||
so the these are the only officially supported exchanges:
|
||||
so these are the only officially supported exchanges:
|
||||
|
||||
- [Bittrex](https://bittrex.com/): "bittrex"
|
||||
- [Binance](https://www.binance.com/): "binance"
|
||||
@@ -438,11 +457,11 @@ A exchange configuration for "binance" would look as follows:
|
||||
},
|
||||
```
|
||||
|
||||
This configuration enables binance, as well as rate limiting to avoid bans from the exchange.
|
||||
This configuration enables binance, as well as rate-limiting to avoid bans from the exchange.
|
||||
`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false.
|
||||
|
||||
!!! Note
|
||||
Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
||||
Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
|
||||
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
|
||||
|
||||
### What values can be used for fiat_display_currency?
|
||||
@@ -456,7 +475,7 @@ The valid values are:
|
||||
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
|
||||
```
|
||||
|
||||
In addition to fiat currencies, a range of cryto currencies are supported.
|
||||
In addition to fiat currencies, a range of crypto currencies is supported.
|
||||
|
||||
The valid values are:
|
||||
|
||||
@@ -467,7 +486,7 @@ The valid values are:
|
||||
## Using Dry-run mode
|
||||
|
||||
We recommend starting the bot in the Dry-run mode to see how your bot will
|
||||
behave and what is the performance of your strategy. In the Dry-run mode the
|
||||
behave and what is the performance of your strategy. In the Dry-run mode, the
|
||||
bot does not engage your money. It only runs a live simulation without
|
||||
creating trades on the exchange.
|
||||
|
||||
@@ -493,14 +512,15 @@ creating trades on the exchange.
|
||||
Once you will be happy with your bot performance running in the Dry-run mode, you can switch it to production mode.
|
||||
|
||||
!!! Note
|
||||
A simulated wallet is available during dry-run mode, and will assume a starting capital of `dry_run_wallet` (defaults to 1000).
|
||||
A simulated wallet is available during dry-run mode and will assume a starting capital of `dry_run_wallet` (defaults to 1000).
|
||||
|
||||
### Considerations for dry-run
|
||||
|
||||
* API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in dry-run mode.
|
||||
* Wallets (`/balance`) are simulated based on `dry_run_wallet`.
|
||||
* Orders are simulated, and will not be posted to the exchange.
|
||||
* Orders are assumed to fill immediately, and will never time out.
|
||||
* Market orders fill based on orderbook volume the moment the order is placed.
|
||||
* Limit orders fill once the price reaches the defined level - or time out based on `unfilledtimeout` settings.
|
||||
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
|
||||
* Open orders (not trades, which are stored in the database) are reset on bot restart.
|
||||
|
||||
@@ -513,7 +533,7 @@ you run it in production mode.
|
||||
### Setup your exchange account
|
||||
|
||||
You will need to create API Keys (usually you get `key` and `secret`, some exchanges require an additional `password`) from the Exchange website and you'll need to insert this into the appropriate fields in the configuration or when asked by the `freqtrade new-config` command.
|
||||
API Keys are usually only required for live trading (trading for real money, bot running in "production mode", executing real orders on the exchange) and are not required for the bot running in dry-run (trade simulation) mode. When you setup the bot in dry-run mode, you may fill these fields with empty values.
|
||||
API Keys are usually only required for live trading (trading for real money, bot running in "production mode", executing real orders on the exchange) and are not required for the bot running in dry-run (trade simulation) mode. When you set up the bot in dry-run mode, you may fill these fields with empty values.
|
||||
|
||||
### To switch your bot in production mode
|
||||
|
||||
@@ -525,7 +545,7 @@ API Keys are usually only required for live trading (trading for real money, bot
|
||||
"dry_run": false,
|
||||
```
|
||||
|
||||
**Insert your Exchange API key (change them by fake api keys):**
|
||||
**Insert your Exchange API key (change them by fake API keys):**
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -543,7 +563,7 @@ API Keys are usually only required for live trading (trading for real money, bot
|
||||
You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange.
|
||||
|
||||
!!! Hint "Keep your secrets secret"
|
||||
To keep your secrets secret, we recommend to use a 2nd configuration for your API keys.
|
||||
To keep your secrets secret, we recommend using a 2nd configuration for your API keys.
|
||||
Simply use the above snippet in a new configuration file (e.g. `config-private.json`) and keep your settings in this file.
|
||||
You can then start the bot with `freqtrade trade --config user_data/config.json --config user_data/config-private.json <...>` to have your keys loaded.
|
||||
|
||||
@@ -553,7 +573,7 @@ You should also make sure to read the [Exchanges](exchanges.md) section of the d
|
||||
|
||||
To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration.
|
||||
|
||||
An example for this can be found in `config_full.json.example`
|
||||
An example for this can be found in `config_examples/config_full.example.json`
|
||||
|
||||
``` json
|
||||
"ccxt_async_config": {
|
||||
|
@@ -271,7 +271,7 @@ mkdir -p user_data/data/binance
|
||||
cp tests/testdata/pairs.json user_data/data/binance
|
||||
```
|
||||
|
||||
If you your configuration directory `user_data` was made by docker, you may get the following error:
|
||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||
|
||||
```
|
||||
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
|
||||
|
@@ -33,3 +33,8 @@ The old section of configuration parameters (`"pairlist"`) has been deprecated i
|
||||
### deprecation of bidVolume and askVolume from volume-pairlist
|
||||
|
||||
Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9.
|
||||
|
||||
### Using order book steps for sell price
|
||||
|
||||
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.
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running.
|
||||
|
||||
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) where you can ask questions.
|
||||
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/p7nuUNVfP7) where you can ask questions.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
@@ -24,82 +24,21 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co
|
||||
|
||||
Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory.
|
||||
|
||||
=== "PC/MAC/Linux"
|
||||
``` bash
|
||||
mkdir ft_userdata
|
||||
cd ft_userdata/
|
||||
# Download the docker-compose file from the repository
|
||||
curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
|
||||
``` bash
|
||||
mkdir ft_userdata
|
||||
cd ft_userdata/
|
||||
# Download the docker-compose file from the repository
|
||||
curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
|
||||
|
||||
# Pull the freqtrade image
|
||||
docker-compose pull
|
||||
# Pull the freqtrade image
|
||||
docker-compose pull
|
||||
|
||||
# Create user directory structure
|
||||
docker-compose run --rm freqtrade create-userdir --userdir user_data
|
||||
# Create user directory structure
|
||||
docker-compose run --rm freqtrade create-userdir --userdir user_data
|
||||
|
||||
# Create configuration - Requires answering interactive questions
|
||||
docker-compose run --rm freqtrade new-config --config user_data/config.json
|
||||
```
|
||||
|
||||
=== "RaspberryPi"
|
||||
``` bash
|
||||
mkdir ft_userdata
|
||||
cd ft_userdata/
|
||||
# Download the docker-compose file from the repository
|
||||
curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml
|
||||
|
||||
# Edit the compose file to use an image named `*_pi` (stable_pi or develop_pi)
|
||||
|
||||
# Pull the freqtrade image
|
||||
docker-compose pull
|
||||
|
||||
# Create user directory structure
|
||||
docker-compose run --rm freqtrade create-userdir --userdir user_data
|
||||
|
||||
# Create configuration - Requires answering interactive questions
|
||||
docker-compose run --rm freqtrade new-config --config user_data/config.json
|
||||
```
|
||||
|
||||
!!! Note "Change your docker Image"
|
||||
You have to change the docker image in the docker-compose file for your Raspberry build to work properly.
|
||||
``` yml
|
||||
image: freqtradeorg/freqtrade:stable_pi
|
||||
# image: freqtradeorg/freqtrade:develop_pi
|
||||
```
|
||||
|
||||
=== "ARM 64 Systenms (Mac M1, Raspberry Pi 4, Jetson Nano)"
|
||||
In case of a Mac M1, make sure that your docker installation is running in native mode
|
||||
Arm64 images are not yet provided via Docker Hub and need to be build locally first.
|
||||
Depending on the device, this may take a few minutes (Apple M1) or multiple hours (Raspberry Pi)
|
||||
|
||||
``` bash
|
||||
# Clone Freqtrade repository
|
||||
git clone https://github.com/freqtrade/freqtrade.git
|
||||
cd freqtrade
|
||||
# Optionally switch to the stable version
|
||||
git checkout stable
|
||||
|
||||
# Modify your docker-compose file to enable building and change the image name
|
||||
# (see the Note Box below for necessary changes)
|
||||
|
||||
# Build image
|
||||
docker-compose build
|
||||
|
||||
# Create user directory structure
|
||||
docker-compose run --rm freqtrade create-userdir --userdir user_data
|
||||
|
||||
# Create configuration - Requires answering interactive questions
|
||||
docker-compose run --rm freqtrade new-config --config user_data/config.json
|
||||
```
|
||||
|
||||
!!! Note "Change your docker Image"
|
||||
You have to change the docker image in the docker-compose file for your arm64 build to work properly.
|
||||
``` yml
|
||||
image: freqtradeorg/freqtrade:custom_arm64
|
||||
build:
|
||||
context: .
|
||||
dockerfile: "./docker/Dockerfile.aarch64"
|
||||
```
|
||||
# Create configuration - Requires answering interactive questions
|
||||
docker-compose run --rm freqtrade new-config --config user_data/config.json
|
||||
```
|
||||
|
||||
The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.
|
||||
The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections.
|
||||
@@ -117,7 +56,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a
|
||||
|
||||
The `SampleStrategy` is run by default.
|
||||
|
||||
!!! Warning "`SampleStrategy` is just a demo!"
|
||||
!!! Danger "`SampleStrategy` is just a demo!"
|
||||
The `SampleStrategy` is there for your reference and give you ideas for your own strategy.
|
||||
Please always backtest your strategy and use dry-run for some time before risking real money!
|
||||
You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md).
|
||||
@@ -167,6 +106,10 @@ Advanced users may edit the docker-compose file further to include all possible
|
||||
|
||||
All freqtrade arguments will be available by running `docker-compose run --rm freqtrade <command> <optional arguments>`.
|
||||
|
||||
!!! Warning "`docker-compose` for trade commands"
|
||||
Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead.
|
||||
This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot.
|
||||
|
||||
!!! Note "`docker-compose run --rm`"
|
||||
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).
|
||||
|
||||
|
@@ -14,11 +14,10 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ
|
||||
|
||||
### Binance sites
|
||||
|
||||
Binance has been split into 3, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized.
|
||||
Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized.
|
||||
|
||||
* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`.
|
||||
* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`.
|
||||
* [binance.je](https://www.binance.je/) - Binance Jersey, trading fiat currencies. Use exchange id: `binanceje`.
|
||||
|
||||
## Kraken
|
||||
|
||||
@@ -54,6 +53,9 @@ Due to the heavy rate-limiting applied by Kraken, the following configuration se
|
||||
|
||||
Bittrex does not support market orders. If you have a message at the bot startup about this, you should change order type values set in your configuration and/or in the strategy from `"market"` to `"limit"`. See some more details on this [here in the FAQ](faq.md#im-getting-the-exchange-bittrex-does-not-support-market-orders-message-and-cannot-run-my-strategy).
|
||||
|
||||
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.
|
||||
|
||||
### Restricted markets
|
||||
|
||||
Bittrex split its exchange into US and International versions.
|
||||
@@ -75,8 +77,9 @@ You can get a list of restricted markets by using the following snippet:
|
||||
``` python
|
||||
import ccxt
|
||||
ct = ccxt.bittrex()
|
||||
_ = ct.load_markets()
|
||||
res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarkets()['result'] if x['IsRestricted']]
|
||||
lm = ct.load_markets()
|
||||
|
||||
res = [p for p, x in lm.items() if 'US' in x['info']['prohibitedIn']]
|
||||
print(res)
|
||||
```
|
||||
|
||||
|
18
docs/faq.md
18
docs/faq.md
@@ -136,6 +136,22 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us
|
||||
> type \path\to\mylogfile.log | findstr "something"
|
||||
```
|
||||
|
||||
### Why does freqtrade not have GPU support?
|
||||
|
||||
First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations.
|
||||
The GPU improvements would only apply to pandas-native calculations - or ones written by yourself.
|
||||
|
||||
For hyperopt, freqtrade is using scikit-optimize, which is built on top of scikit-learn.
|
||||
Their statement about GPU support is [pretty clear](https://scikit-learn.org/stable/faq.html#will-you-add-gpu-support).
|
||||
|
||||
GPU's also are only good at crunching numbers (floating point operations).
|
||||
For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting).
|
||||
As such, GPU's are not too well suited for most parts of hyperopt.
|
||||
|
||||
The benefit of using GPU would therefore be pretty slim - and will not justify the complexity introduced by trying to add GPU support.
|
||||
|
||||
There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity).
|
||||
|
||||
## Hyperopt module
|
||||
|
||||
### How many epochs do I need to get a good Hyperopt result?
|
||||
@@ -156,7 +172,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD
|
||||
|
||||
### Why does it take a long time to run hyperopt?
|
||||
|
||||
* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - or the Freqtrade [discord community](https://discord.gg/MA9v74M). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you.
|
||||
* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [discord community](https://discord.gg/p7nuUNVfP7). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you.
|
||||
|
||||
* If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers:
|
||||
|
||||
|
@@ -51,7 +51,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
|
||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||
[--random-state INT] [--min-trades INT]
|
||||
[--hyperopt-loss NAME]
|
||||
[--hyperopt-loss NAME] [--disable-param-export]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -118,6 +118,8 @@ optional arguments:
|
||||
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
|
||||
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
|
||||
SortinoHyperOptLoss, SortinoHyperOptLossDaily
|
||||
--disable-param-export
|
||||
Disable automatic hyperopt parameter export.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
@@ -237,9 +239,9 @@ class MyAwesomeStrategy(IStrategy):
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
|
||||
dataframe['bb_lowerband'] = boll['lowerband']
|
||||
dataframe['bb_middleband'] = boll['middleband']
|
||||
dataframe['bb_upperband'] = boll['upperband']
|
||||
dataframe['bb_lowerband'] = bollinger['lowerband']
|
||||
dataframe['bb_middleband'] = bollinger['middleband']
|
||||
dataframe['bb_upperband'] = bollinger['upperband']
|
||||
return dataframe
|
||||
```
|
||||
|
||||
@@ -403,6 +405,9 @@ While this strategy is most likely too simple to provide consistent profit, it s
|
||||
!!! Note
|
||||
`self.buy_ema_short.range` will act differently between hyperopt and other modes. For hyperopt, the above example may generate 48 new columns, however for all other modes (backtesting, dry/live), it will only generate the column for the selected value. You should therefore avoid using the resulting column with explicit values (values other than `self.buy_ema_short.value`).
|
||||
|
||||
!!! Note
|
||||
`range` property may also be used with `DecimalParameter` and `CategoricalParameter`. `RealParameter` does not provide this property due to infinite search space.
|
||||
|
||||
??? Hint "Performance tip"
|
||||
By doing the calculation of all possible indicators in `populate_indicators()`, the calculation of the indicator happens only once for every parameter.
|
||||
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).
|
||||
@@ -491,7 +496,7 @@ 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
|
||||
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:
|
||||
buy_params = {
|
||||
@@ -509,7 +514,13 @@ You should understand this result like:
|
||||
* You should not use ADX because `'buy_adx_enabled': False`.
|
||||
* You should **consider** using the RSI indicator (`'buy_rsi_enabled': True`) and the best value is `29.0` (`'buy_rsi': 29.0`)
|
||||
|
||||
Your strategy class can immediately take advantage of these results. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed.
|
||||
### Automatic parameter application to the strategy
|
||||
|
||||
When using Hyperoptable parameters, the result of your hyperopt-run will be written to a json file next to your strategy (so for `MyAwesomeStrategy.py`, the file would be `MyAwesomeStrategy.json`).
|
||||
This file is also updated when using the `hyperopt-show` sub-command, unless `--disable-param-export` is provided to either of the 2 commands.
|
||||
|
||||
|
||||
Your strategy class can also contain these results explicitly. Simply copy hyperopt results block and paste them at class level, replacing old parameters (if any). New parameters will automatically be loaded next time strategy is executed.
|
||||
|
||||
Transferring your whole hyperopt result to your strategy would then look like:
|
||||
|
||||
@@ -525,6 +536,10 @@ class MyAwesomeStrategy(IStrategy):
|
||||
}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Values in the configuration file will overwrite Parameter-file level parameters - and both will overwrite parameters within the strategy.
|
||||
The prevalence is therefore: config > parameter file > strategy
|
||||
|
||||
### Understand Hyperopt ROI results
|
||||
|
||||
If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table:
|
||||
@@ -532,7 +547,7 @@ If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'de
|
||||
```
|
||||
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
|
||||
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722%). Avg duration 180.4 mins. Objective: 1.94367
|
||||
|
||||
# ROI table:
|
||||
minimal_roi = {
|
||||
@@ -587,7 +602,7 @@ If you are optimizing stoploss values (i.e. if optimization search-space contain
|
||||
```
|
||||
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
|
||||
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:
|
||||
buy_params = {
|
||||
@@ -629,7 +644,7 @@ If you are optimizing trailing stop values (i.e. if optimization search-space co
|
||||
```
|
||||
Best result:
|
||||
|
||||
45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48Σ%). Avg duration 150.3 mins. Objective: -1.10161
|
||||
45/100: 606 trades. Avg profit 1.04%. Total profit 0.31555614 BTC ( 630.48%). Avg duration 150.3 mins. Objective: -1.10161
|
||||
|
||||
# Trailing stop:
|
||||
trailing_stop = True
|
||||
|
@@ -23,6 +23,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
|
||||
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
||||
* [`VolumePairList`](#volume-pair-list)
|
||||
* [`AgeFilter`](#agefilter)
|
||||
* [`OffsetFilter`](#offsetfilter)
|
||||
* [`PerformanceFilter`](#performancefilter)
|
||||
* [`PrecisionFilter`](#precisionfilter)
|
||||
* [`PriceFilter`](#pricefilter)
|
||||
@@ -63,17 +64,56 @@ The `refresh_period` setting allows to define the period (in seconds), at which
|
||||
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
|
||||
Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data.
|
||||
|
||||
`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library:
|
||||
`VolumePairList` is per default based on the ticker data from exchange, as reported by the ccxt library:
|
||||
|
||||
* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours.
|
||||
|
||||
```json
|
||||
"pairlists": [{
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"refresh_period": 1800
|
||||
}],
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
`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:
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"refresh_period": 86400,
|
||||
"lookback_days": 7
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
!!! Warning "Range look back and refresh period"
|
||||
When used in conjunction with `lookback_days` and `lookback_timeframe` the `refresh_period` can not be smaller than the candle size in seconds. As this will result in unnecessary requests to the exchanges API.
|
||||
|
||||
!!! Warning "Performance implications when using lookback range"
|
||||
If used in first position in combination with lookback, the computation of the range based 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.
|
||||
|
||||
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
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"refresh_period": 3600,
|
||||
"lookback_timeframe": "1h",
|
||||
"lookback_period": 72
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
!!! Note
|
||||
@@ -81,13 +121,39 @@ Filtering instances (not the first position in the list) will not apply any cach
|
||||
|
||||
#### AgeFilter
|
||||
|
||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`).
|
||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).
|
||||
|
||||
When pairs are first listed on an exchange they can suffer huge price drops and volatility
|
||||
in the first few days while the pair goes through its price-discovery period. Bots can often
|
||||
be caught out buying before the pair has finished dropping in price.
|
||||
|
||||
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days.
|
||||
This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days and listed before `max_days_listed`.
|
||||
|
||||
#### OffsetFilter
|
||||
|
||||
Offsets an incoming pairlist by a given `offset` value.
|
||||
|
||||
As an example it can be used in conjunction with `VolumeFilter` to remove the top X volume pairs. Or to split
|
||||
a larger pairlist on two bot instances.
|
||||
|
||||
Example to remove the first 10 pairs from the pairlist:
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "OffsetFilter",
|
||||
"offset": 10
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
When `OffsetFilter` is used to split a larger pairlist among multiple bots in combination with `VolumeFilter`
|
||||
it can not be guaranteed that pairs won't overlap due to slightly different refresh intervals for the
|
||||
`VolumeFilter`.
|
||||
|
||||
!!! Note
|
||||
An offset larger then the total length of the incoming pairlist will result in an empty pairlist.
|
||||
|
||||
#### PerformanceFilter
|
||||
|
||||
@@ -122,8 +188,8 @@ The `max_price` setting removes pairs where the price is above the specified pri
|
||||
This option is disabled by default, and will only apply if set to > 0.
|
||||
|
||||
The `max_value` setting removes pairs where the minimum value change is above a specified value.
|
||||
This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20$) as the coin has risen sharply since the last limit adaption.
|
||||
As a result of the above, you can only buy for 20$, or 40$ - but not for 25$.
|
||||
This is useful when an exchange has unbalanced limits. For example, if step-size = 1 (so you can only buy 1, or 2, or 3, but not 1.1 Coins) - and the price is pretty high (like 20\$) as the coin has risen sharply since the last limit adaption.
|
||||
As a result of the above, you can only buy for 20\$, or 40\$ - but not for 25\$.
|
||||
On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can result in high value coins / amounts that are unsellable as the amount is slightly below the limit.
|
||||
|
||||
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
|
||||
|
@@ -47,7 +47,7 @@ Also, prices at the "ask" side of the spread are higher than prices at the "bid"
|
||||
|
||||
#### Buy price with Orderbook enabled
|
||||
|
||||
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
|
||||
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
|
||||
|
||||
#### Buy price without Orderbook enabled
|
||||
|
||||
@@ -82,22 +82,9 @@ In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot
|
||||
|
||||
#### Sell price with Orderbook enabled
|
||||
|
||||
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the configured orderbook side are validated for a profitable sell-possibility based on the strategy configuration (`minimal_roi` conditions) and the sell order is placed at the first profitable spot.
|
||||
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_top` entries in the orderbook and uses the entry specified as `ask_strategy.order_book_top` from the configured side (`ask_strategy.price_side`) as selling price.
|
||||
|
||||
!!! Note
|
||||
Using `order_book_max` higher than `order_book_min` only makes sense when ask_strategy.price_side is set to `"ask"`.
|
||||
|
||||
The idea here is to place the sell order early, to be ahead in the queue.
|
||||
|
||||
A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number.
|
||||
|
||||
!!! Warning "Order_book_max > 1 - increased risks for stoplosses!"
|
||||
Using `ask_strategy.order_book_max` higher than 1 will increase the risk the stoploss on exchange is cancelled too early, since an eventual [stoploss on exchange](#understand-order_types) will be cancelled as soon as the order is placed.
|
||||
Also, the sell order will remain on the exchange for `unfilledtimeout.sell` (or until it's filled) - which can lead to missed stoplosses (with or without using stoploss on exchange).
|
||||
|
||||
!!! Warning "Order_book_max > 1 in dry-run"
|
||||
Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly.
|
||||
It is therefore advised to not use this setting for dry-runs.
|
||||
1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
|
||||
|
||||
#### Sell price without Orderbook enabled
|
||||
|
||||
|
@@ -1,14 +1,13 @@
|
||||
## Protections
|
||||
|
||||
!!! Warning "Beta feature"
|
||||
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord, Slack or via Github Issue.
|
||||
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
|
||||
|
||||
Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs.
|
||||
All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys.
|
||||
|
||||
!!! Note
|
||||
Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance.
|
||||
To align your protection with your strategy, you can define protections in the strategy.
|
||||
|
||||
!!! Tip
|
||||
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
|
||||
@@ -47,16 +46,16 @@ 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.
|
||||
|
||||
```json
|
||||
"protections": [
|
||||
``` python
|
||||
protections = [
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period_candles": 24,
|
||||
"trade_limit": 4,
|
||||
"stop_duration_candles": 4,
|
||||
"only_per_pair": false
|
||||
"only_per_pair": False
|
||||
}
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
!!! Note
|
||||
@@ -69,8 +68,8 @@ The below example stops trading for all pairs for 4 candles after the last trade
|
||||
|
||||
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.
|
||||
|
||||
```json
|
||||
"protections": [
|
||||
``` python
|
||||
protections = [
|
||||
{
|
||||
"method": "MaxDrawdown",
|
||||
"lookback_period_candles": 48,
|
||||
@@ -78,7 +77,7 @@ The below sample stops trading for 12 candles if max-drawdown is > 20% consideri
|
||||
"stop_duration_candles": 12,
|
||||
"max_allowed_drawdown": 0.2
|
||||
},
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
#### Low Profit Pairs
|
||||
@@ -88,8 +87,8 @@ 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.
|
||||
|
||||
```json
|
||||
"protections": [
|
||||
``` python
|
||||
protections = [
|
||||
{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period_candles": 6,
|
||||
@@ -97,7 +96,7 @@ The below example will stop trading a pair for 60 minutes if the pair does not h
|
||||
"stop_duration": 60,
|
||||
"required_profit": 0.02
|
||||
}
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
#### Cooldown Period
|
||||
@@ -106,13 +105,13 @@ The below example will stop trading a pair for 60 minutes if the pair does not h
|
||||
|
||||
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
|
||||
|
||||
```json
|
||||
"protections": [
|
||||
``` python
|
||||
protections = [
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"stop_duration_candles": 2
|
||||
}
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
!!! Note
|
||||
@@ -132,46 +131,6 @@ The below example assumes a timeframe of 1 hour:
|
||||
* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
|
||||
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
|
||||
|
||||
```json
|
||||
"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
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
You can use the same in your strategy, the syntax is only slightly different:
|
||||
|
||||
``` python
|
||||
from freqtrade.strategy import IStrategy
|
||||
|
||||
|
@@ -47,6 +47,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/)
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -72,13 +73,9 @@ Alternatively
|
||||
|
||||
## Support
|
||||
|
||||
### Help / Discord / Slack
|
||||
### Help / Discord
|
||||
|
||||
For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel.
|
||||
|
||||
Please check out our [discord server](https://discord.gg/MA9v74M).
|
||||
|
||||
You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw).
|
||||
For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join the Freqtrade [discord server](https://discord.gg/p7nuUNVfP7).
|
||||
|
||||
## Ready to try?
|
||||
|
||||
|
@@ -60,7 +60,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
|
||||
sudo apt-get update
|
||||
|
||||
# install packages
|
||||
sudo apt install -y python3-pip python3-venv python3-pandas git
|
||||
sudo apt install -y python3-pip python3-venv python3-dev python3-pandas git
|
||||
```
|
||||
|
||||
=== "RaspberryPi/Raspbian"
|
||||
@@ -203,6 +203,8 @@ sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||
./configure --prefix=/usr/local
|
||||
make
|
||||
sudo make install
|
||||
# On debian based systems (debian, ubuntu, ...) - updating ldconfig might be necessary.
|
||||
sudo ldconfig
|
||||
cd ..
|
||||
rm -rf ./ta-lib*
|
||||
```
|
||||
|
68
docs/overrides/main.html
Normal file
68
docs/overrides/main.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
<!-- Navigation -->
|
||||
{% block site_nav %}
|
||||
|
||||
<!-- Main navigation -->
|
||||
{% if nav %}
|
||||
{% if page and page.meta and page.meta.hide %}
|
||||
{% set hidden = "hidden" if "navigation" in page.meta.hide %}
|
||||
{% endif %}
|
||||
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" {{ hidden }}>
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div id="widget-wrapper">
|
||||
|
||||
</div>
|
||||
<div class="md-sidebar__inner">
|
||||
{% include "partials/nav.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Table of contents -->
|
||||
{% if page.toc and not "toc.integrate" in features %}
|
||||
{% if page and page.meta and page.meta.hide %}
|
||||
{% set hidden = "hidden" if "toc" in page.meta.hide %}
|
||||
{% endif %}
|
||||
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" {{ hidden }}>
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
{% include "partials/toc.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
|
||||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
||||
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Load binance SDK -->
|
||||
<script async defer src="https://public.bnbstatic.com/static/js/broker-sdk/broker-sdk@1.0.0.min.js"></script>
|
||||
|
||||
<script>
|
||||
window.onload = function () {
|
||||
var sidebar = document.getElementById('widget-wrapper')
|
||||
var newDiv = document.createElement("div");
|
||||
newDiv.id = "widget";
|
||||
try {
|
||||
sidebar.prepend(newDiv);
|
||||
|
||||
window.binanceBrokerPortalSdk.initBrokerSDK('#widget', {
|
||||
apiHost: 'https://www.binance.com',
|
||||
brokerId: 'R4BD3S82',
|
||||
slideTime: 4e4,
|
||||
});
|
||||
} catch(err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
@@ -1,72 +0,0 @@
|
||||
{#-
|
||||
This file was automatically generated - do not edit
|
||||
-#}
|
||||
{% set site_url = config.site_url | d(nav.homepage.url, true) | url %}
|
||||
{% if not config.use_directory_urls and site_url[0] == site_url[-1] == "." %}
|
||||
{% set site_url = site_url ~ "/index.html" %}
|
||||
{% endif %}
|
||||
<header class="md-header" data-md-component="header">
|
||||
<nav class="md-header__inner md-grid" aria-label="{{ lang.t('header.title') }}">
|
||||
<a href="{{ site_url }}" title="{{ config.site_name | e }}" class="md-header__button md-logo"
|
||||
aria-label="{{ config.site_name }}">
|
||||
{% include "partials/logo.html" %}
|
||||
</a>
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
{% include ".icons/material/menu" ~ ".svg" %}
|
||||
</label>
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
<div class="md-header__topic">
|
||||
<span class="md-ellipsis">
|
||||
{{ config.site_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
{% if page and page.meta and page.meta.title %}
|
||||
{{ page.meta.title }}
|
||||
{% else %}
|
||||
{{ page.title }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-header__options">
|
||||
{% if config.extra.alternate %}
|
||||
<div class="md-select">
|
||||
{% set icon = config.theme.icon.alternate or "material/translate" %}
|
||||
<span class="md-header__button md-icon">
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</span>
|
||||
<div class="md-select__inner">
|
||||
<ul class="md-select__list">
|
||||
{% for alt in config.extra.alternate %}
|
||||
<li class="md-select__item">
|
||||
<a href="{{ alt.link | url }}" class="md-select__link">
|
||||
{{ alt.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if "search" in config["plugins"] %}
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
{% include ".icons/material/magnify.svg" %}
|
||||
</label>
|
||||
{% include "partials/search.html" %}
|
||||
{% endif %}
|
||||
{% if config.repo_url %}
|
||||
<div class="md-header__source">
|
||||
{% include "partials/source.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
|
||||
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
|
||||
</header>
|
@@ -170,9 +170,15 @@ Additional features when using plot_config include:
|
||||
* Specify additional subplots
|
||||
* Specify indicator pairs to fill area in between
|
||||
|
||||
The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult.
|
||||
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
||||
It also allows multiple subplots to display both MACD and RSI at the same time.
|
||||
|
||||
Plot type can be configured using `type` key. Possible types are:
|
||||
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
||||
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
||||
|
||||
Extra parameters to `plotly.graph_objects.*` constructor can be specified in `plotly` dict.
|
||||
|
||||
Sample configuration with inline comments explaining the process:
|
||||
|
||||
``` python
|
||||
@@ -198,7 +204,8 @@ Sample configuration with inline comments explaining the process:
|
||||
# Create subplot MACD
|
||||
"MACD": {
|
||||
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||
'macdsignal': {'color': 'orange'}
|
||||
'macdsignal': {'color': 'orange'},
|
||||
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||
},
|
||||
# Additional subplot RSI
|
||||
"RSI": {
|
||||
@@ -213,6 +220,9 @@ Sample configuration with inline comments explaining the process:
|
||||
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
||||
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
||||
|
||||
!!! Warning
|
||||
`plotly` arguments are only supported with plotly library and will not work with freq-ui.
|
||||
|
||||
## Plot profit
|
||||
|
||||

|
||||
@@ -265,6 +275,7 @@ optional arguments:
|
||||
(backtest file)) Default: file
|
||||
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
|
||||
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
|
||||
--auto-open Automatically open generated plot.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
@@ -1,3 +1,4 @@
|
||||
mkdocs-material==7.1.5
|
||||
mkdocs==1.2.2
|
||||
mkdocs-material==7.2.1
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==8.2
|
||||
|
@@ -55,7 +55,7 @@ class AwesomeStrategy(IStrategy):
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
|
||||
# Obtain last available candle. Do not use current_time to look up latest candle, because
|
||||
# current_time points to curret incomplete candle whose data is not available.
|
||||
# current_time points to current incomplete candle whose data is not available.
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
# <...>
|
||||
|
||||
@@ -83,7 +83,7 @@ It is possible to define custom sell signals, indicating that specified position
|
||||
|
||||
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
|
||||
|
||||
Using custom_sell() signals in place of stoplosses though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
||||
Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
|
||||
|
||||
!!! Note
|
||||
Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
|
||||
@@ -243,7 +243,7 @@ class AwesomeStrategy(IStrategy):
|
||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||
|
||||
if current_profit < 0.04:
|
||||
return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss
|
||||
return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss
|
||||
|
||||
# After reaching the desired offset, allow the stoploss to trail by half the profit
|
||||
desired_stoploss = current_profit / 2
|
||||
@@ -454,7 +454,7 @@ class AwesomeStrategy(IStrategy):
|
||||
# ... populate_* methods
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, **kwargs) -> bool:
|
||||
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a buy order.
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
@@ -469,6 +469,7 @@ class AwesomeStrategy(IStrategy):
|
||||
:param amount: Amount in target (quote) currency that's going to be traded.
|
||||
:param rate: Rate that's going to be used when using limit orders
|
||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the buy-order is placed on the exchange.
|
||||
False aborts the process
|
||||
@@ -490,7 +491,8 @@ class AwesomeStrategy(IStrategy):
|
||||
# ... populate_* methods
|
||||
|
||||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||||
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
|
||||
rate: float, time_in_force: str, sell_reason: str,
|
||||
current_time: datetime, **kwargs) -> bool:
|
||||
"""
|
||||
Called right before placing a regular sell order.
|
||||
Timing for this function is critical, so avoid doing heavy computations or
|
||||
@@ -508,6 +510,7 @@ class AwesomeStrategy(IStrategy):
|
||||
:param sell_reason: Sell reason.
|
||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||
'sell_signal', 'force_sell', 'emergency_sell']
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return bool: When True is returned, then the sell-order is placed on the exchange.
|
||||
False aborts the process
|
||||
@@ -521,6 +524,39 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
```
|
||||
|
||||
### Stake size management
|
||||
|
||||
It is possible to manage your risk by reducing or increasing stake amount when placing a new trade.
|
||||
|
||||
```python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_stake: float, min_stake: float, max_stake: float,
|
||||
**kwargs) -> float:
|
||||
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
|
||||
current_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
|
||||
if self.config['stake_amount'] == 'unlimited':
|
||||
# Use entire available wallet during favorable conditions when in compounding mode.
|
||||
return max_stake
|
||||
else:
|
||||
# Compound profits during favorable conditions instead of using a static stake.
|
||||
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']
|
||||
|
||||
# Use default stake amount.
|
||||
return proposed_stake
|
||||
```
|
||||
|
||||
Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.
|
||||
|
||||
!!! Tip
|
||||
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged.
|
||||
|
||||
!!! Tip
|
||||
Returning `0` or `None` will prevent trades from being placed.
|
||||
|
||||
---
|
||||
|
||||
## Derived strategies
|
||||
|
@@ -130,6 +130,44 @@ trades = load_backtest_data(backtest_dir)
|
||||
trades.groupby("pair")["sell_reason"].value_counts()
|
||||
```
|
||||
|
||||
## Plotting daily profit / equity line
|
||||
|
||||
|
||||
```python
|
||||
# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)
|
||||
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
|
||||
import plotly.express as px
|
||||
import pandas as pd
|
||||
|
||||
# strategy = 'SampleStrategy'
|
||||
# config = Configuration.from_files(["user_data/config.json"])
|
||||
# backtest_dir = config["user_data_dir"] / "backtest_results"
|
||||
|
||||
stats = load_backtest_stats(backtest_dir)
|
||||
strategy_stats = stats['strategy'][strategy]
|
||||
|
||||
dates = []
|
||||
profits = []
|
||||
for date_profit in strategy_stats['daily_profit']:
|
||||
dates.append(date_profit[0])
|
||||
profits.append(date_profit[1])
|
||||
|
||||
equity = 0
|
||||
equity_daily = []
|
||||
for daily_profit in profits:
|
||||
equity_daily.append(equity)
|
||||
equity += float(daily_profit)
|
||||
|
||||
|
||||
df = pd.DataFrame({'dates': dates,'equity_daily': equity_daily})
|
||||
|
||||
fig = px.line(df, x="dates", y="equity_daily")
|
||||
fig.show()
|
||||
|
||||
```
|
||||
|
||||
### Load live trading results into a pandas dataframe
|
||||
|
||||
In case you did already some trading and want to analyze your performance
|
||||
|
@@ -11,3 +11,18 @@
|
||||
.rst-versions .rst-other-versions {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
#widget-wrapper {
|
||||
height: calc(220px * 0.5625 + 18px);
|
||||
width: 220px;
|
||||
margin: 0 auto 16px auto;
|
||||
border-style: solid;
|
||||
border-color: var(--md-code-bg-color);
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: calc(76.25em - 1px)) {
|
||||
#widget-wrapper { display: none; }
|
||||
}
|
||||
|
@@ -72,22 +72,32 @@ Example configuration showing the different settings:
|
||||
|
||||
``` json
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "your_telegram_token",
|
||||
"chat_id": "your_telegram_chat_id",
|
||||
"notification_settings": {
|
||||
"status": "silent",
|
||||
"warning": "on",
|
||||
"startup": "off",
|
||||
"buy": "silent",
|
||||
"sell": "on",
|
||||
"buy_cancel": "silent",
|
||||
"sell_cancel": "on",
|
||||
"buy_fill": "off",
|
||||
"sell_fill": "off"
|
||||
},
|
||||
"balance_dust_level": 0.01
|
||||
},
|
||||
"enabled": true,
|
||||
"token": "your_telegram_token",
|
||||
"chat_id": "your_telegram_chat_id",
|
||||
"notification_settings": {
|
||||
"status": "silent",
|
||||
"warning": "on",
|
||||
"startup": "off",
|
||||
"buy": "silent",
|
||||
"sell": {
|
||||
"roi": "silent",
|
||||
"emergency_sell": "on",
|
||||
"force_sell": "on",
|
||||
"sell_signal": "silent",
|
||||
"trailing_stop_loss": "on",
|
||||
"stop_loss": "on",
|
||||
"stoploss_on_exchange": "on",
|
||||
"custom_sell": "silent"
|
||||
},
|
||||
"buy_cancel": "silent",
|
||||
"sell_cancel": "on",
|
||||
"buy_fill": "off",
|
||||
"sell_fill": "off"
|
||||
},
|
||||
"reload": true,
|
||||
"balance_dust_level": 0.01
|
||||
},
|
||||
```
|
||||
|
||||
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange.
|
||||
@@ -96,6 +106,7 @@ Example configuration showing the different settings:
|
||||
|
||||
|
||||
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||
`reload` allows you to disable reload-buttons on selected messages.
|
||||
|
||||
## Create a custom keyboard (command shortcut buttons)
|
||||
|
||||
@@ -154,7 +165,7 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/count` | Displays number of trades used and available
|
||||
| `/locks` | Show currently locked pairs.
|
||||
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
|
||||
| `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
||||
@@ -234,10 +245,10 @@ current max
|
||||
Return a summary of your profit/loss and performance.
|
||||
|
||||
> **ROI:** Close trades
|
||||
> ∙ `0.00485701 BTC (258.45%)`
|
||||
> ∙ `0.00485701 BTC (2.2%) (15.2 Σ%)`
|
||||
> ∙ `62.968 USD`
|
||||
> **ROI:** All trades
|
||||
> ∙ `0.00255280 BTC (143.43%)`
|
||||
> ∙ `0.00255280 BTC (1.5%) (6.43 Σ%)`
|
||||
> ∙ `33.095 EUR`
|
||||
>
|
||||
> **Total Trade Count:** `138`
|
||||
@@ -246,6 +257,10 @@ Return a summary of your profit/loss and performance.
|
||||
> **Avg. Duration:** `2:33:45`
|
||||
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||
|
||||
The relative profit of `1.2%` is the average profit per trade.
|
||||
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||
|
||||
### /forcesell <trade_id>
|
||||
|
||||
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||
|
@@ -614,6 +614,48 @@ Show whitelist when using a [dynamic pairlist](plugins.md#pairlists).
|
||||
freqtrade test-pairlist --config config.json --quote USDT BTC
|
||||
```
|
||||
|
||||
## Webserver mode
|
||||
|
||||
!!! Warning "Experimental"
|
||||
Webserver mode is an experimental mode to increase backesting and strategy development productivity.
|
||||
There may still be bugs - so if you happen to stumble across these, please report them as github issues, thanks.
|
||||
|
||||
Run freqtrade in webserver mode.
|
||||
Freqtrade will start the webserver and allow FreqUI to start and control backtesting processes.
|
||||
This has the advantage that data will not be reloaded between backtesting runs (as long as timeframe and timerange remain identical).
|
||||
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]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
--logfile FILE Log to the file specified. Special values are:
|
||||
'syslog', 'journald'. See the documentation for more
|
||||
details.
|
||||
-V, --version show program's version number and exit
|
||||
-c PATH, --config PATH
|
||||
Specify configuration file (default:
|
||||
`userdir/config.json` or `config.json` whichever
|
||||
exists). Multiple --config options may be used. Can be
|
||||
set to `-` to read config from stdin.
|
||||
-d PATH, --datadir PATH
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
Path to userdata directory.
|
||||
|
||||
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
|
||||
|
||||
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
||||
@@ -702,7 +744,8 @@ You can show the details of any hyperoptimization epoch previously evaluated by
|
||||
usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[-d PATH] [--userdir PATH] [--best]
|
||||
[--profitable] [-n INT] [--print-json]
|
||||
[--hyperopt-filename PATH] [--no-header]
|
||||
[--hyperopt-filename FILENAME] [--no-header]
|
||||
[--disable-param-export]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -714,6 +757,8 @@ optional arguments:
|
||||
Hyperopt result filename.Example: `--hyperopt-
|
||||
filename=hyperopt_results_2020-09-27_16-20-48.pickle`
|
||||
--no-header Do not print epoch details header.
|
||||
--disable-param-export
|
||||
Disable automatic hyperopt parameter export.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
@@ -23,7 +23,7 @@ git clone https://github.com/freqtrade/freqtrade.git
|
||||
|
||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.20‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version).
|
||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.21-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
||||
|
||||
Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows.
|
||||
Other versions must be downloaded from the above link.
|
||||
|
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2021.5'
|
||||
__version__ = '2021.7'
|
||||
|
||||
if __version__ == 'develop':
|
||||
|
||||
|
@@ -20,3 +20,4 @@ from freqtrade.commands.optimize_commands import start_backtesting, start_edge,
|
||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.commands.trade_commands import start_trading
|
||||
from freqtrade.commands.webserver_commands import start_webserver
|
||||
|
@@ -16,6 +16,8 @@ ARGS_STRATEGY = ["strategy", "strategy_path"]
|
||||
|
||||
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
|
||||
|
||||
ARGS_WEBSERVER: List[str] = []
|
||||
|
||||
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
||||
"max_open_trades", "stake_amount", "fee", "pairs"]
|
||||
|
||||
@@ -29,7 +31,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||
"epochs", "spaces", "print_all",
|
||||
"print_colorized", "print_json", "hyperopt_jobs",
|
||||
"hyperopt_random_state", "hyperopt_min_trades",
|
||||
"hyperopt_loss"]
|
||||
"hyperopt_loss", "disableparamexport"]
|
||||
|
||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||
|
||||
@@ -69,7 +71,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
||||
"timerange", "timeframe", "no_trades"]
|
||||
|
||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||
"trade_source", "timeframe"]
|
||||
"trade_source", "timeframe", "plot_auto_open"]
|
||||
|
||||
ARGS_INSTALL_UI = ["erase_ui_only"]
|
||||
|
||||
@@ -85,7 +87,8 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
||||
"hyperoptexportfilename", "export_csv"]
|
||||
|
||||
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header"]
|
||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
||||
"disableparamexport"]
|
||||
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||
@@ -175,7 +178,8 @@ class Arguments:
|
||||
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_show_trades, start_test_pairlist, start_trading,
|
||||
start_webserver)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
# Use custom message when no subhandler is added
|
||||
@@ -383,3 +387,9 @@ class Arguments:
|
||||
)
|
||||
plot_profit_cmd.set_defaults(func=start_plot_profit)
|
||||
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)
|
||||
|
||||
# Add webserver subcommand
|
||||
webserver_cmd = subparsers.add_parser('webserver', help='Webserver module.',
|
||||
parents=[_common_parser])
|
||||
webserver_cmd.set_defaults(func=start_webserver)
|
||||
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
|
||||
|
@@ -183,7 +183,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Applies selections to the template and writes the result to config_path
|
||||
:param config_path: Path object for new config file. Should not exist yet
|
||||
:param selecions: Dict containing selections taken by the user.
|
||||
:param selections: Dict containing selections taken by the user.
|
||||
"""
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
try:
|
||||
@@ -213,7 +213,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
|
||||
def start_new_config(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Create a new strategy from a template
|
||||
Asking the user questions to fill out the templateaccordingly.
|
||||
Asking the user questions to fill out the template accordingly.
|
||||
"""
|
||||
|
||||
config_path = Path(args['config'][0])
|
||||
|
@@ -167,8 +167,9 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"export": Arg(
|
||||
'--export',
|
||||
help='Export backtest results, argument are: trades. '
|
||||
'Example: `--export=trades`',
|
||||
help='Export backtest results (default: trades).',
|
||||
choices=constants.EXPORT_OPTIONS,
|
||||
|
||||
),
|
||||
"exportfilename": Arg(
|
||||
'--export-filename',
|
||||
@@ -177,6 +178,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
|
||||
metavar='PATH',
|
||||
),
|
||||
"disableparamexport": Arg(
|
||||
'--disable-param-export',
|
||||
help="Disable automatic hyperopt parameter export.",
|
||||
action='store_true',
|
||||
),
|
||||
"fee": Arg(
|
||||
'--fee',
|
||||
help='Specify fee ratio. Will be applied twice (on trade entry and exit).',
|
||||
@@ -433,6 +439,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
metavar='INT',
|
||||
default=750,
|
||||
),
|
||||
"plot_auto_open": Arg(
|
||||
'--auto-open',
|
||||
help='Automatically open generated plot.',
|
||||
action='store_true',
|
||||
),
|
||||
"no_trades": Arg(
|
||||
'--no-trades',
|
||||
help='Skip using trades from backtesting file and DB.',
|
||||
|
@@ -8,11 +8,11 @@ from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
||||
refresh_backtest_trades_data)
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -48,7 +48,8 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
# Init exchange
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||
# Manual validations of relevant settings
|
||||
exchange.validate_pairs(config['pairs'])
|
||||
if not config['exchange'].get('skip_pair_validation', False):
|
||||
exchange.validate_pairs(config['pairs'])
|
||||
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
||||
|
||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||
|
@@ -8,9 +8,9 @@ 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.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import render_template, render_template_with_fallback
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@@ -6,9 +6,9 @@ from colorama import init as colorama_init
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.data.btanalysis import get_latest_hyperopt_file
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.optimize.optimize_reports import show_backtest_result
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -67,7 +67,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
if epochs and not no_details:
|
||||
sorted_epochs = sorted(epochs, key=itemgetter('loss'))
|
||||
results = sorted_epochs[0]
|
||||
HyperoptTools.print_epoch_details(results, total_epochs, print_json, no_header)
|
||||
HyperoptTools.show_epoch_details(results, total_epochs, print_json, no_header)
|
||||
|
||||
if epochs and export_csv:
|
||||
HyperoptTools.export_csv_file(
|
||||
@@ -129,11 +129,14 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
|
||||
metrics = val['results_metrics']
|
||||
if 'strategy_name' in metrics:
|
||||
show_backtest_result(metrics['strategy_name'], metrics,
|
||||
strategy_name = metrics['strategy_name']
|
||||
show_backtest_result(strategy_name, metrics,
|
||||
metrics['stake_currency'])
|
||||
|
||||
HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header,
|
||||
header_str="Epoch details")
|
||||
HyperoptTools.try_export_params(config, strategy_name, val)
|
||||
|
||||
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header,
|
||||
header_str="Epoch details")
|
||||
|
||||
|
||||
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||
@@ -197,8 +200,12 @@ def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
|
||||
return x['results_metrics']['duration']
|
||||
else:
|
||||
# New mode
|
||||
avg = x['results_metrics']['holding_avg']
|
||||
return avg.total_seconds() // 60
|
||||
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)
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import csv
|
||||
import logging
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
@@ -12,11 +11,11 @@ from tabulate import tabulate
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import market_is_active, validate_exchanges
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.misc import parse_db_uri_for_logging, plural
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -54,15 +53,21 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
||||
reset = ''
|
||||
|
||||
names = [s['name'] for s in objs]
|
||||
objss_to_print = [{
|
||||
objs_to_print = [{
|
||||
'name': s['name'] if s['name'] else "--",
|
||||
'location': s['location'].name,
|
||||
'status': (red + "LOAD FAILED" + reset if s['class'] is None
|
||||
else "OK" if names.count(s['name']) == 1
|
||||
else yellow + "DUPLICATE NAME" + reset)
|
||||
} for s in objs]
|
||||
|
||||
print(tabulate(objss_to_print, headers='keys', tablefmt='psql', stralign='right'))
|
||||
for idx, s in enumerate(objs):
|
||||
if 'hyperoptable' in s:
|
||||
objs_to_print[idx].update({
|
||||
'hyperoptable': "Yes" if s['hyperoptable']['count'] > 0 else "No",
|
||||
'buy-Params': len(s['hyperoptable'].get('buy', [])),
|
||||
'sell-Params': len(s['hyperoptable'].get('sell', [])),
|
||||
})
|
||||
print(tabulate(objs_to_print, headers='keys', tablefmt='psql', stralign='right'))
|
||||
|
||||
|
||||
def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
@@ -75,6 +80,11 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||
strategy_objs = StrategyResolver.search_all_objects(directory, not args['print_one_column'])
|
||||
# Sort alphabetically
|
||||
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
|
||||
for obj in strategy_objs:
|
||||
if obj['class']:
|
||||
obj['hyperoptable'] = obj['class'].detect_all_parameters()
|
||||
else:
|
||||
obj['hyperoptable'] = {'count': 0}
|
||||
|
||||
if args['print_one_column']:
|
||||
print('\n'.join([s['name'] for s in strategy_objs]))
|
||||
@@ -143,7 +153,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
||||
pairs_only=pairs_only,
|
||||
active_only=active_only)
|
||||
# Sort the pairs/markets by symbol
|
||||
pairs = OrderedDict(sorted(pairs.items()))
|
||||
pairs = dict(sorted(pairs.items()))
|
||||
except Exception as e:
|
||||
raise OperationalException(f"Cannot get markets. Reason: {e}") from e
|
||||
|
||||
@@ -215,7 +225,7 @@ def start_show_trades(args: Dict[str, Any]) -> None:
|
||||
if 'db_url' not in config:
|
||||
raise OperationalException("--db-url is required for this command.")
|
||||
|
||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
|
||||
init_db(config['db_url'], clean_open_orders=False)
|
||||
tfilter = []
|
||||
|
||||
|
@@ -3,9 +3,9 @@ from typing import Any, Dict
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import round_coin_value
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -15,6 +15,7 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
||||
"""
|
||||
Prepare the configuration for the Hyperopt module
|
||||
:param args: Cli args from Arguments()
|
||||
:param method: Bot running mode
|
||||
:return: Configuration
|
||||
"""
|
||||
config = setup_utils_configuration(args, method)
|
||||
|
@@ -4,8 +4,8 @@ from typing import Any, Dict
|
||||
import rapidjson
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -31,7 +31,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
|
||||
results[curr] = pairlists.whitelist
|
||||
|
||||
for curr, pairlist in results.items():
|
||||
if not args.get('print_one_column', False):
|
||||
if not args.get('print_one_column', False) and not args.get('list_pairs_print_json', False):
|
||||
print(f"Pairs for {curr}: ")
|
||||
|
||||
if args.get('print_one_column', False):
|
||||
|
@@ -1,8 +1,8 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
def validate_plot_args(args: Dict[str, Any]) -> None:
|
||||
|
15
freqtrade/commands/webserver_commands.py
Normal file
15
freqtrade/commands/webserver_commands.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
|
||||
|
||||
def start_webserver(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Main entry point for webserver mode
|
||||
"""
|
||||
from freqtrade.configuration import Configuration
|
||||
from freqtrade.rpc.api_server import ApiServer
|
||||
|
||||
# Initialize configuration
|
||||
config = Configuration(args, RunMode.WEBSERVER).get_config()
|
||||
ApiServer(config, standalone=True)
|
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt,
|
||||
is_exchange_officially_supported, validate_exchange)
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.enums import RunMode
|
||||
|
||||
from .check_exchange import remove_credentials
|
||||
from .config_validation import validate_config_consistency
|
||||
@@ -15,6 +15,7 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
|
||||
"""
|
||||
Prepare the configuration for utils subcommands
|
||||
:param args: Cli args from Arguments()
|
||||
:param method: Bot running mode
|
||||
:return: Configuration
|
||||
"""
|
||||
configuration = Configuration(args, method)
|
||||
|
@@ -6,8 +6,8 @@ from jsonschema import Draft4Validator, validators
|
||||
from jsonschema.exceptions import ValidationError, best_match
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -79,6 +79,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
|
||||
_validate_whitelist(conf)
|
||||
_validate_protections(conf)
|
||||
_validate_unlimited_amount(conf)
|
||||
_validate_ask_orderbook(conf)
|
||||
|
||||
# validate configuration before returning
|
||||
logger.info('Validating configuration ...')
|
||||
@@ -149,7 +150,7 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
|
||||
if not conf.get('edge', {}).get('enabled'):
|
||||
return
|
||||
|
||||
if not conf.get('ask_strategy', {}).get('use_sell_signal', True):
|
||||
if not conf.get('use_sell_signal', True):
|
||||
raise OperationalException(
|
||||
"Edge requires `use_sell_signal` to be True, otherwise no sells will happen."
|
||||
)
|
||||
@@ -186,3 +187,23 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
|
||||
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
|
||||
f"Please fix the protection {prot.get('method')}"
|
||||
)
|
||||
|
||||
|
||||
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
|
||||
ask_strategy = conf.get('ask_strategy', {})
|
||||
ob_min = ask_strategy.get('order_book_min')
|
||||
ob_max = ask_strategy.get('order_book_max')
|
||||
if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'):
|
||||
if ob_min != ob_max:
|
||||
raise OperationalException(
|
||||
"Using order_book_max != order_book_min in ask_strategy is no longer supported."
|
||||
"Please pick one value and use `order_book_top` in the future."
|
||||
)
|
||||
else:
|
||||
# Move value to order_book_top
|
||||
ask_strategy['order_book_top'] = ob_min
|
||||
logger.warning(
|
||||
"DEPRECATED: "
|
||||
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` "
|
||||
"for your `ask_strategy` configuration."
|
||||
)
|
||||
|
@@ -12,10 +12,10 @@ 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.load_config import load_config_file, load_file
|
||||
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import setup_logging
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode
|
||||
from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -71,7 +71,7 @@ class Configuration:
|
||||
|
||||
# Merge config options, overwriting old values
|
||||
config = deep_merge_dicts(load_config_file(path), config)
|
||||
|
||||
config['config_files'] = files
|
||||
# Normalize config
|
||||
if 'internals' not in config:
|
||||
config['internals'] = {}
|
||||
@@ -144,7 +144,7 @@ class Configuration:
|
||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||
logger.info('Dry run is disabled')
|
||||
|
||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||
logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"')
|
||||
|
||||
def _process_common_options(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
@@ -260,6 +260,8 @@ class Configuration:
|
||||
self._args_to_config(config, argname='export',
|
||||
logstring='Parameter --export detected: {} ...')
|
||||
|
||||
self._args_to_config(config, argname='disableparamexport',
|
||||
logstring='Parameter --disableparamexport detected: {} ...')
|
||||
# Edge section:
|
||||
if 'stoploss_range' in self.args and self.args["stoploss_range"]:
|
||||
txt_range = eval(self.args["stoploss_range"])
|
||||
@@ -375,6 +377,9 @@ class Configuration:
|
||||
self._args_to_config(config, argname='plot_limit',
|
||||
logstring='Limiting plot to: {}')
|
||||
|
||||
self._args_to_config(config, argname='plot_auto_open',
|
||||
logstring='Parameter --auto-open detected.')
|
||||
|
||||
self._args_to_config(config, argname='trade_source',
|
||||
logstring='Using trades from: {}')
|
||||
|
||||
@@ -457,7 +462,7 @@ class Configuration:
|
||||
pairs_file = Path(self.args["pairs_file"])
|
||||
logger.info(f'Reading pairs file "{pairs_file}".')
|
||||
# Download pairs from the pairs file if no config is specified
|
||||
# or if pairs file is specified explicitely
|
||||
# or if pairs file is specified explicitly
|
||||
if not pairs_file.exists():
|
||||
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
|
||||
config['pairs'] = load_file(pairs_file)
|
||||
|
@@ -3,7 +3,7 @@ Functions to handle deprecated settings
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
@@ -12,23 +12,24 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_conflicting_settings(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str) -> None:
|
||||
section1_config = config.get(section1, {})
|
||||
section2_config = config.get(section2, {})
|
||||
if name1 in section1_config and name2 in section2_config:
|
||||
section_old: str, name_old: str,
|
||||
section_new: Optional[str], name_new: str) -> None:
|
||||
section_new_config = config.get(section_new, {}) if section_new else config
|
||||
section_old_config = config.get(section_old, {})
|
||||
if name_new in section_new_config and name_old in section_old_config:
|
||||
new_name = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
||||
raise OperationalException(
|
||||
f"Conflicting settings `{section1}.{name1}` and `{section2}.{name2}` "
|
||||
f"Conflicting settings `{new_name}` and `{section_old}.{name_old}` "
|
||||
"(DEPRECATED) detected in the configuration file. "
|
||||
"This deprecated setting will be removed in the next versions of Freqtrade. "
|
||||
f"Please delete it from your configuration and use the `{section1}.{name1}` "
|
||||
f"Please delete it from your configuration and use the `{new_name}` "
|
||||
"setting instead."
|
||||
)
|
||||
|
||||
|
||||
def process_removed_setting(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str) -> None:
|
||||
section2: Optional[str], name2: str) -> None:
|
||||
"""
|
||||
:param section1: Removed section
|
||||
:param name1: Removed setting name
|
||||
@@ -37,27 +38,32 @@ def process_removed_setting(config: Dict[str, Any],
|
||||
"""
|
||||
section1_config = config.get(section1, {})
|
||||
if name1 in section1_config:
|
||||
section_2 = f"{section2}.{name2}" if section2 else f"{name2}"
|
||||
raise OperationalException(
|
||||
f"Setting `{section1}.{name1}` has been moved to `{section2}.{name2}. "
|
||||
f"Please delete it from your configuration and use the `{section2}.{name2}` "
|
||||
f"Setting `{section1}.{name1}` has been moved to `{section_2}. "
|
||||
f"Please delete it from your configuration and use the `{section_2}` "
|
||||
"setting instead."
|
||||
)
|
||||
|
||||
|
||||
def process_deprecated_setting(config: Dict[str, Any],
|
||||
section1: str, name1: str,
|
||||
section2: str, name2: str) -> None:
|
||||
section2_config = config.get(section2, {})
|
||||
section_old: str, name_old: str,
|
||||
section_new: Optional[str], name_new: str
|
||||
) -> None:
|
||||
check_conflicting_settings(config, section_old, name_old, section_new, name_new)
|
||||
section_old_config = config.get(section_old, {})
|
||||
|
||||
if name2 in section2_config:
|
||||
if name_old in section_old_config:
|
||||
section_2 = f"{section_new}.{name_new}" if section_new else f"{name_new}"
|
||||
logger.warning(
|
||||
"DEPRECATED: "
|
||||
f"The `{section2}.{name2}` setting is deprecated and "
|
||||
f"The `{section_old}.{name_old}` setting is deprecated and "
|
||||
"will be removed in the next versions of Freqtrade. "
|
||||
f"Please use the `{section1}.{name1}` setting in your configuration instead."
|
||||
f"Please use the `{section_2}` setting in your configuration instead."
|
||||
)
|
||||
section1_config = config.get(section1, {})
|
||||
section1_config[name1] = section2_config[name2]
|
||||
|
||||
section_new_config = config.get(section_new, {}) if section_new else config
|
||||
section_new_config[name_new] = section_old_config[name_old]
|
||||
|
||||
|
||||
def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
@@ -65,15 +71,24 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
# Kept for future deprecated / moved settings
|
||||
# check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
|
||||
# 'experimental', 'use_sell_signal')
|
||||
# process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal',
|
||||
# 'experimental', 'use_sell_signal')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal',
|
||||
None, 'use_sell_signal')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only',
|
||||
None, 'sell_profit_only')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_offset',
|
||||
None, 'sell_profit_offset')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
|
||||
None, 'ignore_roi_if_buy_signal')
|
||||
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after',
|
||||
None, 'ignore_buying_expired_candle_after')
|
||||
|
||||
# Legacy way - having them in experimental ...
|
||||
process_removed_setting(config, 'experimental', 'use_sell_signal',
|
||||
'ask_strategy', 'use_sell_signal')
|
||||
None, 'use_sell_signal')
|
||||
process_removed_setting(config, 'experimental', 'sell_profit_only',
|
||||
'ask_strategy', 'sell_profit_only')
|
||||
None, 'sell_profit_only')
|
||||
process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal',
|
||||
'ask_strategy', 'ignore_roi_if_buy_signal')
|
||||
None, 'ignore_roi_if_buy_signal')
|
||||
|
||||
if (config.get('edge', {}).get('enabled', False)
|
||||
and 'capital_available_percentage' in config.get('edge', {})):
|
||||
|
@@ -43,7 +43,7 @@ def load_file(path: Path) -> Dict[str, Any]:
|
||||
with path.open('r') as file:
|
||||
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||
except FileNotFoundError:
|
||||
raise OperationalException(f'File file "{path}" not found!')
|
||||
raise OperationalException(f'File "{path}" not found!')
|
||||
return config
|
||||
|
||||
|
||||
|
@@ -12,6 +12,7 @@ PROCESS_THROTTLE_SECS = 5 # sec
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
TIMEOUT_UNITS = ['minutes', 'seconds']
|
||||
EXPORT_OPTIONS = ['none', 'trades']
|
||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
|
||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||
@@ -25,9 +26,9 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'AgeFilter', 'PerformanceFilter', 'PrecisionFilter',
|
||||
'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter',
|
||||
'SpreadFilter', 'VolatilityFilter']
|
||||
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
|
||||
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
|
||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
|
||||
DRY_RUN_WALLET = 1000
|
||||
@@ -39,6 +40,7 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
||||
|
||||
LAST_BT_RESULT_FN = '.last_result.json'
|
||||
FTHYPT_FILEVERSION = 'fthypt_fileversion'
|
||||
|
||||
USERPATH_HYPEROPTS = 'hyperopts'
|
||||
USERPATH_STRATEGIES = 'strategies'
|
||||
@@ -61,7 +63,7 @@ DUST_PER_COIN = {
|
||||
}
|
||||
|
||||
|
||||
# Soure files with destination directories within user-directory
|
||||
# Source files with destination directories within user-directory
|
||||
USER_DATA_FILES = {
|
||||
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
|
||||
@@ -111,6 +113,10 @@ CONF_SCHEMA = {
|
||||
'maximum': 1,
|
||||
'default': 0.99
|
||||
},
|
||||
'available_capital': {
|
||||
'type': 'number',
|
||||
'minimum': 0,
|
||||
},
|
||||
'amend_last_stake_amount': {'type': 'boolean', 'default': False},
|
||||
'last_stake_amount_min_ratio': {
|
||||
'type': 'number', 'minimum': 0.0, 'maximum': 1.0, 'default': 0.5
|
||||
@@ -133,6 +139,11 @@ CONF_SCHEMA = {
|
||||
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||
'trailing_only_offset_is_reached': {'type': 'boolean'},
|
||||
'use_sell_signal': {'type': 'boolean'},
|
||||
'sell_profit_only': {'type': 'boolean'},
|
||||
'sell_profit_offset': {'type': 'number'},
|
||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
||||
'ignore_buying_expired_candle_after': {'type': 'number'},
|
||||
'bot_name': {'type': 'string'},
|
||||
'unfilledtimeout': {
|
||||
'type': 'object',
|
||||
@@ -153,7 +164,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'bid'},
|
||||
'use_order_book': {'type': 'boolean'},
|
||||
'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1},
|
||||
'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, },
|
||||
'check_depth_of_market': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
@@ -162,7 +173,7 @@ CONF_SCHEMA = {
|
||||
}
|
||||
},
|
||||
},
|
||||
'required': ['ask_last_balance']
|
||||
'required': ['price_side']
|
||||
},
|
||||
'ask_strategy': {
|
||||
'type': 'object',
|
||||
@@ -175,13 +186,9 @@ CONF_SCHEMA = {
|
||||
'exclusiveMaximum': False,
|
||||
},
|
||||
'use_order_book': {'type': 'boolean'},
|
||||
'order_book_min': {'type': 'integer', 'minimum': 1},
|
||||
'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50},
|
||||
'use_sell_signal': {'type': 'boolean'},
|
||||
'sell_profit_only': {'type': 'boolean'},
|
||||
'sell_profit_offset': {'type': 'number'},
|
||||
'ignore_roi_if_buy_signal': {'type': 'boolean'}
|
||||
}
|
||||
'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, },
|
||||
},
|
||||
'required': ['price_side']
|
||||
},
|
||||
'order_types': {
|
||||
'type': 'object',
|
||||
@@ -260,7 +267,13 @@ CONF_SCHEMA = {
|
||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||
'default': 'off'
|
||||
},
|
||||
'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'sell': {
|
||||
'type': ['string', 'object'],
|
||||
'additionalProperties': {
|
||||
'type': 'string',
|
||||
'enum': TELEGRAM_SETTING_OPTIONS
|
||||
}
|
||||
},
|
||||
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
|
||||
'sell_fill': {
|
||||
'type': 'string',
|
||||
@@ -268,7 +281,8 @@ CONF_SCHEMA = {
|
||||
'default': 'off'
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
'reload': {'type': 'boolean'},
|
||||
},
|
||||
'required': ['enabled', 'token', 'chat_id'],
|
||||
},
|
||||
@@ -302,6 +316,8 @@ CONF_SCHEMA = {
|
||||
'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password']
|
||||
},
|
||||
'db_url': {'type': 'string'},
|
||||
'export': {'type': 'string', 'enum': EXPORT_OPTIONS, 'default': 'trades'},
|
||||
'disableparamexport': {'type': 'boolean'},
|
||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||
'forcebuy_enable': {'type': 'boolean'},
|
||||
'disable_dataframe_checks': {'type': 'boolean'},
|
||||
|
@@ -49,7 +49,7 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *,
|
||||
fill_missing: bool = True,
|
||||
drop_incomplete: bool = True) -> DataFrame:
|
||||
"""
|
||||
Clense a OHLCV dataframe by
|
||||
Cleanse a OHLCV dataframe by
|
||||
* Grouping it by date (removes duplicate tics)
|
||||
* dropping last candles if requested
|
||||
* Filling up missing data (if requested)
|
||||
|
@@ -12,9 +12,9 @@ from pandas import DataFrame
|
||||
|
||||
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.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@@ -52,8 +52,8 @@ class HDF5DataHandler(IDataHandler):
|
||||
"""
|
||||
Store data in hdf5 file.
|
||||
:param pair: Pair - used to generate filename
|
||||
:timeframe: Timeframe - used to generate filename
|
||||
:data: Dataframe containing OHLCV data
|
||||
:param timeframe: Timeframe - used to generate filename
|
||||
:param data: Dataframe containing OHLCV data
|
||||
:return: None
|
||||
"""
|
||||
key = self._pair_ohlcv_key(pair, timeframe)
|
||||
|
@@ -113,6 +113,7 @@ def refresh_data(datadir: Path,
|
||||
:param timeframe: Timeframe (e.g. "5m")
|
||||
:param pairs: List of pairs to load
|
||||
:param exchange: Exchange object
|
||||
:param data_format: dataformat to use
|
||||
:param timerange: Limit data to be loaded to this timerange
|
||||
"""
|
||||
data_handler = get_datahandler(datadir, data_format)
|
||||
@@ -193,8 +194,8 @@ def _download_pair_history(datadir: Path,
|
||||
new_data = exchange.get_historic_ohlcv(pair=pair,
|
||||
timeframe=timeframe,
|
||||
since_ms=since_ms if since_ms else
|
||||
int(arrow.utcnow().shift(
|
||||
days=-new_pairs_days).float_timestamp) * 1000
|
||||
arrow.utcnow().shift(
|
||||
days=-new_pairs_days).int_timestamp * 1000
|
||||
)
|
||||
# TODO: Maybe move parsing to exchange class (?)
|
||||
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
|
||||
@@ -271,7 +272,7 @@ def _download_trades_history(exchange: Exchange,
|
||||
if timerange.stoptype == 'date':
|
||||
until = timerange.stopts * 1000
|
||||
else:
|
||||
since = int(arrow.utcnow().shift(days=-new_pairs_days).float_timestamp) * 1000
|
||||
since = arrow.utcnow().shift(days=-new_pairs_days).int_timestamp * 1000
|
||||
|
||||
trades = data_handler.trades_load(pair)
|
||||
|
||||
|
@@ -49,8 +49,8 @@ class IDataHandler(ABC):
|
||||
"""
|
||||
Store ohlcv data.
|
||||
:param pair: Pair - used to generate filename
|
||||
:timeframe: Timeframe - used to generate filename
|
||||
:data: Dataframe containing OHLCV data
|
||||
:param timeframe: Timeframe - used to generate filename
|
||||
:param data: Dataframe containing OHLCV data
|
||||
:return: None
|
||||
"""
|
||||
|
||||
@@ -245,8 +245,8 @@ def get_datahandler(datadir: Path, data_format: str = None,
|
||||
data_handler: IDataHandler = None) -> IDataHandler:
|
||||
"""
|
||||
:param datadir: Folder to save data
|
||||
:data_format: dataformat to use
|
||||
:data_handler: returns this datahandler if it exists or initializes a new one
|
||||
:param data_format: dataformat to use
|
||||
:param data_handler: returns this datahandler if it exists or initializes a new one
|
||||
"""
|
||||
|
||||
if not data_handler:
|
||||
|
@@ -55,8 +55,8 @@ class JsonDataHandler(IDataHandler):
|
||||
format looks as follows:
|
||||
[[<date>,<open>,<high>,<low>,<close>]]
|
||||
:param pair: Pair - used to generate filename
|
||||
:timeframe: Timeframe - used to generate filename
|
||||
:data: Dataframe containing OHLCV data
|
||||
:param timeframe: Timeframe - used to generate filename
|
||||
:param data: Dataframe containing OHLCV data
|
||||
:return: None
|
||||
"""
|
||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||
|
@@ -13,11 +13,11 @@ from pandas import DataFrame
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
||||
from freqtrade.enums import RunMode, SellType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.exchange import timeframe_to_seconds
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.strategy.interface import IStrategy, SellType
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -301,7 +301,7 @@ class Edge:
|
||||
def _process_expectancy(self, results: DataFrame) -> Dict[str, Any]:
|
||||
"""
|
||||
This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs
|
||||
The calulation will be done per pair and per strategy.
|
||||
The calculation will be done per pair and per strategy.
|
||||
"""
|
||||
# Removing pairs having less than min_trades_number
|
||||
min_trades_number = self.edge_config.get('min_trade_number', 10)
|
||||
|
7
freqtrade/enums/__init__.py
Normal file
7
freqtrade/enums/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# flake8: noqa: F401
|
||||
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.state import State
|
15
freqtrade/enums/backteststate.py
Normal file
15
freqtrade/enums/backteststate.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class BacktestState(Enum):
|
||||
"""
|
||||
Bot application states
|
||||
"""
|
||||
STARTUP = 1
|
||||
DATALOAD = 2
|
||||
ANALYZE = 3
|
||||
CONVERT = 4
|
||||
BACKTEST = 5
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
19
freqtrade/enums/rpcmessagetype.py
Normal file
19
freqtrade/enums/rpcmessagetype.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RPCMessageType(Enum):
|
||||
STATUS = 'status'
|
||||
WARNING = 'warning'
|
||||
STARTUP = 'startup'
|
||||
BUY = 'buy'
|
||||
BUY_FILL = 'buy_fill'
|
||||
BUY_CANCEL = 'buy_cancel'
|
||||
SELL = 'sell'
|
||||
SELL_FILL = 'sell_fill'
|
||||
SELL_CANCEL = 'sell_cancel'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
@@ -1,23 +1,6 @@
|
||||
# pragma pylint: disable=too-few-public-methods
|
||||
|
||||
"""
|
||||
Bot state constant
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class State(Enum):
|
||||
"""
|
||||
Bot application states
|
||||
"""
|
||||
RUNNING = 1
|
||||
STOPPED = 2
|
||||
RELOAD_CONFIG = 3
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
|
||||
class RunMode(Enum):
|
||||
"""
|
||||
Bot running mode (backtest, hyperopt, ...)
|
||||
@@ -31,6 +14,7 @@ class RunMode(Enum):
|
||||
UTIL_EXCHANGE = "util_exchange"
|
||||
UTIL_NO_EXCHANGE = "util_no_exchange"
|
||||
PLOT = "plot"
|
||||
WEBSERVER = "webserver"
|
||||
OTHER = "other"
|
||||
|
||||
|
20
freqtrade/enums/selltype.py
Normal file
20
freqtrade/enums/selltype.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SellType(Enum):
|
||||
"""
|
||||
Enum to distinguish between sell reasons
|
||||
"""
|
||||
ROI = "roi"
|
||||
STOP_LOSS = "stop_loss"
|
||||
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||
SELL_SIGNAL = "sell_signal"
|
||||
FORCE_SELL = "force_sell"
|
||||
EMERGENCY_SELL = "emergency_sell"
|
||||
CUSTOM_SELL = "custom_sell"
|
||||
NONE = ""
|
||||
|
||||
def __str__(self):
|
||||
# explicitly convert to String to help with exporting data.
|
||||
return self.value
|
9
freqtrade/enums/signaltype.py
Normal file
9
freqtrade/enums/signaltype.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SignalType(Enum):
|
||||
"""
|
||||
Enum to distinguish between buy and sell signals
|
||||
"""
|
||||
BUY = "buy"
|
||||
SELL = "sell"
|
13
freqtrade/enums/state.py
Normal file
13
freqtrade/enums/state.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class State(Enum):
|
||||
"""
|
||||
Bot application states
|
||||
"""
|
||||
RUNNING = 1
|
||||
STOPPED = 2
|
||||
RELOAD_CONFIG = 3
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
@@ -47,7 +47,7 @@ class InvalidOrderException(ExchangeError):
|
||||
class RetryableOrderError(InvalidOrderException):
|
||||
"""
|
||||
This is returned when the order is not found.
|
||||
This Error will be repeated with increasing backof (in line with DDosError).
|
||||
This Error will be repeated with increasing backoff (in line with DDosError).
|
||||
"""
|
||||
|
||||
|
||||
@@ -75,6 +75,6 @@ class DDosProtection(TemporaryError):
|
||||
|
||||
class StrategyError(FreqtradeException):
|
||||
"""
|
||||
Errors with custom user-code deteced.
|
||||
Errors with custom user-code detected.
|
||||
Usually caused by errors in the strategy.
|
||||
"""
|
||||
|
@@ -68,6 +68,7 @@ class Binance(Exchange):
|
||||
amount=amount, price=rate, params=params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
|
@@ -22,8 +22,8 @@ from pandas import DataFrame
|
||||
from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException, RetryableOrderError,
|
||||
TemporaryError)
|
||||
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)
|
||||
@@ -88,6 +88,11 @@ class Exchange:
|
||||
|
||||
# Cache for 10 minutes ...
|
||||
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10)
|
||||
# Cache values for 1800 to avoid frequent polling of the exchange for prices
|
||||
# Caching only applies to RPC methods, so prices for open trades are still
|
||||
# refreshed once every iteration.
|
||||
self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
||||
self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
||||
|
||||
# Holds candles
|
||||
self._klines: Dict[Tuple[str, str], DataFrame] = {}
|
||||
@@ -99,6 +104,7 @@ class Exchange:
|
||||
logger.info('Instance is running with dry_run enabled')
|
||||
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||
exchange_config = config['exchange']
|
||||
self.log_responses = exchange_config.get('log_responses', False)
|
||||
|
||||
# Deep merge ft_has with default ft_has options
|
||||
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
||||
@@ -221,10 +227,15 @@ class Exchange:
|
||||
"""exchange ccxt precisionMode"""
|
||||
return self._api.precisionMode
|
||||
|
||||
def _log_exchange_response(self, endpoint, response) -> None:
|
||||
""" Log exchange responses """
|
||||
if self.log_responses:
|
||||
logger.info(f"API {endpoint}: {response}")
|
||||
|
||||
def ohlcv_candle_limit(self, timeframe: str) -> int:
|
||||
"""
|
||||
Exchange ohlcv candle limit
|
||||
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limts
|
||||
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
|
||||
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
|
||||
:param timeframe: Timeframe to check
|
||||
:return: Candle limit as integer
|
||||
@@ -376,7 +387,7 @@ class Exchange:
|
||||
# its contents depend on the exchange.
|
||||
# It can also be a string or similar ... so we need to verify that first.
|
||||
elif (isinstance(self.markets[pair].get('info', None), dict)
|
||||
and self.markets[pair].get('info', {}).get('IsRestricted', False)):
|
||||
and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
|
||||
# Warn users about restricted pairs in whitelist.
|
||||
# We cannot determine reliably if Users are affected.
|
||||
logger.warning(f"Pair {pair} is restricted for some users on this exchange."
|
||||
@@ -464,11 +475,11 @@ class Exchange:
|
||||
return endpoint in self._api.has and self._api.has[endpoint]
|
||||
|
||||
def amount_to_precision(self, pair: str, amount: float) -> float:
|
||||
'''
|
||||
"""
|
||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
||||
based on our definitions.
|
||||
'''
|
||||
"""
|
||||
if self.markets[pair]['precision']['amount']:
|
||||
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
||||
precision=self.markets[pair]['precision']['amount'],
|
||||
@@ -478,14 +489,14 @@ class Exchange:
|
||||
return amount
|
||||
|
||||
def price_to_precision(self, pair: str, price: float) -> float:
|
||||
'''
|
||||
"""
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
||||
which does not support rounding up
|
||||
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||
align with amount_to_precision().
|
||||
Rounds up
|
||||
'''
|
||||
"""
|
||||
if self.markets[pair]['precision']['price']:
|
||||
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
||||
# precision=self.markets[pair]['precision']['price'],
|
||||
@@ -540,7 +551,7 @@ class Exchange:
|
||||
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
|
||||
DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||
amount_reserve_percent = (
|
||||
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
|
||||
)
|
||||
# it should not be more than 50%
|
||||
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
|
||||
@@ -550,11 +561,13 @@ class Exchange:
|
||||
# See also #2575 at github.
|
||||
return max(min_stake_amounts) * amount_reserve_percent
|
||||
|
||||
# Dry-run methods
|
||||
|
||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||
_amount = self.amount_to_precision(pair, amount)
|
||||
dry_order = {
|
||||
dry_order: Dict[str, Any] = {
|
||||
'id': order_id,
|
||||
'symbol': pair,
|
||||
'price': rate,
|
||||
@@ -565,31 +578,115 @@ class Exchange:
|
||||
'side': side,
|
||||
'remaining': _amount,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'timestamp': int(arrow.utcnow().int_timestamp * 1000),
|
||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||
'status': "closed" if ordertype == "market" else "open",
|
||||
'fee': None,
|
||||
'info': {}
|
||||
}
|
||||
self._store_dry_order(dry_order, pair)
|
||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||
|
||||
if dry_order["type"] == "market":
|
||||
# Update market order pricing
|
||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||
dry_order.update({
|
||||
'average': average,
|
||||
'cost': dry_order['amount'] * average,
|
||||
})
|
||||
dry_order = self.add_dry_order_fee(pair, dry_order)
|
||||
|
||||
dry_order = self.check_dry_limit_order_filled(dry_order)
|
||||
|
||||
self._dry_run_open_orders[dry_order["id"]] = dry_order
|
||||
# Copy order and close it - so the returned order is open unless it's a market order
|
||||
return dry_order
|
||||
|
||||
def _store_dry_order(self, dry_order: Dict, pair: str) -> None:
|
||||
closed_order = dry_order.copy()
|
||||
if closed_order['type'] in ["market", "limit"]:
|
||||
closed_order.update({
|
||||
'status': 'closed',
|
||||
'filled': closed_order['amount'],
|
||||
'remaining': 0,
|
||||
'fee': {
|
||||
'currency': self.get_pair_quote_currency(pair),
|
||||
'cost': dry_order['cost'] * self.get_fee(pair),
|
||||
'rate': self.get_fee(pair)
|
||||
}
|
||||
})
|
||||
if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
closed_order["info"].update({"stopPrice": closed_order["price"]})
|
||||
self._dry_run_open_orders[closed_order["id"]] = closed_order
|
||||
def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]:
|
||||
dry_order.update({
|
||||
'fee': {
|
||||
'currency': self.get_pair_quote_currency(pair),
|
||||
'cost': dry_order['cost'] * self.get_fee(pair),
|
||||
'rate': self.get_fee(pair)
|
||||
}
|
||||
})
|
||||
return dry_order
|
||||
|
||||
def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float:
|
||||
"""
|
||||
Get the market order fill price based on orderbook interpolation
|
||||
"""
|
||||
if self.exchange_has('fetchL2OrderBook'):
|
||||
ob = self.fetch_l2_order_book(pair, 20)
|
||||
ob_type = 'asks' if side == 'buy' else 'bids'
|
||||
|
||||
remaining_amount = amount
|
||||
filled_amount = 0
|
||||
for book_entry in ob[ob_type]:
|
||||
book_entry_price = book_entry[0]
|
||||
book_entry_coin_volume = book_entry[1]
|
||||
if remaining_amount > 0:
|
||||
if remaining_amount < book_entry_coin_volume:
|
||||
filled_amount += remaining_amount * book_entry_price
|
||||
else:
|
||||
filled_amount += book_entry_coin_volume * book_entry_price
|
||||
remaining_amount -= book_entry_coin_volume
|
||||
else:
|
||||
break
|
||||
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
|
||||
return self.price_to_precision(pair, forecast_avg_filled_price)
|
||||
|
||||
return rate
|
||||
|
||||
def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool:
|
||||
if not self.exchange_has('fetchL2OrderBook'):
|
||||
return True
|
||||
ob = self.fetch_l2_order_book(pair, 1)
|
||||
if side == 'buy':
|
||||
price = ob['asks'][0][0]
|
||||
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
||||
if limit >= price:
|
||||
return True
|
||||
else:
|
||||
price = ob['bids'][0][0]
|
||||
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
||||
if limit <= price:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Check dry-run limit order fill and update fee (if it filled).
|
||||
"""
|
||||
if order['status'] != "closed" and order['type'] in ["limit"]:
|
||||
pair = order['symbol']
|
||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
||||
order.update({
|
||||
'status': 'closed',
|
||||
'filled': order['amount'],
|
||||
'remaining': 0,
|
||||
})
|
||||
self.add_dry_order_fee(pair, order)
|
||||
|
||||
return order
|
||||
|
||||
def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
|
||||
"""
|
||||
Return dry-run order
|
||||
Only call if running in dry-run mode.
|
||||
"""
|
||||
try:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
order = self.check_dry_limit_order_filled(order)
|
||||
return order
|
||||
except KeyError as e:
|
||||
# Gracefully handle errors with dry-run orders.
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||
|
||||
# Order handling
|
||||
|
||||
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict:
|
||||
@@ -600,8 +697,10 @@ class Exchange:
|
||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||
|
||||
return self._api.create_order(pair, ordertype, side,
|
||||
amount, rate_for_order, params)
|
||||
order = self._api.create_order(pair, ordertype, side,
|
||||
amount, rate_for_order, params)
|
||||
self._log_exchange_response('create_order', order)
|
||||
return order
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
@@ -667,6 +766,134 @@ class Exchange:
|
||||
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
try:
|
||||
order = self._api.fetch_order(order_id, pair)
|
||||
self._log_exchange_response('fetch_order', order)
|
||||
return order
|
||||
except ccxt.OrderNotFound as e:
|
||||
raise RetryableOrderError(
|
||||
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to fetch_stoploss_order to allow easy overriding in other classes
|
||||
fetch_stoploss_order = fetch_order
|
||||
|
||||
def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
|
||||
stoploss_order: bool = False) -> Dict:
|
||||
"""
|
||||
Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
|
||||
the stoploss_order parameter
|
||||
:param order_id: OrderId to fetch order
|
||||
:param pair: Pair corresponding to order_id
|
||||
:param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order.
|
||||
"""
|
||||
if stoploss_order:
|
||||
return self.fetch_stoploss_order(order_id, pair)
|
||||
return self.fetch_order(order_id, pair)
|
||||
|
||||
def check_order_canceled_empty(self, order: Dict) -> bool:
|
||||
"""
|
||||
Verify if an order has been cancelled without being partially filled
|
||||
: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')
|
||||
and order.get('filled') == 0.0)
|
||||
|
||||
@retrier
|
||||
def cancel_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self.fetch_dry_run_order(order_id)
|
||||
|
||||
order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']})
|
||||
return order
|
||||
except InvalidOrderException:
|
||||
return {}
|
||||
|
||||
try:
|
||||
order = self._api.cancel_order(order_id, pair)
|
||||
self._log_exchange_response('cancel_order', order)
|
||||
return order
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not cancel order. Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to cancel_stoploss_order to allow easy overriding in other classes
|
||||
cancel_stoploss_order = cancel_order
|
||||
|
||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||
if not isinstance(corder, dict):
|
||||
return False
|
||||
|
||||
required = ('fee', 'status', 'amount')
|
||||
return all(k in corder for k in required)
|
||||
|
||||
def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
|
||||
"""
|
||||
Cancel order returning a result.
|
||||
Creates a fake result if cancel order returns a non-usable result
|
||||
and fetch_order does not work (certain exchanges don't return cancelled orders)
|
||||
:param order_id: Orderid to cancel
|
||||
:param pair: Pair corresponding to order_id
|
||||
:param amount: Amount to use for fake response
|
||||
:return: Result from either cancel_order if usable, or fetch_order
|
||||
"""
|
||||
try:
|
||||
corder = self.cancel_order(order_id, pair)
|
||||
if self.is_cancel_order_result_suitable(corder):
|
||||
return corder
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not cancel order {order_id} for {pair}.")
|
||||
try:
|
||||
order = self.fetch_order(order_id, pair)
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not fetch cancelled order {order_id}.")
|
||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
||||
|
||||
return order
|
||||
|
||||
def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
|
||||
"""
|
||||
Cancel stoploss order returning a result.
|
||||
Creates a fake result if cancel order returns a non-usable result
|
||||
and fetch_order does not work (certain exchanges don't return cancelled orders)
|
||||
:param order_id: stoploss-order-id to cancel
|
||||
:param pair: Pair corresponding to order_id
|
||||
:param amount: Amount to use for fake response
|
||||
:return: Result from either cancel_order if usable, or fetch_order
|
||||
"""
|
||||
corder = self.cancel_stoploss_order(order_id, pair)
|
||||
if self.is_cancel_order_result_suitable(corder):
|
||||
return corder
|
||||
try:
|
||||
order = self.fetch_stoploss_order(order_id, pair)
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
|
||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
||||
|
||||
return order
|
||||
|
||||
@retrier
|
||||
def get_balances(self) -> dict:
|
||||
|
||||
@@ -713,6 +940,8 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Pricing info
|
||||
|
||||
@retrier
|
||||
def fetch_ticker(self, pair: str) -> dict:
|
||||
try:
|
||||
@@ -729,6 +958,230 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@staticmethod
|
||||
def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]],
|
||||
range_required: bool = True):
|
||||
"""
|
||||
Get next greater value in the list.
|
||||
Used by fetch_l2_order_book if the api only supports a limited range
|
||||
"""
|
||||
if not limit_range:
|
||||
return limit
|
||||
|
||||
result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
|
||||
if not range_required and limit > result:
|
||||
# Range is not required - we can use None as parameter.
|
||||
return None
|
||||
return result
|
||||
|
||||
@retrier
|
||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
"""
|
||||
Get L2 order book from exchange.
|
||||
Can be limited to a certain amount (if supported).
|
||||
Returns a dict in the format
|
||||
{'asks': [price, volume], 'bids': [price, volume]}
|
||||
"""
|
||||
limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'],
|
||||
self._ft_has['l2_limit_range_required'])
|
||||
try:
|
||||
|
||||
return self._api.fetch_l2_order_book(pair, limit1)
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching order book.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_rate(self, pair: str, refresh: bool, side: str) -> float:
|
||||
"""
|
||||
Calculates bid/ask target
|
||||
bid rate - between current ask price and last price
|
||||
ask rate - either using ticker bid or first bid based on orderbook
|
||||
or remain static in any other case since it's not updating.
|
||||
:param pair: Pair to get rate for
|
||||
:param refresh: allow cached data
|
||||
:param side: "buy" or "sell"
|
||||
:return: float: Price
|
||||
:raises PricingError if orderbook price could not be determined.
|
||||
"""
|
||||
cache_rate: TTLCache = self._buy_rate_cache if side == "buy" else self._sell_rate_cache
|
||||
[strat_name, name] = ['bid_strategy', 'Buy'] if side == "buy" else ['ask_strategy', 'Sell']
|
||||
|
||||
if not refresh:
|
||||
rate = cache_rate.get(pair)
|
||||
# Check if cache has been invalidated
|
||||
if rate:
|
||||
logger.debug(f"Using cached {side} rate for {pair}.")
|
||||
return rate
|
||||
|
||||
conf_strategy = self._config.get(strat_name, {})
|
||||
|
||||
if conf_strategy.get('use_order_book', False) and ('use_order_book' in conf_strategy):
|
||||
|
||||
order_book_top = conf_strategy.get('order_book_top', 1)
|
||||
order_book = self.fetch_l2_order_book(pair, order_book_top)
|
||||
logger.debug('order_book %s', order_book)
|
||||
# top 1 = index 0
|
||||
try:
|
||||
rate = order_book[f"{conf_strategy['price_side']}s"][order_book_top - 1][0]
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning(
|
||||
f"{name} Price at location {order_book_top} from orderbook could not be "
|
||||
f"determined. Orderbook: {order_book}"
|
||||
)
|
||||
raise PricingError from e
|
||||
price_side = {conf_strategy['price_side'].capitalize()}
|
||||
logger.debug(f"{name} price from orderbook {price_side}"
|
||||
f"side - top {order_book_top} order book {side} rate {rate:.8f}")
|
||||
else:
|
||||
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 side == 'buy' and ticker_rate > ticker['last']:
|
||||
balance = conf_strategy['ask_last_balance']
|
||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||
elif side == 'sell' and ticker_rate < ticker['last']:
|
||||
balance = conf_strategy.get('bid_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
|
||||
rate = ticker_rate
|
||||
|
||||
if rate is None:
|
||||
raise PricingError(f"{name}-Rate for {pair} was empty.")
|
||||
cache_rate[pair] = rate
|
||||
|
||||
return rate
|
||||
|
||||
# Fee handling
|
||||
|
||||
@retrier
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
||||
"""
|
||||
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
|
||||
The "since" argument passed in is coming from the database and is in UTC,
|
||||
as timezone-native datetime object.
|
||||
From the python documentation:
|
||||
> Naive datetime instances are assumed to represent local time
|
||||
Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
|
||||
transformation from local timezone to UTC.
|
||||
This works for timezones UTC+ since then the result will contain trades from a few hours
|
||||
instead of from the last 5 seconds, however fails for UTC- timezones,
|
||||
since we're then asking for trades with a "since" argument in the future.
|
||||
|
||||
:param order_id order_id: Order-id as given when creating the order
|
||||
:param pair: Pair the order is for
|
||||
:param since: datetime object of the order creation time. Assumes object is in UTC.
|
||||
"""
|
||||
if self._config['dry_run']:
|
||||
return []
|
||||
if not self.exchange_has('fetchMyTrades'):
|
||||
return []
|
||||
try:
|
||||
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||
# since needs to be int in milliseconds
|
||||
my_trades = self._api.fetch_my_trades(
|
||||
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
|
||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||
|
||||
self._log_exchange_response('get_trades_for_order', matched_trades)
|
||||
return matched_trades
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||
return order['id']
|
||||
|
||||
@retrier
|
||||
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
|
||||
price: float = 1, taker_or_maker: str = 'maker') -> float:
|
||||
try:
|
||||
if self._config['dry_run'] and self._config.get('fee', None) is not None:
|
||||
return self._config['fee']
|
||||
# validate that markets are loaded before trying to get fee
|
||||
if self._api.markets is None or len(self._api.markets) == 0:
|
||||
self._api.load_markets()
|
||||
|
||||
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
|
||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@staticmethod
|
||||
def order_has_fee(order: Dict) -> bool:
|
||||
"""
|
||||
Verifies if the passed in order dict has the needed keys to extract fees,
|
||||
and that these keys (currency, cost) are not empty.
|
||||
:param order: Order or trade (one trade) dict
|
||||
:return: True if the fee substructure contains currency and cost, false otherwise
|
||||
"""
|
||||
if not isinstance(order, dict):
|
||||
return False
|
||||
return ('fee' in order and order['fee'] is not None
|
||||
and (order['fee'].keys() >= {'currency', 'cost'})
|
||||
and order['fee']['currency'] is not None
|
||||
and order['fee']['cost'] is not None
|
||||
)
|
||||
|
||||
def calculate_fee_rate(self, order: Dict) -> Optional[float]:
|
||||
"""
|
||||
Calculate fee rate if it's not given by the exchange.
|
||||
:param order: Order or trade (one trade) dict
|
||||
"""
|
||||
if order['fee'].get('rate') is not None:
|
||||
return order['fee'].get('rate')
|
||||
fee_curr = order['fee']['currency']
|
||||
# Calculate fee based on order details
|
||||
if fee_curr in self.get_pair_base_currency(order['symbol']):
|
||||
# Base currency - divide by amount
|
||||
return round(
|
||||
order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8)
|
||||
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
|
||||
# Quote currency - divide by cost
|
||||
return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None
|
||||
else:
|
||||
# If Fee currency is a different currency
|
||||
if not order['cost']:
|
||||
# If cost is None or 0.0 -> falsy, return None
|
||||
return None
|
||||
try:
|
||||
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
|
||||
tick = self.fetch_ticker(comb)
|
||||
|
||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
||||
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
|
||||
except ExchangeError:
|
||||
return None
|
||||
|
||||
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
|
||||
"""
|
||||
Extract tuple of cost, currency, rate.
|
||||
Requires order_has_fee to run first!
|
||||
:param order: Order or trade (one trade) dict
|
||||
:return: Tuple with cost, currency, rate of the given fee dict
|
||||
"""
|
||||
return (order['fee']['cost'],
|
||||
order['fee']['currency'],
|
||||
self.calculate_fee_rate(order))
|
||||
|
||||
# Historic data
|
||||
|
||||
def get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int) -> List:
|
||||
"""
|
||||
@@ -835,8 +1288,8 @@ class Exchange:
|
||||
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
ohlcv_df = ohlcv_to_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
results_df[(pair, timeframe)] = ohlcv_df
|
||||
if cache:
|
||||
self._klines[(pair, timeframe)] = ohlcv_df
|
||||
@@ -896,6 +1349,8 @@ class Exchange:
|
||||
raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
|
||||
f'for pair {pair}. Message: {e}') from e
|
||||
|
||||
# Fetch historic trades
|
||||
|
||||
@retrier_async
|
||||
async def _async_fetch_trades(self, pair: str,
|
||||
since: Optional[int] = None,
|
||||
@@ -1054,292 +1509,6 @@ class Exchange:
|
||||
self._async_get_trade_history(pair=pair, since=since,
|
||||
until=until, from_id=from_id))
|
||||
|
||||
def check_order_canceled_empty(self, order: Dict) -> bool:
|
||||
"""
|
||||
Verify if an order has been cancelled without being partially filled
|
||||
: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')
|
||||
and order.get('filled') == 0.0)
|
||||
|
||||
@retrier
|
||||
def cancel_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
order = self._dry_run_open_orders.get(order_id)
|
||||
if order:
|
||||
order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']})
|
||||
return order
|
||||
else:
|
||||
return {}
|
||||
|
||||
try:
|
||||
return self._api.cancel_order(order_id, pair)
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not cancel order. Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to cancel_stoploss_order to allow easy overriding in other classes
|
||||
cancel_stoploss_order = cancel_order
|
||||
|
||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||
if not isinstance(corder, dict):
|
||||
return False
|
||||
|
||||
required = ('fee', 'status', 'amount')
|
||||
return all(k in corder for k in required)
|
||||
|
||||
def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
|
||||
"""
|
||||
Cancel order returning a result.
|
||||
Creates a fake result if cancel order returns a non-usable result
|
||||
and fetch_order does not work (certain exchanges don't return cancelled orders)
|
||||
:param order_id: Orderid to cancel
|
||||
:param pair: Pair corresponding to order_id
|
||||
:param amount: Amount to use for fake response
|
||||
:return: Result from either cancel_order if usable, or fetch_order
|
||||
"""
|
||||
try:
|
||||
corder = self.cancel_order(order_id, pair)
|
||||
if self.is_cancel_order_result_suitable(corder):
|
||||
return corder
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not cancel order {order_id} for {pair}.")
|
||||
try:
|
||||
order = self.fetch_order(order_id, pair)
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not fetch cancelled order {order_id}.")
|
||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
||||
|
||||
return order
|
||||
|
||||
def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
|
||||
"""
|
||||
Cancel stoploss order returning a result.
|
||||
Creates a fake result if cancel order returns a non-usable result
|
||||
and fetch_order does not work (certain exchanges don't return cancelled orders)
|
||||
:param order_id: stoploss-order-id to cancel
|
||||
:param pair: Pair corresponding to order_id
|
||||
:param amount: Amount to use for fake response
|
||||
:return: Result from either cancel_order if usable, or fetch_order
|
||||
"""
|
||||
corder = self.cancel_stoploss_order(order_id, pair)
|
||||
if self.is_cancel_order_result_suitable(corder):
|
||||
return corder
|
||||
try:
|
||||
order = self.fetch_stoploss_order(order_id, pair)
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
|
||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
||||
|
||||
return order
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
return order
|
||||
except KeyError as e:
|
||||
# Gracefully handle errors with dry-run orders.
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||
try:
|
||||
return self._api.fetch_order(order_id, pair)
|
||||
except ccxt.OrderNotFound as e:
|
||||
raise RetryableOrderError(
|
||||
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to fetch_stoploss_order to allow easy overriding in other classes
|
||||
fetch_stoploss_order = fetch_order
|
||||
|
||||
def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
|
||||
stoploss_order: bool = False) -> Dict:
|
||||
"""
|
||||
Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
|
||||
the stoploss_order parameter
|
||||
:param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order.
|
||||
"""
|
||||
if stoploss_order:
|
||||
return self.fetch_stoploss_order(order_id, pair)
|
||||
return self.fetch_order(order_id, pair)
|
||||
|
||||
@staticmethod
|
||||
def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]],
|
||||
range_required: bool = True):
|
||||
"""
|
||||
Get next greater value in the list.
|
||||
Used by fetch_l2_order_book if the api only supports a limited range
|
||||
"""
|
||||
if not limit_range:
|
||||
return limit
|
||||
|
||||
result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
|
||||
if not range_required and limit > result:
|
||||
# Range is not required - we can use None as parameter.
|
||||
return None
|
||||
return result
|
||||
|
||||
@retrier
|
||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
"""
|
||||
Get L2 order book from exchange.
|
||||
Can be limited to a certain amount (if supported).
|
||||
Returns a dict in the format
|
||||
{'asks': [price, volume], 'bids': [price, volume]}
|
||||
"""
|
||||
limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'],
|
||||
self._ft_has['l2_limit_range_required'])
|
||||
try:
|
||||
|
||||
return self._api.fetch_l2_order_book(pair, limit1)
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching order book.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
||||
"""
|
||||
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
|
||||
The "since" argument passed in is coming from the database and is in UTC,
|
||||
as timezone-native datetime object.
|
||||
From the python documentation:
|
||||
> Naive datetime instances are assumed to represent local time
|
||||
Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
|
||||
transformation from local timezone to UTC.
|
||||
This works for timezones UTC+ since then the result will contain trades from a few hours
|
||||
instead of from the last 5 seconds, however fails for UTC- timezones,
|
||||
since we're then asking for trades with a "since" argument in the future.
|
||||
|
||||
:param order_id order_id: Order-id as given when creating the order
|
||||
:param pair: Pair the order is for
|
||||
:param since: datetime object of the order creation time. Assumes object is in UTC.
|
||||
"""
|
||||
if self._config['dry_run']:
|
||||
return []
|
||||
if not self.exchange_has('fetchMyTrades'):
|
||||
return []
|
||||
try:
|
||||
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||
# since needs to be int in milliseconds
|
||||
my_trades = self._api.fetch_my_trades(
|
||||
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
|
||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||
|
||||
return matched_trades
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||
return order['id']
|
||||
|
||||
@retrier
|
||||
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
|
||||
price: float = 1, taker_or_maker: str = 'maker') -> float:
|
||||
try:
|
||||
if self._config['dry_run'] and self._config.get('fee', None) is not None:
|
||||
return self._config['fee']
|
||||
# validate that markets are loaded before trying to get fee
|
||||
if self._api.markets is None or len(self._api.markets) == 0:
|
||||
self._api.load_markets()
|
||||
|
||||
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
|
||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@staticmethod
|
||||
def order_has_fee(order: Dict) -> bool:
|
||||
"""
|
||||
Verifies if the passed in order dict has the needed keys to extract fees,
|
||||
and that these keys (currency, cost) are not empty.
|
||||
:param order: Order or trade (one trade) dict
|
||||
:return: True if the fee substructure contains currency and cost, false otherwise
|
||||
"""
|
||||
if not isinstance(order, dict):
|
||||
return False
|
||||
return ('fee' in order and order['fee'] is not None
|
||||
and (order['fee'].keys() >= {'currency', 'cost'})
|
||||
and order['fee']['currency'] is not None
|
||||
and order['fee']['cost'] is not None
|
||||
)
|
||||
|
||||
def calculate_fee_rate(self, order: Dict) -> Optional[float]:
|
||||
"""
|
||||
Calculate fee rate if it's not given by the exchange.
|
||||
:param order: Order or trade (one trade) dict
|
||||
"""
|
||||
if order['fee'].get('rate') is not None:
|
||||
return order['fee'].get('rate')
|
||||
fee_curr = order['fee']['currency']
|
||||
# Calculate fee based on order details
|
||||
if fee_curr in self.get_pair_base_currency(order['symbol']):
|
||||
# Base currency - divide by amount
|
||||
return round(
|
||||
order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8)
|
||||
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
|
||||
# Quote currency - divide by cost
|
||||
return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None
|
||||
else:
|
||||
# If Fee currency is a different currency
|
||||
if not order['cost']:
|
||||
# If cost is None or 0.0 -> falsy, return None
|
||||
return None
|
||||
try:
|
||||
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
|
||||
tick = self.fetch_ticker(comb)
|
||||
|
||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
||||
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
|
||||
except ExchangeError:
|
||||
return None
|
||||
|
||||
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
|
||||
"""
|
||||
Extract tuple of cost, currency, rate.
|
||||
Requires order_has_fee to run first!
|
||||
:param order: Order or trade (one trade) dict
|
||||
:return: Tuple with cost, currency, rate of the given fee dict
|
||||
"""
|
||||
return (order['fee']['cost'],
|
||||
order['fee']['currency'],
|
||||
self.calculate_fee_rate(order))
|
||||
|
||||
|
||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||
|
@@ -69,6 +69,7 @@ class Ftx(Exchange):
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, params=params)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
logger.info('stoploss order added for %s. '
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
@@ -93,18 +94,26 @@ class Ftx(Exchange):
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
return order
|
||||
except KeyError as e:
|
||||
# Gracefully handle errors with dry-run orders.
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
|
||||
try:
|
||||
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
|
||||
|
||||
order = [order for order in orders if order['id'] == order_id]
|
||||
self._log_exchange_response('fetch_stoploss_order', order)
|
||||
if len(order) == 1:
|
||||
if order[0].get('status') == 'closed':
|
||||
# Trigger order was triggered ...
|
||||
real_order_id = order[0].get('info', {}).get('orderId')
|
||||
|
||||
order1 = self._api.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order1)
|
||||
# Fake type to stop - as this was really a stop order.
|
||||
order1['id_stop'] = order1['id']
|
||||
order1['id'] = order_id
|
||||
order1['type'] = 'stop'
|
||||
order1['status_stop'] = 'triggered'
|
||||
return order1
|
||||
return order[0]
|
||||
else:
|
||||
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
|
||||
@@ -125,7 +134,9 @@ class Ftx(Exchange):
|
||||
if self._config['dry_run']:
|
||||
return {}
|
||||
try:
|
||||
return self._api.cancel_order(order_id, pair, params={'type': 'stop'})
|
||||
order = self._api.cancel_order(order_id, pair, params={'type': 'stop'})
|
||||
self._log_exchange_response('cancel_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not cancel order. Message: {e}') from e
|
||||
@@ -139,5 +150,5 @@ class Ftx(Exchange):
|
||||
|
||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||
if order['type'] == 'stop':
|
||||
return safe_value_fallback2(order['info'], order, 'orderId', 'id')
|
||||
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||
return order['id']
|
||||
|
@@ -49,7 +49,7 @@ class Kraken(Exchange):
|
||||
orders = self._api.fetch_open_orders()
|
||||
order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1],
|
||||
x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"],
|
||||
# Don't remove the below comment, this can be important for debuggung
|
||||
# Don't remove the below comment, this can be important for debugging
|
||||
# x["side"], x["amount"],
|
||||
) for x in orders]
|
||||
for bal in balances:
|
||||
@@ -103,6 +103,7 @@ class Kraken(Exchange):
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=stop_price, params=params)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
logger.info('stoploss order added for %s. '
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
|
@@ -10,13 +10,13 @@ from threading import Lock
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from cachetools import TTLCache
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.data.converter import order_book_to_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.enums import RPCMessageType, SellType, State
|
||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, PricingError)
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
@@ -26,9 +26,8 @@ from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||
from freqtrade.state import State
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||
from freqtrade.rpc import RPCManager
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -48,6 +47,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
:param config: configuration dict, you can use Configuration.get_config()
|
||||
to get the config dict.
|
||||
"""
|
||||
self.active_pair_whitelist: List[str] = []
|
||||
|
||||
logger.info('Starting freqtrade %s', __version__)
|
||||
|
||||
@@ -57,12 +57,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Init objects
|
||||
self.config = config
|
||||
|
||||
# Cache values for 1800 to avoid frequent polling of the exchange for prices
|
||||
# Caching only applies to RPC methods, so prices for open trades are still
|
||||
# refreshed once every iteration.
|
||||
self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
||||
self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
|
||||
|
||||
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
|
||||
|
||||
# Check config consistency here since strategies can set certain options
|
||||
@@ -76,12 +70,19 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
PairLocks.timeframe = self.config['timeframe']
|
||||
|
||||
self.protections = ProtectionManager(self.config, self.strategy.protections)
|
||||
|
||||
# RPC runs in separate threads, can start handling external commands just after
|
||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||
# so anything in the Freqtradebot instance should be ready (initialized), including
|
||||
# the initial state of the bot.
|
||||
# Keep this at the end of this initialization method.
|
||||
self.rpc: RPCManager = RPCManager(self)
|
||||
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
|
||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
||||
|
||||
self.protections = ProtectionManager(self.config)
|
||||
|
||||
# Attach Dataprovider to Strategy baseclass
|
||||
IStrategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
@@ -97,13 +98,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
initial_state = self.config.get('initial_state')
|
||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||
|
||||
# RPC runs in separate threads, can start handling external commands just after
|
||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||
# so anything in the Freqtradebot instance should be ready (initialized), including
|
||||
# the initial state of the bot.
|
||||
# Keep this at the end of this initialization method.
|
||||
self.rpc: RPCManager = RPCManager(self)
|
||||
# Protect sell-logic from forcesell and viceversa
|
||||
# Protect sell-logic from forcesell and vice versa
|
||||
self._sell_lock = Lock()
|
||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||
|
||||
@@ -187,7 +182,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if self.get_free_open_trades():
|
||||
self.enter_positions()
|
||||
|
||||
Trade.query.session.flush()
|
||||
Trade.commit()
|
||||
|
||||
def process_stopped(self) -> None:
|
||||
"""
|
||||
@@ -342,7 +337,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Assume this as the open order
|
||||
trade.open_order_id = order.order_id
|
||||
if fo:
|
||||
logger.info(f"Found {order} for trade {trade}.jj")
|
||||
logger.info(f"Found {order} for trade {trade}.")
|
||||
self.update_trade_state(trade, order.order_id, fo,
|
||||
stoploss_order=order.ft_order_side == 'stoploss')
|
||||
|
||||
@@ -394,51 +389,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return trades_created
|
||||
|
||||
def get_buy_rate(self, pair: str, refresh: bool) -> float:
|
||||
"""
|
||||
Calculates bid target between current ask price and last price
|
||||
:param pair: Pair to get rate for
|
||||
:param refresh: allow cached data
|
||||
:return: float: Price
|
||||
"""
|
||||
if not refresh:
|
||||
rate = self._buy_rate_cache.get(pair)
|
||||
# Check if cache has been invalidated
|
||||
if rate:
|
||||
logger.debug(f"Using cached buy rate for {pair}.")
|
||||
return rate
|
||||
|
||||
bid_strategy = self.config.get('bid_strategy', {})
|
||||
if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False):
|
||||
|
||||
order_book_top = bid_strategy.get('order_book_top', 1)
|
||||
order_book = self.exchange.fetch_l2_order_book(pair, order_book_top)
|
||||
logger.debug('order_book %s', order_book)
|
||||
# top 1 = index 0
|
||||
try:
|
||||
rate_from_l2 = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0]
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning(
|
||||
"Buy Price from orderbook could not be determined."
|
||||
f"Orderbook: {order_book}"
|
||||
)
|
||||
raise PricingError from e
|
||||
logger.info(f"Buy price from orderbook {bid_strategy['price_side'].capitalize()} side "
|
||||
f"- top {order_book_top} order book buy rate {rate_from_l2:.8f}")
|
||||
used_rate = rate_from_l2
|
||||
else:
|
||||
logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price")
|
||||
ticker = self.exchange.fetch_ticker(pair)
|
||||
ticker_rate = ticker[bid_strategy['price_side']]
|
||||
if ticker['last'] and ticker_rate > ticker['last']:
|
||||
balance = bid_strategy['ask_last_balance']
|
||||
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
|
||||
used_rate = ticker_rate
|
||||
|
||||
self._buy_rate_cache[pair] = used_rate
|
||||
|
||||
return used_rate
|
||||
|
||||
def create_trade(self, pair: str) -> bool:
|
||||
"""
|
||||
Check the implemented trading strategy for buy signals.
|
||||
@@ -474,16 +424,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
if buy and not sell:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
|
||||
if not stake_amount:
|
||||
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
|
||||
return False
|
||||
|
||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||
f"{stake_amount} ...")
|
||||
|
||||
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)
|
||||
else:
|
||||
@@ -522,6 +466,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
Executes a limit buy for the given pair
|
||||
:param pair: pair for which we want to create a LIMIT_BUY
|
||||
:param stake_amount: amount of stake-currency for the pair
|
||||
:return: True if a buy order is created, false if it fails.
|
||||
"""
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
@@ -530,20 +475,29 @@ class FreqtradeBot(LoggingMixin):
|
||||
buy_limit_requested = price
|
||||
else:
|
||||
# Calculate price
|
||||
buy_limit_requested = self.get_buy_rate(pair, True)
|
||||
buy_limit_requested = self.exchange.get_rate(pair, refresh=True, side="buy")
|
||||
|
||||
if not buy_limit_requested:
|
||||
raise PricingError('Could not determine buy price.')
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
|
||||
self.strategy.stoploss)
|
||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||
logger.warning(
|
||||
f"Can't open a new trade for {pair}: stake amount "
|
||||
f"is too small ({stake_amount} < {min_stake_amount})"
|
||||
)
|
||||
|
||||
if not self.edge:
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=datetime.now(timezone.utc),
|
||||
current_rate=buy_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)
|
||||
|
||||
if not stake_amount:
|
||||
return False
|
||||
|
||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
||||
f"{stake_amount} ...")
|
||||
|
||||
amount = stake_amount / buy_limit_requested
|
||||
order_type = self.strategy.order_types['buy']
|
||||
if forcebuy:
|
||||
@@ -620,7 +574,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.update_trade_state(trade, order_id, order)
|
||||
|
||||
Trade.query.session.add(trade)
|
||||
Trade.query.session.flush()
|
||||
Trade.commit()
|
||||
|
||||
# Updating wallets
|
||||
self.wallets.update()
|
||||
@@ -655,7 +609,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
Sends rpc notification when a buy cancel occurred.
|
||||
"""
|
||||
current_rate = self.get_buy_rate(trade.pair, False)
|
||||
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy")
|
||||
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
@@ -706,6 +660,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if (self.strategy.order_types.get('stoploss_on_exchange') and
|
||||
self.handle_stoploss_on_exchange(trade)):
|
||||
trades_closed += 1
|
||||
Trade.commit()
|
||||
continue
|
||||
# Check if we can sell our current pair
|
||||
if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
|
||||
@@ -720,56 +675,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return trades_closed
|
||||
|
||||
def _order_book_gen(self, pair: str, side: str, order_book_max: int = 1,
|
||||
order_book_min: int = 1):
|
||||
"""
|
||||
Helper generator to query orderbook in loop (used for early sell-order placing)
|
||||
"""
|
||||
order_book = self.exchange.fetch_l2_order_book(pair, order_book_max)
|
||||
for i in range(order_book_min, order_book_max + 1):
|
||||
yield order_book[side][i - 1][0]
|
||||
|
||||
def get_sell_rate(self, pair: str, refresh: bool) -> float:
|
||||
"""
|
||||
Get sell rate - either using ticker bid or first bid based on orderbook
|
||||
The orderbook portion is only used for rpc messaging, which would otherwise fail
|
||||
for BitMex (has no bid/ask in fetch_ticker)
|
||||
or remain static in any other case since it's not updating.
|
||||
:param pair: Pair to get rate for
|
||||
:param refresh: allow cached data
|
||||
:return: Bid rate
|
||||
"""
|
||||
if not refresh:
|
||||
rate = self._sell_rate_cache.get(pair)
|
||||
# Check if cache has been invalidated
|
||||
if rate:
|
||||
logger.debug(f"Using cached sell rate for {pair}.")
|
||||
return rate
|
||||
|
||||
ask_strategy = self.config.get('ask_strategy', {})
|
||||
if ask_strategy.get('use_order_book', False):
|
||||
# This code is only used for notifications, selling uses the generator directly
|
||||
logger.info(
|
||||
f"Getting price from order book {ask_strategy['price_side'].capitalize()} side."
|
||||
)
|
||||
try:
|
||||
rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s"))
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning("Sell Price at location from orderbook could not be determined.")
|
||||
raise PricingError from e
|
||||
else:
|
||||
ticker = self.exchange.fetch_ticker(pair)
|
||||
ticker_rate = ticker[ask_strategy['price_side']]
|
||||
if ticker['last'] and ticker_rate < ticker['last']:
|
||||
balance = ask_strategy.get('bid_last_balance', 0.0)
|
||||
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
|
||||
rate = ticker_rate
|
||||
|
||||
if rate is None:
|
||||
raise PricingError(f"Sell-Rate for {pair} was empty.")
|
||||
self._sell_rate_cache[pair] = rate
|
||||
return rate
|
||||
|
||||
def handle_trade(self, trade: Trade) -> bool:
|
||||
"""
|
||||
Sells the current pair if the threshold is reached and updates the trade record.
|
||||
@@ -782,46 +687,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
(buy, sell) = (False, False)
|
||||
|
||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||
|
||||
if (config_ask_strategy.get('use_sell_signal', True) or
|
||||
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
|
||||
if (self.config.get('use_sell_signal', True) or
|
||||
self.config.get('ignore_roi_if_buy_signal', False)):
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||
self.strategy.timeframe)
|
||||
|
||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
|
||||
|
||||
if config_ask_strategy.get('use_order_book', False):
|
||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
||||
logger.debug(f'Using order book between {order_book_min} and {order_book_max} '
|
||||
f'for selling {trade.pair}...')
|
||||
|
||||
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
|
||||
order_book_min=order_book_min,
|
||||
order_book_max=order_book_max)
|
||||
for i in range(order_book_min, order_book_max + 1):
|
||||
try:
|
||||
sell_rate = next(order_book)
|
||||
except (IndexError, KeyError) as e:
|
||||
logger.warning(
|
||||
f"Sell Price at location {i} from orderbook could not be determined."
|
||||
)
|
||||
raise PricingError from e
|
||||
logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: "
|
||||
f"{sell_rate:0.8f}")
|
||||
# Assign sell-rate to cache - otherwise sell-rate is never updated in the cache,
|
||||
# resulting in outdated RPC messages
|
||||
self._sell_rate_cache[trade.pair] = sell_rate
|
||||
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
|
||||
else:
|
||||
logger.debug('checking sell')
|
||||
sell_rate = self.get_sell_rate(trade.pair, True)
|
||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||
return True
|
||||
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):
|
||||
return True
|
||||
|
||||
logger.debug('Found no sell signal for %s.', trade)
|
||||
return False
|
||||
@@ -915,8 +791,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
||||
|
||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||
if stoploss_order and (self.config.get('trailing_stop', False)
|
||||
or self.config.get('use_custom_stoploss', False)):
|
||||
# Triggered Orders are now real orders - so don't replace stoploss anymore
|
||||
if (
|
||||
stoploss_order
|
||||
and stoploss_order.get('status_stop') != 'triggered'
|
||||
and (self.config.get('trailing_stop', False)
|
||||
or self.config.get('use_custom_stoploss', False))
|
||||
):
|
||||
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||
# in which case we cancel stoploss order and put another one with new
|
||||
# value immediately
|
||||
@@ -928,7 +809,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
Check to see if stoploss on exchange should be updated
|
||||
in case of trailing stoploss on exchange
|
||||
:param Trade: Corresponding Trade
|
||||
:param trade: Corresponding Trade
|
||||
:param order: Current on exchange stoploss order
|
||||
:return: None
|
||||
"""
|
||||
@@ -1036,6 +917,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
elif order['side'] == 'sell':
|
||||
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
Trade.commit()
|
||||
|
||||
def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||
"""
|
||||
@@ -1233,7 +1115,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') == 'closed':
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
Trade.query.session.flush()
|
||||
Trade.commit()
|
||||
|
||||
# Lock pair for one candle to prevent immediate re-buys
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
@@ -1250,7 +1132,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
# Use cached rates here - it was updated seconds ago.
|
||||
current_rate = self.get_sell_rate(trade.pair, False) if not fill else None
|
||||
current_rate = self.exchange.get_rate(
|
||||
trade.pair, refresh=False, side="sell") if not fill else None
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
@@ -1295,7 +1178,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
current_rate = self.get_sell_rate(trade.pair, False)
|
||||
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell")
|
||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||
gain = "profit" if profit_ratio > 0 else "loss"
|
||||
|
||||
@@ -1374,6 +1257,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Handling of this will happen in check_handle_timeout.
|
||||
return True
|
||||
trade.update(order)
|
||||
Trade.commit()
|
||||
|
||||
# Updating wallets when order is closed
|
||||
if not trade.is_open:
|
||||
|
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator, List
|
||||
from typing.io import IO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import rapidjson
|
||||
|
||||
@@ -56,6 +57,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool =
|
||||
"""
|
||||
Dump JSON data into a file
|
||||
:param filename: file to create
|
||||
:param is_zip: if file should be zip
|
||||
:param data: JSON Data to save
|
||||
:return:
|
||||
"""
|
||||
@@ -213,3 +215,16 @@ def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]:
|
||||
"""
|
||||
for chunk in range(0, len(lst), n):
|
||||
yield (lst[chunk:chunk + n])
|
||||
|
||||
|
||||
def parse_db_uri_for_logging(uri: str):
|
||||
"""
|
||||
Helper method to parse the DB URI and return the same DB URI with the password censored
|
||||
if it contains it. Otherwise, return the DB URI unchanged
|
||||
:param uri: DB URI to parse for logging
|
||||
"""
|
||||
parsed_db_uri = urlparse(uri)
|
||||
if not parsed_db_uri.netloc: # No need for censoring as no password was provided
|
||||
return uri
|
||||
pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0]
|
||||
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
|
||||
|
@@ -17,16 +17,18 @@ from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
||||
from freqtrade.data.converter import trim_dataframes
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import BacktestState, SellType
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.optimize.bt_progress import BTProgress
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||
store_backtest_stats)
|
||||
from freqtrade.persistence import LocalTrade, PairLocks, Trade
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from freqtrade.plugins.protectionmanager import ProtectionManager
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -56,6 +58,7 @@ class Backtesting:
|
||||
|
||||
LoggingMixin.show_output = False
|
||||
self.config = config
|
||||
self.results: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Reset keys for backtesting
|
||||
remove_credentials(self.config)
|
||||
@@ -115,6 +118,10 @@ class Backtesting:
|
||||
|
||||
# Get maximum required startup period
|
||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
||||
|
||||
self.progress = BTProgress()
|
||||
self.abort = False
|
||||
|
||||
def __del__(self):
|
||||
LoggingMixin.show_output = True
|
||||
@@ -127,6 +134,8 @@ class Backtesting:
|
||||
"""
|
||||
self.strategy: IStrategy = strategy
|
||||
strategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.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
|
||||
@@ -136,13 +145,15 @@ class Backtesting:
|
||||
if hasattr(strategy, 'protections'):
|
||||
conf = deepcopy(conf)
|
||||
conf['protections'] = strategy.protections
|
||||
self.protections = ProtectionManager(conf)
|
||||
self.protections = ProtectionManager(self.config, strategy.protections)
|
||||
|
||||
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
|
||||
"""
|
||||
Loads backtest data and returns the data combined with the timerange
|
||||
as tuple.
|
||||
"""
|
||||
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')))
|
||||
|
||||
@@ -166,6 +177,7 @@ class Backtesting:
|
||||
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
|
||||
self.required_startup, min_date)
|
||||
|
||||
self.progress.set_new_value(1)
|
||||
return data, timerange
|
||||
|
||||
def prepare_backtest(self, enable_protections):
|
||||
@@ -180,6 +192,15 @@ class Backtesting:
|
||||
self.rejected_trades = 0
|
||||
self.dataprovider.clear_cache()
|
||||
|
||||
def check_abort(self):
|
||||
"""
|
||||
Check if abort was requested, raise DependencyException if that's the case
|
||||
Only applies to Interactive backtest mode (webserver mode)
|
||||
"""
|
||||
if self.abort:
|
||||
self.abort = False
|
||||
raise DependencyException("Stop requested")
|
||||
|
||||
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
|
||||
"""
|
||||
Helper function to convert a processed dataframes into lists for performance reasons.
|
||||
@@ -190,8 +211,12 @@ class Backtesting:
|
||||
# and eventually change the constants for indexes at the top
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||
data: Dict = {}
|
||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||
|
||||
# Create dict with data
|
||||
for pair, pair_data in processed.items():
|
||||
self.check_abort()
|
||||
self.progress.increment()
|
||||
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
|
||||
@@ -224,6 +249,26 @@ class Backtesting:
|
||||
# sell at open price.
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
# Special case: trailing triggers within same candle as trade opened. Assume most
|
||||
# pessimistic price movement, which is moving just enough to arm stoploss and
|
||||
# immediately going down to stop price.
|
||||
if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0:
|
||||
if (
|
||||
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
|
||||
and self.strategy.trailing_only_offset_is_reached
|
||||
and self.strategy.trailing_stop_positive_offset is not None
|
||||
and self.strategy.trailing_stop_positive
|
||||
):
|
||||
# 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)))
|
||||
else:
|
||||
# Worst case: price ticks tiny bit above open and dives down.
|
||||
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
|
||||
assert stop_rate < sell_row[HIGH_IDX]
|
||||
return stop_rate
|
||||
|
||||
# Set close_rate to stoploss
|
||||
return trade.stop_loss
|
||||
elif sell.sell_type == (SellType.ROI):
|
||||
@@ -290,7 +335,18 @@ class Backtesting:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
||||
except DependencyException:
|
||||
return None
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) or 0
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
||||
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)
|
||||
|
||||
if not stake_amount:
|
||||
return None
|
||||
|
||||
order_type = self.strategy.order_types['buy']
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
@@ -382,10 +438,13 @@ class Backtesting:
|
||||
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
|
||||
open_trade_count = 0
|
||||
|
||||
self.progress.init_step(BacktestState.BACKTEST, int(
|
||||
(end_date - start_date) / timedelta(minutes=self.timeframe_min)))
|
||||
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
while tmp <= end_date:
|
||||
open_trade_count_start = open_trade_count
|
||||
|
||||
self.check_abort()
|
||||
for i, pair in enumerate(data):
|
||||
row_index = indexes[pair]
|
||||
try:
|
||||
@@ -428,7 +487,7 @@ class Backtesting:
|
||||
for trade in open_trades[pair]:
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||
# Sell occured
|
||||
# Sell occurred
|
||||
if trade_entry:
|
||||
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
||||
open_trade_count -= 1
|
||||
@@ -441,6 +500,7 @@ class Backtesting:
|
||||
self.protections.global_stop(tmp)
|
||||
|
||||
# Move time one configured time_interval ahead.
|
||||
self.progress.increment()
|
||||
tmp += timedelta(minutes=self.timeframe_min)
|
||||
|
||||
trades += self.handle_left_open(open_trades, data=data)
|
||||
@@ -456,6 +516,8 @@ class Backtesting:
|
||||
}
|
||||
|
||||
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
|
||||
self.progress.init_step(BacktestState.ANALYZE, 0)
|
||||
|
||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||
backtest_start_time = datetime.now(timezone.utc)
|
||||
self._set_strategy(strat)
|
||||
@@ -482,6 +544,7 @@ class Backtesting:
|
||||
"No data left after adjusting for startup candles.")
|
||||
|
||||
min_date, max_date = history.get_timerange(preprocessed)
|
||||
|
||||
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).')
|
||||
@@ -516,11 +579,12 @@ class Backtesting:
|
||||
for strat in self.strategylist:
|
||||
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
|
||||
if len(self.strategylist) > 0:
|
||||
stats = generate_backtest_stats(data, self.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
||||
if self.config.get('export', False):
|
||||
store_backtest_stats(self.config['exportfilename'], stats)
|
||||
self.results = generate_backtest_stats(data, self.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
||||
if self.config.get('export', 'none') == 'trades':
|
||||
store_backtest_stats(self.config['exportfilename'], self.results)
|
||||
|
||||
# Show backtest results
|
||||
show_backtest_results(self.config, stats)
|
||||
show_backtest_results(self.config, self.results)
|
||||
|
33
freqtrade/optimize/bt_progress.py
Normal file
33
freqtrade/optimize/bt_progress.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from freqtrade.enums import BacktestState
|
||||
|
||||
|
||||
class BTProgress:
|
||||
_action: BacktestState = BacktestState.STARTUP
|
||||
_progress: float = 0
|
||||
_max_steps: float = 0
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def init_step(self, action: BacktestState, max_steps: float):
|
||||
self._action = action
|
||||
self._max_steps = max_steps
|
||||
self._proress = 0
|
||||
|
||||
def set_new_value(self, new_value: float):
|
||||
self._progress = new_value
|
||||
|
||||
def increment(self):
|
||||
self._progress += 1
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
"""
|
||||
Get progress as ratio, capped to be between 0 and 1 (to avoid small calculation errors).
|
||||
"""
|
||||
return max(min(round(self._progress / self._max_steps, 5)
|
||||
if self._max_steps > 0 else 0, 1), 0)
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
return str(self._action)
|
@@ -12,7 +12,6 @@ from math import ceil
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
import progressbar
|
||||
import rapidjson
|
||||
from colorama import Fore, Style
|
||||
@@ -20,16 +19,16 @@ from colorama import init as colorama_init
|
||||
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
||||
from freqtrade.data.converter import trim_dataframes
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.misc import file_dump_json, plural
|
||||
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
|
||||
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
|
||||
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
|
||||
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
|
||||
|
||||
@@ -78,8 +77,11 @@ 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
|
||||
|
||||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||
|
||||
@@ -163,13 +165,9 @@ class Hyperopt:
|
||||
While not a valid json object - this allows appending easily.
|
||||
:param epoch: result dictionary for this epoch.
|
||||
"""
|
||||
def default_parser(x):
|
||||
if isinstance(x, np.integer):
|
||||
return int(x)
|
||||
return str(x)
|
||||
|
||||
epoch[FTHYPT_FILEVERSION] = 2
|
||||
with self.results_file.open('a') as f:
|
||||
rapidjson.dump(epoch, f, default=default_parser,
|
||||
rapidjson.dump(epoch, f, default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN)
|
||||
f.write("\n")
|
||||
|
||||
@@ -201,6 +199,25 @@ class Hyperopt:
|
||||
|
||||
return result
|
||||
|
||||
def _get_no_optimize_details(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get non-optimized parameters
|
||||
"""
|
||||
result: Dict[str, Any] = {}
|
||||
strategy = self.backtesting.strategy
|
||||
if not HyperoptTools.has_space(self.config, 'roi'):
|
||||
result['roi'] = {str(k): v for k, v in strategy.minimal_roi.items()}
|
||||
if not HyperoptTools.has_space(self.config, 'stoploss'):
|
||||
result['stoploss'] = {'stoploss': strategy.stoploss}
|
||||
if not HyperoptTools.has_space(self.config, 'trailing'):
|
||||
result['trailing'] = {
|
||||
'trailing_stop': strategy.trailing_stop,
|
||||
'trailing_stop_positive': strategy.trailing_stop_positive,
|
||||
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
|
||||
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
|
||||
}
|
||||
return result
|
||||
|
||||
def print_results(self, results) -> None:
|
||||
"""
|
||||
Log results if it is better than any previous evaluation
|
||||
@@ -310,7 +327,8 @@ class Hyperopt:
|
||||
results_explanation = HyperoptTools.format_results_explanation_string(
|
||||
strat_stats, self.config['stake_currency'])
|
||||
|
||||
not_optimized = self.backtesting.strategy.get_params_dict()
|
||||
not_optimized = self.backtesting.strategy.get_no_optimize_params()
|
||||
not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details())
|
||||
|
||||
trade_count = strat_stats['total_trades']
|
||||
total_profit = strat_stats['profit_total']
|
||||
@@ -324,7 +342,8 @@ class Hyperopt:
|
||||
loss = self.calculate_loss(results=backtesting_results['results'],
|
||||
trade_count=trade_count,
|
||||
min_date=min_date, max_date=max_date,
|
||||
config=self.config, processed=processed)
|
||||
config=self.config, processed=processed,
|
||||
backtest_stats=strat_stats)
|
||||
return {
|
||||
'loss': loss,
|
||||
'params_dict': params_dict,
|
||||
@@ -469,8 +488,14 @@ class Hyperopt:
|
||||
f"saved to '{self.results_file}'.")
|
||||
|
||||
if self.current_best_epoch:
|
||||
HyperoptTools.print_epoch_details(self.current_best_epoch, self.total_epochs,
|
||||
self.print_json)
|
||||
if self.auto_hyperopt:
|
||||
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)
|
||||
else:
|
||||
# This is printed when Ctrl+C is pressed quickly, before first epochs have
|
||||
# a chance to be evaluated.
|
||||
|
@@ -5,7 +5,7 @@ This module defines the interface for the loss-function for hyperopt
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
@@ -22,6 +22,7 @@ class IHyperOptLoss(ABC):
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
config: Dict, processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
|
@@ -9,23 +9,11 @@ from pandas import DataFrame
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
||||
# This is assumed to be expected avg profit * expected trade count.
|
||||
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
|
||||
# expected max profit = 3.85
|
||||
#
|
||||
# Note, this is ratio. 3.85 stated above means 385Σ%, 3.0 means 300Σ%.
|
||||
#
|
||||
# In this implementation it's only used in calculation of the resulting value
|
||||
# of the objective function as a normalization coefficient and does not
|
||||
# represent any limit for profits as in the Freqtrade legacy default loss function.
|
||||
EXPECTED_MAX_PROFIT = 3.0
|
||||
|
||||
|
||||
class OnlyProfitHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the loss function for hyperopt.
|
||||
|
||||
This implementation takes only profit into account.
|
||||
This implementation takes only absolute profit into account, not looking at any other indicator.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@@ -34,5 +22,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Objective function, returns smaller number for better results.
|
||||
"""
|
||||
total_profit = results['profit_ratio'].sum()
|
||||
return 1 - total_profit / EXPECTED_MAX_PROFIT
|
||||
total_profit = results['profit_abs'].sum()
|
||||
return -1 * total_profit
|
||||
|
239
freqtrade/optimize/hyperopt_tools.py
Normal file → Executable file
239
freqtrade/optimize/hyperopt_tools.py
Normal file → Executable file
@@ -1,25 +1,82 @@
|
||||
|
||||
import io
|
||||
import locale
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
import rapidjson
|
||||
import tabulate
|
||||
from colorama import Fore, Style
|
||||
from pandas import isna, json_normalize
|
||||
|
||||
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import round_coin_value, round_dict
|
||||
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
|
||||
|
||||
|
||||
def hyperopt_serializer(x):
|
||||
if isinstance(x, np.integer):
|
||||
return int(x)
|
||||
if isinstance(x, np.bool_):
|
||||
return bool(x)
|
||||
|
||||
return str(x)
|
||||
|
||||
|
||||
class HyperoptTools():
|
||||
|
||||
@staticmethod
|
||||
def get_strategy_filename(config: Dict, strategy_name: str) -> Optional[Path]:
|
||||
"""
|
||||
Get Strategy-location (filename) from strategy_name
|
||||
"""
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||
strategy_objs = StrategyResolver.search_all_objects(directory, False)
|
||||
strategies = [s for s in strategy_objs if s['name'] == strategy_name]
|
||||
if strategies:
|
||||
strategy = strategies[0]
|
||||
|
||||
return Path(strategy['location'])
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def export_params(params, strategy_name: str, filename: Path):
|
||||
"""
|
||||
Generate files
|
||||
"""
|
||||
final_params = deepcopy(params['params_not_optimized'])
|
||||
final_params = deep_merge_dicts(params['params_details'], final_params)
|
||||
final_params = {
|
||||
'strategy_name': strategy_name,
|
||||
'params': final_params,
|
||||
'ft_stratparam_v': 1,
|
||||
'export_time': datetime.now(timezone.utc),
|
||||
}
|
||||
logger.info(f"Dumping parameters to {filename}")
|
||||
rapidjson.dump(final_params, filename.open('w'), indent=2,
|
||||
default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
|
||||
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
|
||||
# Export parameters ...
|
||||
fn = HyperoptTools.get_strategy_filename(config, strategy_name)
|
||||
if fn:
|
||||
HyperoptTools.export_params(params, strategy_name, fn.with_suffix('.json'))
|
||||
else:
|
||||
logger.warning("Strategy not found, not exporting parameter file.")
|
||||
|
||||
@staticmethod
|
||||
def has_space(config: Dict[str, Any], space: str) -> bool:
|
||||
"""
|
||||
@@ -74,8 +131,8 @@ class HyperoptTools():
|
||||
return epochs
|
||||
|
||||
@staticmethod
|
||||
def print_epoch_details(results, total_epochs: int, print_json: bool,
|
||||
no_header: bool = False, header_str: str = None) -> None:
|
||||
def show_epoch_details(results, total_epochs: int, print_json: bool,
|
||||
no_header: bool = False, header_str: str = None) -> None:
|
||||
"""
|
||||
Display details of the hyperopt result
|
||||
"""
|
||||
@@ -93,7 +150,7 @@ class HyperoptTools():
|
||||
if print_json:
|
||||
result_dict: Dict = {}
|
||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
|
||||
HyperoptTools._params_update_for_json(result_dict, params, s)
|
||||
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
|
||||
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||
|
||||
else:
|
||||
@@ -101,55 +158,62 @@ class HyperoptTools():
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:")
|
||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:")
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _params_update_for_json(result_dict, params, space: str) -> None:
|
||||
if space in params:
|
||||
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
||||
if (space in params) or (space in non_optimized):
|
||||
space_params = HyperoptTools._space_params(params, space)
|
||||
if space in ['buy', 'sell']:
|
||||
result_dict.setdefault('params', {}).update(space_params)
|
||||
elif space == 'roi':
|
||||
# TODO: get rid of OrderedDict when support for python 3.6 will be
|
||||
# dropped (dicts keep the order as the language feature)
|
||||
space_non_optimized = HyperoptTools._space_params(non_optimized, space)
|
||||
all_space_params = space_params
|
||||
|
||||
# Merge non optimized params if there are any
|
||||
if len(space_non_optimized) > 0:
|
||||
all_space_params = {**space_params, **space_non_optimized}
|
||||
|
||||
if space in ['buy', 'sell']:
|
||||
result_dict.setdefault('params', {}).update(all_space_params)
|
||||
elif space == 'roi':
|
||||
# Convert keys in min_roi dict to strings because
|
||||
# rapidjson cannot dump dicts with integer keys...
|
||||
# OrderedDict is used to keep the numeric order of the items
|
||||
# in the dict.
|
||||
result_dict['minimal_roi'] = OrderedDict(
|
||||
(str(k), v) for k, v in space_params.items()
|
||||
)
|
||||
result_dict['minimal_roi'] = {str(k): v for k, v in all_space_params.items()}
|
||||
else: # 'stoploss', 'trailing'
|
||||
result_dict.update(space_params)
|
||||
result_dict.update(all_space_params)
|
||||
|
||||
@staticmethod
|
||||
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
|
||||
if space in params or space in non_optimized:
|
||||
space_params = HyperoptTools._space_params(params, space, 5)
|
||||
result = f"\n# {header}\n"
|
||||
if space == 'stoploss':
|
||||
result += f"stoploss = {space_params.get('stoploss')}"
|
||||
elif space == 'roi':
|
||||
# TODO: get rid of OrderedDict when support for python 3.6 will be
|
||||
# dropped (dicts keep the order as the language feature)
|
||||
minimal_roi_result = rapidjson.dumps(
|
||||
OrderedDict(
|
||||
(str(k), v) for k, v in space_params.items()
|
||||
),
|
||||
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||
result += f"minimal_roi = {minimal_roi_result}"
|
||||
elif space == 'trailing':
|
||||
no_params = HyperoptTools._space_params(non_optimized, space, 5)
|
||||
appendix = ''
|
||||
if not space_params and not no_params:
|
||||
# No parameters - don't print
|
||||
return
|
||||
if not space_params:
|
||||
# Not optimized parameters - append string
|
||||
appendix = NON_OPT_PARAM_APPENDIX
|
||||
|
||||
for k, v in space_params.items():
|
||||
result += f'{k} = {v}\n'
|
||||
result = f"\n# {header}\n"
|
||||
if space == "stoploss":
|
||||
stoploss = safe_value_fallback2(space_params, no_params, space, space)
|
||||
result += (f"stoploss = {stoploss}{appendix}")
|
||||
|
||||
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()
|
||||
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||
result += f"minimal_roi = {minimal_roi_result}"
|
||||
elif space == "trailing":
|
||||
for k, v in (space_params or no_params).items():
|
||||
result += f"{k} = {v}{appendix}\n"
|
||||
|
||||
else:
|
||||
no_params = HyperoptTools._space_params(non_optimized, space, 5)
|
||||
# Buy / sell parameters
|
||||
|
||||
result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}"
|
||||
result += f"{space}_params = {HyperoptTools._pprint_dict(space_params, no_params)}"
|
||||
|
||||
result = result.replace("\n", "\n ")
|
||||
print(result)
|
||||
@@ -163,7 +227,7 @@ class HyperoptTools():
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _pprint(params, non_optimized, indent: int = 4):
|
||||
def _pprint_dict(params, non_optimized, indent: int = 4):
|
||||
"""
|
||||
Pretty-print hyperopt results (based on 2 dicts - with add. comment)
|
||||
"""
|
||||
@@ -175,7 +239,7 @@ class HyperoptTools():
|
||||
result += " " * indent + f'"{k}": '
|
||||
result += f'"{param}",' if isinstance(param, str) else f'{param},'
|
||||
if k in non_optimized:
|
||||
result += " # value loaded from strategy"
|
||||
result += NON_OPT_PARAM_APPENDIX
|
||||
result += "\n"
|
||||
result += '}'
|
||||
return result
|
||||
@@ -195,9 +259,9 @@ class HyperoptTools():
|
||||
f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. "
|
||||
f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. "
|
||||
f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} "
|
||||
f"({results_metrics['profit_total'] * 100: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). "
|
||||
f"({results_metrics['profit_total'] * 100: 7.2f}%). "
|
||||
f"Avg duration {results_metrics['holding_avg']} min."
|
||||
).encode(locale.getpreferredencoding(), 'replace').decode('utf-8')
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_explanation_string(results, total_epochs) -> str:
|
||||
@@ -206,6 +270,47 @@ class HyperoptTools():
|
||||
f"{results['results_explanation']} " +
|
||||
f"Objective: {results['loss']:.5f}")
|
||||
|
||||
@staticmethod
|
||||
def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str:
|
||||
|
||||
trials['Best'] = ''
|
||||
|
||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||
# Ensure compatibility with older versions of hyperopt results
|
||||
trials['results_metrics.winsdrawslosses'] = 'N/A'
|
||||
|
||||
if not has_drawdown:
|
||||
# Ensure compatibility with older versions of hyperopt results
|
||||
trials['results_metrics.max_drawdown_abs'] = None
|
||||
trials['results_metrics.max_drawdown'] = None
|
||||
|
||||
if not legacy_mode:
|
||||
# New mode, using backtest result for metrics
|
||||
trials['results_metrics.winsdrawslosses'] = trials.apply(
|
||||
lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} "
|
||||
f"{x['results_metrics.losses']:>4}", axis=1)
|
||||
trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.winsdrawslosses',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_total_abs',
|
||||
'results_metrics.profit_total', 'results_metrics.holding_avg',
|
||||
'results_metrics.max_drawdown', 'results_metrics.max_drawdown_abs',
|
||||
'loss', 'is_initial_point', 'is_best']]
|
||||
|
||||
else:
|
||||
# Legacy mode
|
||||
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.winsdrawslosses', 'results_metrics.avg_profit',
|
||||
'results_metrics.total_profit', 'results_metrics.profit',
|
||||
'results_metrics.duration', 'results_metrics.max_drawdown',
|
||||
'results_metrics.max_drawdown_abs', 'loss', 'is_initial_point',
|
||||
'is_best']]
|
||||
|
||||
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
|
||||
'Total profit', 'Profit', 'Avg duration', 'Max Drawdown',
|
||||
'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_best']
|
||||
|
||||
return trials
|
||||
|
||||
@staticmethod
|
||||
def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool,
|
||||
print_colorized: bool, remove_header: int) -> str:
|
||||
@@ -216,36 +321,13 @@ class HyperoptTools():
|
||||
return ''
|
||||
|
||||
tabulate.PRESERVE_WHITESPACE = True
|
||||
|
||||
trials = json_normalize(results, max_level=1)
|
||||
trials['Best'] = ''
|
||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||
# Ensure compatibility with older versions of hyperopt results
|
||||
trials['results_metrics.winsdrawslosses'] = 'N/A'
|
||||
legacy_mode = True
|
||||
|
||||
if 'results_metrics.total_trades' in trials:
|
||||
legacy_mode = False
|
||||
# New mode, using backtest result for metrics
|
||||
trials['results_metrics.winsdrawslosses'] = trials.apply(
|
||||
lambda x: f"{x['results_metrics.wins']} {x['results_metrics.draws']:>4} "
|
||||
f"{x['results_metrics.losses']:>4}", axis=1)
|
||||
trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades',
|
||||
'results_metrics.winsdrawslosses',
|
||||
'results_metrics.profit_mean', 'results_metrics.profit_total_abs',
|
||||
'results_metrics.profit_total', 'results_metrics.holding_avg',
|
||||
'loss', 'is_initial_point', 'is_best']]
|
||||
else:
|
||||
# Legacy mode
|
||||
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
|
||||
'results_metrics.winsdrawslosses',
|
||||
'results_metrics.avg_profit', 'results_metrics.total_profit',
|
||||
'results_metrics.profit', 'results_metrics.duration',
|
||||
'loss', 'is_initial_point', 'is_best']]
|
||||
legacy_mode = 'results_metrics.total_trades' not in trials
|
||||
has_drawdown = 'results_metrics.max_drawdown_abs' in trials.columns
|
||||
|
||||
trials = HyperoptTools.prepare_trials_columns(trials, legacy_mode, has_drawdown)
|
||||
|
||||
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
|
||||
'Total profit', 'Profit', 'Avg duration', 'Objective',
|
||||
'is_initial_point', 'is_best']
|
||||
trials['is_profit'] = False
|
||||
trials.loc[trials['is_initial_point'], 'Best'] = '* '
|
||||
trials.loc[trials['is_best'], 'Best'] = 'Best'
|
||||
@@ -268,6 +350,21 @@ class HyperoptTools():
|
||||
)
|
||||
|
||||
stake_currency = config['stake_currency']
|
||||
|
||||
if has_drawdown:
|
||||
trials['Max Drawdown'] = trials.apply(
|
||||
lambda x: '{} {}'.format(
|
||||
round_coin_value(x['max_drawdown_abs'], stake_currency),
|
||||
'({:,.2f}%)'.format(x['Max Drawdown'] * perc_multi).rjust(10, ' ')
|
||||
).rjust(25 + len(stake_currency))
|
||||
if x['Max Drawdown'] != 0.0 else '--'.rjust(25 + len(stake_currency)),
|
||||
axis=1
|
||||
)
|
||||
else:
|
||||
trials = trials.drop(columns=['Max Drawdown'])
|
||||
|
||||
trials = trials.drop(columns=['max_drawdown_abs'])
|
||||
|
||||
trials['Profit'] = trials.apply(
|
||||
lambda x: '{} {}'.format(
|
||||
round_coin_value(x['Total profit'], stake_currency),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user