Compare commits
365 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ab7807cee5 | ||
|
1353c59f18 | ||
|
5d038552ae | ||
|
22ff67c8f8 | ||
|
31449987c0 | ||
|
cff0527919 | ||
|
fce31447ed | ||
|
98118f5e95 | ||
|
dddbc799f9 | ||
|
83f6259594 | ||
|
0b68402c10 | ||
|
1d56c87a34 | ||
|
53231d94a9 | ||
|
76539bc700 | ||
|
d0d9921b42 | ||
|
c14c0f60a1 | ||
|
8180393bbc | ||
|
7f621416a1 | ||
|
6810192992 | ||
|
8ae604d473 | ||
|
0d349cb355 | ||
|
8f1d2ff070 | ||
|
46389e343b | ||
|
b9980330a5 | ||
|
0104c9fde6 | ||
|
99b67348b2 | ||
|
ceb50a7807 | ||
|
bd98ff6332 | ||
|
006436a18d | ||
|
730c9ce471 | ||
|
82335027b7 | ||
|
1ec99e6b76 | ||
|
d325236f96 | ||
|
421265243c | ||
|
16899b9df1 | ||
|
9427b5e924 | ||
|
87a34b4306 | ||
|
56629d882e | ||
|
7c7a8190ab | ||
|
be4807d85c | ||
|
83b4cd7b39 | ||
|
ec33011255 | ||
|
29c6a9263d | ||
|
7e4fe23bf9 | ||
|
f12a8afd41 | ||
|
2e1551a2eb | ||
|
f8fab5c4f8 | ||
|
6b672cd0b9 | ||
|
191616e4e5 | ||
|
109824c9a8 | ||
|
fb86d8f8ff | ||
|
73f0e6e704 | ||
|
4d60a4cf4e | ||
|
8ffd6f2469 | ||
|
89ea8dbef2 | ||
|
e8e3ca0c3c | ||
|
83861fabde | ||
|
5ed85963a9 | ||
|
aa0c3dced8 | ||
|
fa0fcfb492 | ||
|
97e58a42f4 | ||
|
f88fe5d950 | ||
|
37849f8496 | ||
|
52c9a2c37f | ||
|
bf6682d37f | ||
|
854d0c481f | ||
|
4a215821cd | ||
|
9621734adc | ||
|
853bd06841 | ||
|
3f2addb729 | ||
|
320dca19cb | ||
|
f092a92399 | ||
|
d00a955af9 | ||
|
d6bd018da4 | ||
|
8c88173b74 | ||
|
23947cf30b | ||
|
e52c181a2a | ||
|
6ebc2f3897 | ||
|
ef4ab601a9 | ||
|
26176d4c91 | ||
|
0612658ec7 | ||
|
34120f6eb8 | ||
|
7b4c1ec3ce | ||
|
da16474b25 | ||
|
7243c8ee56 | ||
|
ab85c5bb49 | ||
|
05f0cc787c | ||
|
164105acf2 | ||
|
c09b641860 | ||
|
08b52926c8 | ||
|
4eb96cfc4f | ||
|
2d6bfe1592 | ||
|
2424ac94c2 | ||
|
2640dfee93 | ||
|
916776bb53 | ||
|
13da8f9368 | ||
|
5f483acdd0 | ||
|
59e846d554 | ||
|
4bc693c17c | ||
|
d85fd3060a | ||
|
52c147c88e | ||
|
2a1835b165 | ||
|
88b2f3f0d1 | ||
|
6063f2f91f | ||
|
42d9e3a28f | ||
|
5243214a36 | ||
|
2af1c80fd5 | ||
|
b8f6f09de8 | ||
|
7d2bd00f0c | ||
|
b58d6d38b5 | ||
|
d1dab23283 | ||
|
cf89a773da | ||
|
887d78171c | ||
|
ac55215fca | ||
|
d56da41679 | ||
|
894853b300 | ||
|
74d8a985e2 | ||
|
7e64a91720 | ||
|
aed44ef6b3 | ||
|
a8c6c3e2fa | ||
|
8cbc2ce18d | ||
|
a87fd6fcc7 | ||
|
21b22760a7 | ||
|
6c3753ac7f | ||
|
81fb0c5726 | ||
|
16572f90e3 | ||
|
e73203acb8 | ||
|
a262618809 | ||
|
0d11f0bd75 | ||
|
78874fa865 | ||
|
38fc5d680b | ||
|
4d4471480b | ||
|
aaa6468983 | ||
|
55838b574c | ||
|
684de9c7d0 | ||
|
3ca97223f2 | ||
|
d8ff79a2fa | ||
|
f4d39f2a12 | ||
|
19fcbc92a7 | ||
|
0539bd5280 | ||
|
86725847ed | ||
|
ffa6797958 | ||
|
e1e2829ef3 | ||
|
8e8f328bba | ||
|
5cb3735a57 | ||
|
28d6c3419b | ||
|
58a92dc3da | ||
|
72f61f4682 | ||
|
5c8779b155 | ||
|
6c913fa617 | ||
|
9c54c9a2bf | ||
|
e602ac3406 | ||
|
69e8da30e5 | ||
|
09af776b66 | ||
|
442e9d20e1 | ||
|
835614517b | ||
|
0309b06b54 | ||
|
f29b04b4d2 | ||
|
4146b45c6e | ||
|
066ea45ce0 | ||
|
95d11bd0d2 | ||
|
2831a78d0e | ||
|
df5e6aa58b | ||
|
3439e6c5c4 | ||
|
299dfe560a | ||
|
b8c12f6576 | ||
|
2e7367d647 | ||
|
9999017953 | ||
|
71410a5a1e | ||
|
cd8610cb24 | ||
|
ffcc47d8dd | ||
|
cf1a726198 | ||
|
a143f7bc43 | ||
|
66efb5ccf1 | ||
|
42d9e2e7dc | ||
|
fd6018f67a | ||
|
adffd402ea | ||
|
7a092271c5 | ||
|
5f63fdd8ad | ||
|
64e680d7ee | ||
|
1156f5e686 | ||
|
0daf77f313 | ||
|
cd2866eaec | ||
|
7a9768ffa6 | ||
|
7caa6cfe31 | ||
|
e513871fd5 | ||
|
6eab20e337 | ||
|
2d04c2dd4f | ||
|
f6da9e358a | ||
|
3e7c9bd485 | ||
|
06293b2489 | ||
|
f799a81c44 | ||
|
667f1b8b8c | ||
|
340f25bd42 | ||
|
b7eec3fc82 | ||
|
8975558595 | ||
|
7997298538 | ||
|
cf2ae788d7 | ||
|
5d3a67d324 | ||
|
b80a219d03 | ||
|
380e6628e0 | ||
|
2591a34db4 | ||
|
7a9208a8d7 | ||
|
cd940daaf4 | ||
|
f64ed6b878 | ||
|
8cdc795a44 | ||
|
685d18940a | ||
|
ec713ff5ae | ||
|
8ae193f638 | ||
|
07da21e633 | ||
|
8165cc11df | ||
|
2ed20eee4e | ||
|
8962b6d5c9 | ||
|
077374ac42 | ||
|
886abe36c9 | ||
|
d7bbda9659 | ||
|
6a0ab83684 | ||
|
46f3ec1d0a | ||
|
7c1402ef11 | ||
|
f231a08202 | ||
|
a0718ad8cb | ||
|
fd9c8df049 | ||
|
5f5fc513fa | ||
|
43532a2ffa | ||
|
ecddaa663b | ||
|
5f0d1d609e | ||
|
0b0b9c5d45 | ||
|
10651599dd | ||
|
2a383f8785 | ||
|
5aa0d3e05c | ||
|
a39898a5b3 | ||
|
44e374878c | ||
|
f299c4188b | ||
|
38cbf2f902 | ||
|
94bc5356b7 | ||
|
e39c2f4a96 | ||
|
9d2b3b2edb | ||
|
623cee61e6 | ||
|
2a2856e2dd | ||
|
8363faf358 | ||
|
80569c5f21 | ||
|
a33865e8c2 | ||
|
a2bc9d60a0 | ||
|
491af5a0cb | ||
|
fa7dc742d0 | ||
|
3d911557d1 | ||
|
23bad8fd9f | ||
|
028dd8be3e | ||
|
62859455f7 | ||
|
8fdcb600bc | ||
|
cedddd02da | ||
|
f43bd250a2 | ||
|
53984a059f | ||
|
59b00ad662 | ||
|
f676156ec7 | ||
|
23278e52db | ||
|
e8f2c09f08 | ||
|
d1db847612 | ||
|
7f0afe1244 | ||
|
6bb045f565 | ||
|
48750b0ef8 | ||
|
1b5cb3427e | ||
|
52502193c4 | ||
|
72337a0ab7 | ||
|
72cf3147b8 | ||
|
1628a0a4f0 | ||
|
299285a7bb | ||
|
8c2f763193 | ||
|
14e87ed4a1 | ||
|
a4a8abfdc0 | ||
|
fa1d1679f0 | ||
|
378b214a56 | ||
|
584d095295 | ||
|
11f1ce2d9f | ||
|
1e782781a6 | ||
|
b86a602ad4 | ||
|
4b53c2bca4 | ||
|
14c66afecc | ||
|
0538af8875 | ||
|
1ef702479b | ||
|
09e9141e51 | ||
|
d23ea36c05 | ||
|
f4ab2dab4c | ||
|
65e4c052f1 | ||
|
2d6bc9aadc | ||
|
482213fbb3 | ||
|
06759234b6 | ||
|
40b61bbfe3 | ||
|
234f6c2f5e | ||
|
688442507e | ||
|
64de911e16 | ||
|
56647bb498 | ||
|
52b6f6b940 | ||
|
0574a40693 | ||
|
8d4f7ce84f | ||
|
80890e0f59 | ||
|
887b2fdb5e | ||
|
301598bac9 | ||
|
c9b3766fa3 | ||
|
63e1cba597 | ||
|
2d343c8f73 | ||
|
cb74c9bcde | ||
|
176006da29 | ||
|
66b77d2f53 | ||
|
62110dc2fc | ||
|
350fcc071e | ||
|
b38f68b3b0 | ||
|
becccca3d1 | ||
|
ba10bd7756 | ||
|
4b6b7f8343 | ||
|
a3d0889dab | ||
|
816c8295f1 | ||
|
f82d39e1b0 | ||
|
bb4993dc20 | ||
|
c0654f3caf | ||
|
c59a1be154 | ||
|
1de248fe38 | ||
|
6a59740f83 | ||
|
32e6ea314c | ||
|
18bbfdd341 | ||
|
f227f6a755 | ||
|
bf0e75e2a5 | ||
|
b93ad8840a | ||
|
482f1faa88 | ||
|
f5dc10e4ae | ||
|
677078350f | ||
|
d528c44974 | ||
|
133ca9c770 | ||
|
a38b33cd9c | ||
|
9dfbc1a7ff | ||
|
d2111c088b | ||
|
6977ffdbf9 | ||
|
c410599a52 | ||
|
2be8e8070a | ||
|
ace2879265 | ||
|
0ea56548e4 | ||
|
ce228f19dc | ||
|
9df366d943 | ||
|
253b7b763e | ||
|
201e714343 | ||
|
287604efd2 | ||
|
7623691a5f | ||
|
17e605e130 | ||
|
fe588e5722 | ||
|
98a2811605 | ||
|
1f14c6bacd | ||
|
44e8184519 | ||
|
700529fe06 | ||
|
f174c74094 | ||
|
f3e1eddc91 | ||
|
5e0fa21d60 | ||
|
dd4d458ca8 | ||
|
6d8fadd560 | ||
|
1dee0eed75 | ||
|
48347b49fd | ||
|
15bb0af1b3 | ||
|
f3de74f817 | ||
|
6e70ae6e95 | ||
|
5769b9244f | ||
|
8de9c46110 | ||
|
3cb1a9a5a9 | ||
|
c42a924df8 | ||
|
ff96cf154c | ||
|
7a652b07d5 | ||
|
b736691e0e |
@@ -7,8 +7,8 @@ services:
|
||||
dockerfile: ".devcontainer/Dockerfile"
|
||||
volumes:
|
||||
# Allow git usage within container
|
||||
- "/home/${USER}/.ssh:/home/ftuser/.ssh:ro"
|
||||
- "/home/${USER}/.gitconfig:/home/ftuser/.gitconfig:ro"
|
||||
- "${HOME}/.ssh:/home/ftuser/.ssh:ro"
|
||||
- "${HOME}/.gitconfig:/home/ftuser/.gitconfig:ro"
|
||||
- ..:/freqtrade:cached
|
||||
# Persist bash-history
|
||||
- freqtrade-vscode-server:/home/ftuser/.vscode-server
|
||||
|
41
.github/workflows/ci.yml
vendored
41
.github/workflows/ci.yml
vendored
@@ -19,14 +19,14 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-18.04, macos-latest ]
|
||||
os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ]
|
||||
python-version: [3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||
|
||||
- name: Coveralls
|
||||
if: (startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8')
|
||||
if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8')
|
||||
env:
|
||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||
@@ -88,12 +88,16 @@ jobs:
|
||||
run: |
|
||||
cp config.json.example config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --print-all
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
|
||||
- name: Flake8
|
||||
run: |
|
||||
flake8
|
||||
|
||||
- name: Sort imports (isort)
|
||||
run: |
|
||||
isort --check .
|
||||
|
||||
- name: Mypy
|
||||
run: |
|
||||
mypy freqtrade scripts
|
||||
@@ -121,7 +125,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -150,7 +154,7 @@ jobs:
|
||||
run: |
|
||||
cp config.json.example config.json
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --print-all
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
|
||||
- name: Flake8
|
||||
run: |
|
||||
@@ -172,7 +176,7 @@ jobs:
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
docs_check:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -180,6 +184,17 @@ jobs:
|
||||
run: |
|
||||
./tests/test_docs.sh
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Documentation build
|
||||
run: |
|
||||
pip install -r docs/requirements-docs.txt
|
||||
pip install mkdocs
|
||||
mkdocs build
|
||||
|
||||
- name: Slack Notification
|
||||
uses: homoluctus/slatify@v1.8.0
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
@@ -190,7 +205,7 @@ jobs:
|
||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
cleanup-prior-runs:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Cleanup previous runs on this branch
|
||||
uses: rokroskar/workflow-run-cleanup-action@v0.2.2
|
||||
@@ -201,7 +216,7 @@ jobs:
|
||||
# Notify on slack only once - when CI completes (and after deploy) in case it's successfull
|
||||
notify-complete:
|
||||
needs: [ build, build_windows, docs_check ]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
uses: homoluctus/slatify@v1.8.0
|
||||
@@ -214,13 +229,13 @@ jobs:
|
||||
|
||||
deploy:
|
||||
needs: [ build, build_windows, docs_check ]
|
||||
runs-on: ubuntu-18.04
|
||||
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
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
@@ -236,7 +251,7 @@ jobs:
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release')
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.pypi_test_password }}
|
||||
@@ -244,7 +259,7 @@ jobs:
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release')
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.pypi_password }}
|
||||
|
@@ -33,7 +33,7 @@ jobs:
|
||||
- script:
|
||||
- cp config.json.example config.json
|
||||
- freqtrade create-userdir --userdir user_data
|
||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt
|
||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily
|
||||
name: hyperopt
|
||||
- script: flake8
|
||||
name: flake8
|
||||
|
@@ -12,8 +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 [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE)
|
||||
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/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -65,6 +64,14 @@ Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using
|
||||
mypy freqtrade
|
||||
```
|
||||
|
||||
### 4. Ensure all imports are correct
|
||||
|
||||
#### Run isort
|
||||
|
||||
``` bash
|
||||
isort .
|
||||
```
|
||||
|
||||
## (Core)-Committer Guide
|
||||
|
||||
### Process: Pull Requests
|
||||
|
34
README.md
34
README.md
@@ -55,9 +55,8 @@ Please find the complete documentation on our [website](https://www.freqtrade.io
|
||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:freqtrade/freqtrade.git
|
||||
git clone -b develop https://github.com/freqtrade/freqtrade.git
|
||||
cd freqtrade
|
||||
git checkout develop
|
||||
./setup.sh --install
|
||||
```
|
||||
|
||||
@@ -111,17 +110,17 @@ optional arguments:
|
||||
|
||||
Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
||||
|
||||
- `/start`: Starts the trader
|
||||
- `/stop`: Stops the trader
|
||||
- `/status [table]`: Lists all open trades
|
||||
- `/count`: Displays number of open trades
|
||||
- `/start`: Starts the trader.
|
||||
- `/stop`: Stops the trader.
|
||||
- `/stopbuy`: Stop entering new trades.
|
||||
- `/status [table]`: Lists all open trades.
|
||||
- `/profit`: Lists cumulative profit from all finished trades
|
||||
- `/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
|
||||
- `/daily <n>`: Shows profit or loss per day, over the last n days
|
||||
- `/help`: Show help message
|
||||
- `/version`: Show version
|
||||
- `/balance`: Show account balance per currency.
|
||||
- `/daily <n>`: Shows profit or loss per day, over the last n days.
|
||||
- `/help`: Show help message.
|
||||
- `/version`: Show version.
|
||||
|
||||
## Development branches
|
||||
|
||||
@@ -133,12 +132,13 @@ The project is currently setup in two main branches:
|
||||
|
||||
## Support
|
||||
|
||||
### Help / Slack
|
||||
### Help / Discord / Slack
|
||||
|
||||
For any questions not covered by the documentation or for further
|
||||
information about the bot, we encourage you to join our slack channel.
|
||||
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.
|
||||
|
||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE).
|
||||
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-jaut7r4m-Y17k4x5mcQES9a9swKuxbg).
|
||||
|
||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||
|
||||
@@ -166,10 +166,10 @@ Please read our
|
||||
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
to understand the requirements before sending your pull-requests.
|
||||
|
||||
Coding is not a neccessity to contribute - maybe start with improving our documentation?
|
||||
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 [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). 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/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). 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`.
|
||||
|
||||
@@ -177,7 +177,7 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/
|
||||
|
||||
### Up-to-date clock
|
||||
|
||||
The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges.
|
||||
The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges.
|
||||
|
||||
### Min hardware required
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl
Normal file
Binary file not shown.
@@ -7,10 +7,10 @@ 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.18-cp37-cp37m-win_amd64.whl
|
||||
pip install build_helpers\TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl
|
||||
}
|
||||
if ($pyv -eq '3.8') {
|
||||
pip install build_helpers\TA_Lib-0.4.18-cp38-cp38-win_amd64.whl
|
||||
pip install build_helpers\TA_Lib-0.4.19-cp38-cp38-win_amd64.whl
|
||||
}
|
||||
|
||||
pip install -r requirements-dev.txt
|
||||
|
@@ -17,8 +17,13 @@ else
|
||||
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
|
||||
@@ -32,9 +37,6 @@ if [ $? -ne 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Tag image for upload
|
||||
docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG
|
||||
docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed tagging image"
|
||||
return 1
|
||||
|
@@ -5,15 +5,15 @@
|
||||
"tradable_balance_ratio": 0.99,
|
||||
"fiat_display_currency": "USD",
|
||||
"timeframe": "5m",
|
||||
"dry_run": false,
|
||||
"dry_run": true,
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0,
|
||||
"use_order_book": false,
|
||||
"ask_last_balance": 0.0,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"amount_reserve_percent": 0.05,
|
||||
"amend_last_stake_amount": false,
|
||||
"last_stake_amount_min_ratio": 0.5,
|
||||
"dry_run": false,
|
||||
"dry_run": true,
|
||||
"cancel_open_orders_on_exit": false,
|
||||
"timeframe": "5m",
|
||||
"trailing_stop": false,
|
||||
@@ -67,7 +67,13 @@
|
||||
{"method": "AgeFilter", "min_days_listed": 10},
|
||||
{"method": "PrecisionFilter"},
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010},
|
||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005}
|
||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
|
||||
{
|
||||
"method": "RangeStabilityFilter",
|
||||
"lookback_days": 10,
|
||||
"min_rate_of_change": 0.01,
|
||||
"refresh_period": 1440
|
||||
}
|
||||
],
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
|
@@ -27,12 +27,11 @@
|
||||
"use_sell_signal": true,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
|
||||
},
|
||||
"exchange": {
|
||||
"name": "kraken",
|
||||
"key": "",
|
||||
"secret": "",
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_key",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": true,
|
||||
|
@@ -5,6 +5,3 @@ FROM freqtradeorg/freqtrade:${sourceimage}
|
||||
COPY requirements-plot.txt /freqtrade/
|
||||
|
||||
RUN pip install -r requirements-plot.txt --no-cache-dir
|
||||
|
||||
# Empty the ENTRYPOINT to allow all commands
|
||||
ENTRYPOINT []
|
||||
|
@@ -27,9 +27,9 @@ class MyAwesomeHyperOpt2(MyAwesomeHyperOpt):
|
||||
and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case:
|
||||
|
||||
```
|
||||
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt ...
|
||||
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ...
|
||||
or
|
||||
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 ...
|
||||
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ...
|
||||
```
|
||||
|
||||
## Creating and using a custom loss function
|
||||
|
@@ -162,6 +162,8 @@ A backtesting result will look like that:
|
||||
|-----------------------+---------------------|
|
||||
| Backtesting from | 2019-01-01 00:00:00 |
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Max open trades | 3 |
|
||||
| | |
|
||||
| Total trades | 429 |
|
||||
| First trade | 2019-01-01 18:30:00 |
|
||||
| First trade Pair | EOS/USDT |
|
||||
@@ -233,6 +235,8 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||
|-----------------------+---------------------|
|
||||
| Backtesting from | 2019-01-01 00:00:00 |
|
||||
| Backtesting to | 2019-05-01 00:00:00 |
|
||||
| Max open trades | 3 |
|
||||
| | |
|
||||
| Total trades | 429 |
|
||||
| First trade | 2019-01-01 18:30:00 |
|
||||
| First trade Pair | EOS/USDT |
|
||||
@@ -251,16 +255,17 @@ 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`) - to clearly see settings for this.
|
||||
- `Total trades`: Identical to the total trades of the backtest output table.
|
||||
- `First trade`: First trade entered.
|
||||
- `First trade pair`: Which pair was part of the first trade.
|
||||
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
|
||||
- `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table.
|
||||
- `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).
|
||||
- `Best day` / `Worst day`: Best and worst day based on daily profit.
|
||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||
- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
|
||||
- `Drawdown Start` / `Drawdown End`: Start and end datetimes for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
|
||||
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
|
||||
|
||||
### Assumptions made by backtesting
|
||||
|
@@ -303,7 +303,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} ...]]
|
||||
[--dmmp] [--print-all] [--no-color] [--print-json]
|
||||
[-j JOBS] [--random-state INT] [--min-trades INT]
|
||||
[--continue] [--hyperopt-loss NAME]
|
||||
[--hyperopt-loss NAME]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -349,18 +349,14 @@ optional arguments:
|
||||
reproducible hyperopt results.
|
||||
--min-trades INT Set minimal desired number of trades for evaluations
|
||||
in the hyperopt optimization path (default: 1).
|
||||
--continue Continue hyperopt from previous runs. By default,
|
||||
temporary files will be removed and hyperopt will
|
||||
start from scratch.
|
||||
--hyperopt-loss NAME Specify the class name of the hyperopt loss function
|
||||
class (IHyperOptLoss). Different functions can
|
||||
generate completely different results, since the
|
||||
target for optimization is different. Built-in
|
||||
Hyperopt-loss-functions are: DefaultHyperOptLoss,
|
||||
Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss,
|
||||
OnlyProfitHyperOptLoss, SharpeHyperOptLoss,
|
||||
SharpeHyperOptLossDaily, SortinoHyperOptLoss,
|
||||
SortinoHyperOptLossDaily.(default:
|
||||
`DefaultHyperOptLoss`).
|
||||
SortinoHyperOptLossDaily.
|
||||
|
||||
Common arguments:
|
||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||
|
@@ -59,8 +59,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
|
||||
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
|
||||
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||
| `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.** Set 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
|
||||
@@ -87,6 +87,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
|
||||
| `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
|
||||
| `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 below](#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||
@@ -176,7 +177,7 @@ In the example above this would mean:
|
||||
This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly.
|
||||
|
||||
!!! Note
|
||||
The minimum last stake amount can be configured using `amend_last_stake_amount` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange.
|
||||
The minimum last stake amount can be configured using `last_stake_amount_min_ratio` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange.
|
||||
|
||||
#### Static stake amount
|
||||
|
||||
@@ -313,22 +314,21 @@ Configuration:
|
||||
}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
!!! Note "Market order support"
|
||||
Not all exchanges support "market" orders.
|
||||
The following message will be shown if your exchange does not support market orders:
|
||||
`"Exchange <yourexchange> does not support market orders."`
|
||||
`"Exchange <yourexchange> does not support market orders."` and the bot will refuse to start.
|
||||
|
||||
!!! Note
|
||||
Stoploss on exchange interval is not mandatory. Do not change its value if you are
|
||||
!!! Warning "Using market orders"
|
||||
Please carefully read the section [Market order pricing](#market-order-pricing) section when using market orders.
|
||||
|
||||
!!! Note "Stoploss on exchange"
|
||||
`stoploss_on_exchange_interval` is not mandatory. Do not change its value if you are
|
||||
unsure of what you are doing. For more information about how stoploss works please
|
||||
refer to [the stoploss documentation](stoploss.md).
|
||||
|
||||
!!! Note
|
||||
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 "Using market orders"
|
||||
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
|
||||
|
||||
!!! 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.
|
||||
|
||||
@@ -574,144 +574,7 @@ Assuming both buy and sell are using market orders, a configuration similar to t
|
||||
```
|
||||
|
||||
Obviously, if only one side is using limit orders, different pricing combinations can be used.
|
||||
|
||||
## Pairlists and Pairlist Handlers
|
||||
|
||||
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
||||
|
||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
|
||||
|
||||
Additionaly, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
||||
|
||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler.
|
||||
|
||||
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
|
||||
|
||||
### Available Pairlist Handlers
|
||||
|
||||
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
||||
* [`VolumePairList`](#volume-pair-list)
|
||||
* [`AgeFilter`](#agefilter)
|
||||
* [`PrecisionFilter`](#precisionfilter)
|
||||
* [`PriceFilter`](#pricefilter)
|
||||
* [`ShuffleFilter`](#shufflefilter)
|
||||
* [`SpreadFilter`](#spreadfilter)
|
||||
|
||||
!!! Tip "Testing pairlists"
|
||||
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility subcommand to test your configuration quickly.
|
||||
|
||||
#### Static Pair List
|
||||
|
||||
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration.
|
||||
|
||||
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
```
|
||||
|
||||
#### Volume Pair List
|
||||
|
||||
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
||||
|
||||
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
|
||||
|
||||
When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange.
|
||||
|
||||
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
|
||||
|
||||
`VolumePairList` is 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": [{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"refresh_period": 1800,
|
||||
}],
|
||||
```
|
||||
|
||||
#### AgeFilter
|
||||
|
||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`).
|
||||
|
||||
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.
|
||||
|
||||
#### PrecisionFilter
|
||||
|
||||
Filters low-value coins which would not allow setting stoplosses.
|
||||
|
||||
#### PriceFilter
|
||||
|
||||
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
||||
|
||||
* `min_price`
|
||||
* `max_price`
|
||||
* `low_price_ratio`
|
||||
|
||||
The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs.
|
||||
This option is disabled by default, and will only apply if set to > 0.
|
||||
|
||||
The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs.
|
||||
This option is disabled by default, and will only apply if set to > 0.
|
||||
|
||||
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
|
||||
This option is disabled by default, and will only apply if set to > 0.
|
||||
|
||||
For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
|
||||
|
||||
Calculation example:
|
||||
|
||||
Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly.
|
||||
|
||||
!!! Warning "Low priced pairs"
|
||||
Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding.
|
||||
|
||||
#### ShuffleFilter
|
||||
|
||||
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
|
||||
|
||||
!!! Tip
|
||||
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order.
|
||||
|
||||
#### SpreadFilter
|
||||
|
||||
Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`).
|
||||
|
||||
Example:
|
||||
|
||||
If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out.
|
||||
|
||||
### Full example of Pairlist Handlers
|
||||
|
||||
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 priceunit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value.
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"pair_whitelist": [],
|
||||
"pair_blacklist": ["BNB/BTC"]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
},
|
||||
{"method": "AgeFilter", "min_days_listed": 10},
|
||||
{"method": "PrecisionFilter"},
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
|
||||
{"method": "ShuffleFilter", "seed": 42}
|
||||
],
|
||||
```
|
||||
--8<-- "includes/pairlists.md"
|
||||
|
||||
## Switch to Dry-run mode
|
||||
|
||||
|
@@ -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 in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) 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/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) where you can ask questions.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -96,7 +96,7 @@ Below is an outline of exception inheritance hierarchy:
|
||||
|
||||
## Modules
|
||||
|
||||
### Dynamic Pairlist
|
||||
### Pairlists
|
||||
|
||||
You have a great idea for a new pair selection algorithm you would like to try out? Great.
|
||||
Hopefully you also want to contribute this back upstream.
|
||||
|
34
docs/edge.md
34
docs/edge.md
@@ -82,20 +82,34 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv
|
||||
$$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$
|
||||
|
||||
???+ Example "Worked example of $R$ calculation"
|
||||
Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.<br>
|
||||
Your potential profit is calculated as:<br>
|
||||
Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100, which will give you 10 shares (100 / 10).
|
||||
|
||||
Your potential profit is calculated as:
|
||||
|
||||
$\begin{aligned}
|
||||
\text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\
|
||||
&= (15 - 10) * \frac{100}{15}\\
|
||||
&= 33.33
|
||||
\end{aligned}$<br>
|
||||
Since the price might go to $0, the $100 dolars invested could turn into 0. We can compute the Risk Reward Ratio as follows:<br>
|
||||
\text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \frac{\text{investment}}{\text{entry_price}} \\
|
||||
&= (15 - 10) * (100 / 10) \\
|
||||
&= 50
|
||||
\end{aligned}$
|
||||
|
||||
Since the price might go to $0, the $100 dollars invested could turn into 0.
|
||||
|
||||
We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$).
|
||||
|
||||
$\begin{aligned}
|
||||
\text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\
|
||||
&= (10 - 8.5) * (100 / 10)\\
|
||||
&= 15
|
||||
\end{aligned}$
|
||||
|
||||
We can compute the Risk Reward Ratio as follows:
|
||||
|
||||
$\begin{aligned}
|
||||
R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\
|
||||
&= \frac{33.33}{100}\\
|
||||
&= 0.333...
|
||||
&= \frac{50}{15}\\
|
||||
&= 3.33
|
||||
\end{aligned}$<br>
|
||||
What it effectivelly means is that the strategy have the potential to make $0.33 for each $1 invested.
|
||||
What it effectively means is that the strategy have the potential to make 3.33$ for each $1 invested.
|
||||
|
||||
On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows:
|
||||
|
||||
|
@@ -23,7 +23,8 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f
|
||||
## Kraken
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled.
|
||||
Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
|
||||
|
||||
### Historic Kraken data
|
||||
|
||||
@@ -75,8 +76,7 @@ print(res)
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide.
|
||||
|
||||
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||
|
||||
### Using subaccounts
|
||||
|
||||
@@ -99,10 +99,10 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll
|
||||
|
||||
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
|
||||
|
||||
|
||||
## Random notes for other exchanges
|
||||
|
||||
* The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed:
|
||||
|
||||
```shell
|
||||
$ pip3 install web3
|
||||
```
|
||||
|
10
docs/faq.md
10
docs/faq.md
@@ -140,18 +140,12 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce
|
||||
It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going.
|
||||
|
||||
```bash
|
||||
freqtrade hyperopt -e 1000
|
||||
```
|
||||
|
||||
or if you want intermediate result to see
|
||||
|
||||
```bash
|
||||
for i in {1..100}; do freqtrade hyperopt -e 1000; done
|
||||
freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000
|
||||
```
|
||||
|
||||
### 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/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) - or the Freqtrade [discord community](https://discord.gg/X89cVG). 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 [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) - or the Freqtrade [discord community](https://discord.gg/X89cVG). 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:
|
||||
|
||||
|
121
docs/hyperopt.md
121
docs/hyperopt.md
@@ -37,12 +37,20 @@ pip install -r requirements-hyperopt.txt
|
||||
Before we start digging into Hyperopt, we recommend you to take a look at
|
||||
the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py).
|
||||
|
||||
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy.
|
||||
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar.
|
||||
|
||||
The simplest way to get started is to use `freqtrade new-hyperopt --hyperopt AwesomeHyperopt`.
|
||||
This will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`.
|
||||
!!! Tip "About this page"
|
||||
For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class.
|
||||
|
||||
### Checklist on all tasks / possibilities in hyperopt
|
||||
The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`.
|
||||
|
||||
``` bash
|
||||
freqtrade new-hyperopt --hyperopt AwesomeHyperopt
|
||||
```
|
||||
|
||||
### Hyperopt checklist
|
||||
|
||||
Checklist on all tasks / possibilities in hyperopt
|
||||
|
||||
Depending on the space you want to optimize, only some of the below are required:
|
||||
|
||||
@@ -54,17 +62,15 @@ Depending on the space you want to optimize, only some of the below are required
|
||||
!!! Note
|
||||
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work.
|
||||
|
||||
Optional - can also be loaded from a strategy:
|
||||
Optional in hyperopt - can also be loaded from a strategy (recommended):
|
||||
|
||||
* copy `populate_indicators` from your strategy - otherwise default-strategy will be used
|
||||
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
|
||||
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
|
||||
|
||||
!!! Note
|
||||
Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead.
|
||||
|
||||
!!! Note
|
||||
You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods.
|
||||
Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead.
|
||||
|
||||
Rarely you may also need to override:
|
||||
|
||||
@@ -80,17 +86,20 @@ Rarely you may also need to override:
|
||||
# Have a working strategy at hand.
|
||||
freqtrade new-hyperopt --hyperopt EmptyHyperopt
|
||||
|
||||
freqtrade hyperopt --hyperopt EmptyHyperopt --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100
|
||||
freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100
|
||||
```
|
||||
|
||||
### 1. Install a Custom Hyperopt File
|
||||
### Create a Custom Hyperopt File
|
||||
|
||||
Put your hyperopt file into the directory `user_data/hyperopts`.
|
||||
Let assume you want a hyperopt file `AwesomeHyperopt.py`:
|
||||
|
||||
Let assume you want a hyperopt file `awesome_hyperopt.py`:
|
||||
Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py`
|
||||
``` bash
|
||||
freqtrade new-hyperopt --hyperopt AwesomeHyperopt
|
||||
```
|
||||
|
||||
### 2. Configure your Guards and Triggers
|
||||
This command will create a new hyperopt file from a template, allowing you to get started quickly.
|
||||
|
||||
### Configure your Guards and Triggers
|
||||
|
||||
There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing:
|
||||
|
||||
@@ -102,14 +111,16 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
||||
1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10.
|
||||
2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band".
|
||||
|
||||
Hyperoptimization will, for each eval round, pick one trigger and possibly
|
||||
multiple guards. The constructed strategy will be something like
|
||||
"*buy exactly when close price touches lower Bollinger band, BUT only if
|
||||
!!! Hint "Guards and Triggers"
|
||||
Technically, there is no difference between Guards and Triggers.
|
||||
However, this guide will make this distinction to make it clear that signals should not be "sticking".
|
||||
Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
|
||||
|
||||
Hyper-optimization will, for each epoch round, pick one trigger and possibly
|
||||
multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if
|
||||
ADX > 10*".
|
||||
|
||||
If you have updated the buy strategy, i.e. changed the contents of
|
||||
`populate_buy_trend()` method, you have to update the `guards` and
|
||||
`triggers` your hyperopt must use correspondingly.
|
||||
If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly.
|
||||
|
||||
#### Sell optimization
|
||||
|
||||
@@ -126,7 +137,7 @@ To avoid naming collisions in the search-space, please prefix all sell-spaces wi
|
||||
|
||||
The Strategy class exposes the timeframe value as the `self.timeframe` attribute.
|
||||
The same value is available as class-attribute `HyperoptName.timeframe`.
|
||||
In the case of the linked sample-value this would be `SampleHyperOpt.timeframe`.
|
||||
In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`.
|
||||
|
||||
## Solving a Mystery
|
||||
|
||||
@@ -192,27 +203,25 @@ So let's write the buy strategy using these values:
|
||||
return populate_buy_trend
|
||||
```
|
||||
|
||||
Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`)
|
||||
with different value combinations. It will then use the given historical data and make
|
||||
buys based on the buy signals generated with the above function and based on the results
|
||||
it will end with telling you which parameter combination produced the best profits.
|
||||
Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations.
|
||||
It will use the given historical data and make buys based on the buy signals generated with the above function.
|
||||
Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)).
|
||||
|
||||
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
||||
When you want to test an indicator that isn't used by the bot currently, remember to
|
||||
add it to the `populate_indicators()` method in your custom hyperopt file.
|
||||
!!! Note
|
||||
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
||||
When you want to test an indicator that isn't used by the bot currently, remember to
|
||||
add it to the `populate_indicators()` method in your strategy or hyperopt file.
|
||||
|
||||
## Loss-functions
|
||||
|
||||
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
|
||||
|
||||
By default, Freqtrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses.
|
||||
|
||||
A different loss function can be specified by using the `--hyperopt-loss <Class-name>` argument.
|
||||
A loss function must be specified via the `--hyperopt-loss <Class-name>` argument (or optionally via the configuration under the `"hyperopt_loss"` key).
|
||||
This class should be in its own file within the `user_data/hyperopts/` directory.
|
||||
|
||||
Currently, the following loss functions are builtin:
|
||||
|
||||
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function)
|
||||
* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses.
|
||||
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
|
||||
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation)
|
||||
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation)
|
||||
@@ -229,21 +238,20 @@ Because hyperopt tries a lot of combinations to find the best parameters it will
|
||||
We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
|
||||
|
||||
```bash
|
||||
freqtrade hyperopt --config config.json --hyperopt <hyperoptname> -e 500 --spaces all
|
||||
freqtrade hyperopt --config config.json --hyperopt <hyperoptname> --hyperopt-loss <hyperoptlossname> --strategy <strategyname> -e 500 --spaces all
|
||||
```
|
||||
|
||||
Use `<hyperoptname>` as the name of the custom hyperopt used.
|
||||
|
||||
The `-e` option will set how many evaluations hyperopt will do. We recommend
|
||||
running at least several thousand evaluations.
|
||||
The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs.
|
||||
Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results.
|
||||
|
||||
The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below.
|
||||
|
||||
!!! Note
|
||||
By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`.
|
||||
|
||||
!!! Warning
|
||||
When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed.
|
||||
Hyperopt will store hyperopt results with the timestamp of the hyperopt start time.
|
||||
Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename <filename>` to read and display older hyperopt results.
|
||||
You can find a list of filenames with `ls -l user_data/hyperopt_results/`.
|
||||
|
||||
### Execute Hyperopt with different historical data source
|
||||
|
||||
@@ -251,13 +259,13 @@ If you would like to hyperopt parameters using an alternate historical data set
|
||||
you have on-disk, use the `--datadir PATH` option. By default, hyperopt
|
||||
uses data from directory `user_data/data`.
|
||||
|
||||
### Running Hyperopt with Smaller Testset
|
||||
### Running Hyperopt with a smaller test-set
|
||||
|
||||
Use the `--timerange` argument to change how much of the testset you want to use.
|
||||
Use the `--timerange` argument to change how much of the test-set you want to use.
|
||||
For example, to use one month of data, pass the following parameter to the hyperopt call:
|
||||
|
||||
```bash
|
||||
freqtrade hyperopt --timerange 20180401-20180501
|
||||
freqtrade hyperopt --hyperopt <hyperoptname> --strategy <strategyname> --timerange 20180401-20180501
|
||||
```
|
||||
|
||||
### Running Hyperopt using methods from a strategy
|
||||
@@ -265,16 +273,15 @@ freqtrade hyperopt --timerange 20180401-20180501
|
||||
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
|
||||
|
||||
```bash
|
||||
freqtrade hyperopt --strategy SampleStrategy --hyperopt SampleHyperopt
|
||||
freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
### Running Hyperopt with Smaller Search Space
|
||||
|
||||
Use the `--spaces` option to limit the search space used by hyperopt.
|
||||
Letting Hyperopt optimize everything is a huuuuge search space. Often it
|
||||
might make more sense to start by just searching for initial buy algorithm.
|
||||
Or maybe you just want to optimize your stoploss or roi table for that awesome
|
||||
new buy strategy you have.
|
||||
Letting Hyperopt optimize everything is a huuuuge search space.
|
||||
Often it might make more sense to start by just searching for initial buy algorithm.
|
||||
Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have.
|
||||
|
||||
Legal values are:
|
||||
|
||||
@@ -318,7 +325,7 @@ The initial state for generation of these random values (random state) is contro
|
||||
|
||||
If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used.
|
||||
|
||||
If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyperoptimization results with same random state value used.
|
||||
If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyper-optimization results with same random state value used.
|
||||
|
||||
## Understand the Hyperopt Result
|
||||
|
||||
@@ -371,7 +378,7 @@ By default, hyperopt prints colorized results -- epochs with positive profit are
|
||||
You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option.
|
||||
|
||||
!!! Note "Windows and color output"
|
||||
Windows does not support color-output nativly, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL.
|
||||
Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL.
|
||||
|
||||
### Understand Hyperopt ROI results
|
||||
|
||||
@@ -419,7 +426,9 @@ These ranges should be sufficient in most cases. The minutes in the steps (ROI d
|
||||
|
||||
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
|
||||
|
||||
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
|
||||
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps).
|
||||
|
||||
A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
|
||||
|
||||
### Understand Hyperopt Stoploss results
|
||||
|
||||
@@ -441,7 +450,7 @@ Stoploss: -0.27996
|
||||
|
||||
In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy:
|
||||
|
||||
```
|
||||
``` python
|
||||
# Optimal stoploss designed for the strategy
|
||||
# This attribute will be overridden if the config file contains "stoploss"
|
||||
stoploss = -0.27996
|
||||
@@ -475,7 +484,7 @@ Trailing stop:
|
||||
|
||||
In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy:
|
||||
|
||||
```
|
||||
``` python
|
||||
# Trailing stop
|
||||
# These attributes will be overridden if the config file contains corresponding values.
|
||||
trailing_stop = True
|
||||
@@ -494,10 +503,14 @@ Override the `trailing_space()` method and define the desired range in it if you
|
||||
|
||||
## Show details of Hyperopt results
|
||||
|
||||
After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` subcommands. The usage of these subcommands is described in the [Utils](utils.md#list-hyperopt-results) chapter.
|
||||
After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter.
|
||||
|
||||
## Validate backtesting results
|
||||
|
||||
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
|
||||
|
||||
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
|
||||
|
||||
Should results don't match, please double-check to make sure you transferred all conditions correctly.
|
||||
Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
|
||||
You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
|
||||
|
170
docs/includes/pairlists.md
Normal file
170
docs/includes/pairlists.md
Normal file
@@ -0,0 +1,170 @@
|
||||
## Pairlists and Pairlist Handlers
|
||||
|
||||
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
|
||||
|
||||
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
|
||||
|
||||
Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
|
||||
|
||||
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler.
|
||||
|
||||
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
|
||||
|
||||
### Available Pairlist Handlers
|
||||
|
||||
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
|
||||
* [`VolumePairList`](#volume-pair-list)
|
||||
* [`AgeFilter`](#agefilter)
|
||||
* [`PrecisionFilter`](#precisionfilter)
|
||||
* [`PriceFilter`](#pricefilter)
|
||||
* [`ShuffleFilter`](#shufflefilter)
|
||||
* [`SpreadFilter`](#spreadfilter)
|
||||
* [`RangeStabilityFilter`](#rangestabilityfilter)
|
||||
|
||||
!!! Tip "Testing pairlists"
|
||||
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly.
|
||||
|
||||
#### Static Pair List
|
||||
|
||||
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration.
|
||||
|
||||
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
```
|
||||
|
||||
By default, only currently enabled pairs are allowed.
|
||||
To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration.
|
||||
This can be useful for backtesting expired pairs (like quarterly spot-markets).
|
||||
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
|
||||
|
||||
#### Volume Pair List
|
||||
|
||||
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
||||
|
||||
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
|
||||
|
||||
When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange.
|
||||
|
||||
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
|
||||
|
||||
`VolumePairList` is 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": [{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"refresh_period": 1800
|
||||
}],
|
||||
```
|
||||
|
||||
#### AgeFilter
|
||||
|
||||
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`).
|
||||
|
||||
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.
|
||||
|
||||
#### PrecisionFilter
|
||||
|
||||
Filters low-value coins which would not allow setting stoplosses.
|
||||
|
||||
#### PriceFilter
|
||||
|
||||
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
||||
|
||||
* `min_price`
|
||||
* `max_price`
|
||||
* `low_price_ratio`
|
||||
|
||||
The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs.
|
||||
This option is disabled by default, and will only apply if set to > 0.
|
||||
|
||||
The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs.
|
||||
This option is disabled by default, and will only apply if set to > 0.
|
||||
|
||||
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
|
||||
This option is disabled by default, and will only apply if set to > 0.
|
||||
|
||||
For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied.
|
||||
|
||||
Calculation example:
|
||||
|
||||
Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly.
|
||||
|
||||
!!! Warning "Low priced pairs"
|
||||
Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding.
|
||||
|
||||
#### ShuffleFilter
|
||||
|
||||
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
|
||||
|
||||
!!! Tip
|
||||
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order.
|
||||
|
||||
#### SpreadFilter
|
||||
|
||||
Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`).
|
||||
|
||||
Example:
|
||||
|
||||
If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out.
|
||||
|
||||
#### RangeStabilityFilter
|
||||
|
||||
Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
|
||||
|
||||
In the below example:
|
||||
If the trading range over the last 10 days is <1%, remove the pair from the whitelist.
|
||||
|
||||
```json
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "RangeStabilityFilter",
|
||||
"lookback_days": 10,
|
||||
"min_rate_of_change": 0.01,
|
||||
"refresh_period": 1440
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
!!! Tip
|
||||
This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit.
|
||||
|
||||
### Full example of Pairlist Handlers
|
||||
|
||||
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value.
|
||||
|
||||
```json
|
||||
"exchange": {
|
||||
"pair_whitelist": [],
|
||||
"pair_blacklist": ["BNB/BTC"]
|
||||
},
|
||||
"pairlists": [
|
||||
{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
},
|
||||
{"method": "AgeFilter", "min_days_listed": 10},
|
||||
{"method": "PrecisionFilter"},
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
|
||||
{
|
||||
"method": "RangeStabilityFilter",
|
||||
"lookback_days": 10,
|
||||
"min_rate_of_change": 0.01,
|
||||
"refresh_period": 1440
|
||||
},
|
||||
{"method": "ShuffleFilter", "seed": 42}
|
||||
],
|
||||
```
|
@@ -59,11 +59,14 @@ Alternatively
|
||||
|
||||
## Support
|
||||
|
||||
### Help / Slack
|
||||
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community.
|
||||
### Help / Discord / Slack
|
||||
|
||||
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel.
|
||||
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-jaut7r4m-Y17k4x5mcQES9a9swKuxbg).
|
||||
|
||||
## Ready to try?
|
||||
|
||||
Begin by reading our installation guide [for docker](docker.md), or for [installation without docker](installation.md).
|
||||
Begin by reading our installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md).
|
||||
|
@@ -1,2 +1,3 @@
|
||||
mkdocs-material==5.5.13
|
||||
mkdocs-material==6.1.6
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==8.0.1
|
||||
|
@@ -104,32 +104,42 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
|
||||
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters]
|
||||
```
|
||||
|
||||
## Available commands
|
||||
## Available endpoints
|
||||
|
||||
| Command | Description |
|
||||
|----------|-------------|
|
||||
| `ping` | Simple command testing the API Readiness - requires no authentication.
|
||||
| `start` | Starts the trader
|
||||
| `stop` | Stops the trader
|
||||
| `start` | Starts the trader.
|
||||
| `stop` | Stops the trader.
|
||||
| `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `reload_config` | Reloads the configuration file
|
||||
| `reload_config` | Reloads the configuration file.
|
||||
| `trades` | List last trades.
|
||||
| `delete_trade <trade_id>` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||
| `show_config` | Shows part of the current configuration with relevant settings to operation
|
||||
| `logs` | Shows last log messages
|
||||
| `status` | Lists all open trades
|
||||
| `count` | Displays number of trades used and available
|
||||
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||
| `show_config` | Shows part of the current configuration with relevant settings to operation.
|
||||
| `logs` | Shows last log messages.
|
||||
| `status` | Lists all open trades.
|
||||
| `count` | Displays number of trades used and available.
|
||||
| `locks` | Displays currently locked pairs.
|
||||
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
|
||||
| `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)
|
||||
| `performance` | Show performance of each finished trade grouped by pair
|
||||
| `balance` | Show account balance per currency
|
||||
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
||||
| `whitelist` | Show the current whitelist
|
||||
| `performance` | Show performance of each finished trade grouped by pair.
|
||||
| `balance` | Show account balance per currency.
|
||||
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
|
||||
| `whitelist` | Show the current whitelist.
|
||||
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||
| `edge` | Show validated pairs by Edge if it is enabled.
|
||||
| `version` | Show version
|
||||
| `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha**
|
||||
| `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha**
|
||||
| `plot_config` | Get plot config from the strategy (or nothing if not configured). **Alpha**
|
||||
| `strategies` | List strategies in strategy directory. **Alpha**
|
||||
| `strategy <strategy>` | Get specific Strategy content. **Alpha**
|
||||
| `available_pairs` | List available backtest data. **Alpha**
|
||||
| `version` | Show version.
|
||||
|
||||
!!! Warning "Alpha status"
|
||||
Endpoints labeled with *Alpha status* above may change at any time without notice.
|
||||
|
||||
Possible commands can be listed from the rest-client script using the `help` command.
|
||||
|
||||
@@ -140,6 +150,12 @@ python3 scripts/rest_client.py help
|
||||
``` output
|
||||
Possible commands:
|
||||
|
||||
available_pairs
|
||||
Return available pair (backtest data) based on timeframe / stake_currency selection
|
||||
|
||||
:param timeframe: Only pairs with this timeframe available.
|
||||
:param stake_currency: Only pairs that include this timeframe
|
||||
|
||||
balance
|
||||
Get the account balance.
|
||||
|
||||
@@ -179,9 +195,27 @@ logs
|
||||
|
||||
:param limit: Limits log messages to the last <limit> logs. No limit to get all the trades.
|
||||
|
||||
pair_candles
|
||||
Return live dataframe for <pair><timeframe>.
|
||||
|
||||
:param pair: Pair to get data for
|
||||
:param timeframe: Only pairs with this timeframe available.
|
||||
:param limit: Limit result to the last n candles.
|
||||
|
||||
pair_history
|
||||
Return historic, analyzed dataframe
|
||||
|
||||
:param pair: Pair to get data for
|
||||
:param timeframe: Only pairs with this timeframe available.
|
||||
:param strategy: Strategy to analyze and get values for
|
||||
:param timerange: Timerange to get data for (same format than --timerange endpoints)
|
||||
|
||||
performance
|
||||
Return the performance of the different coins.
|
||||
|
||||
plot_config
|
||||
Return plot configuration if the strategy defines one.
|
||||
|
||||
profit
|
||||
Return the profit summary.
|
||||
|
||||
@@ -204,6 +238,14 @@ stop
|
||||
stopbuy
|
||||
Stop buying (but handle sells gracefully). Use `reload_config` to reset.
|
||||
|
||||
strategies
|
||||
Lists available strategies
|
||||
|
||||
strategy
|
||||
Get strategy details
|
||||
|
||||
:param strategy: Strategy class name
|
||||
|
||||
trades
|
||||
Return trades history.
|
||||
|
||||
@@ -215,7 +257,6 @@ version
|
||||
whitelist
|
||||
Show the current whitelist.
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Advanced API usage using JWT tokens
|
||||
|
@@ -43,52 +43,6 @@ sqlite3
|
||||
.schema <table_name>
|
||||
```
|
||||
|
||||
### Trade table structure
|
||||
|
||||
```sql
|
||||
CREATE TABLE trades(
|
||||
id INTEGER NOT NULL,
|
||||
exchange VARCHAR NOT NULL,
|
||||
pair VARCHAR NOT NULL,
|
||||
is_open BOOLEAN NOT NULL,
|
||||
fee_open FLOAT NOT NULL,
|
||||
fee_open_cost FLOAT,
|
||||
fee_open_currency VARCHAR,
|
||||
fee_close FLOAT NOT NULL,
|
||||
fee_close_cost FLOAT,
|
||||
fee_close_currency VARCHAR,
|
||||
open_rate FLOAT,
|
||||
open_rate_requested FLOAT,
|
||||
open_trade_price FLOAT,
|
||||
close_rate FLOAT,
|
||||
close_rate_requested FLOAT,
|
||||
close_profit FLOAT,
|
||||
close_profit_abs FLOAT,
|
||||
stake_amount FLOAT NOT NULL,
|
||||
amount FLOAT,
|
||||
open_date DATETIME NOT NULL,
|
||||
close_date DATETIME,
|
||||
open_order_id VARCHAR,
|
||||
stop_loss FLOAT,
|
||||
stop_loss_pct FLOAT,
|
||||
initial_stop_loss FLOAT,
|
||||
initial_stop_loss_pct FLOAT,
|
||||
stoploss_order_id VARCHAR,
|
||||
stoploss_last_update DATETIME,
|
||||
max_rate FLOAT,
|
||||
min_rate FLOAT,
|
||||
sell_reason VARCHAR,
|
||||
strategy VARCHAR,
|
||||
timeframe INTEGER,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (is_open IN (0, 1))
|
||||
);
|
||||
CREATE INDEX ix_trades_stoploss_order_id ON trades (stoploss_order_id);
|
||||
CREATE INDEX ix_trades_pair ON trades (pair);
|
||||
CREATE INDEX ix_trades_is_open ON trades (is_open);
|
||||
|
||||
```
|
||||
|
||||
## Get all trades in the table
|
||||
|
||||
```sql
|
||||
@@ -98,11 +52,11 @@ SELECT * FROM trades;
|
||||
## Fix trade still open after a manual sell on the exchange
|
||||
|
||||
!!! Warning
|
||||
Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell <tradeid> should be used to accomplish the same thing.
|
||||
It is strongly advised to backup your database file before making any manual changes.
|
||||
Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell <tradeid> should be used to accomplish the same thing.
|
||||
It is strongly advised to backup your database file before making any manual changes.
|
||||
|
||||
!!! Note
|
||||
This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration.
|
||||
This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration.
|
||||
|
||||
```sql
|
||||
UPDATE trades
|
||||
@@ -128,23 +82,12 @@ SET is_open=0,
|
||||
WHERE id=31;
|
||||
```
|
||||
|
||||
## Manually insert a new trade
|
||||
|
||||
```sql
|
||||
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
|
||||
VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
|
||||
```
|
||||
|
||||
### Insert trade example
|
||||
|
||||
```sql
|
||||
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
|
||||
VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2020-06-28 12:44:24.000000')
|
||||
```
|
||||
|
||||
## Remove trade from the database
|
||||
|
||||
Maybe you'd like to remove a trade from the database, because something went wrong.
|
||||
!!! Tip "Use RPC Methods to delete trades"
|
||||
Consider using `/delete <tradeid>` via telegram or rest API. That's the recommended way to deleting trades.
|
||||
|
||||
If you'd still like to remove a trade from the database directly, you can use the below query.
|
||||
|
||||
```sql
|
||||
DELETE FROM trades WHERE id = <tradeid>;
|
||||
|
@@ -23,11 +23,12 @@ These modes can be configured with these values:
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now.
|
||||
<ins>Do not set too low stoploss value if using stop loss on exchange!</ins>
|
||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now.
|
||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
||||
|
||||
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
|
||||
|
||||
Enable or Disable stop loss on exchange.
|
||||
If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled.
|
||||
|
||||
@@ -35,18 +36,23 @@ If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the st
|
||||
`stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this.
|
||||
If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type.
|
||||
|
||||
Calculation example: we bought the asset at 100$.
|
||||
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$.
|
||||
Calculation example: we bought the asset at 100\$.
|
||||
Stop-price is 95\$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$.
|
||||
|
||||
For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order.
|
||||
|
||||
!!! Note
|
||||
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
|
||||
|
||||
### stoploss_on_exchange_interval
|
||||
|
||||
In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary.
|
||||
The bot cannot do these every 5 seconds (at each iteration), otherwise it would get banned by the exchange.
|
||||
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
|
||||
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
||||
|
||||
### emergencysell
|
||||
|
||||
`emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.
|
||||
The below is the default which is used if not changed in strategy or configuration file.
|
||||
|
||||
@@ -84,6 +90,7 @@ Example of stop loss:
|
||||
```
|
||||
|
||||
For example, simplified math:
|
||||
|
||||
* the bot buys an asset at a price of 100$
|
||||
* the stop loss is defined at -10%
|
||||
* the stop loss would get triggered once the asset drops below 90$
|
||||
@@ -107,7 +114,7 @@ For example, simplified math:
|
||||
* the stop loss would get triggered once the asset drops below 90$
|
||||
* assuming the asset now increases to 102$
|
||||
* the stop loss will now be -10% of 102$ = 91.8$
|
||||
* now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91.8$.
|
||||
* now the asset drops in value to 101\$, the stop loss will still be 91.8$ and would trigger at 91.8$.
|
||||
|
||||
In summary: The stoploss will be adjusted to be always be -10% of the highest observed price.
|
||||
|
||||
@@ -133,8 +140,8 @@ For example, simplified math:
|
||||
* the stop loss is defined at -10%
|
||||
* the stop loss would get triggered once the asset drops below 90$
|
||||
* assuming the asset now increases to 102$
|
||||
* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increasements with -2%)
|
||||
* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$
|
||||
* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increments with -2%)
|
||||
* now the asset drops in value to 101\$, the stop loss will still be 99.96$ and would trigger at 99.96$
|
||||
|
||||
The 0.02 would translate to a -2% stop loss.
|
||||
Before this, `stoploss` is used for the trailing stoploss.
|
||||
@@ -151,7 +158,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai
|
||||
trailing_only_offset_is_reached = True
|
||||
```
|
||||
|
||||
Configuration (offset is buyprice + 3%):
|
||||
Configuration (offset is buy-price + 3%):
|
||||
|
||||
``` python
|
||||
stoploss = -0.10
|
||||
@@ -169,7 +176,7 @@ For example, simplified math:
|
||||
* stoploss will remain at 90$ unless asset increases to or above our configured offset
|
||||
* assuming the asset now increases to 103$ (where we have the offset configured)
|
||||
* the stop loss will now be -2% of 103$ = 100.94$
|
||||
* now the asset drops in value to 101$, the stop loss will still be 100.94$ and would trigger at 100.94$
|
||||
* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$
|
||||
|
||||
!!! Tip
|
||||
Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade.
|
||||
|
@@ -312,12 +312,17 @@ The name of the variable can be chosen at will, but should be prefixed with `cus
|
||||
class Awesomestrategy(IStrategy):
|
||||
# Create custom dictionary
|
||||
cust_info = {}
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# Check if the entry already exists
|
||||
if not metadata["pair"] in self._cust_info:
|
||||
# Create empty entry for this pair
|
||||
self._cust_info[metadata["pair"]] = {}
|
||||
|
||||
if "crosstime" in self.cust_info[metadata["pair"]:
|
||||
self.cust_info[metadata["pair"]["crosstime"] += 1
|
||||
self.cust_info[metadata["pair"]]["crosstime"] += 1
|
||||
else:
|
||||
self.cust_info[metadata["pair"]["crosstime"] = 1
|
||||
self.cust_info[metadata["pair"]]["crosstime"] = 1
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
@@ -688,18 +693,18 @@ Locked pairs will show the message `Pair <pair> is currently locked.`.
|
||||
|
||||
Sometimes it may be desired to lock a pair after certain events happen (e.g. multiple losing trades in a row).
|
||||
|
||||
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until)`.
|
||||
`until` must be a datetime object in the future, after which trading will be reenabled for that pair.
|
||||
Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`.
|
||||
`until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked.
|
||||
|
||||
Locks can also be lifted manually, by calling `self.unlock_pair(pair)`.
|
||||
|
||||
To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
|
||||
|
||||
!!! Note
|
||||
Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs.
|
||||
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
|
||||
|
||||
!!! Warning
|
||||
Locking pairs is not functioning during backtesting.
|
||||
Locking pairs is not available during backtesting.
|
||||
|
||||
#### Pair locking example
|
||||
|
||||
@@ -765,8 +770,6 @@ To get additional Ideas for strategies, head over to our [strategy repository](h
|
||||
Feel free to use any of them as inspiration for your own strategies.
|
||||
We're happy to accept Pull Requests containing new Strategies to that repo.
|
||||
|
||||
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) which is a great place to get and/or share ideas.
|
||||
|
||||
## Next step
|
||||
|
||||
Now you have a perfect strategy you probably want to backtest it.
|
||||
|
@@ -35,12 +35,30 @@ Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it fo
|
||||
|
||||
Don't forget to start the conversation with your bot, by clicking `/START` button
|
||||
|
||||
### 2. Get your user id
|
||||
### 2. Telegram user_id
|
||||
|
||||
#### Get your user id
|
||||
|
||||
Talk to the [userinfobot](https://telegram.me/userinfobot)
|
||||
|
||||
Get your "Id", you will use it for the config parameter `chat_id`.
|
||||
|
||||
#### Use Group id
|
||||
|
||||
You can use bots in telegram groups by just adding them to the group. You can find the group id by first adding a [RawDataBot](https://telegram.me/rawdatabot) to your group. The Group id is shown as id in the `"chat"` section, which the RawDataBot will send to you:
|
||||
|
||||
``` json
|
||||
"chat":{
|
||||
"id":-1001332619709
|
||||
}
|
||||
```
|
||||
|
||||
For the Freqtrade configuration, you can then use the the full value (including `-` if it's there) as string:
|
||||
|
||||
```json
|
||||
"chat_id": "-1001332619709"
|
||||
```
|
||||
|
||||
## Control telegram noise
|
||||
|
||||
Freqtrade provides means to control the verbosity of your telegram bot.
|
||||
|
@@ -423,7 +423,7 @@ freqtrade test-pairlist --config config.json --quote USDT BTC
|
||||
|
||||
## List Hyperopt results
|
||||
|
||||
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` subcommand.
|
||||
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
||||
|
||||
```
|
||||
usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
@@ -432,10 +432,11 @@ usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||
[--max-trades INT] [--min-avg-time FLOAT]
|
||||
[--max-avg-time FLOAT] [--min-avg-profit FLOAT]
|
||||
[--max-avg-profit FLOAT]
|
||||
[--min-total-profit FLOAT] [--max-total-profit FLOAT]
|
||||
[--min-total-profit FLOAT]
|
||||
[--max-total-profit FLOAT]
|
||||
[--min-objective FLOAT] [--max-objective FLOAT]
|
||||
[--no-color] [--print-json] [--no-details]
|
||||
[--export-csv FILE]
|
||||
[--hyperopt-filename PATH] [--export-csv FILE]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@@ -443,24 +444,27 @@ optional arguments:
|
||||
--profitable Select only profitable epochs.
|
||||
--min-trades INT Select epochs with more than INT trades.
|
||||
--max-trades INT Select epochs with less than INT trades.
|
||||
--min-avg-time FLOAT Select epochs on above average time.
|
||||
--max-avg-time FLOAT Select epochs on under average time.
|
||||
--min-avg-time FLOAT Select epochs above average time.
|
||||
--max-avg-time FLOAT Select epochs below average time.
|
||||
--min-avg-profit FLOAT
|
||||
Select epochs on above average profit.
|
||||
Select epochs above average profit.
|
||||
--max-avg-profit FLOAT
|
||||
Select epochs on below average profit.
|
||||
Select epochs below average profit.
|
||||
--min-total-profit FLOAT
|
||||
Select epochs on above total profit.
|
||||
Select epochs above total profit.
|
||||
--max-total-profit FLOAT
|
||||
Select epochs on below total profit.
|
||||
Select epochs below total profit.
|
||||
--min-objective FLOAT
|
||||
Select epochs on above objective (- is added by default).
|
||||
Select epochs above objective.
|
||||
--max-objective FLOAT
|
||||
Select epochs on below objective (- is added by default).
|
||||
Select epochs below objective.
|
||||
--no-color Disable colorization of hyperopt results. May be
|
||||
useful if you are redirecting output to a file.
|
||||
--print-json Print best result detailization in JSON format.
|
||||
--print-json Print output in JSON format.
|
||||
--no-details Do not print best epoch details.
|
||||
--hyperopt-filename FILENAME
|
||||
Hyperopt result filename.Example: `--hyperopt-
|
||||
filename=hyperopt_results_2020-09-27_16-20-48.pickle`
|
||||
--export-csv FILE Export to CSV-File. This will disable table print.
|
||||
Example: --export-csv hyperopt.csv
|
||||
|
||||
@@ -481,6 +485,10 @@ Common arguments:
|
||||
Path to userdata directory.
|
||||
```
|
||||
|
||||
!!! Note
|
||||
`hyperopt-list` will automatically use the latest available hyperopt results file.
|
||||
You can override this using the `--hyperopt-filename` argument, and specify another, available filename (without path!).
|
||||
|
||||
### Examples
|
||||
|
||||
List all results, print details of the best result at the end:
|
||||
@@ -501,17 +509,41 @@ 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]
|
||||
[--no-header]
|
||||
[--hyperopt-filename PATH] [--no-header]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--best Select only best epochs.
|
||||
--profitable Select only profitable epochs.
|
||||
-n INT, --index INT Specify the index of the epoch to print details for.
|
||||
--print-json Print best result detailization in JSON format.
|
||||
--print-json Print output in JSON format.
|
||||
--hyperopt-filename FILENAME
|
||||
Hyperopt result filename.Example: `--hyperopt-
|
||||
filename=hyperopt_results_2020-09-27_16-20-48.pickle`
|
||||
--no-header Do not print epoch details header.
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
|
||||
!!! Note
|
||||
`hyperopt-show` will automatically use the latest available hyperopt results file.
|
||||
You can override this using the `--hyperopt-filename` argument, and specify another, available filename (without path!).
|
||||
|
||||
### Examples
|
||||
|
||||
Print details for the epoch 168 (the number of the epoch is shown by the `hyperopt-list` subcommand or by Hyperopt itself during hyperoptimization run):
|
||||
|
@@ -21,7 +21,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 precompiled 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.18‑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 precompiled 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.19‑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.
|
||||
@@ -32,7 +32,7 @@ python -m venv .env
|
||||
.env\Scripts\activate.ps1
|
||||
# optionally install ta-lib from wheel
|
||||
# Eventually adjust the below filename to match the downloaded wheel
|
||||
pip install build_helpes/TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl
|
||||
pip install build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
freqtrade
|
||||
@@ -50,8 +50,8 @@ freqtrade
|
||||
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||
```
|
||||
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||
|
||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first.
|
||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first.
|
||||
|
||||
---
|
||||
|
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2020.9'
|
||||
__version__ = '2020.11'
|
||||
|
||||
if __version__ == 'develop':
|
||||
|
||||
|
@@ -8,5 +8,6 @@ To launch Freqtrade as a module
|
||||
|
||||
from freqtrade import main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main.main()
|
||||
|
@@ -8,23 +8,15 @@ Note: Be careful with file-scoped imports in these subfiles.
|
||||
"""
|
||||
from freqtrade.commands.arguments import Arguments
|
||||
from freqtrade.commands.build_config_commands import start_new_config
|
||||
from freqtrade.commands.data_commands import (start_convert_data,
|
||||
start_download_data,
|
||||
from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
|
||||
start_list_data)
|
||||
from freqtrade.commands.deploy_commands import (start_create_userdir,
|
||||
start_new_hyperopt,
|
||||
from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt,
|
||||
start_new_strategy)
|
||||
from freqtrade.commands.hyperopt_commands import (start_hyperopt_list,
|
||||
start_hyperopt_show)
|
||||
from freqtrade.commands.list_commands import (start_list_exchanges,
|
||||
start_list_hyperopts,
|
||||
start_list_markets,
|
||||
start_list_strategies,
|
||||
start_list_timeframes,
|
||||
start_show_trades)
|
||||
from freqtrade.commands.optimize_commands import (start_backtesting,
|
||||
start_edge, start_hyperopt)
|
||||
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
|
||||
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_show_trades)
|
||||
from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt
|
||||
from freqtrade.commands.pairlist_commands import start_test_pairlist
|
||||
from freqtrade.commands.plot_commands import (start_plot_dataframe,
|
||||
start_plot_profit)
|
||||
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.commands.trade_commands import start_trading
|
||||
|
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional
|
||||
from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS
|
||||
from freqtrade.constants import DEFAULT_CONFIG
|
||||
|
||||
|
||||
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
|
||||
|
||||
ARGS_STRATEGY = ["strategy", "strategy_path"]
|
||||
@@ -26,7 +27,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||
"use_max_market_positions", "print_all",
|
||||
"print_colorized", "print_json", "hyperopt_jobs",
|
||||
"hyperopt_random_state", "hyperopt_min_trades",
|
||||
"hyperopt_continue", "hyperopt_loss"]
|
||||
"hyperopt_loss"]
|
||||
|
||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||
|
||||
@@ -75,10 +76,10 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
||||
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit",
|
||||
"hyperopt_list_min_objective", "hyperopt_list_max_objective",
|
||||
"print_colorized", "print_json", "hyperopt_list_no_details",
|
||||
"export_csv"]
|
||||
"hyperoptexportfilename", "export_csv"]
|
||||
|
||||
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
|
||||
"print_json", "hyperopt_show_no_header"]
|
||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header"]
|
||||
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||
@@ -161,16 +162,14 @@ class Arguments:
|
||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||
self._build_args(optionlist=['version'], parser=self.parser)
|
||||
|
||||
from freqtrade.commands import (start_create_userdir, start_convert_data,
|
||||
start_download_data, start_list_data,
|
||||
start_hyperopt_list, start_hyperopt_show,
|
||||
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
|
||||
start_download_data, start_edge, start_hyperopt,
|
||||
start_hyperopt_list, start_hyperopt_show, start_list_data,
|
||||
start_list_exchanges, start_list_hyperopts,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_new_config,
|
||||
start_new_hyperopt, start_new_strategy,
|
||||
start_plot_dataframe, start_plot_profit, start_show_trades,
|
||||
start_backtesting, start_hyperopt, start_edge,
|
||||
start_test_pairlist, start_trading)
|
||||
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)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
# Use custom message when no subhandler is added
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from questionary import Separator, prompt
|
||||
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.exchange import available_exchanges, MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.misc import render_template
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
|
||||
from freqtrade.misc import render_template
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -46,7 +48,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
Interactive questions built using https://github.com/tmbo/questionary
|
||||
:returns: Dict with keys to put into template
|
||||
"""
|
||||
questions = [
|
||||
questions: List[Dict[str, Any]] = [
|
||||
{
|
||||
"type": "confirm",
|
||||
"name": "dry_run",
|
||||
|
@@ -4,6 +4,7 @@ Definition of cli arguments used in arguments.py
|
||||
from argparse import ArgumentTypeError
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
|
||||
|
||||
|
||||
def check_int_positive(value: str) -> int:
|
||||
@@ -252,23 +253,19 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
metavar='INT',
|
||||
default=1,
|
||||
),
|
||||
"hyperopt_continue": Arg(
|
||||
"--continue",
|
||||
help="Continue hyperopt from previous runs. "
|
||||
"By default, temporary files will be removed and hyperopt will start from scratch.",
|
||||
default=False,
|
||||
action='store_true',
|
||||
),
|
||||
"hyperopt_loss": Arg(
|
||||
'--hyperopt-loss',
|
||||
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
||||
'Different functions can generate completely different results, '
|
||||
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
|
||||
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, '
|
||||
'SortinoHyperOptLoss, SortinoHyperOptLossDaily.'
|
||||
'(default: `%(default)s`).',
|
||||
f'{", ".join(HYPEROPT_LOSS_BUILTIN)}',
|
||||
metavar='NAME',
|
||||
default=constants.DEFAULT_HYPEROPT_LOSS,
|
||||
),
|
||||
"hyperoptexportfilename": Arg(
|
||||
'--hyperopt-filename',
|
||||
help='Hyperopt result filename.'
|
||||
'Example: `--hyperopt-filename=hyperopt_results_2020-09-27_16-20-48.pickle`',
|
||||
metavar='FILENAME',
|
||||
),
|
||||
# List exchanges
|
||||
"print_one_column": Arg(
|
||||
@@ -357,13 +354,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
'--data-format-ohlcv',
|
||||
help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
default='json'
|
||||
),
|
||||
"dataformat_trades": Arg(
|
||||
'--data-format-trades',
|
||||
help='Storage format for downloaded trades data. (default: `%(default)s`).',
|
||||
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||
default='jsongz'
|
||||
),
|
||||
"exchange": Arg(
|
||||
'--exchange',
|
||||
|
@@ -1,21 +1,19 @@
|
||||
import logging
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import arrow
|
||||
|
||||
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,
|
||||
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.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -30,12 +28,15 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
"You can only specify one or the other.")
|
||||
timerange = TimeRange()
|
||||
if 'days' in config:
|
||||
time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d")
|
||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||
timerange = TimeRange.parse_timerange(f'{time_since}-')
|
||||
|
||||
if 'timerange' in config:
|
||||
timerange = timerange.parse_timerange(config['timerange'])
|
||||
|
||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||
config['stake_currency'] = ''
|
||||
|
||||
if 'pairs' not in config:
|
||||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
@@ -105,8 +106,9 @@ def start_list_data(args: Dict[str, Any]) -> None:
|
||||
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
from freqtrade.data.history.idatahandler import get_datahandler
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.data.history.idatahandler import get_datahandler
|
||||
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
|
||||
|
||||
paircombs = dhc.ohlcv_get_available_data(config['datadir'])
|
||||
|
@@ -4,13 +4,13 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.configuration.directory_operations import (copy_sample_files,
|
||||
create_userdata_dir)
|
||||
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import render_template, render_template_with_fallback
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None:
|
||||
|
||||
if new_path.exists():
|
||||
raise OperationalException(f"`{new_path}` already exists. "
|
||||
"Please choose another Strategy Name.")
|
||||
"Please choose another Hyperopt Name.")
|
||||
deploy_new_hyperopt(args['hyperopt'], new_path, args['template'])
|
||||
else:
|
||||
raise OperationalException("`new-hyperopt` requires --hyperopt to be set.")
|
||||
|
@@ -5,9 +5,11 @@ from typing import Any, Dict, List
|
||||
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.exceptions import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -40,8 +42,9 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
||||
}
|
||||
|
||||
results_file = (config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||
results_file = get_latest_hyperopt_file(
|
||||
config['user_data_dir'] / 'hyperopt_results',
|
||||
config.get('hyperoptexportfilename'))
|
||||
|
||||
# Previous evaluations
|
||||
epochs = Hyperopt.load_previous_results(results_file)
|
||||
@@ -80,8 +83,10 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
||||
|
||||
print_json = config.get('print_json', False)
|
||||
no_header = config.get('hyperopt_show_no_header', False)
|
||||
results_file = (config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||
results_file = get_latest_hyperopt_file(
|
||||
config['user_data_dir'] / 'hyperopt_results',
|
||||
config.get('hyperoptexportfilename'))
|
||||
|
||||
n = config.get('hyperopt_show_index', -1)
|
||||
|
||||
filteroptions = {
|
||||
|
@@ -5,20 +5,20 @@ from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from colorama import init as colorama_init
|
||||
from colorama import Fore, Style
|
||||
import rapidjson
|
||||
from colorama import Fore, Style
|
||||
from colorama import init as colorama_init
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import (available_exchanges, ccxt_exchanges,
|
||||
market_is_active)
|
||||
from freqtrade.exchange import available_exchanges, ccxt_exchanges, market_is_active
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -203,15 +203,16 @@ def start_show_trades(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Show trades
|
||||
"""
|
||||
from freqtrade.persistence import init, Trade
|
||||
import json
|
||||
|
||||
from freqtrade.persistence import Trade, init_db
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||
|
||||
if 'db_url' not in config:
|
||||
raise OperationalException("--db-url is required for this command.")
|
||||
|
||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||
init(config['db_url'], clean_open_orders=False)
|
||||
init_db(config['db_url'], clean_open_orders=False)
|
||||
tfilter = []
|
||||
|
||||
if config.get('trade_ids'):
|
||||
|
@@ -6,6 +6,7 @@ from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -58,6 +59,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||
# Import here to avoid loading hyperopt module when it's not used
|
||||
try:
|
||||
from filelock import FileLock, Timeout
|
||||
|
||||
from freqtrade.optimize.hyperopt import Hyperopt
|
||||
except ImportError as e:
|
||||
raise OperationalException(
|
||||
@@ -98,6 +100,7 @@ def start_edge(args: Dict[str, Any]) -> None:
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize.edge_cli import EdgeCli
|
||||
|
||||
# Initialize configuration
|
||||
config = setup_optimize_configuration(args, RunMode.EDGE)
|
||||
logger.info('Starting freqtrade in Edge mode')
|
||||
|
@@ -7,6 +7,7 @@ from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.check_exchange import check_exchange, remove_credentials
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
from freqtrade.configuration.configuration import Configuration
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.configuration.configuration import Configuration
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
|
@@ -2,11 +2,11 @@ import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason,
|
||||
is_exchange_bad, is_exchange_known_ccxt,
|
||||
is_exchange_officially_supported)
|
||||
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, is_exchange_bad,
|
||||
is_exchange_known_ccxt, is_exchange_officially_supported)
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
from .check_exchange import remove_credentials
|
||||
from .config_validation import validate_config_consistency
|
||||
from .configuration import Configuration
|
||||
from .check_exchange import remove_credentials
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -9,6 +9,7 @@ from freqtrade import constants
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -136,6 +137,10 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
|
||||
"Edge and VolumePairList are incompatible, "
|
||||
"Edge will override whatever pairs VolumePairlist selects."
|
||||
)
|
||||
if not conf.get('ask_strategy', {}).get('use_sell_signal', True):
|
||||
raise OperationalException(
|
||||
"Edge requires `use_sell_signal` to be True, otherwise no sells will happen."
|
||||
)
|
||||
|
||||
|
||||
def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
||||
|
@@ -10,14 +10,14 @@ from typing import Any, Callable, Dict, List, Optional
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration.check_exchange import check_exchange
|
||||
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
|
||||
from freqtrade.configuration.directory_operations import (create_datadir,
|
||||
create_userdata_dir)
|
||||
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
|
||||
from freqtrade.configuration.load_config import load_config_file
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.loggers import setup_logging
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -263,6 +263,9 @@ class Configuration:
|
||||
self._args_to_config(config, argname='hyperopt_path',
|
||||
logstring='Using additional Hyperopt lookup path: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperoptexportfilename',
|
||||
logstring='Using hyperopt file: {}')
|
||||
|
||||
self._args_to_config(config, argname='epochs',
|
||||
logstring='Parameter --epochs detected ... '
|
||||
'Will run Hyperopt with for {} epochs ...'
|
||||
@@ -295,9 +298,6 @@ class Configuration:
|
||||
self._args_to_config(config, argname='hyperopt_min_trades',
|
||||
logstring='Parameter --min-trades detected: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_continue',
|
||||
logstring='Hyperopt continue: {}')
|
||||
|
||||
self._args_to_config(config, argname='hyperopt_loss',
|
||||
logstring='Using Hyperopt loss class name: {}')
|
||||
|
||||
|
@@ -3,8 +3,9 @@ import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.constants import USER_DATA_FILES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import rapidjson
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -52,11 +52,11 @@ class TimeRange:
|
||||
:return: None (Modifies the object in place)
|
||||
"""
|
||||
if (not self.starttype or (startup_candles
|
||||
and min_date.timestamp >= self.startts)):
|
||||
and min_date.int_timestamp >= self.startts)):
|
||||
# If no startts was defined, or backtest-data starts at the defined backtest-date
|
||||
logger.warning("Moving start-date by %s candles to account for startup time.",
|
||||
startup_candles)
|
||||
self.startts = (min_date.timestamp + timeframe_secs * startup_candles)
|
||||
self.startts = (min_date.int_timestamp + timeframe_secs * startup_candles)
|
||||
self.starttype = 'date'
|
||||
|
||||
@staticmethod
|
||||
@@ -89,7 +89,7 @@ class TimeRange:
|
||||
if stype[0]:
|
||||
starts = rvals[index]
|
||||
if stype[0] == 'date' and len(starts) == 8:
|
||||
start = arrow.get(starts, 'YYYYMMDD').timestamp
|
||||
start = arrow.get(starts, 'YYYYMMDD').int_timestamp
|
||||
elif len(starts) == 13:
|
||||
start = int(starts) // 1000
|
||||
else:
|
||||
@@ -98,7 +98,7 @@ class TimeRange:
|
||||
if stype[1]:
|
||||
stops = rvals[index]
|
||||
if stype[1] == 'date' and len(stops) == 8:
|
||||
stop = arrow.get(stops, 'YYYYMMDD').timestamp
|
||||
stop = arrow.get(stops, 'YYYYMMDD').int_timestamp
|
||||
elif len(stops) == 13:
|
||||
stop = int(stops) // 1000
|
||||
else:
|
||||
|
@@ -11,7 +11,6 @@ DEFAULT_EXCHANGE = 'bittrex'
|
||||
PROCESS_THROTTLE_SECS = 5 # sec
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
|
||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
|
||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||
@@ -21,9 +20,12 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||
ORDERBOOK_SIDES = ['ask', 'bid']
|
||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
|
||||
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
|
||||
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'AgeFilter', 'PrecisionFilter', 'PriceFilter',
|
||||
'ShuffleFilter', 'SpreadFilter']
|
||||
'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter']
|
||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
|
||||
DRY_RUN_WALLET = 1000
|
||||
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
@@ -363,3 +365,6 @@ CANCEL_REASON = {
|
||||
# List of pairs with their timeframes
|
||||
PairWithTimeframe = Tuple[str, str]
|
||||
ListPairsWithTimeframes = List[PairWithTimeframe]
|
||||
|
||||
# Type for trades list
|
||||
TradeList = List[List]
|
||||
|
@@ -2,17 +2,17 @@
|
||||
Helpers when analyzing backtest data
|
||||
"""
|
||||
import logging
|
||||
from datetime import timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, Union, Tuple, Any, Optional
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from datetime import timezone
|
||||
|
||||
from freqtrade import persistence
|
||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||
from freqtrade.misc import json_load
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.persistence import Trade, init_db
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,10 +21,11 @@ BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index",
|
||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||
|
||||
|
||||
def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
|
||||
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
|
||||
"""
|
||||
Get latest backtest export based on '.last_result.json'.
|
||||
:param directory: Directory to search for last result
|
||||
:param variant: 'backtest' or 'hyperopt' - the method to return
|
||||
:return: string containing the filename of the latest backtest result
|
||||
:raises: ValueError in the following cases:
|
||||
* Directory does not exist
|
||||
@@ -44,10 +45,57 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
|
||||
with filename.open() as file:
|
||||
data = json_load(file)
|
||||
|
||||
if 'latest_backtest' not in data:
|
||||
if f'latest_{variant}' not in data:
|
||||
raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.")
|
||||
|
||||
return data['latest_backtest']
|
||||
return data[f'latest_{variant}']
|
||||
|
||||
|
||||
def get_latest_backtest_filename(directory: Union[Path, str]) -> str:
|
||||
"""
|
||||
Get latest backtest export based on '.last_result.json'.
|
||||
:param directory: Directory to search for last result
|
||||
:return: string containing the filename of the latest backtest result
|
||||
:raises: ValueError in the following cases:
|
||||
* Directory does not exist
|
||||
* `directory/.last_result.json` does not exist
|
||||
* `directory/.last_result.json` has the wrong content
|
||||
"""
|
||||
return get_latest_optimize_filename(directory, 'backtest')
|
||||
|
||||
|
||||
def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
|
||||
"""
|
||||
Get latest hyperopt export based on '.last_result.json'.
|
||||
:param directory: Directory to search for last result
|
||||
:return: string containing the filename of the latest hyperopt result
|
||||
:raises: ValueError in the following cases:
|
||||
* Directory does not exist
|
||||
* `directory/.last_result.json` does not exist
|
||||
* `directory/.last_result.json` has the wrong content
|
||||
"""
|
||||
try:
|
||||
return get_latest_optimize_filename(directory, 'hyperopt')
|
||||
except ValueError:
|
||||
# Return default (legacy) pickle filename
|
||||
return 'hyperopt_results.pickle'
|
||||
|
||||
|
||||
def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = None) -> Path:
|
||||
"""
|
||||
Get latest hyperopt export based on '.last_result.json'.
|
||||
:param directory: Directory to search for last result
|
||||
:return: string containing the filename of the latest hyperopt result
|
||||
:raises: ValueError in the following cases:
|
||||
* Directory does not exist
|
||||
* `directory/.last_result.json` does not exist
|
||||
* `directory/.last_result.json` has the wrong content
|
||||
"""
|
||||
if isinstance(directory, str):
|
||||
directory = Path(directory)
|
||||
if predef_filename:
|
||||
return directory / predef_filename
|
||||
return directory / get_latest_hyperopt_filename(directory)
|
||||
|
||||
|
||||
def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
@@ -169,7 +217,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
|
||||
Can also serve as protection to load the correct result.
|
||||
:return: Dataframe containing Trades
|
||||
"""
|
||||
persistence.init(db_url, clean_open_orders=False)
|
||||
init_db(db_url, clean_open_orders=False)
|
||||
|
||||
columns = ["pair", "open_date", "close_date", "profit", "profit_percent",
|
||||
"open_rate", "close_rate", "amount", "trade_duration", "sell_reason",
|
||||
|
@@ -10,8 +10,8 @@ from typing import Any, Dict, List
|
||||
import pandas as pd
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS,
|
||||
DEFAULT_TRADES_COLUMNS)
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -168,7 +168,7 @@ def trades_remove_duplicates(trades: List[List]) -> List[List]:
|
||||
return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))]
|
||||
|
||||
|
||||
def trades_dict_to_list(trades: List[Dict]) -> List[List]:
|
||||
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
||||
"""
|
||||
Convert fetch_trades result into a List (to be more memory efficient).
|
||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||
@@ -177,16 +177,18 @@ def trades_dict_to_list(trades: List[Dict]) -> List[List]:
|
||||
return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades]
|
||||
|
||||
|
||||
def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame:
|
||||
def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame:
|
||||
"""
|
||||
Converts trades list to OHLCV list
|
||||
TODO: This should get a dedicated test
|
||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||
:param timeframe: Timeframe to resample data to
|
||||
:return: OHLCV Dataframe.
|
||||
:raises: ValueError if no trades are provided
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||
if not trades:
|
||||
raise ValueError('Trade-list empty.')
|
||||
df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms',
|
||||
utc=True,)
|
||||
|
@@ -8,7 +8,6 @@ import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from arrow import Arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||
@@ -17,6 +16,7 @@ from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class DataProvider:
|
||||
:param timeframe: Timeframe to get data for
|
||||
:param dataframe: analyzed dataframe
|
||||
"""
|
||||
self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime)
|
||||
self.__cached_pairs[(pair, timeframe)] = (dataframe, datetime.now(timezone.utc))
|
||||
|
||||
def add_pairlisthandler(self, pairlists) -> None:
|
||||
"""
|
||||
@@ -87,7 +87,8 @@ class DataProvider:
|
||||
"""
|
||||
return load_pair_history(pair=pair,
|
||||
timeframe=timeframe or self._config['timeframe'],
|
||||
datadir=self._config['datadir']
|
||||
datadir=self._config['datadir'],
|
||||
data_format=self._config.get('dataformat_ohlcv', 'json')
|
||||
)
|
||||
|
||||
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
|
||||
|
@@ -5,10 +5,8 @@ Includes:
|
||||
* load data for a pair (or a list of pairs) from disk
|
||||
* download data from exchange and store to disk
|
||||
"""
|
||||
|
||||
from .history_utils import (convert_trades_to_ohlcv, # noqa: F401
|
||||
get_timerange, load_data, load_pair_history,
|
||||
refresh_backtest_ohlcv_data,
|
||||
refresh_backtest_trades_data, refresh_data,
|
||||
# flake8: noqa: F401
|
||||
from .history_utils import (convert_trades_to_ohlcv, get_timerange, load_data, load_pair_history,
|
||||
refresh_backtest_ohlcv_data, refresh_backtest_trades_data, refresh_data,
|
||||
validate_backtest_data)
|
||||
from .idatahandler import get_datahandler # noqa: F401
|
||||
from .idatahandler import get_datahandler
|
||||
|
@@ -3,15 +3,16 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade import misc
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS,
|
||||
DEFAULT_TRADES_COLUMNS,
|
||||
ListPairsWithTimeframes)
|
||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
|
||||
ListPairsWithTimeframes, TradeList)
|
||||
|
||||
from .idatahandler import IDataHandler
|
||||
|
||||
from .idatahandler import IDataHandler, TradeList
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -175,7 +176,8 @@ class HDF5DataHandler(IDataHandler):
|
||||
if timerange.stoptype == 'date':
|
||||
where.append(f"timestamp < {timerange.stopts * 1e3}")
|
||||
|
||||
trades = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||
return trades.values.tolist()
|
||||
|
||||
def trades_purge(self, pair: str) -> bool:
|
||||
|
@@ -9,15 +9,14 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe,
|
||||
ohlcv_to_dataframe,
|
||||
trades_remove_duplicates,
|
||||
trades_to_ohlcv)
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||
trades_remove_duplicates, trades_to_ohlcv)
|
||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.misc import format_ms_time
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -215,10 +214,9 @@ def _download_pair_history(datadir: Path,
|
||||
data_handler.ohlcv_store(pair, timeframe, data=data)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}. '
|
||||
f'Error: {e}'
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}.'
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -305,10 +303,9 @@ def _download_trades_history(exchange: Exchange,
|
||||
logger.info(f"New Amount of trades: {len(trades)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Failed to download historic trades for pair: "{pair}". '
|
||||
f'Error: {e}'
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -357,9 +354,12 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
|
||||
if erase:
|
||||
if data_handler_ohlcv.ohlcv_purge(pair, timeframe):
|
||||
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
||||
ohlcv = trades_to_ohlcv(trades, timeframe)
|
||||
# Store ohlcv
|
||||
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv)
|
||||
try:
|
||||
ohlcv = trades_to_ohlcv(trades, timeframe)
|
||||
# Store ohlcv
|
||||
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv)
|
||||
except ValueError:
|
||||
logger.exception(f'Could not convert {pair} to OHLCV.')
|
||||
|
||||
|
||||
def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||
|
@@ -13,15 +13,12 @@ from typing import List, Optional, Type
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe,
|
||||
trades_remove_duplicates, trim_dataframe)
|
||||
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
||||
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Type for trades list
|
||||
TradeList = List[List]
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IDataHandler(ABC):
|
||||
|
@@ -8,11 +8,11 @@ from pandas import DataFrame, read_json, to_datetime
|
||||
|
||||
from freqtrade import misc
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS,
|
||||
ListPairsWithTimeframes)
|
||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList
|
||||
from freqtrade.data.converter import trades_dict_to_list
|
||||
|
||||
from .idatahandler import IDataHandler, TradeList
|
||||
from .idatahandler import IDataHandler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -9,11 +9,12 @@ import utils_find_1st as utf1st
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, DATETIME_PRINT_FORMAT
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.data.history import get_timerange, load_data, refresh_data
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.strategy.interface import SellType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -86,7 +87,7 @@ class Edge:
|
||||
heartbeat = self.edge_config.get('process_throttle_secs')
|
||||
|
||||
if (self._last_updated > 0) and (
|
||||
self._last_updated + heartbeat > arrow.utcnow().timestamp):
|
||||
self._last_updated + heartbeat > arrow.utcnow().int_timestamp):
|
||||
return False
|
||||
|
||||
data: Dict[str, Any] = {}
|
||||
@@ -145,7 +146,7 @@ class Edge:
|
||||
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||
self._cached_pairs = self._process_expectancy(trades_df)
|
||||
self._last_updated = arrow.utcnow().timestamp
|
||||
self._last_updated = arrow.utcnow().int_timestamp
|
||||
|
||||
return True
|
||||
|
||||
@@ -309,8 +310,10 @@ class Edge:
|
||||
|
||||
# Calculating number of losing trades, average win and average loss
|
||||
df['nb_loss_trades'] = df['nb_trades'] - df['nb_win_trades']
|
||||
df['average_win'] = df['profit_sum'] / df['nb_win_trades']
|
||||
df['average_loss'] = df['loss_sum'] / df['nb_loss_trades']
|
||||
df['average_win'] = np.where(df['nb_win_trades'] == 0, 0.0,
|
||||
df['profit_sum'] / df['nb_win_trades'])
|
||||
df['average_loss'] = np.where(df['nb_loss_trades'] == 0, 0.0,
|
||||
df['loss_sum'] / df['nb_loss_trades'])
|
||||
|
||||
# Win rate = number of profitable trades / number of trades
|
||||
df['winrate'] = df['nb_win_trades'] / df['nb_trades']
|
||||
|
@@ -1,19 +1,16 @@
|
||||
# flake8: noqa: F401
|
||||
# isort: off
|
||||
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS
|
||||
from freqtrade.exchange.exchange import Exchange
|
||||
from freqtrade.exchange.exchange import (get_exchange_bad_reason,
|
||||
is_exchange_bad,
|
||||
is_exchange_known_ccxt,
|
||||
is_exchange_officially_supported,
|
||||
ccxt_exchanges,
|
||||
available_exchanges)
|
||||
from freqtrade.exchange.exchange import (timeframe_to_seconds,
|
||||
timeframe_to_minutes,
|
||||
timeframe_to_msecs,
|
||||
timeframe_to_next_date,
|
||||
timeframe_to_prev_date)
|
||||
from freqtrade.exchange.exchange import (market_is_active)
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.binance import Binance
|
||||
# isort: on
|
||||
from freqtrade.exchange.bibox import Bibox
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
||||
get_exchange_bad_reason, is_exchange_bad,
|
||||
is_exchange_known_ccxt, is_exchange_officially_supported,
|
||||
market_is_active, timeframe_to_minutes, timeframe_to_msecs,
|
||||
timeframe_to_next_date, timeframe_to_prev_date,
|
||||
timeframe_to_seconds)
|
||||
from freqtrade.exchange.ftx import Ftx
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
|
@@ -4,6 +4,7 @@ from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -4,12 +4,12 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -20,20 +20,9 @@ class Binance(Exchange):
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||
}
|
||||
|
||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
"""
|
||||
get order book level 2 from exchange
|
||||
|
||||
20180619: binance support limits but only on specific range
|
||||
"""
|
||||
limit_range = [5, 10, 20, 50, 100, 500, 1000]
|
||||
# get next-higher step in the limit_range list
|
||||
limit = min(list(filter(lambda x: limit <= x, limit_range)))
|
||||
|
||||
return super().fetch_l2_order_book(pair, limit)
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
|
23
freqtrade/exchange/bittrex.py
Normal file
23
freqtrade/exchange/bittrex.py
Normal file
@@ -0,0 +1,23 @@
|
||||
""" Bittrex exchange subclass """
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Bittrex(Exchange):
|
||||
"""
|
||||
Bittrex exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
may still not work as expected.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"l2_limit_range": [1, 25, 500],
|
||||
}
|
@@ -3,8 +3,8 @@ import logging
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, RetryableOrderError,
|
||||
TemporaryError)
|
||||
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -13,20 +13,20 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
import arrow
|
||||
import ccxt
|
||||
import ccxt.async_support as ccxt_async
|
||||
from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
|
||||
TRUNCATE, decimal_to_precision)
|
||||
from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE,
|
||||
decimal_to_precision)
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException,
|
||||
RetryableOrderError, TemporaryError)
|
||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT,
|
||||
BAD_EXCHANGES, retrier, retrier_async)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException, RetryableOrderError,
|
||||
TemporaryError)
|
||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier,
|
||||
retrier_async)
|
||||
from freqtrade.misc import deep_merge_dicts, safe_value_fallback2
|
||||
|
||||
|
||||
CcxtModuleType = Any
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class Exchange:
|
||||
"ohlcv_partial_candle": True,
|
||||
"trades_pagination": "time", # Possible are "time" or "id"
|
||||
"trades_pagination_arg": "since",
|
||||
|
||||
"l2_limit_range": None,
|
||||
}
|
||||
_ft_has: Dict = {}
|
||||
|
||||
@@ -124,7 +124,8 @@ class Exchange:
|
||||
|
||||
# Check if all pairs are available
|
||||
self.validate_stakecurrency(config['stake_currency'])
|
||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||
if not exchange_config.get('skip_pair_validation'):
|
||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||
self.validate_ordertypes(config.get('order_types', {}))
|
||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||
self.validate_required_startup_candles(config.get('startup_candle_count', 0))
|
||||
@@ -282,7 +283,7 @@ class Exchange:
|
||||
asyncio.get_event_loop().run_until_complete(
|
||||
self._api_async.load_markets(reload=reload))
|
||||
|
||||
except ccxt.BaseError as e:
|
||||
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
||||
logger.warning('Could not load async markets. Reason: %s', e)
|
||||
return
|
||||
|
||||
@@ -291,7 +292,7 @@ class Exchange:
|
||||
try:
|
||||
self._api.load_markets()
|
||||
self._load_async_markets()
|
||||
self._last_markets_refresh = arrow.utcnow().timestamp
|
||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||
except ccxt.BaseError as e:
|
||||
logger.warning('Unable to initialize markets. Reason: %s', e)
|
||||
|
||||
@@ -300,14 +301,14 @@ class Exchange:
|
||||
# Check whether markets have to be reloaded
|
||||
if (self._last_markets_refresh > 0) and (
|
||||
self._last_markets_refresh + self.markets_refresh_interval
|
||||
> arrow.utcnow().timestamp):
|
||||
> arrow.utcnow().int_timestamp):
|
||||
return None
|
||||
logger.debug("Performing scheduled market reload..")
|
||||
try:
|
||||
self._api.load_markets(reload=True)
|
||||
# Also reload async markets to avoid issues with newly listed pairs
|
||||
self._load_async_markets(reload=True)
|
||||
self._last_markets_refresh = arrow.utcnow().timestamp
|
||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Could not reload markets.")
|
||||
|
||||
@@ -501,7 +502,7 @@ class Exchange:
|
||||
'side': side,
|
||||
'remaining': _amount,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'timestamp': int(arrow.utcnow().timestamp * 1000),
|
||||
'timestamp': int(arrow.utcnow().int_timestamp * 1000),
|
||||
'status': "closed" if ordertype == "market" else "open",
|
||||
'fee': None,
|
||||
'info': {}
|
||||
@@ -523,7 +524,7 @@ class Exchange:
|
||||
'rate': self.get_fee(pair)
|
||||
}
|
||||
})
|
||||
if closed_order["type"] in ["stop_loss_limit"]:
|
||||
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
|
||||
|
||||
@@ -678,15 +679,31 @@ class Exchange:
|
||||
:param pair: Pair to download
|
||||
:param timeframe: Timeframe to get data for
|
||||
:param since_ms: Timestamp in milliseconds to get history from
|
||||
:returns List with candle (OHLCV) data
|
||||
:return: List with candle (OHLCV) data
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||
since_ms=since_ms))
|
||||
|
||||
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
||||
since_ms: int) -> DataFrame:
|
||||
"""
|
||||
Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe
|
||||
:param pair: Pair to download
|
||||
:param timeframe: Timeframe to get data for
|
||||
:param since_ms: Timestamp in milliseconds to get history from
|
||||
:return: OHLCV DataFrame
|
||||
"""
|
||||
ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms)
|
||||
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str,
|
||||
timeframe: str,
|
||||
since_ms: int) -> List:
|
||||
"""
|
||||
Download historic ohlcv
|
||||
"""
|
||||
|
||||
one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit
|
||||
logger.debug(
|
||||
@@ -696,15 +713,20 @@ class Exchange:
|
||||
)
|
||||
input_coroutines = [self._async_get_candle_history(
|
||||
pair, timeframe, since) for since in
|
||||
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
||||
range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)]
|
||||
|
||||
results = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
|
||||
# Combine gathered results
|
||||
data: List = []
|
||||
for p, timeframe, res in results:
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(res)
|
||||
data.extend(new_data)
|
||||
# Sort data again after extending the result - above calls return in "async order"
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
logger.info("Downloaded data for %s with length %s.", pair, len(data))
|
||||
@@ -741,9 +763,8 @@ class Exchange:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
continue
|
||||
pair = res[0]
|
||||
timeframe = res[1]
|
||||
ticks = res[2]
|
||||
# Deconstruct tuple (has 3 elements)
|
||||
pair, timeframe, ticks = res
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if ticks:
|
||||
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
||||
@@ -759,7 +780,7 @@ class Exchange:
|
||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||
|
||||
return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0)
|
||||
+ interval_in_sec) >= arrow.utcnow().timestamp)
|
||||
+ interval_in_sec) >= arrow.utcnow().int_timestamp)
|
||||
|
||||
@retrier_async
|
||||
async def _async_get_candle_history(self, pair: str, timeframe: str,
|
||||
@@ -1069,6 +1090,16 @@ class Exchange:
|
||||
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]]):
|
||||
"""
|
||||
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
|
||||
return min([x for x in limit_range if limit <= x] + [max(limit_range)])
|
||||
|
||||
@retrier
|
||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
"""
|
||||
@@ -1077,9 +1108,10 @@ class Exchange:
|
||||
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'])
|
||||
try:
|
||||
|
||||
return self._api.fetch_l2_order_book(pair, limit)
|
||||
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.'
|
||||
|
@@ -4,12 +4,12 @@ from typing import Any, Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -4,12 +4,12 @@ from typing import Any, Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ class Kraken(Exchange):
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop-loss' and stop_loss > float(order['price'])
|
||||
return (order['type'] in ('stop-loss', 'stop-loss-limit')
|
||||
and stop_loss > float(order['price']))
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
@@ -77,8 +78,15 @@ class Kraken(Exchange):
|
||||
Creates a stoploss market order.
|
||||
Stoploss market orders is the only stoploss type supported by kraken.
|
||||
"""
|
||||
params = self._params.copy()
|
||||
|
||||
ordertype = "stop-loss"
|
||||
if order_types.get('stoploss', 'market') == 'limit':
|
||||
ordertype = "stop-loss-limit"
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
limit_rate = stop_price * limit_price_pct
|
||||
params['price2'] = self.price_to_precision(pair, limit_rate)
|
||||
else:
|
||||
ordertype = "stop-loss"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
@@ -88,8 +96,6 @@ class Kraken(Exchange):
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
|
@@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from math import isclose
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -12,17 +12,17 @@ from typing import Any, Dict, List, Optional
|
||||
import arrow
|
||||
from cachetools import TTLCache
|
||||
|
||||
from freqtrade import __version__, constants, persistence
|
||||
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.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, PricingError)
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||
from freqtrade.persistence import Order, Trade
|
||||
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||
from freqtrade.state import State
|
||||
@@ -30,6 +30,7 @@ from freqtrade.strategy.interface import IStrategy, SellType
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -57,8 +58,8 @@ class FreqtradeBot:
|
||||
# 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(maxsize=100, ttl=1800)
|
||||
self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800)
|
||||
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)
|
||||
|
||||
@@ -67,10 +68,12 @@ class FreqtradeBot:
|
||||
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
|
||||
persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
|
||||
init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run'])
|
||||
|
||||
self.wallets = Wallets(self.config, self.exchange)
|
||||
|
||||
PairLocks.timeframe = self.config['timeframe']
|
||||
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
|
||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
||||
@@ -122,7 +125,7 @@ class FreqtradeBot:
|
||||
self.check_for_open_trades()
|
||||
|
||||
self.rpc.cleanup()
|
||||
persistence.cleanup()
|
||||
cleanup_db()
|
||||
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
@@ -344,27 +347,27 @@ class FreqtradeBot:
|
||||
whitelist = copy.deepcopy(self.active_pair_whitelist)
|
||||
if not whitelist:
|
||||
logger.info("Active pair whitelist is empty.")
|
||||
else:
|
||||
# Remove pairs for currently opened trades from the whitelist
|
||||
for trade in Trade.get_open_trades():
|
||||
if trade.pair in whitelist:
|
||||
whitelist.remove(trade.pair)
|
||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||
return trades_created
|
||||
# Remove pairs for currently opened trades from the whitelist
|
||||
for trade in Trade.get_open_trades():
|
||||
if trade.pair in whitelist:
|
||||
whitelist.remove(trade.pair)
|
||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||
|
||||
if not whitelist:
|
||||
logger.info("No currency pair in active pair whitelist, "
|
||||
"but checking to sell open trades.")
|
||||
else:
|
||||
# Create entity and execute trade for each pair from whitelist
|
||||
for pair in whitelist:
|
||||
try:
|
||||
trades_created += self.create_trade(pair)
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to create trade for %s: %s', pair, exception)
|
||||
if not whitelist:
|
||||
logger.info("No currency pair in active pair whitelist, "
|
||||
"but checking to sell open trades.")
|
||||
return trades_created
|
||||
# Create entity and execute trade for each pair from whitelist
|
||||
for pair in whitelist:
|
||||
try:
|
||||
trades_created += self.create_trade(pair)
|
||||
except DependencyException as exception:
|
||||
logger.warning('Unable to create trade for %s: %s', pair, exception)
|
||||
|
||||
if not trades_created:
|
||||
logger.debug("Found no buy signals for whitelisted currencies. "
|
||||
"Trying again...")
|
||||
if not trades_created:
|
||||
logger.debug("Found no buy signals for whitelisted currencies. "
|
||||
"Trying again...")
|
||||
|
||||
return trades_created
|
||||
|
||||
@@ -936,8 +939,8 @@ class FreqtradeBot:
|
||||
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
|
||||
stoploss_order=True)
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair,
|
||||
timeframe_to_next_date(self.config['timeframe']))
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
self._notify_sell(trade, "stoploss")
|
||||
return True
|
||||
|
||||
@@ -1263,7 +1266,8 @@ class FreqtradeBot:
|
||||
Trade.session.flush()
|
||||
|
||||
# Lock pair for one candle to prevent immediate rebuys
|
||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']))
|
||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||
reason='Auto lock')
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import logging
|
||||
import sys
|
||||
from logging import Formatter
|
||||
from logging.handlers import (BufferingHandler, RotatingFileHandler,
|
||||
SysLogHandler)
|
||||
from logging.handlers import BufferingHandler, RotatingFileHandler, SysLogHandler
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
|
||||
@@ -37,6 +37,13 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
|
||||
)
|
||||
|
||||
|
||||
def get_existing_handlers(handlertype):
|
||||
"""
|
||||
Returns Existing handler or None (if the handler has not yet been added to the root handlers).
|
||||
"""
|
||||
return next((h for h in logging.root.handlers if isinstance(h, handlertype)), None)
|
||||
|
||||
|
||||
def setup_logging_pre() -> None:
|
||||
"""
|
||||
Early setup for logging.
|
||||
@@ -71,18 +78,24 @@ def setup_logging(config: Dict[str, Any]) -> None:
|
||||
# config['logfilename']), which defaults to '/dev/log', applicable for most
|
||||
# of the systems.
|
||||
address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else '/dev/log'
|
||||
handler = SysLogHandler(address=address)
|
||||
handler_sl = get_existing_handlers(SysLogHandler)
|
||||
if handler_sl:
|
||||
logging.root.removeHandler(handler_sl)
|
||||
handler_sl = SysLogHandler(address=address)
|
||||
# No datetime field for logging into syslog, to allow syslog
|
||||
# to perform reduction of repeating messages if this is set in the
|
||||
# syslog config. The messages should be equal for this.
|
||||
handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
||||
logging.root.addHandler(handler)
|
||||
handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
||||
logging.root.addHandler(handler_sl)
|
||||
elif s[0] == 'journald':
|
||||
try:
|
||||
from systemd.journal import JournaldLogHandler
|
||||
except ImportError:
|
||||
raise OperationalException("You need the systemd python package be installed in "
|
||||
"order to use logging to journald.")
|
||||
handler_jd = get_existing_handlers(JournaldLogHandler)
|
||||
if handler_jd:
|
||||
logging.root.removeHandler(handler_jd)
|
||||
handler_jd = JournaldLogHandler()
|
||||
# No datetime field for logging into journald, to allow syslog
|
||||
# to perform reduction of repeating messages if this is set in the
|
||||
@@ -90,6 +103,9 @@ def setup_logging(config: Dict[str, Any]) -> None:
|
||||
handler_jd.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
|
||||
logging.root.addHandler(handler_jd)
|
||||
else:
|
||||
handler_rf = get_existing_handlers(RotatingFileHandler)
|
||||
if handler_rf:
|
||||
logging.root.removeHandler(handler_rf)
|
||||
handler_rf = RotatingFileHandler(logfile,
|
||||
maxBytes=1024 * 1024 * 10, # 10Mb
|
||||
backupCount=10)
|
||||
|
@@ -7,6 +7,7 @@ import logging
|
||||
import sys
|
||||
from typing import Any, List
|
||||
|
||||
|
||||
# check min. python version
|
||||
if sys.version_info < (3, 6):
|
||||
sys.exit("Freqtrade requires Python version >= 3.6")
|
||||
|
@@ -12,6 +12,7 @@ from typing.io import IO
|
||||
import numpy as np
|
||||
import rapidjson
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -41,7 +42,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
||||
return dates.dt.to_pydatetime()
|
||||
|
||||
|
||||
def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None:
|
||||
def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None:
|
||||
"""
|
||||
Dump JSON data into a file
|
||||
:param filename: file to create
|
||||
@@ -52,12 +53,14 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None:
|
||||
if is_zip:
|
||||
if filename.suffix != '.gz':
|
||||
filename = filename.with_suffix('.gz')
|
||||
logger.info(f'dumping json to "{filename}"')
|
||||
if log:
|
||||
logger.info(f'dumping json to "{filename}"')
|
||||
|
||||
with gzip.open(filename, 'w') as fp:
|
||||
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||
with gzip.open(filename, 'w') as fpz:
|
||||
rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||
else:
|
||||
logger.info(f'dumping json to "{filename}"')
|
||||
if log:
|
||||
logger.info(f'dumping json to "{filename}"')
|
||||
with open(filename, 'w') as fp:
|
||||
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
|
@@ -4,31 +4,39 @@
|
||||
This module contains the backtesting logic
|
||||
"""
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import (TimeRange, remove_credentials,
|
||||
validate_config_consistency)
|
||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.converter import trim_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats,
|
||||
show_backtest_results,
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||
store_backtest_stats)
|
||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Indexes for backtest tuples
|
||||
DATE_IDX = 0
|
||||
BUY_IDX = 1
|
||||
OPEN_IDX = 2
|
||||
CLOSE_IDX = 3
|
||||
SELL_IDX = 4
|
||||
LOW_IDX = 5
|
||||
HIGH_IDX = 6
|
||||
|
||||
|
||||
class BacktestResult(NamedTuple):
|
||||
"""
|
||||
@@ -116,7 +124,7 @@ class Backtesting:
|
||||
"""
|
||||
Load strategy into backtesting
|
||||
"""
|
||||
self.strategy = strategy
|
||||
self.strategy: IStrategy = strategy
|
||||
# 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
|
||||
@@ -148,12 +156,14 @@ class Backtesting:
|
||||
|
||||
return data, timerange
|
||||
|
||||
def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]:
|
||||
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.
|
||||
|
||||
Used by backtest() - so keep this optimized for performance.
|
||||
"""
|
||||
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||
# and eventually change the constants for indexes at the top
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||
data: Dict = {}
|
||||
# Create dict with data
|
||||
@@ -173,10 +183,10 @@ class Backtesting:
|
||||
|
||||
# Convert from Pandas to list for performance reasons
|
||||
# (Looping Pandas is slow.)
|
||||
data[pair] = [x for x in df_analyzed.itertuples()]
|
||||
data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)]
|
||||
return data
|
||||
|
||||
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
|
||||
def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple,
|
||||
trade_dur: int) -> float:
|
||||
"""
|
||||
Get close rate for backtesting result
|
||||
@@ -187,12 +197,12 @@ class Backtesting:
|
||||
return trade.stop_loss
|
||||
elif sell.sell_type == (SellType.ROI):
|
||||
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
|
||||
if roi is not None:
|
||||
if roi is not None and roi_entry is not None:
|
||||
if roi == -1 and roi_entry % self.timeframe_min == 0:
|
||||
# When forceselling with ROI=-1, the roi time will always be equal to trade_dur.
|
||||
# If that entry is a multiple of the timeframe (so on candle open)
|
||||
# - we'll use open instead of close
|
||||
return sell_row.open
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
|
||||
close_rate = - (trade.open_rate * roi + trade.open_rate *
|
||||
@@ -200,91 +210,79 @@ class Backtesting:
|
||||
|
||||
if (trade_dur > 0 and trade_dur == roi_entry
|
||||
and roi_entry % self.timeframe_min == 0
|
||||
and sell_row.open > close_rate):
|
||||
and sell_row[OPEN_IDX] > close_rate):
|
||||
# new ROI entry came into effect.
|
||||
# use Open rate if open_rate > calculated sell rate
|
||||
return sell_row.open
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
# Use the maximum between close_rate and low as we
|
||||
# cannot sell outside of a candle.
|
||||
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
||||
return max(close_rate, sell_row.low)
|
||||
return max(close_rate, sell_row[LOW_IDX])
|
||||
|
||||
else:
|
||||
# This should not be reached...
|
||||
return sell_row.open
|
||||
return sell_row[OPEN_IDX]
|
||||
else:
|
||||
return sell_row.open
|
||||
return sell_row[OPEN_IDX]
|
||||
|
||||
def _get_sell_trade_entry(
|
||||
self, pair: str, buy_row: DataFrame,
|
||||
partial_ohlcv: List, trade_count_lock: Dict,
|
||||
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
|
||||
def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[BacktestResult]:
|
||||
|
||||
trade = Trade(
|
||||
pair=pair,
|
||||
open_rate=buy_row.open,
|
||||
open_date=buy_row.date,
|
||||
stake_amount=stake_amount,
|
||||
amount=round(stake_amount / buy_row.open, 8),
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
)
|
||||
logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
|
||||
# calculate win/lose forwards from buy point
|
||||
for sell_row in partial_ohlcv:
|
||||
if max_open_trades > 0:
|
||||
# Increase trade_count_lock for every iteration
|
||||
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
|
||||
sell_row[BUY_IDX], sell_row[SELL_IDX],
|
||||
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
|
||||
if sell.sell_flag:
|
||||
trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
|
||||
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy,
|
||||
sell_row.sell, low=sell_row.low, high=sell_row.high)
|
||||
if sell.sell_flag:
|
||||
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
|
||||
return BacktestResult(pair=pair,
|
||||
profit_percent=trade.calc_profit_ratio(rate=closerate),
|
||||
profit_abs=trade.calc_profit(rate=closerate),
|
||||
open_date=buy_row.date,
|
||||
open_rate=buy_row.open,
|
||||
open_fee=self.fee,
|
||||
close_date=sell_row.date,
|
||||
close_rate=closerate,
|
||||
close_fee=self.fee,
|
||||
amount=trade.amount,
|
||||
trade_duration=trade_dur,
|
||||
open_at_end=False,
|
||||
sell_reason=sell.sell_type
|
||||
)
|
||||
if partial_ohlcv:
|
||||
# no sell condition found - trade stil open at end of backtest period
|
||||
sell_row = partial_ohlcv[-1]
|
||||
bt_res = BacktestResult(pair=pair,
|
||||
profit_percent=trade.calc_profit_ratio(rate=sell_row.open),
|
||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||
open_date=buy_row.date,
|
||||
open_rate=buy_row.open,
|
||||
open_fee=self.fee,
|
||||
close_date=sell_row.date,
|
||||
close_rate=sell_row.open,
|
||||
close_fee=self.fee,
|
||||
amount=trade.amount,
|
||||
trade_duration=int((
|
||||
sell_row.date - buy_row.date).total_seconds() // 60),
|
||||
open_at_end=True,
|
||||
sell_reason=SellType.FORCE_SELL
|
||||
)
|
||||
logger.debug(f"{pair} - Force selling still open trade, "
|
||||
f"profit percent: {bt_res.profit_percent}, "
|
||||
f"profit abs: {bt_res.profit_abs}")
|
||||
|
||||
return bt_res
|
||||
return BacktestResult(pair=trade.pair,
|
||||
profit_percent=trade.calc_profit_ratio(rate=closerate),
|
||||
profit_abs=trade.calc_profit(rate=closerate),
|
||||
open_date=trade.open_date,
|
||||
open_rate=trade.open_rate,
|
||||
open_fee=self.fee,
|
||||
close_date=sell_row[DATE_IDX],
|
||||
close_rate=closerate,
|
||||
close_fee=self.fee,
|
||||
amount=trade.amount,
|
||||
trade_duration=trade_dur,
|
||||
open_at_end=False,
|
||||
sell_reason=sell.sell_type
|
||||
)
|
||||
return None
|
||||
|
||||
def handle_left_open(self, open_trades: Dict[str, List[Trade]],
|
||||
data: Dict[str, List[Tuple]]) -> List[BacktestResult]:
|
||||
"""
|
||||
Handling of left open trades at the end of backtesting
|
||||
"""
|
||||
trades = []
|
||||
for pair in open_trades.keys():
|
||||
if len(open_trades[pair]) > 0:
|
||||
for trade in open_trades[pair]:
|
||||
sell_row = data[pair][-1]
|
||||
trade_entry = BacktestResult(pair=trade.pair,
|
||||
profit_percent=trade.calc_profit_ratio(
|
||||
rate=sell_row[OPEN_IDX]),
|
||||
profit_abs=trade.calc_profit(sell_row[OPEN_IDX]),
|
||||
open_date=trade.open_date,
|
||||
open_rate=trade.open_rate,
|
||||
open_fee=self.fee,
|
||||
close_date=sell_row[DATE_IDX],
|
||||
close_rate=sell_row[OPEN_IDX],
|
||||
close_fee=self.fee,
|
||||
amount=trade.amount,
|
||||
trade_duration=int((
|
||||
sell_row[DATE_IDX] - trade.open_date
|
||||
).total_seconds() // 60),
|
||||
open_at_end=True,
|
||||
sell_reason=SellType.FORCE_SELL
|
||||
)
|
||||
trades.append(trade_entry)
|
||||
return trades
|
||||
|
||||
def backtest(self, processed: Dict, stake_amount: float,
|
||||
start_date: arrow.Arrow, end_date: arrow.Arrow,
|
||||
start_date: datetime, end_date: datetime,
|
||||
max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame:
|
||||
"""
|
||||
Implement backtesting functionality
|
||||
@@ -306,19 +304,21 @@ class Backtesting:
|
||||
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
|
||||
)
|
||||
trades = []
|
||||
trade_count_lock: Dict = {}
|
||||
|
||||
# Use dict of lists with data for performance
|
||||
# (looping lists is a lot faster than pandas DataFrames)
|
||||
data: Dict = self._get_ohlcv_as_lists(processed)
|
||||
|
||||
lock_pair_until: Dict = {}
|
||||
# Indexes per pair, so some pairs are allowed to have a missing start.
|
||||
indexes: Dict = {}
|
||||
tmp = start_date + timedelta(minutes=self.timeframe_min)
|
||||
|
||||
open_trades: Dict[str, List] = defaultdict(list)
|
||||
open_trade_count = 0
|
||||
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
while tmp < end_date:
|
||||
while tmp <= end_date:
|
||||
open_trade_count_start = open_trade_count
|
||||
|
||||
for i, pair in enumerate(data):
|
||||
if pair not in indexes:
|
||||
@@ -332,42 +332,52 @@ class Backtesting:
|
||||
continue
|
||||
|
||||
# Waits until the time-counter reaches the start of the data for this pair.
|
||||
if row.date > tmp.datetime:
|
||||
if row[DATE_IDX] > tmp:
|
||||
continue
|
||||
|
||||
indexes[pair] += 1
|
||||
|
||||
if row.buy == 0 or row.sell == 1:
|
||||
continue # skip rows where no buy signal or that would immediately sell off
|
||||
# without positionstacking, we can only have one open trade per pair.
|
||||
# max_open_trades must be respected
|
||||
# don't open on the last row
|
||||
if ((position_stacking or len(open_trades[pair]) == 0)
|
||||
and (max_open_trades <= 0 or open_trade_count_start < max_open_trades)
|
||||
and tmp != end_date
|
||||
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1):
|
||||
# Enter trade
|
||||
trade = Trade(
|
||||
pair=pair,
|
||||
open_rate=row[OPEN_IDX],
|
||||
open_date=row[DATE_IDX],
|
||||
stake_amount=stake_amount,
|
||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
||||
fee_open=self.fee,
|
||||
fee_close=self.fee,
|
||||
is_open=True,
|
||||
)
|
||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||
# This emulates previous behaviour - not sure if this is correct
|
||||
# Prevents buying if the trade-slot was freed in this candle
|
||||
open_trade_count_start += 1
|
||||
open_trade_count += 1
|
||||
# logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
|
||||
open_trades[pair].append(trade)
|
||||
|
||||
if (not position_stacking and pair in lock_pair_until
|
||||
and row.date <= lock_pair_until[pair]):
|
||||
# without positionstacking, we can only have one open trade per pair.
|
||||
continue
|
||||
|
||||
if max_open_trades > 0:
|
||||
# Check if max_open_trades has already been reached for the given date
|
||||
if not trade_count_lock.get(row.date, 0) < max_open_trades:
|
||||
continue
|
||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||
|
||||
# since indexes has been incremented before, we need to go one step back to
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:],
|
||||
trade_count_lock, stake_amount,
|
||||
max_open_trades)
|
||||
|
||||
if trade_entry:
|
||||
logger.debug(f"{pair} - Locking pair till "
|
||||
f"close_date={trade_entry.close_date}")
|
||||
lock_pair_until[pair] = trade_entry.close_date
|
||||
trades.append(trade_entry)
|
||||
else:
|
||||
# Set lock_pair_until to end of testing period if trade could not be closed
|
||||
lock_pair_until[pair] = end_date.datetime
|
||||
for trade in open_trades[pair]:
|
||||
# since indexes has been incremented before, we need to go one step back to
|
||||
# also check the buying candle for sell conditions.
|
||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
||||
# Sell occured
|
||||
if trade_entry:
|
||||
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
||||
open_trade_count -= 1
|
||||
open_trades[pair].remove(trade)
|
||||
trades.append(trade_entry)
|
||||
|
||||
# Move time one configured time_interval ahead.
|
||||
tmp += timedelta(minutes=self.timeframe_min)
|
||||
|
||||
trades += self.handle_left_open(open_trades, data=data)
|
||||
|
||||
return DataFrame.from_records(trades, columns=BacktestResult._fields)
|
||||
|
||||
def start(self) -> None:
|
||||
@@ -413,8 +423,8 @@ class Backtesting:
|
||||
results = self.backtest(
|
||||
processed=preprocessed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
start_date=min_date.datetime,
|
||||
end_date=max_date.datetime,
|
||||
max_open_trades=max_open_trades,
|
||||
position_stacking=position_stacking,
|
||||
)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
DefaultHyperOptLoss
|
||||
ShortTradeDurHyperOptLoss
|
||||
This module defines the default HyperoptLoss class which is being used for
|
||||
Hyperoptimization.
|
||||
"""
|
||||
@@ -26,7 +26,7 @@ EXPECTED_MAX_PROFIT = 3.0
|
||||
MAX_ACCEPTED_TRADE_DURATION = 300
|
||||
|
||||
|
||||
class DefaultHyperOptLoss(IHyperOptLoss):
|
||||
class ShortTradeDurHyperOptLoss(IHyperOptLoss):
|
||||
"""
|
||||
Defines the default loss function for hyperopt
|
||||
"""
|
||||
@@ -50,3 +50,7 @@ class DefaultHyperOptLoss(IHyperOptLoss):
|
||||
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
|
||||
result = trade_loss + profit_loss + duration_loss
|
||||
return result
|
||||
|
||||
|
||||
# Create an alias for This to allow the legacy Method to work as well.
|
||||
DefaultHyperOptLoss = ShortTradeDurHyperOptLoss
|
||||
|
@@ -7,12 +7,12 @@ import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import (TimeRange, remove_credentials,
|
||||
validate_config_consistency)
|
||||
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -10,6 +10,7 @@ import logging
|
||||
import random
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from math import ceil
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
@@ -21,24 +22,22 @@ import rapidjson
|
||||
import tabulate
|
||||
from colorama import Fore, Style
|
||||
from colorama import init as colorama_init
|
||||
from joblib import (Parallel, cpu_count, delayed, dump, load,
|
||||
wrap_non_picklable_objects)
|
||||
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
|
||||
from pandas import DataFrame, isna, json_normalize
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
|
||||
from freqtrade.data.converter import trim_dataframe
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import plural, round_dict
|
||||
from freqtrade.misc import file_dump_json, plural, round_dict
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_loss_interface import \
|
||||
IHyperOptLoss # noqa: F401
|
||||
from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver,
|
||||
HyperOptResolver)
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
|
||||
from freqtrade.strategy import IStrategy
|
||||
|
||||
|
||||
# Suppress scikit-learn FutureWarnings from skopt
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=FutureWarning)
|
||||
@@ -77,19 +76,16 @@ class Hyperopt:
|
||||
|
||||
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
|
||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||
|
||||
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
self.results_file = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_results.pickle')
|
||||
'hyperopt_results' / f'hyperopt_results_{time_now}.pickle')
|
||||
self.data_pickle_file = (self.config['user_data_dir'] /
|
||||
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
|
||||
self.total_epochs = config.get('epochs', 0)
|
||||
|
||||
self.current_best_loss = 100
|
||||
|
||||
if not self.config.get('hyperopt_continue'):
|
||||
self.clean_hyperopt()
|
||||
else:
|
||||
logger.info("Continuing on previous hyperopt results.")
|
||||
self.clean_hyperopt()
|
||||
|
||||
self.num_epochs_saved = 0
|
||||
|
||||
@@ -98,14 +94,14 @@ class Hyperopt:
|
||||
|
||||
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
|
||||
if hasattr(self.custom_hyperopt, 'populate_indicators'):
|
||||
self.backtesting.strategy.advise_indicators = \
|
||||
self.custom_hyperopt.populate_indicators # type: ignore
|
||||
self.backtesting.strategy.advise_indicators = ( # type: ignore
|
||||
self.custom_hyperopt.populate_indicators) # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||
self.backtesting.strategy.advise_buy = ( # type: ignore
|
||||
self.custom_hyperopt.populate_buy_trend) # type: ignore
|
||||
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||
self.backtesting.strategy.advise_sell = \
|
||||
self.custom_hyperopt.populate_sell_trend # type: ignore
|
||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||
self.custom_hyperopt.populate_sell_trend) # type: ignore
|
||||
|
||||
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
|
||||
if self.config.get('use_max_market_positions', True):
|
||||
@@ -165,6 +161,10 @@ class Hyperopt:
|
||||
self.num_epochs_saved = num_epochs
|
||||
logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
|
||||
f"saved to '{self.results_file}'.")
|
||||
# Store hyperopt filename
|
||||
latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN)
|
||||
file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
|
||||
log=False)
|
||||
|
||||
@staticmethod
|
||||
def _read_results(results_file: Path) -> List:
|
||||
@@ -262,6 +262,11 @@ class Hyperopt:
|
||||
),
|
||||
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||
params_result += f"minimal_roi = {minimal_roi_result}"
|
||||
elif space == 'trailing':
|
||||
|
||||
for k, v in space_params.items():
|
||||
params_result += f'{k} = {v}\n'
|
||||
|
||||
else:
|
||||
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
|
||||
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
|
||||
@@ -503,16 +508,16 @@ class Hyperopt:
|
||||
params_details = self._get_params_details(params_dict)
|
||||
|
||||
if self.has_space('roi'):
|
||||
self.backtesting.strategy.minimal_roi = \
|
||||
self.custom_hyperopt.generate_roi_table(params_dict)
|
||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
||||
self.custom_hyperopt.generate_roi_table(params_dict))
|
||||
|
||||
if self.has_space('buy'):
|
||||
self.backtesting.strategy.advise_buy = \
|
||||
self.custom_hyperopt.buy_strategy_generator(params_dict)
|
||||
self.backtesting.strategy.advise_buy = ( # type: ignore
|
||||
self.custom_hyperopt.buy_strategy_generator(params_dict))
|
||||
|
||||
if self.has_space('sell'):
|
||||
self.backtesting.strategy.advise_sell = \
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict)
|
||||
self.backtesting.strategy.advise_sell = ( # type: ignore
|
||||
self.custom_hyperopt.sell_strategy_generator(params_dict))
|
||||
|
||||
if self.has_space('stoploss'):
|
||||
self.backtesting.strategy.stoploss = params_dict['stoploss']
|
||||
@@ -533,8 +538,8 @@ class Hyperopt:
|
||||
backtesting_results = self.backtesting.backtest(
|
||||
processed=processed,
|
||||
stake_amount=self.config['stake_amount'],
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
start_date=min_date.datetime,
|
||||
end_date=max_date.datetime,
|
||||
max_open_trades=self.max_open_trades,
|
||||
position_stacking=self.position_stacking,
|
||||
)
|
||||
@@ -657,8 +662,6 @@ class Hyperopt:
|
||||
self.backtesting.strategy.dp = None # type: ignore
|
||||
IStrategy.dp = None # type: ignore
|
||||
|
||||
self.epochs = self.load_previous_results(self.results_file)
|
||||
|
||||
cpus = cpu_count()
|
||||
logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
|
||||
config_jobs = self.config.get('hyperopt_jobs', -1)
|
||||
|
@@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import round_dict
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -6,8 +6,8 @@ Hyperoptimization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
import numpy as np
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
@@ -6,8 +6,8 @@ Hyperoptimization.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from pandas import DataFrame
|
||||
import numpy as np
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||
|
||||
|
@@ -4,14 +4,15 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from arrow import Arrow
|
||||
from pandas import DataFrame
|
||||
from numpy import int64
|
||||
from pandas import DataFrame
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
|
||||
from freqtrade.data.btanalysis import calculate_max_drawdown, calculate_market_change
|
||||
from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown
|
||||
from freqtrade.misc import file_dump_json
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -267,9 +268,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
||||
'profit_total': results['profit_percent'].sum(),
|
||||
'profit_total_abs': results['profit_abs'].sum(),
|
||||
'backtest_start': min_date.datetime,
|
||||
'backtest_start_ts': min_date.timestamp * 1000,
|
||||
'backtest_start_ts': min_date.int_timestamp * 1000,
|
||||
'backtest_end': max_date.datetime,
|
||||
'backtest_end_ts': max_date.timestamp * 1000,
|
||||
'backtest_end_ts': max_date.int_timestamp * 1000,
|
||||
'backtest_days': backtest_days,
|
||||
|
||||
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
|
||||
@@ -395,6 +396,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
metrics = [
|
||||
('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)),
|
||||
('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)),
|
||||
('Max open trades', strat_results['max_open_trades']),
|
||||
('', ''), # Empty line to improve readability
|
||||
('Total trades', strat_results['total_trades']),
|
||||
('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)),
|
||||
('First trade Pair', min_trade['pair']),
|
||||
|
@@ -2,9 +2,10 @@
|
||||
Minimum age (days listed) pair list filter
|
||||
"""
|
||||
import logging
|
||||
import arrow
|
||||
from typing import Any, Dict
|
||||
|
||||
import arrow
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
@@ -36,7 +37,7 @@ class AgeFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
@@ -48,7 +49,7 @@ class AgeFilter(IPairList):
|
||||
return (f"{self.name} - Filtering pairs with age less than "
|
||||
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.")
|
||||
|
||||
def _validate_pair(self, ticker: dict) -> bool:
|
||||
def _validate_pair(self, ticker: Dict) -> bool:
|
||||
"""
|
||||
Validate age for the ticker
|
||||
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||
|
@@ -36,7 +36,7 @@ class IPairList(ABC):
|
||||
self._pairlist_pos = pairlist_pos
|
||||
self.refresh_period = self._pairlistconfig.get('refresh_period', 1800)
|
||||
self._last_refresh = 0
|
||||
self._log_cache = TTLCache(maxsize=1024, ttl=self.refresh_period)
|
||||
self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -68,7 +68,7 @@ class IPairList(ABC):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
|
||||
|
@@ -4,8 +4,9 @@ Precision pair list filter
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,7 +32,7 @@ class PrecisionFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
@@ -35,7 +35,7 @@ class PriceFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
@@ -25,7 +25,7 @@ class ShuffleFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return False
|
||||
|
@@ -24,7 +24,7 @@ class SpreadFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
@@ -24,11 +24,13 @@ class StaticPairList(IPairList):
|
||||
raise OperationalException(f"{self.name} can only be used in the first position "
|
||||
"in the list of Pairlist Handlers.")
|
||||
|
||||
self._allow_inactive = self._pairlistconfig.get('allow_inactive', False)
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return False
|
||||
@@ -47,7 +49,10 @@ class StaticPairList(IPairList):
|
||||
:param tickers: Tickers (from exchange.get_tickers()).
|
||||
:return: List of pairs
|
||||
"""
|
||||
return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist'])
|
||||
if self._allow_inactive:
|
||||
return self._config['exchange']['pair_whitelist']
|
||||
else:
|
||||
return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist'])
|
||||
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||
"""
|
||||
|
@@ -49,7 +49,7 @@ class VolumePairList(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty Dict is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
@@ -7,10 +7,10 @@ from typing import Dict, List
|
||||
|
||||
from cachetools import TTLCache, cached
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
from freqtrade.resolvers import PairListResolver
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
89
freqtrade/pairlist/rangestabilityfilter.py
Normal file
89
freqtrade/pairlist/rangestabilityfilter.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Rate of change pairlist filter
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import arrow
|
||||
from cachetools.ttl import TTLCache
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RangeStabilityFilter(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._days = pairlistconfig.get('lookback_days', 10)
|
||||
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
||||
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
|
||||
|
||||
self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period)
|
||||
|
||||
if self._days < 1:
|
||||
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
|
||||
if self._days > exchange.ohlcv_candle_limit:
|
||||
raise OperationalException("RangeStabilityFilter requires lookback_days to not "
|
||||
"exceed exchange max request size "
|
||||
f"({exchange.ohlcv_candle_limit})")
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
Short whitelist method description - used for startup-messages
|
||||
"""
|
||||
return (f"{self.name} - Filtering pairs with rate of change below "
|
||||
f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.")
|
||||
|
||||
def _validate_pair(self, ticker: Dict) -> bool:
|
||||
"""
|
||||
Validate trading range
|
||||
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||
:return: True if the pair can stay, False if it should be removed
|
||||
"""
|
||||
pair = ticker['symbol']
|
||||
# Check symbol in cache
|
||||
if pair in self._pair_cache:
|
||||
return self._pair_cache[pair]
|
||||
|
||||
since_ms = int(arrow.utcnow()
|
||||
.floor('day')
|
||||
.shift(days=-self._days)
|
||||
.float_timestamp) * 1000
|
||||
|
||||
daily_candles = self._exchange.get_historic_ohlcv_as_df(pair=pair,
|
||||
timeframe='1d',
|
||||
since_ms=since_ms)
|
||||
result = False
|
||||
if daily_candles is not None and not daily_candles.empty:
|
||||
highest_high = daily_candles['high'].max()
|
||||
lowest_low = daily_candles['low'].min()
|
||||
pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0
|
||||
if pct_change >= self._min_rate_of_change:
|
||||
result = True
|
||||
else:
|
||||
self.log_on_refresh(logger.info,
|
||||
f"Removed {pair} from whitelist, "
|
||||
f"because rate of change over {plural(self._days, 'day')} is "
|
||||
f"{pct_change:.3f}, which is below the "
|
||||
f"threshold of {self._min_rate_of_change}.")
|
||||
result = False
|
||||
self._pair_cache[pair] = result
|
||||
|
||||
return result
|
@@ -1,4 +1,4 @@
|
||||
# flake8: noqa: F401
|
||||
|
||||
from freqtrade.persistence.models import (Order, Trade, clean_dry_run_db,
|
||||
cleanup, init)
|
||||
from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
|
@@ -3,6 +3,7 @@ from typing import List
|
||||
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@@ -7,8 +7,8 @@ from decimal import Decimal
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer,
|
||||
String, create_engine, desc, func, inspect)
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String,
|
||||
create_engine, desc, func, inspect)
|
||||
from sqlalchemy.exc import NoSuchModuleError
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import Query, relationship
|
||||
@@ -17,10 +17,12 @@ from sqlalchemy.orm.session import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy.sql.schema import UniqueConstraint
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.misc import safe_value_fallback
|
||||
from freqtrade.persistence.migrations import check_migrate
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -28,7 +30,7 @@ _DECL_BASE: Any = declarative_base()
|
||||
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||
|
||||
|
||||
def init(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
def init_db(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
registers all known command handlers
|
||||
@@ -62,6 +64,9 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
# Copy session attributes to order object too
|
||||
Order.session = Trade.session
|
||||
Order.query = Order.session.query_property()
|
||||
PairLock.session = Trade.session
|
||||
PairLock.query = PairLock.session.query_property()
|
||||
|
||||
previous_tables = inspect(engine).get_table_names()
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
||||
@@ -71,7 +76,7 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
|
||||
clean_dry_run_db()
|
||||
|
||||
|
||||
def cleanup() -> None:
|
||||
def cleanup_db() -> None:
|
||||
"""
|
||||
Flushes all pending operations to disk.
|
||||
:return: None
|
||||
@@ -166,12 +171,12 @@ class Order(_DECL_BASE):
|
||||
"""
|
||||
Get all non-closed orders - useful when trying to batch-update orders
|
||||
"""
|
||||
filtered_orders = [o for o in orders if o.order_id == order['id']]
|
||||
filtered_orders = [o for o in orders if o.order_id == order.get('id')]
|
||||
if filtered_orders:
|
||||
oobj = filtered_orders[0]
|
||||
oobj.update_from_ccxt_object(order)
|
||||
else:
|
||||
logger.warning(f"Did not find order for {order['id']}.")
|
||||
logger.warning(f"Did not find order for {order}.")
|
||||
|
||||
@staticmethod
|
||||
def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
|
||||
@@ -250,7 +255,7 @@ class Trade(_DECL_BASE):
|
||||
self.recalc_open_trade_price()
|
||||
|
||||
def __repr__(self):
|
||||
open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed'
|
||||
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
|
||||
|
||||
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
||||
@@ -265,7 +270,6 @@ class Trade(_DECL_BASE):
|
||||
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
|
||||
'stake_amount': round(self.stake_amount, 8),
|
||||
'strategy': self.strategy,
|
||||
'ticker_interval': self.timeframe, # DEPRECATED
|
||||
'timeframe': self.timeframe,
|
||||
|
||||
'fee_open': self.fee_open,
|
||||
@@ -276,7 +280,7 @@ class Trade(_DECL_BASE):
|
||||
'fee_close_currency': self.fee_close_currency,
|
||||
|
||||
'open_date_hum': arrow.get(self.open_date).humanize(),
|
||||
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000),
|
||||
'open_rate': self.open_rate,
|
||||
'open_rate_requested': self.open_rate_requested,
|
||||
@@ -284,27 +288,30 @@ class Trade(_DECL_BASE):
|
||||
|
||||
'close_date_hum': (arrow.get(self.close_date).humanize()
|
||||
if self.close_date else None),
|
||||
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.close_date else None),
|
||||
'close_timestamp': int(self.close_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
|
||||
'close_rate': self.close_rate,
|
||||
'close_rate_requested': self.close_rate_requested,
|
||||
'close_profit': self.close_profit,
|
||||
'close_profit_abs': self.close_profit_abs,
|
||||
'close_profit': self.close_profit, # Deprecated
|
||||
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||
'close_profit_abs': self.close_profit_abs, # Deprecated
|
||||
|
||||
'profit_ratio': self.close_profit,
|
||||
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
|
||||
'profit_abs': self.close_profit_abs,
|
||||
|
||||
'sell_reason': self.sell_reason,
|
||||
'sell_order_status': self.sell_order_status,
|
||||
'stop_loss': self.stop_loss, # Deprecated - should not be used
|
||||
'stop_loss_abs': self.stop_loss,
|
||||
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
|
||||
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
|
||||
'stoploss_order_id': self.stoploss_order_id,
|
||||
'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S")
|
||||
'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT)
|
||||
if self.stoploss_last_update else None),
|
||||
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
|
||||
'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used
|
||||
'initial_stop_loss_abs': self.initial_stop_loss,
|
||||
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
|
||||
if self.initial_stop_loss_pct else None),
|
||||
@@ -390,7 +397,7 @@ class Trade(_DECL_BASE):
|
||||
if self.is_open:
|
||||
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.')
|
||||
self.close(safe_value_fallback(order, 'average', 'price'))
|
||||
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'):
|
||||
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'):
|
||||
self.stoploss_order_id = None
|
||||
self.close_rate_requested = self.stop_loss
|
||||
if self.is_open:
|
||||
@@ -398,7 +405,7 @@ class Trade(_DECL_BASE):
|
||||
self.close(order['average'])
|
||||
else:
|
||||
raise ValueError(f'Unknown order type: {order_type}')
|
||||
cleanup()
|
||||
cleanup_db()
|
||||
|
||||
def close(self, rate: float) -> None:
|
||||
"""
|
||||
@@ -653,3 +660,56 @@ class Trade(_DECL_BASE):
|
||||
trade.stop_loss = None
|
||||
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
||||
|
||||
|
||||
class PairLock(_DECL_BASE):
|
||||
"""
|
||||
Pair Locks database model.
|
||||
"""
|
||||
__tablename__ = 'pairlocks'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
pair = Column(String, nullable=False, index=True)
|
||||
reason = Column(String, nullable=True)
|
||||
# Time the pair was locked (start time)
|
||||
lock_time = Column(DateTime, nullable=False)
|
||||
# Time until the pair is locked (end time)
|
||||
lock_end_time = Column(DateTime, nullable=False, index=True)
|
||||
|
||||
active = Column(Boolean, nullable=False, default=True, index=True)
|
||||
|
||||
def __repr__(self):
|
||||
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
||||
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
|
||||
return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, '
|
||||
f'lock_end_time={lock_end_time})')
|
||||
|
||||
@staticmethod
|
||||
def query_pair_locks(pair: Optional[str], now: datetime) -> Query:
|
||||
"""
|
||||
Get all locks for this pair
|
||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||
"""
|
||||
|
||||
filters = [PairLock.lock_end_time > now,
|
||||
# Only active locks
|
||||
PairLock.active.is_(True), ]
|
||||
if pair:
|
||||
filters.append(PairLock.pair == pair)
|
||||
return PairLock.query.filter(
|
||||
*filters
|
||||
)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'pair': self.pair,
|
||||
'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT),
|
||||
'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),
|
||||
'lock_end_time': self.lock_end_time.strftime(DATETIME_PRINT_FORMAT),
|
||||
'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc
|
||||
).timestamp() * 1000),
|
||||
'reason': self.reason,
|
||||
'active': self.active,
|
||||
}
|
||||
|
99
freqtrade/persistence/pairlock_middleware.py
Normal file
99
freqtrade/persistence/pairlock_middleware.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
|
||||
from freqtrade.exchange import timeframe_to_next_date
|
||||
from freqtrade.persistence.models import PairLock
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PairLocks():
|
||||
"""
|
||||
Pairlocks middleware class
|
||||
Abstracts the database layer away so it becomes optional - which will be necessary to support
|
||||
backtesting and hyperopt in the future.
|
||||
"""
|
||||
|
||||
use_db = True
|
||||
locks: List[PairLock] = []
|
||||
|
||||
timeframe: str = ''
|
||||
|
||||
@staticmethod
|
||||
def lock_pair(pair: str, until: datetime, reason: str = None) -> None:
|
||||
lock = PairLock(
|
||||
pair=pair,
|
||||
lock_time=datetime.now(timezone.utc),
|
||||
lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until),
|
||||
reason=reason,
|
||||
active=True
|
||||
)
|
||||
if PairLocks.use_db:
|
||||
PairLock.session.add(lock)
|
||||
PairLock.session.flush()
|
||||
else:
|
||||
PairLocks.locks.append(lock)
|
||||
|
||||
@staticmethod
|
||||
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]:
|
||||
"""
|
||||
Get all currently active locks for this pair
|
||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||
defaults to datetime.now(timezone.utc)
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
if PairLocks.use_db:
|
||||
return PairLock.query_pair_locks(pair, now).all()
|
||||
else:
|
||||
locks = [lock for lock in PairLocks.locks if (
|
||||
lock.lock_end_time >= now
|
||||
and lock.active is True
|
||||
and (pair is None or lock.pair == pair)
|
||||
)]
|
||||
return locks
|
||||
|
||||
@staticmethod
|
||||
def unlock_pair(pair: str, now: Optional[datetime] = None) -> None:
|
||||
"""
|
||||
Release all locks for this pair.
|
||||
:param pair: Pair to unlock
|
||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||
defaults to datetime.now(timezone.utc)
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
logger.info(f"Releasing all locks for {pair}.")
|
||||
locks = PairLocks.get_pair_locks(pair, now)
|
||||
for lock in locks:
|
||||
lock.active = False
|
||||
if PairLocks.use_db:
|
||||
PairLock.session.flush()
|
||||
|
||||
@staticmethod
|
||||
def is_global_lock(now: Optional[datetime] = None) -> bool:
|
||||
"""
|
||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||
defaults to datetime.now(timezone.utc)
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
return len(PairLocks.get_pair_locks('*', now)) > 0
|
||||
|
||||
@staticmethod
|
||||
def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool:
|
||||
"""
|
||||
:param pair: Pair to check for
|
||||
:param now: Datetime object (generated via datetime.now(timezone.utc)).
|
||||
defaults to datetime.now(timezone.utc)
|
||||
"""
|
||||
if not now:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now)
|
@@ -5,33 +5,31 @@ from typing import Any, Dict, List
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.btanalysis import (calculate_max_drawdown,
|
||||
combine_dataframes_with_mean,
|
||||
create_cum_profit,
|
||||
extract_trades_of_period,
|
||||
load_trades)
|
||||
from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframes_with_mean,
|
||||
create_cum_profit, extract_trades_of_period, load_trades)
|
||||
from freqtrade.data.converter import trim_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.data.history import get_timerange, load_data
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_prev_date
|
||||
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds
|
||||
from freqtrade.misc import pair_to_filename
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.strategy import IStrategy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
try:
|
||||
from plotly.subplots import make_subplots
|
||||
from plotly.offline import plot
|
||||
import plotly.graph_objects as go
|
||||
from plotly.offline import plot
|
||||
from plotly.subplots import make_subplots
|
||||
except ImportError:
|
||||
logger.exception("Module plotly not found \n Please install using `pip3 install plotly`")
|
||||
exit(1)
|
||||
|
||||
|
||||
def init_plotscript(config):
|
||||
def init_plotscript(config, startup_candles: int = 0):
|
||||
"""
|
||||
Initialize objects needed for plotting
|
||||
:return: Dict with candle (OHLCV) data, trades and pairs
|
||||
@@ -50,9 +48,16 @@ def init_plotscript(config):
|
||||
pairs=pairs,
|
||||
timeframe=config.get('timeframe', '5m'),
|
||||
timerange=timerange,
|
||||
startup_candles=startup_candles,
|
||||
data_format=config.get('dataformat_ohlcv', 'json'),
|
||||
)
|
||||
|
||||
if startup_candles:
|
||||
min_date, max_date = get_timerange(data)
|
||||
logger.info(f"Loading data from {min_date} to {max_date}")
|
||||
timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')),
|
||||
startup_candles, min_date)
|
||||
|
||||
no_trades = False
|
||||
filename = config.get('exportfilename')
|
||||
if config.get('no_trades', False):
|
||||
@@ -74,6 +79,7 @@ def init_plotscript(config):
|
||||
return {"ohlcv": data,
|
||||
"trades": trades,
|
||||
"pairs": pairs,
|
||||
"timerange": timerange,
|
||||
}
|
||||
|
||||
|
||||
@@ -476,7 +482,8 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||
IStrategy.dp = DataProvider(config, exchange)
|
||||
plot_elements = init_plotscript(config)
|
||||
plot_elements = init_plotscript(config, strategy.startup_candle_count)
|
||||
timerange = plot_elements['timerange']
|
||||
trades = plot_elements['trades']
|
||||
pair_counter = 0
|
||||
for pair, data in plot_elements["ohlcv"].items():
|
||||
@@ -484,6 +491,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
logger.info("analyse pair %s", pair)
|
||||
|
||||
df_analyzed = strategy.analyze_ticker(data, {'pair': pair})
|
||||
df_analyzed = trim_dataframe(df_analyzed, timerange)
|
||||
trades_pair = trades.loc[trades['pair'] == pair]
|
||||
trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user