Merge pull request #6134 from freqtrade/new_release
New release 2021.12
This commit is contained in:
commit
b530600718
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@ -5,9 +5,17 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
- package-ecosystem: pip
|
- package-ecosystem: pip
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
target-branch: develop
|
target-branch: develop
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
target-branch: develop
|
||||||
|
80
.github/workflows/ci.yml
vendored
80
.github/workflows/ci.yml
vendored
@ -101,16 +101,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: error
|
||||||
job_name: '*Freqtrade CI ${{ matrix.os }}*'
|
details: Freqtrade CI failed on ${{ matrix.os }}
|
||||||
mention: 'here'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
mention_if: 'failure'
|
|
||||||
channel: '#notifications'
|
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
build_macos:
|
build_macos:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@ -194,17 +191,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: error
|
||||||
job_name: '*Freqtrade CI ${{ matrix.os }}*'
|
details: Test Succeeded!
|
||||||
mention: 'here'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
mention_if: 'failure'
|
|
||||||
channel: '#notifications'
|
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
|
|
||||||
@ -257,16 +250,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: error
|
||||||
job_name: '*Freqtrade CI windows*'
|
details: Test Failed
|
||||||
mention: 'here'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
mention_if: 'failure'
|
|
||||||
channel: '#notifications'
|
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
docs_check:
|
docs_check:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@ -288,14 +278,13 @@ jobs:
|
|||||||
pip install mkdocs
|
pip install mkdocs
|
||||||
mkdocs build
|
mkdocs build
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: error
|
||||||
job_name: '*Freqtrade Docs*'
|
details: Freqtrade doc test failed!
|
||||||
channel: '#notifications'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
cleanup-prior-runs:
|
cleanup-prior-runs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@ -306,7 +295,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|
||||||
# Notify on slack only once - when CI completes (and after deploy) in case it's successfull
|
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||||
notify-complete:
|
notify-complete:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
@ -320,14 +309,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Slack Notification
|
- name: Discord notification
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: always() && steps.check.outputs.has-permission && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: info
|
||||||
job_name: '*Freqtrade CI*'
|
details: Test Completed!
|
||||||
channel: '#notifications'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
||||||
@ -385,7 +373,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: crazy-max/ghaction-docker-buildx@v1
|
uses: crazy-max/ghaction-docker-buildx@v3.3.1
|
||||||
with:
|
with:
|
||||||
buildx-version: latest
|
buildx-version: latest
|
||||||
qemu-version: latest
|
qemu-version: latest
|
||||||
@ -400,17 +388,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
build_helpers/publish_docker_multi.sh
|
build_helpers/publish_docker_multi.sh
|
||||||
|
|
||||||
|
- name: Discord notification
|
||||||
- name: Slack Notification
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
uses: lazy-actions/slatify@v3.0.0
|
|
||||||
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
with:
|
with:
|
||||||
type: ${{ job.status }}
|
severity: info
|
||||||
job_name: '*Freqtrade CI Deploy*'
|
details: Deploy Succeeded!
|
||||||
mention: 'here'
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
mention_if: 'failure'
|
|
||||||
channel: '#notifications'
|
|
||||||
url: ${{ secrets.SLACK_WEBHOOK }}
|
|
||||||
|
|
||||||
|
|
||||||
deploy_arm:
|
deploy_arm:
|
||||||
|
2
.github/workflows/docker_update_readme.yml
vendored
2
.github/workflows/docker_update_readme.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Docker Hub Description
|
- name: Docker Hub Description
|
||||||
uses: peter-evans/dockerhub-description@v2.1.0
|
uses: peter-evans/dockerhub-description@v2.4.3
|
||||||
env:
|
env:
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
55
.travis.yml
55
.travis.yml
@ -1,55 +0,0 @@
|
|||||||
os:
|
|
||||||
- linux
|
|
||||||
dist: bionic
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- 3.8
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- IMAGE_NAME=freqtradeorg/freqtrade
|
|
||||||
install:
|
|
||||||
- cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies; cd ..
|
|
||||||
- export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
|
|
||||||
- export TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
|
||||||
- export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
|
||||||
- pip install -r requirements-dev.txt
|
|
||||||
- pip install -e .
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
include:
|
|
||||||
- stage: tests
|
|
||||||
script:
|
|
||||||
- pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
|
||||||
# Allow failure for coveralls
|
|
||||||
# - coveralls || true
|
|
||||||
name: pytest
|
|
||||||
- script:
|
|
||||||
- cp config_examples/config_bittrex.example.json config.json
|
|
||||||
- freqtrade create-userdir --userdir user_data
|
|
||||||
- freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
|
|
||||||
name: backtest
|
|
||||||
- script:
|
|
||||||
- cp config_examples/config_bittrex.example.json config.json
|
|
||||||
- freqtrade create-userdir --userdir user_data
|
|
||||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily
|
|
||||||
name: hyperopt
|
|
||||||
- script: flake8
|
|
||||||
name: flake8
|
|
||||||
- script:
|
|
||||||
# Test Documentation boxes -
|
|
||||||
# !!! <TYPE>: is not allowed!
|
|
||||||
# !!! <TYPE> "title" - Title needs to be quoted!
|
|
||||||
- grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*; test $? -ne 0
|
|
||||||
name: doc syntax
|
|
||||||
- script: mypy freqtrade scripts
|
|
||||||
name: mypy
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
slack:
|
|
||||||
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
|
||||||
cache:
|
|
||||||
pip: True
|
|
||||||
directories:
|
|
||||||
- $HOME/dependencies
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.22-cp37-cp37m-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.22-cp37-cp37m-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.22-cp38-cp38-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.22-cp38-cp38-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.22-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.22-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
@ -6,13 +6,13 @@ python -m pip install --upgrade pip
|
|||||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||||
|
|
||||||
if ($pyv -eq '3.7') {
|
if ($pyv -eq '3.7') {
|
||||||
pip install build_helpers\TA_Lib-0.4.21-cp37-cp37m-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.22-cp37-cp37m-win_amd64.whl
|
||||||
}
|
}
|
||||||
if ($pyv -eq '3.8') {
|
if ($pyv -eq '3.8') {
|
||||||
pip install build_helpers\TA_Lib-0.4.21-cp38-cp38-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.22-cp38-cp38-win_amd64.whl
|
||||||
}
|
}
|
||||||
if ($pyv -eq '3.9') {
|
if ($pyv -eq '3.9') {
|
||||||
pip install build_helpers\TA_Lib-0.4.21-cp39-cp39-win_amd64.whl
|
pip install build_helpers\TA_Lib-0.4.22-cp39-cp39-win_amd64.whl
|
||||||
}
|
}
|
||||||
|
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
|
@ -13,7 +13,7 @@ A sample of this can be found below, which is identical to the Default Hyperopt
|
|||||||
|
|
||||||
``` python
|
``` python
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ The result of backtesting will confirm if your bot has better odds of making a p
|
|||||||
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
|
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
|
||||||
|
|
||||||
!!! Warning "Using dynamic pairlists for backtesting"
|
!!! Warning "Using dynamic pairlists for backtesting"
|
||||||
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
Using dynamic pairlists is possible (not all of the handlers are allowed to be used in backtest mode), however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
|
||||||
Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed.
|
Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed.
|
||||||
Please read the [pairlists documentation](plugins.md#pairlists) for more information.
|
Please read the [pairlists documentation](plugins.md#pairlists) for more information.
|
||||||
|
|
||||||
|
@ -56,7 +56,11 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
|
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
|
||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||||
|
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
||||||
|
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||||
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
||||||
|
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
||||||
|
|
||||||
* Generate backtest report output
|
* Generate backtest report output
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -126,14 +126,16 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
|
| `exchange.uid` | API uid to use for the exchange. Only required when you are in production mode and for exchanges that use uid for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
||||||
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers). <br> **Datatype:** List
|
||||||
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. 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_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs. <br> **Datatype:** Dict
|
||||||
| `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_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.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.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
|
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
|
||||||
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
| `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
|
| `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
|
||||||
|
@ -324,9 +324,8 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade
|
|||||||
This documents some decisions taken for the CI Pipeline.
|
This documents some decisions taken for the CI Pipeline.
|
||||||
|
|
||||||
* CI runs on all OS variants, Linux (ubuntu), macOS and Windows.
|
* CI runs on all OS variants, Linux (ubuntu), macOS and Windows.
|
||||||
* Docker images are build for the branches `stable` and `develop`.
|
* Docker images are build for the branches `stable` and `develop`, and are built as multiarch builds, supporting multiple platforms via the same tag.
|
||||||
* Docker images containing Plot dependencies are also available as `stable_plot` and `develop_plot`.
|
* Docker images containing Plot dependencies are also available as `stable_plot` and `develop_plot`.
|
||||||
* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:stable_pi` and `develop_pi`.
|
|
||||||
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
|
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
|
||||||
* Full docker image rebuilds are run once a week via schedule.
|
* Full docker image rebuilds are run once a week via schedule.
|
||||||
* Deployments run on ubuntu.
|
* Deployments run on ubuntu.
|
||||||
|
@ -199,6 +199,11 @@ OKEX requires a passphrase for each api key, you will therefore need to add this
|
|||||||
!!! Warning
|
!!! Warning
|
||||||
OKEX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
|
OKEX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode.
|
||||||
|
|
||||||
|
## Gate.io
|
||||||
|
|
||||||
|
Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0).
|
||||||
|
The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value.
|
||||||
|
|
||||||
## All exchanges
|
## All exchanges
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
@ -196,7 +196,7 @@ Trade count is used as a tie breaker.
|
|||||||
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
||||||
Not defining this parameter (or setting it to 0) will use all-time performance.
|
Not defining this parameter (or setting it to 0) will use all-time performance.
|
||||||
|
|
||||||
The optional `min_profit` parameter defines the minimum profit a pair must have to be considered.
|
The optional `min_profit` (as ratio -> a setting of `0.01` corresponds to 1%) parameter defines the minimum profit a pair must have to be considered.
|
||||||
Pairs below this level will be filtered out.
|
Pairs below this level will be filtered out.
|
||||||
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without a way to recover.
|
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without a way to recover.
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ Using this parameter without `minutes` is highly discouraged, as it can lead to
|
|||||||
{
|
{
|
||||||
"method": "PerformanceFilter",
|
"method": "PerformanceFilter",
|
||||||
"minutes": 1440, // rolling 24h
|
"minutes": 1440, // rolling 24h
|
||||||
"min_profit": 0.01
|
"min_profit": 0.01 // minimal profit 1%
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
@ -220,6 +220,9 @@ As this Filter uses past performance of the bot, it'll have some startup-period
|
|||||||
|
|
||||||
Filters low-value coins which would not allow setting stoplosses.
|
Filters low-value coins which would not allow setting stoplosses.
|
||||||
|
|
||||||
|
!!! Warning "Backtesting"
|
||||||
|
`PrecisionFilter` does not support backtesting mode using multiple strategies.
|
||||||
|
|
||||||
#### PriceFilter
|
#### PriceFilter
|
||||||
|
|
||||||
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
||||||
@ -257,7 +260,7 @@ Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 -
|
|||||||
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.
|
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
|
!!! 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.
|
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. ShuffleFilter will automatically detect runmodes and apply the `seed` only for backtesting modes - if a `seed` value is set.
|
||||||
|
|
||||||
#### SpreadFilter
|
#### SpreadFilter
|
||||||
|
|
||||||
|
@ -36,6 +36,10 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito
|
|||||||
|
|
||||||
These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation).
|
These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation).
|
||||||
|
|
||||||
|
!!! Note "ARM64 systems"
|
||||||
|
If you are running an ARM64 system (like a MacOS M1 or an Oracle VM), please use [docker](docker_quickstart.md) to run freqtrade.
|
||||||
|
While native installation is possible with some manual effort, this is not supported at the moment.
|
||||||
|
|
||||||
### Install guide
|
### Install guide
|
||||||
|
|
||||||
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
@ -52,6 +56,10 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
|
|||||||
!!! Note
|
!!! Note
|
||||||
Python3.7 or higher and the corresponding pip are assumed to be available.
|
Python3.7 or higher and the corresponding pip are assumed to be available.
|
||||||
|
|
||||||
|
!!! Warning "Python 3.10 support"
|
||||||
|
Due to issues with dependencies, freqtrade is currently unable to support python 3.10.
|
||||||
|
We're working on supporting python 3.10, are however dependant on support from dependencies.
|
||||||
|
|
||||||
=== "Debian/Ubuntu"
|
=== "Debian/Ubuntu"
|
||||||
#### Install necessary dependencies
|
#### Install necessary dependencies
|
||||||
|
|
||||||
|
110
docs/plotting.md
110
docs/plotting.md
@ -164,16 +164,17 @@ The resulting plot will have the following elements:
|
|||||||
|
|
||||||
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
||||||
|
|
||||||
Additional features when using plot_config include:
|
Additional features when using `plot_config` include:
|
||||||
|
|
||||||
* Specify colors per indicator
|
* Specify colors per indicator
|
||||||
* Specify additional subplots
|
* Specify additional subplots
|
||||||
* Specify indicator pairs to fill area in between
|
* Specify indicator pairs to fill area in between
|
||||||
|
|
||||||
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
||||||
It also allows multiple subplots to display both MACD and RSI at the same time.
|
It also allows multiple subplots to display both MACD and RSI at the same time.
|
||||||
|
|
||||||
Plot type can be configured using `type` key. Possible types are:
|
Plot type can be configured using `type` key. Possible types are:
|
||||||
|
|
||||||
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
||||||
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
||||||
|
|
||||||
@ -182,40 +183,89 @@ Extra parameters to `plotly.graph_objects.*` constructor can be specified in `pl
|
|||||||
Sample configuration with inline comments explaining the process:
|
Sample configuration with inline comments explaining the process:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
plot_config = {
|
@property
|
||||||
'main_plot': {
|
def plot_config(self):
|
||||||
# Configuration for main plot indicators.
|
"""
|
||||||
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
There are a lot of solutions how to build the return dictionary.
|
||||||
'ema10': {'color': 'red'},
|
The only important point is the return value.
|
||||||
'ema50': {'color': '#CCCCCC'},
|
Example:
|
||||||
# By omitting color, a random color is selected.
|
plot_config = {'main_plot': {}, 'subplots': {}}
|
||||||
'sar': {},
|
|
||||||
# fill area between senkou_a and senkou_b
|
"""
|
||||||
'senkou_a': {
|
plot_config = {}
|
||||||
'color': 'green', #optional
|
plot_config['main_plot'] = {
|
||||||
'fill_to': 'senkou_b',
|
# Configuration for main plot indicators.
|
||||||
'fill_label': 'Ichimoku Cloud', #optional
|
# Assumes 2 parameters, emashort and emalong to be specified.
|
||||||
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
f'ema_{self.emashort.value}': {'color': 'red'},
|
||||||
},
|
f'ema_{self.emalong.value}': {'color': '#CCCCCC'},
|
||||||
# plot senkou_b, too. Not only the area to it.
|
# By omitting color, a random color is selected.
|
||||||
'senkou_b': {}
|
'sar': {},
|
||||||
|
# fill area between senkou_a and senkou_b
|
||||||
|
'senkou_a': {
|
||||||
|
'color': 'green', #optional
|
||||||
|
'fill_to': 'senkou_b',
|
||||||
|
'fill_label': 'Ichimoku Cloud', #optional
|
||||||
|
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||||
},
|
},
|
||||||
'subplots': {
|
# plot senkou_b, too. Not only the area to it.
|
||||||
# Create subplot MACD
|
'senkou_b': {}
|
||||||
"MACD": {
|
}
|
||||||
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
plot_config['subplots'] = {
|
||||||
'macdsignal': {'color': 'orange'},
|
# Create subplot MACD
|
||||||
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
"MACD": {
|
||||||
},
|
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||||
# Additional subplot RSI
|
'macdsignal': {'color': 'orange'},
|
||||||
"RSI": {
|
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||||
'rsi': {'color': 'red'}
|
},
|
||||||
}
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return plot_config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? Note "As attribute (former method)"
|
||||||
|
Assigning plot_config is also possible as Attribute (this used to be the default way).
|
||||||
|
This has the disadvantage that strategy parameters are not available, preventing certain configurations from working.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
plot_config = {
|
||||||
|
'main_plot': {
|
||||||
|
# Configuration for main plot indicators.
|
||||||
|
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
||||||
|
'ema10': {'color': 'red'},
|
||||||
|
'ema50': {'color': '#CCCCCC'},
|
||||||
|
# By omitting color, a random color is selected.
|
||||||
|
'sar': {},
|
||||||
|
# fill area between senkou_a and senkou_b
|
||||||
|
'senkou_a': {
|
||||||
|
'color': 'green', #optional
|
||||||
|
'fill_to': 'senkou_b',
|
||||||
|
'fill_label': 'Ichimoku Cloud', #optional
|
||||||
|
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||||
|
},
|
||||||
|
# plot senkou_b, too. Not only the area to it.
|
||||||
|
'senkou_b': {}
|
||||||
|
},
|
||||||
|
'subplots': {
|
||||||
|
# Create subplot MACD
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||||
|
},
|
||||||
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
||||||
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.3
|
mkdocs==1.2.3
|
||||||
mkdocs-material==7.3.6
|
mkdocs-material==8.1.3
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.1
|
pymdown-extensions==9.1
|
||||||
|
@ -127,6 +127,21 @@ The provided exit-tag is then used as sell-reason - and shown as such in backtes
|
|||||||
!!! Note
|
!!! Note
|
||||||
`sell_reason` is limited to 100 characters, remaining data will be truncated.
|
`sell_reason` is limited to 100 characters, remaining data will be truncated.
|
||||||
|
|
||||||
|
## Strategy version
|
||||||
|
|
||||||
|
You can implement custom strategy versioning by using the "version" method, and returning the version you would like this strategy to have.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def version(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns version of the strategy.
|
||||||
|
"""
|
||||||
|
return "1.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
You should make sure to implement proper version control (like a git repository) alongside this, as freqtrade will not keep historic versions of your strategy, so it's up to the user to be able to eventually roll back to a prior version of the strategy.
|
||||||
|
|
||||||
## Derived strategies
|
## Derived strategies
|
||||||
|
|
||||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||||
|
@ -387,8 +387,10 @@ class AwesomeStrategy(IStrategy):
|
|||||||
**Example**:
|
**Example**:
|
||||||
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
||||||
|
|
||||||
!!! Warning "No backtesting support"
|
!!! Warning "Backtesting"
|
||||||
Custom entry-prices are currently not supported during backtesting.
|
While Custom prices are supported in backtesting (starting with 2021.12), prices will be moved to within the candle's high/low prices.
|
||||||
|
This behavior is currently being tested, and might be changed at a later point.
|
||||||
|
`custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices.
|
||||||
|
|
||||||
## Custom order timeout rules
|
## Custom order timeout rules
|
||||||
|
|
||||||
|
@ -50,7 +50,9 @@ candles.head()
|
|||||||
```python
|
```python
|
||||||
# Load strategy using values set above
|
# Load strategy using values set above
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
strategy = StrategyResolver.load_strategy(config)
|
strategy = StrategyResolver.load_strategy(config)
|
||||||
|
strategy.dp = DataProvider(config, None, None)
|
||||||
|
|
||||||
# Generate buy/sell signals using strategy
|
# Generate buy/sell signals using strategy
|
||||||
df = strategy.analyze_ticker(candles, {'pair': pair})
|
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||||
@ -228,7 +230,7 @@ graph = generate_candlestick_graph(pair=pair,
|
|||||||
# Show graph inline
|
# Show graph inline
|
||||||
# graph.show()
|
# graph.show()
|
||||||
|
|
||||||
# Render graph in a separate window
|
# Render graph in a seperate window
|
||||||
graph.show(renderer="browser")
|
graph.show(renderer="browser")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -50,7 +50,7 @@ Sample configuration (tested using IFTTT).
|
|||||||
|
|
||||||
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert your event and key to the url.
|
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert your event and key to the url.
|
||||||
|
|
||||||
You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use `"format": "form"` or `"format": "json"` respectively. Example configuration for Mattermost Cloud integration:
|
You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw data. Use `"format": "form"`, `"format": "json"`, or `"format": "raw"` respectively. Example configuration for Mattermost Cloud integration:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"webhook": {
|
"webhook": {
|
||||||
@ -63,7 +63,36 @@ You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
The result would be POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
|
The result would be a POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
|
||||||
|
|
||||||
|
When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be ouput in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"webhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://<YOURHOOKURL>",
|
||||||
|
"format": "raw",
|
||||||
|
"webhookstatus": {
|
||||||
|
"data": "Status: {status}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header.
|
||||||
|
|
||||||
|
Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"webhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://<YOURHOOKURL>",
|
||||||
|
"retries": 3,
|
||||||
|
"retry_delay": 0.2,
|
||||||
|
"webhookstatus": {
|
||||||
|
"status": "Status: {status}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
||||||
|
|
||||||
@ -75,11 +104,13 @@ Possible parameters are:
|
|||||||
* `trade_id`
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
* `limit`
|
* ~~`limit` # Deprecated - should no longer be used.~~
|
||||||
|
* `open_rate`
|
||||||
* `amount`
|
* `amount`
|
||||||
* `open_date`
|
* `open_date`
|
||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
@ -98,6 +129,7 @@ Possible parameters are:
|
|||||||
* `open_date`
|
* `open_date`
|
||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
* `current_rate`
|
* `current_rate`
|
||||||
@ -116,7 +148,10 @@ Possible parameters are:
|
|||||||
* `open_date`
|
* `open_date`
|
||||||
* `stake_amount`
|
* `stake_amount`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
|
* `order_type`
|
||||||
|
* `current_rate`
|
||||||
* `buy_tag`
|
* `buy_tag`
|
||||||
|
|
||||||
### Webhooksell
|
### Webhooksell
|
||||||
@ -134,6 +169,7 @@ Possible parameters are:
|
|||||||
* `profit_amount`
|
* `profit_amount`
|
||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `sell_reason`
|
* `sell_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
@ -156,6 +192,7 @@ Possible parameters are:
|
|||||||
* `profit_amount`
|
* `profit_amount`
|
||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `sell_reason`
|
* `sell_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
@ -178,6 +215,7 @@ Possible parameters are:
|
|||||||
* `profit_amount`
|
* `profit_amount`
|
||||||
* `profit_ratio`
|
* `profit_ratio`
|
||||||
* `stake_currency`
|
* `stake_currency`
|
||||||
|
* `base_currency`
|
||||||
* `fiat_currency`
|
* `fiat_currency`
|
||||||
* `sell_reason`
|
* `sell_reason`
|
||||||
* `order_type`
|
* `order_type`
|
||||||
|
@ -23,9 +23,9 @@ git clone https://github.com/freqtrade/freqtrade.git
|
|||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.21-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
|
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib‑0.4.22‑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.
|
Freqtrade provides these dependencies for the latest 3 Python versions (3.7, 3.8 and 3.9) and for 64bit Windows.
|
||||||
Other versions must be downloaded from the above link.
|
Other versions must be downloaded from the above link.
|
||||||
|
|
||||||
``` powershell
|
``` powershell
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2021.11'
|
__version__ = '2021.12'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if __version__ == 'develop':
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from cachetools.ttl import TTLCache
|
from cachetools import TTLCache
|
||||||
|
|
||||||
|
|
||||||
class PeriodicCache(TTLCache):
|
class PeriodicCache(TTLCache):
|
||||||
|
@ -50,6 +50,8 @@ USERPATH_STRATEGIES = 'strategies'
|
|||||||
USERPATH_NOTEBOOKS = 'notebooks'
|
USERPATH_NOTEBOOKS = 'notebooks'
|
||||||
|
|
||||||
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
|
||||||
|
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
|
||||||
|
|
||||||
ENV_VAR_PREFIX = 'FREQTRADE__'
|
ENV_VAR_PREFIX = 'FREQTRADE__'
|
||||||
|
|
||||||
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
|
||||||
@ -312,10 +314,16 @@ CONF_SCHEMA = {
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'enabled': {'type': 'boolean'},
|
'enabled': {'type': 'boolean'},
|
||||||
|
'url': {'type': 'string'},
|
||||||
|
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
|
||||||
|
'retries': {'type': 'integer', 'minimum': 0},
|
||||||
|
'retry_delay': {'type': 'number', 'minimum': 0},
|
||||||
'webhookbuy': {'type': 'object'},
|
'webhookbuy': {'type': 'object'},
|
||||||
'webhookbuycancel': {'type': 'object'},
|
'webhookbuycancel': {'type': 'object'},
|
||||||
|
'webhookbuyfill': {'type': 'object'},
|
||||||
'webhooksell': {'type': 'object'},
|
'webhooksell': {'type': 'object'},
|
||||||
'webhooksellcancel': {'type': 'object'},
|
'webhooksellcancel': {'type': 'object'},
|
||||||
|
'webhooksellfill': {'type': 'object'},
|
||||||
'webhookstatus': {'type': 'object'},
|
'webhookstatus': {'type': 'object'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -387,6 +395,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
},
|
},
|
||||||
|
'unknown_fee_rate': {'type': 'number'},
|
||||||
'outdated_offset': {'type': 'integer', 'minimum': 1},
|
'outdated_offset': {'type': 'integer', 'minimum': 1},
|
||||||
'markets_refresh_interval': {'type': 'integer'},
|
'markets_refresh_interval': {'type': 'integer'},
|
||||||
'ccxt_config': {'type': 'object'},
|
'ccxt_config': {'type': 'object'},
|
||||||
|
@ -6,7 +6,6 @@ from typing import List, Optional
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade import misc
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
|
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
|
||||||
ListPairsWithTimeframes, TradeList)
|
ListPairsWithTimeframes, TradeList)
|
||||||
@ -61,10 +60,10 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
|
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||||
|
|
||||||
ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc')
|
_data.loc[:, self._columns].to_hdf(
|
||||||
ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date'])
|
filename, key, mode='a', complevel=9, complib='blosc',
|
||||||
|
format='table', data_columns=['date']
|
||||||
ds.close()
|
)
|
||||||
|
|
||||||
def _ohlcv_load(self, pair: str, timeframe: str,
|
def _ohlcv_load(self, pair: str, timeframe: str,
|
||||||
timerange: Optional[TimeRange] = None) -> pd.DataFrame:
|
timerange: Optional[TimeRange] = None) -> pd.DataFrame:
|
||||||
@ -99,19 +98,6 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
'low': 'float', 'close': 'float', 'volume': 'float'})
|
'low': 'float', 'close': 'float', 'volume': 'float'})
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
|
||||||
"""
|
|
||||||
Remove data for this pair
|
|
||||||
:param pair: Delete data for this pair.
|
|
||||||
:param timeframe: Timeframe (e.g. "5m")
|
|
||||||
:return: True when deleted, false if file did not exist.
|
|
||||||
"""
|
|
||||||
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
|
def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
Append data to existing data structures
|
Append data to existing data structures
|
||||||
@ -142,11 +128,11 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
key = self._pair_trades_key(pair)
|
key = self._pair_trades_key(pair)
|
||||||
|
|
||||||
ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair),
|
pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf(
|
||||||
mode='a', complevel=9, complib='blosc')
|
self._pair_trades_filename(self._datadir, pair), key,
|
||||||
ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
|
mode='a', complevel=9, complib='blosc',
|
||||||
format='table', data_columns=['timestamp'])
|
format='table', data_columns=['timestamp']
|
||||||
ds.close()
|
)
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: TradeList):
|
||||||
"""
|
"""
|
||||||
@ -180,17 +166,9 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||||
return trades.values.tolist()
|
return trades.values.tolist()
|
||||||
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
@classmethod
|
||||||
"""
|
def _get_file_extension(cls):
|
||||||
Remove data for this pair
|
return "h5"
|
||||||
:param pair: Delete data for this pair.
|
|
||||||
:return: True when deleted, false if file did not exist.
|
|
||||||
"""
|
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
|
||||||
@ -199,15 +177,3 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _pair_trades_key(cls, pair: str) -> str:
|
def _pair_trades_key(cls, pair: str) -> str:
|
||||||
return f"{pair}/trades"
|
return f"{pair}/trades"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.h5')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-trades.h5')
|
|
||||||
return filename
|
|
||||||
|
@ -12,6 +12,7 @@ from typing import List, Optional, Type
|
|||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
||||||
@ -26,6 +27,13 @@ class IDataHandler(ABC):
|
|||||||
def __init__(self, datadir: Path) -> None:
|
def __init__(self, datadir: Path) -> None:
|
||||||
self._datadir = datadir
|
self._datadir = datadir
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_file_extension(cls) -> str:
|
||||||
|
"""
|
||||||
|
Get file extension for this particular datahandler
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@abstractclassmethod
|
@abstractclassmethod
|
||||||
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
|
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
@ -70,7 +78,6 @@ class IDataHandler(ABC):
|
|||||||
:return: DataFrame with ohlcv data, or empty DataFrame
|
:return: DataFrame with ohlcv data, or empty DataFrame
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Remove data for this pair
|
Remove data for this pair
|
||||||
@ -78,6 +85,11 @@ class IDataHandler(ABC):
|
|||||||
:param timeframe: Timeframe (e.g. "5m")
|
:param timeframe: Timeframe (e.g. "5m")
|
||||||
:return: True when deleted, false if file did not exist.
|
:return: True when deleted, false if file did not exist.
|
||||||
"""
|
"""
|
||||||
|
filename = self._pair_data_filename(self._datadir, pair, timeframe)
|
||||||
|
if filename.exists():
|
||||||
|
filename.unlink()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
|
||||||
@ -123,13 +135,17 @@ class IDataHandler(ABC):
|
|||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
def trades_purge(self, pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Remove data for this pair
|
Remove data for this pair
|
||||||
:param pair: Delete data for this pair.
|
:param pair: Delete data for this pair.
|
||||||
:return: True when deleted, false if file did not exist.
|
:return: True when deleted, false if file did not exist.
|
||||||
"""
|
"""
|
||||||
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
if filename.exists():
|
||||||
|
filename.unlink()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
||||||
"""
|
"""
|
||||||
@ -141,6 +157,18 @@ class IDataHandler(ABC):
|
|||||||
"""
|
"""
|
||||||
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
||||||
|
pair_s = misc.pair_to_filename(pair)
|
||||||
|
filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}')
|
||||||
|
return filename
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
||||||
|
pair_s = misc.pair_to_filename(pair)
|
||||||
|
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
||||||
|
return filename
|
||||||
|
|
||||||
def ohlcv_load(self, pair, timeframe: str,
|
def ohlcv_load(self, pair, timeframe: str,
|
||||||
timerange: Optional[TimeRange] = None,
|
timerange: Optional[TimeRange] = None,
|
||||||
fill_missing: bool = True,
|
fill_missing: bool = True,
|
||||||
|
@ -174,34 +174,10 @@ class JsonDataHandler(IDataHandler):
|
|||||||
pass
|
pass
|
||||||
return tradesdata
|
return tradesdata
|
||||||
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
|
||||||
"""
|
|
||||||
Remove data for this pair
|
|
||||||
:param pair: Delete data for this pair.
|
|
||||||
:return: True when deleted, false if file did not exist.
|
|
||||||
"""
|
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
|
||||||
if filename.exists():
|
|
||||||
filename.unlink()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
return "json.gz" if cls._use_zip else "json"
|
return "json.gz" if cls._use_zip else "json"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
|
|
||||||
pair_s = misc.pair_to_filename(pair)
|
|
||||||
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
|
|
||||||
return filename
|
|
||||||
|
|
||||||
|
|
||||||
class JsonGzDataHandler(JsonDataHandler):
|
class JsonGzDataHandler(JsonDataHandler):
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# flake8: noqa: F401
|
# flake8: noqa: F401
|
||||||
from freqtrade.enums.backteststate import BacktestState
|
from freqtrade.enums.backteststate import BacktestState
|
||||||
|
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||||
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||||
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
|
||||||
from freqtrade.enums.selltype import SellType
|
from freqtrade.enums.selltype import SellType
|
||||||
|
6
freqtrade/enums/ordertypevalue.py
Normal file
6
freqtrade/enums/ordertypevalue.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class OrderTypeValues(str, Enum):
|
||||||
|
limit = 'limit'
|
||||||
|
market = 'market'
|
@ -5,6 +5,7 @@ from freqtrade.exchange.exchange import Exchange
|
|||||||
# isort: on
|
# isort: on
|
||||||
from freqtrade.exchange.bibox import Bibox
|
from freqtrade.exchange.bibox import Bibox
|
||||||
from freqtrade.exchange.binance import Binance
|
from freqtrade.exchange.binance import Binance
|
||||||
|
from freqtrade.exchange.bitpanda import Bitpanda
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
from freqtrade.exchange.bybit import Bybit
|
from freqtrade.exchange.bybit import Bybit
|
||||||
from freqtrade.exchange.coinbasepro import Coinbasepro
|
from freqtrade.exchange.coinbasepro import Coinbasepro
|
||||||
|
37
freqtrade/exchange/bitpanda.py
Normal file
37
freqtrade/exchange/bitpanda.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
""" Bitpanda exchange subclass """
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Bitpanda(Exchange):
|
||||||
|
"""
|
||||||
|
Bitpanda exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
|
with this exchange.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||||
|
params: Optional[Dict] = None) -> List:
|
||||||
|
"""
|
||||||
|
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
|
||||||
|
The "since" argument passed in is coming from the database and is in UTC,
|
||||||
|
as timezone-native datetime object.
|
||||||
|
From the python documentation:
|
||||||
|
> Naive datetime instances are assumed to represent local time
|
||||||
|
Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
|
||||||
|
transformation from local timezone to UTC.
|
||||||
|
This works for timezones UTC+ since then the result will contain trades from a few hours
|
||||||
|
instead of from the last 5 seconds, however fails for UTC- timezones,
|
||||||
|
since we're then asking for trades with a "since" argument in the future.
|
||||||
|
|
||||||
|
:param order_id order_id: Order-id as given when creating the order
|
||||||
|
:param pair: Pair the order is for
|
||||||
|
:param since: datetime object of the order creation time. Assumes object is in UTC.
|
||||||
|
"""
|
||||||
|
params = {'to': int(datetime.now(timezone.utc).timestamp() * 1000)}
|
||||||
|
return super().get_trades_for_order(order_id, pair, since, params)
|
@ -685,16 +685,20 @@ class Exchange:
|
|||||||
if not self.exchange_has('fetchL2OrderBook'):
|
if not self.exchange_has('fetchL2OrderBook'):
|
||||||
return True
|
return True
|
||||||
ob = self.fetch_l2_order_book(pair, 1)
|
ob = self.fetch_l2_order_book(pair, 1)
|
||||||
if side == 'buy':
|
try:
|
||||||
price = ob['asks'][0][0]
|
if side == 'buy':
|
||||||
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
price = ob['asks'][0][0]
|
||||||
if limit >= price:
|
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
||||||
return True
|
if limit >= price:
|
||||||
else:
|
return True
|
||||||
price = ob['bids'][0][0]
|
else:
|
||||||
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
price = ob['bids'][0][0]
|
||||||
if limit <= price:
|
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
||||||
return True
|
if limit <= price:
|
||||||
|
return True
|
||||||
|
except IndexError:
|
||||||
|
# Ignore empty orderbooks when filling - can be filled with the next iteration.
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
|
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@ -1087,7 +1091,8 @@ class Exchange:
|
|||||||
# Fee handling
|
# Fee handling
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||||
|
params: Optional[Dict] = None) -> List:
|
||||||
"""
|
"""
|
||||||
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
|
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
|
||||||
The "since" argument passed in is coming from the database and is in UTC,
|
The "since" argument passed in is coming from the database and is in UTC,
|
||||||
@ -1111,8 +1116,10 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||||
# since needs to be int in milliseconds
|
# since needs to be int in milliseconds
|
||||||
|
_params = params if params else {}
|
||||||
my_trades = self._api.fetch_my_trades(
|
my_trades = self._api.fetch_my_trades(
|
||||||
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
|
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
|
||||||
|
params=_params)
|
||||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||||
|
|
||||||
self._log_exchange_response('get_trades_for_order', matched_trades)
|
self._log_exchange_response('get_trades_for_order', matched_trades)
|
||||||
@ -1190,9 +1197,11 @@ class Exchange:
|
|||||||
tick = self.fetch_ticker(comb)
|
tick = self.fetch_ticker(comb)
|
||||||
|
|
||||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
||||||
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
|
|
||||||
except ExchangeError:
|
except ExchangeError:
|
||||||
return None
|
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
|
||||||
|
if not fee_to_quote_rate:
|
||||||
|
return None
|
||||||
|
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
|
||||||
|
|
||||||
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
|
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
|
||||||
"""
|
"""
|
||||||
@ -1263,7 +1272,7 @@ class Exchange:
|
|||||||
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
results = await asyncio.gather(*input_coro, return_exceptions=True)
|
||||||
for res in results:
|
for res in results:
|
||||||
if isinstance(res, Exception):
|
if isinstance(res, Exception):
|
||||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||||
if raise_:
|
if raise_:
|
||||||
raise
|
raise
|
||||||
continue
|
continue
|
||||||
@ -1294,7 +1303,7 @@ class Exchange:
|
|||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
# Gather coroutines to run
|
# Gather coroutines to run
|
||||||
for pair, timeframe in set(pair_list):
|
for pair, timeframe in set(pair_list):
|
||||||
if ((pair, timeframe) not in self._klines
|
if ((pair, timeframe) not in self._klines or not cache
|
||||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||||
if not since_ms and self.required_candle_call_count > 1:
|
if not since_ms and self.required_candle_call_count > 1:
|
||||||
# Multiple calls for one pair - to get more history
|
# Multiple calls for one pair - to get more history
|
||||||
@ -1317,27 +1326,30 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
cached_pairs.append((pair, timeframe))
|
cached_pairs.append((pair, timeframe))
|
||||||
|
|
||||||
results = asyncio.get_event_loop().run_until_complete(
|
|
||||||
asyncio.gather(*input_coroutines, return_exceptions=True))
|
|
||||||
|
|
||||||
results_df = {}
|
results_df = {}
|
||||||
# handle caching
|
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
|
||||||
for res in results:
|
for input_coro in chunks(input_coroutines, 100):
|
||||||
if isinstance(res, Exception):
|
results = asyncio.get_event_loop().run_until_complete(
|
||||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
asyncio.gather(*input_coro, return_exceptions=True))
|
||||||
continue
|
|
||||||
# Deconstruct tuple (has 3 elements)
|
# handle caching
|
||||||
pair, timeframe, ticks = res
|
for res in results:
|
||||||
# keeping last candle time as last refreshed time of the pair
|
if isinstance(res, Exception):
|
||||||
if ticks:
|
logger.warning(f"Async code raised an exception: {repr(res)}")
|
||||||
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
continue
|
||||||
# keeping parsed dataframe in cache
|
# Deconstruct tuple (has 3 elements)
|
||||||
ohlcv_df = ohlcv_to_dataframe(
|
pair, timeframe, ticks = res
|
||||||
ticks, timeframe, pair=pair, fill_missing=True,
|
# keeping last candle time as last refreshed time of the pair
|
||||||
drop_incomplete=self._ohlcv_partial_candle)
|
if ticks:
|
||||||
results_df[(pair, timeframe)] = ohlcv_df
|
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
|
||||||
if cache:
|
# keeping parsed dataframe in cache
|
||||||
self._klines[(pair, timeframe)] = ohlcv_df
|
ohlcv_df = ohlcv_to_dataframe(
|
||||||
|
ticks, timeframe, pair=pair, fill_missing=True,
|
||||||
|
drop_incomplete=self._ohlcv_partial_candle)
|
||||||
|
results_df[(pair, timeframe)] = ohlcv_df
|
||||||
|
if cache:
|
||||||
|
self._klines[(pair, timeframe)] = ohlcv_df
|
||||||
|
|
||||||
# Return cached klines
|
# Return cached klines
|
||||||
for pair, timeframe in cached_pairs:
|
for pair, timeframe in cached_pairs:
|
||||||
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)
|
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)
|
||||||
|
@ -278,7 +278,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.")
|
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.")
|
||||||
self.update_trade_state(trade, order.order_id,
|
self.update_trade_state(trade, order.order_id,
|
||||||
stoploss_order=order.ft_order_side == 'stoploss')
|
stoploss_order=order.ft_order_side == 'stoploss',
|
||||||
|
send_msg=False)
|
||||||
|
|
||||||
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -286,7 +287,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order = trade.select_order('buy', False)
|
order = trade.select_order('buy', False)
|
||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
||||||
self.update_trade_state(trade, order.order_id)
|
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||||
|
|
||||||
def handle_insufficient_funds(self, trade: Trade):
|
def handle_insufficient_funds(self, trade: Trade):
|
||||||
"""
|
"""
|
||||||
@ -308,7 +309,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order = trade.select_order('buy', False)
|
order = trade.select_order('buy', False)
|
||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
||||||
self.update_trade_state(trade, order.order_id)
|
self.update_trade_state(trade, order.order_id, send_msg=False)
|
||||||
|
|
||||||
def refind_lost_order(self, trade):
|
def refind_lost_order(self, trade):
|
||||||
"""
|
"""
|
||||||
@ -466,8 +467,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *,
|
||||||
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
|
ordertype: Optional[str] = None, buy_tag: Optional[str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a limit buy for the given pair
|
Executes a limit buy for the given pair
|
||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
@ -510,10 +511,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"{stake_amount} ...")
|
f"{stake_amount} ...")
|
||||||
|
|
||||||
amount = stake_amount / enter_limit_requested
|
amount = stake_amount / enter_limit_requested
|
||||||
order_type = self.strategy.order_types['buy']
|
order_type = ordertype or self.strategy.order_types['buy']
|
||||||
if forcebuy:
|
|
||||||
# Forcebuy can define a different ordertype
|
|
||||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||||
@ -581,10 +579,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
trade.orders.append(order_obj)
|
trade.orders.append(order_obj)
|
||||||
|
|
||||||
# Update fees if order is closed
|
|
||||||
if order_status == 'closed':
|
|
||||||
self.update_trade_state(trade, order_id, order)
|
|
||||||
|
|
||||||
Trade.query.session.add(trade)
|
Trade.query.session.add(trade)
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
@ -593,19 +587,25 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
self._notify_enter(trade, order_type)
|
self._notify_enter(trade, order_type)
|
||||||
|
|
||||||
|
# Update fees if order is closed
|
||||||
|
if order_status == 'closed':
|
||||||
|
self.update_trade_state(trade, order_id, order)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_enter(self, trade: Trade, order_type: str) -> None:
|
def _notify_enter(self, trade: Trade, order_type: Optional[str] = None,
|
||||||
|
fill: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy occurred.
|
Sends rpc notification when a buy occurred.
|
||||||
"""
|
"""
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY,
|
'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.buy_tag,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'limit': trade.open_rate,
|
'limit': trade.open_rate, # Deprecated (?)
|
||||||
|
'open_rate': trade.open_rate,
|
||||||
'order_type': order_type,
|
'order_type': order_type,
|
||||||
'stake_amount': trade.stake_amount,
|
'stake_amount': trade.stake_amount,
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
@ -644,22 +644,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Send the message
|
# Send the message
|
||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
|
||||||
msg = {
|
|
||||||
'trade_id': trade.id,
|
|
||||||
'type': RPCMessageType.BUY_FILL,
|
|
||||||
'buy_tag': trade.buy_tag,
|
|
||||||
'exchange': self.exchange.name.capitalize(),
|
|
||||||
'pair': trade.pair,
|
|
||||||
'open_rate': trade.open_rate,
|
|
||||||
'stake_amount': trade.stake_amount,
|
|
||||||
'stake_currency': self.config['stake_currency'],
|
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
|
||||||
'amount': trade.amount,
|
|
||||||
'open_date': trade.open_date,
|
|
||||||
}
|
|
||||||
self.rpc.send_msg(msg)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# SELL / exit positions / close trades logic and methods
|
# SELL / exit positions / close trades logic and methods
|
||||||
#
|
#
|
||||||
@ -682,7 +666,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trades_closed += 1
|
trades_closed += 1
|
||||||
|
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning('Unable to sell trade %s: %s', trade.pair, exception)
|
logger.warning(f'Unable to sell trade {trade.pair}: {exception}')
|
||||||
|
|
||||||
# Updating wallets if any trade occurred
|
# Updating wallets if any trade occurred
|
||||||
if trades_closed:
|
if trades_closed:
|
||||||
@ -868,7 +852,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(
|
logger.info(
|
||||||
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
|
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
|
||||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||||
self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag)
|
self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag=exit_tag)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -926,8 +910,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if max_timeouts > 0 and canceled_count >= max_timeouts:
|
if max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||||
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
||||||
f'timed out {max_timeouts} times.')
|
f'timed out {max_timeouts} times.')
|
||||||
self.execute_trade_exit(trade, order.get('price'), sell_reason=SellCheckTuple(
|
try:
|
||||||
sell_type=SellType.EMERGENCY_SELL))
|
self.execute_trade_exit(
|
||||||
|
trade, order.get('price'),
|
||||||
|
sell_reason=SellCheckTuple(sell_type=SellType.EMERGENCY_SELL))
|
||||||
|
except DependencyException as exception:
|
||||||
|
logger.warning(f'Unable to emergency sell trade {trade.pair}: {exception}')
|
||||||
|
|
||||||
def cancel_all_open_orders(self) -> None:
|
def cancel_all_open_orders(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1081,7 +1069,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade: Trade,
|
trade: Trade,
|
||||||
limit: float,
|
limit: float,
|
||||||
sell_reason: SellCheckTuple,
|
sell_reason: SellCheckTuple,
|
||||||
exit_tag: Optional[str] = None) -> bool:
|
*,
|
||||||
|
exit_tag: Optional[str] = None,
|
||||||
|
ordertype: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a trade exit for the given trade and limit
|
Executes a trade exit for the given trade and limit
|
||||||
:param trade: Trade instance
|
:param trade: Trade instance
|
||||||
@ -1119,14 +1110,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
order_type = self.strategy.order_types[sell_type]
|
order_type = ordertype or self.strategy.order_types[sell_type]
|
||||||
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
||||||
# Emergency sells (default to market!)
|
# Emergency sells (default to market!)
|
||||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||||
if sell_reason.sell_type == SellType.FORCE_SELL:
|
|
||||||
# Force sells (default to the sell_type defined in the strategy,
|
|
||||||
# but we allow this value to be changed)
|
|
||||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
|
||||||
|
|
||||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
@ -1158,16 +1145,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.sell_order_status = ''
|
trade.sell_order_status = ''
|
||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
trade.sell_reason = exit_tag or sell_reason.sell_reason
|
trade.sell_reason = exit_tag or sell_reason.sell_reason
|
||||||
# In case of market sell orders the order can be closed immediately
|
|
||||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
|
||||||
self.update_trade_state(trade, trade.open_order_id, order)
|
|
||||||
Trade.commit()
|
|
||||||
|
|
||||||
# Lock pair for one candle to prevent immediate re-buys
|
# Lock pair for one candle to prevent immediate re-buys
|
||||||
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
|
||||||
reason='Auto lock')
|
reason='Auto lock')
|
||||||
|
|
||||||
self._notify_exit(trade, order_type)
|
self._notify_exit(trade, order_type)
|
||||||
|
# In case of market sell orders the order can be closed immediately
|
||||||
|
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||||
|
self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
|
Trade.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -1264,13 +1251,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
#
|
#
|
||||||
|
|
||||||
def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
|
def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
|
||||||
stoploss_order: bool = False) -> bool:
|
stoploss_order: bool = False, send_msg: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks trades with open orders and updates the amount if necessary
|
Checks trades with open orders and updates the amount if necessary
|
||||||
Handles closing both buy and sell orders.
|
Handles closing both buy and sell orders.
|
||||||
:param trade: Trade object of the trade we're analyzing
|
:param trade: Trade object of the trade we're analyzing
|
||||||
:param order_id: Order-id of the order we're analyzing
|
:param order_id: Order-id of the order we're analyzing
|
||||||
:param action_order: Already acquired order object
|
:param action_order: Already acquired order object
|
||||||
|
:param send_msg: Send notification - should always be True except in "recovery" methods
|
||||||
:return: True if order has been cancelled without being filled partially, False otherwise
|
:return: True if order has been cancelled without being filled partially, False otherwise
|
||||||
"""
|
"""
|
||||||
if not order_id:
|
if not order_id:
|
||||||
@ -1310,13 +1298,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# Updating wallets when order is closed
|
# Updating wallets when order is closed
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
if not stoploss_order and not trade.open_order_id:
|
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||||
self._notify_exit(trade, '', True)
|
self._notify_exit(trade, '', True)
|
||||||
self.handle_protections(trade.pair)
|
self.handle_protections(trade.pair)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
elif not trade.open_order_id:
|
elif send_msg and not trade.open_order_id:
|
||||||
# Buy fill
|
# Buy fill
|
||||||
self._notify_enter_fill(trade)
|
self._notify_enter(trade, fill=True)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -342,10 +342,7 @@ class Backtesting:
|
|||||||
# use Open rate if open_rate > calculated sell rate
|
# use Open rate if open_rate > calculated sell rate
|
||||||
return sell_row[OPEN_IDX]
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
# Use the maximum between close_rate and low as we
|
return close_rate
|
||||||
# cannot sell outside of a candle.
|
|
||||||
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
|
||||||
return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# This should not be reached...
|
# This should not be reached...
|
||||||
@ -366,6 +363,17 @@ class Backtesting:
|
|||||||
|
|
||||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||||
|
# call the custom exit price,with default value as previous closerate
|
||||||
|
current_profit = trade.calc_profit_ratio(closerate)
|
||||||
|
if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL):
|
||||||
|
# Custom exit pricing only for sell-signals
|
||||||
|
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||||
|
default_retval=closerate)(
|
||||||
|
pair=trade.pair, trade=trade,
|
||||||
|
current_time=sell_row[DATE_IDX],
|
||||||
|
proposed_rate=closerate, current_profit=current_profit)
|
||||||
|
# Use the maximum between close_rate and low as we cannot sell outside of a candle.
|
||||||
|
closerate = min(max(closerate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
||||||
|
|
||||||
# Confirm trade exit:
|
# Confirm trade exit:
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
@ -424,13 +432,21 @@ class Backtesting:
|
|||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
return None
|
return None
|
||||||
|
# let's call the custom entry price, using the open price as default price
|
||||||
|
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||||
|
default_retval=row[OPEN_IDX])(
|
||||||
|
pair=pair, current_time=row[DATE_IDX].to_pydatetime(),
|
||||||
|
proposed_rate=row[OPEN_IDX]) # default value is the open rate
|
||||||
|
|
||||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) or 0
|
# Move rate to within the candle's low/high rate
|
||||||
|
propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX])
|
||||||
|
|
||||||
|
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
|
||||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||||
|
|
||||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||||
default_retval=stake_amount)(
|
default_retval=stake_amount)(
|
||||||
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate,
|
||||||
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
|
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||||
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||||
|
|
||||||
@ -441,7 +457,7 @@ class Backtesting:
|
|||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
# Confirm trade entry:
|
# Confirm trade entry:
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX],
|
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
|
||||||
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
|
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -450,10 +466,10 @@ class Backtesting:
|
|||||||
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
|
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
|
||||||
trade = LocalTrade(
|
trade = LocalTrade(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
open_rate=row[OPEN_IDX],
|
open_rate=propose_rate,
|
||||||
open_date=row[DATE_IDX].to_pydatetime(),
|
open_date=row[DATE_IDX].to_pydatetime(),
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
amount=round(stake_amount / propose_rate, 8),
|
||||||
fee_open=self.fee,
|
fee_open=self.fee,
|
||||||
fee_close=self.fee,
|
fee_close=self.fee,
|
||||||
is_open=True,
|
is_open=True,
|
||||||
|
@ -68,14 +68,14 @@ class PerformanceFilter(IPairList):
|
|||||||
# - then pair name alphametically
|
# - then pair name alphametically
|
||||||
sorted_df = list_df.merge(performance, on='pair', how='left')\
|
sorted_df = list_df.merge(performance, on='pair', how='left')\
|
||||||
.fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
|
.fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
|
||||||
.sort_values(by=['profit'], ascending=False)
|
.sort_values(by=['profit_ratio'], ascending=False)
|
||||||
if self._min_profit is not None:
|
if self._min_profit is not None:
|
||||||
removed = sorted_df[sorted_df['profit'] < self._min_profit]
|
removed = sorted_df[sorted_df['profit_ratio'] < self._min_profit]
|
||||||
for _, row in removed.iterrows():
|
for _, row in removed.iterrows():
|
||||||
self.log_once(
|
self.log_once(
|
||||||
f"Removing pair {row['pair']} since {row['profit']} is "
|
f"Removing pair {row['pair']} since {row['profit_ratio']} is "
|
||||||
f"below {self._min_profit}", logger.info)
|
f"below {self._min_profit}", logger.info)
|
||||||
sorted_df = sorted_df[sorted_df['profit'] >= self._min_profit]
|
sorted_df = sorted_df[sorted_df['profit_ratio'] >= self._min_profit]
|
||||||
|
|
||||||
pairlist = sorted_df['pair'].tolist()
|
pairlist = sorted_df['pair'].tolist()
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -18,7 +19,15 @@ class ShuffleFilter(IPairList):
|
|||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
self._seed = pairlistconfig.get('seed')
|
# Apply seed in backtesting mode to get comparable results,
|
||||||
|
# but not in live modes to get a non-repeating order of pairs during live modes.
|
||||||
|
if config.get('runmode') in (RunMode.LIVE, RunMode.DRY_RUN):
|
||||||
|
self._seed = None
|
||||||
|
logger.info("Live mode detected, not applying seed.")
|
||||||
|
else:
|
||||||
|
self._seed = pairlistconfig.get('seed')
|
||||||
|
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
|
||||||
|
|
||||||
self._random = random.Random(self._seed)
|
self._random = random.Random(self._seed)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from cachetools.ttl import TTLCache
|
from cachetools import TTLCache
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
@ -8,7 +8,7 @@ from functools import partial
|
|||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from cachetools.ttl import TTLCache
|
from cachetools import TTLCache
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
|
@ -6,7 +6,7 @@ from copy import deepcopy
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from cachetools.ttl import TTLCache
|
from cachetools import TTLCache
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
PairList manager class
|
PairList manager class
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from functools import partial
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from cachetools import TTLCache, cached
|
from cachetools import TTLCache, cached
|
||||||
|
|
||||||
from freqtrade.constants import ListPairsWithTimeframes
|
from freqtrade.constants import ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.resolvers import PairListResolver
|
from freqtrade.resolvers import PairListResolver
|
||||||
@ -17,7 +18,7 @@ from freqtrade.resolvers import PairListResolver
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PairListManager():
|
class PairListManager(LoggingMixin):
|
||||||
|
|
||||||
def __init__(self, exchange, config: dict) -> None:
|
def __init__(self, exchange, config: dict) -> None:
|
||||||
self._exchange = exchange
|
self._exchange = exchange
|
||||||
@ -41,6 +42,9 @@ class PairListManager():
|
|||||||
if not self._pairlist_handlers:
|
if not self._pairlist_handlers:
|
||||||
raise OperationalException("No Pairlist Handlers defined")
|
raise OperationalException("No Pairlist Handlers defined")
|
||||||
|
|
||||||
|
refresh_period = config.get('pairlist_refresh_period', 3600)
|
||||||
|
LoggingMixin.__init__(self, logger, refresh_period)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def whitelist(self) -> List[str]:
|
def whitelist(self) -> List[str]:
|
||||||
"""The current whitelist"""
|
"""The current whitelist"""
|
||||||
@ -108,9 +112,10 @@ class PairListManager():
|
|||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.error(f"Pair blacklist contains an invalid Wildcard: {err}")
|
logger.error(f"Pair blacklist contains an invalid Wildcard: {err}")
|
||||||
return []
|
return []
|
||||||
for pair in deepcopy(pairlist):
|
log_once = partial(self.log_once, logmethod=logmethod)
|
||||||
|
for pair in pairlist.copy():
|
||||||
if pair in blacklist:
|
if pair in blacklist:
|
||||||
logmethod(f"Pair {pair} in your blacklist. Removing it from whitelist...")
|
log_once(f"Pair {pair} in your blacklist. Removing it from whitelist...")
|
||||||
pairlist.remove(pair)
|
pairlist.remove(pair)
|
||||||
return pairlist
|
return pairlist
|
||||||
|
|
||||||
|
@ -33,6 +33,9 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
|||||||
if settings[setting] is not None:
|
if settings[setting] is not None:
|
||||||
btconfig[setting] = settings[setting]
|
btconfig[setting] = settings[setting]
|
||||||
|
|
||||||
|
# Force dry-run for backtesting
|
||||||
|
btconfig['dry_run'] = True
|
||||||
|
|
||||||
# Start backtesting
|
# Start backtesting
|
||||||
# Initialize backtesting object
|
# Initialize backtesting object
|
||||||
def run_backtest():
|
def run_backtest():
|
||||||
|
@ -4,6 +4,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
|
from freqtrade.enums import OrderTypeValues
|
||||||
|
|
||||||
|
|
||||||
class Ping(BaseModel):
|
class Ping(BaseModel):
|
||||||
@ -125,25 +126,26 @@ class Daily(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class UnfilledTimeout(BaseModel):
|
class UnfilledTimeout(BaseModel):
|
||||||
buy: int
|
buy: Optional[int]
|
||||||
sell: int
|
sell: Optional[int]
|
||||||
unit: str
|
unit: Optional[str]
|
||||||
exit_timeout_count: Optional[int]
|
exit_timeout_count: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class OrderTypes(BaseModel):
|
class OrderTypes(BaseModel):
|
||||||
buy: str
|
buy: OrderTypeValues
|
||||||
sell: str
|
sell: OrderTypeValues
|
||||||
emergencysell: Optional[str]
|
emergencysell: Optional[OrderTypeValues]
|
||||||
forcesell: Optional[str]
|
forcesell: Optional[OrderTypeValues]
|
||||||
forcebuy: Optional[str]
|
forcebuy: Optional[OrderTypeValues]
|
||||||
stoploss: str
|
stoploss: OrderTypeValues
|
||||||
stoploss_on_exchange: bool
|
stoploss_on_exchange: bool
|
||||||
stoploss_on_exchange_interval: Optional[int]
|
stoploss_on_exchange_interval: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
class ShowConfig(BaseModel):
|
class ShowConfig(BaseModel):
|
||||||
version: str
|
version: str
|
||||||
|
strategy_version: Optional[str]
|
||||||
api_version: float
|
api_version: float
|
||||||
dry_run: bool
|
dry_run: bool
|
||||||
stake_currency: str
|
stake_currency: str
|
||||||
@ -158,7 +160,7 @@ class ShowConfig(BaseModel):
|
|||||||
trailing_stop_positive_offset: Optional[float]
|
trailing_stop_positive_offset: Optional[float]
|
||||||
trailing_only_offset_is_reached: Optional[bool]
|
trailing_only_offset_is_reached: Optional[bool]
|
||||||
unfilledtimeout: UnfilledTimeout
|
unfilledtimeout: UnfilledTimeout
|
||||||
order_types: OrderTypes
|
order_types: Optional[OrderTypes]
|
||||||
use_custom_stoploss: Optional[bool]
|
use_custom_stoploss: Optional[bool]
|
||||||
timeframe: Optional[str]
|
timeframe: Optional[str]
|
||||||
timeframe_ms: int
|
timeframe_ms: int
|
||||||
@ -274,10 +276,12 @@ class Logs(BaseModel):
|
|||||||
class ForceBuyPayload(BaseModel):
|
class ForceBuyPayload(BaseModel):
|
||||||
pair: str
|
pair: str
|
||||||
price: Optional[float]
|
price: Optional[float]
|
||||||
|
ordertype: Optional[OrderTypeValues]
|
||||||
|
|
||||||
|
|
||||||
class ForceSellPayload(BaseModel):
|
class ForceSellPayload(BaseModel):
|
||||||
tradeid: str
|
tradeid: str
|
||||||
|
ordertype: Optional[OrderTypeValues]
|
||||||
|
|
||||||
|
|
||||||
class BlacklistPayload(BaseModel):
|
class BlacklistPayload(BaseModel):
|
||||||
|
@ -3,7 +3,7 @@ from copy import deepcopy
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Query
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
@ -29,7 +29,9 @@ logger = logging.getLogger(__name__)
|
|||||||
# API version
|
# API version
|
||||||
# Pre-1.1, no version was provided
|
# Pre-1.1, no version was provided
|
||||||
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
|
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
|
||||||
API_VERSION = 1.1
|
# 1.11: forcebuy and forcesell accept ordertype
|
||||||
|
# 1.12: add blacklist delete endpoint
|
||||||
|
API_VERSION = 1.12
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
@ -120,16 +122,19 @@ def edge(rpc: RPC = Depends(get_rpc)):
|
|||||||
@router.get('/show_config', response_model=ShowConfig, tags=['info'])
|
@router.get('/show_config', response_model=ShowConfig, tags=['info'])
|
||||||
def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)):
|
def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)):
|
||||||
state = ''
|
state = ''
|
||||||
|
strategy_version = None
|
||||||
if rpc:
|
if rpc:
|
||||||
state = rpc._freqtrade.state
|
state = rpc._freqtrade.state
|
||||||
resp = RPC._rpc_show_config(config, state)
|
strategy_version = rpc._freqtrade.strategy.version()
|
||||||
|
resp = RPC._rpc_show_config(config, state, strategy_version)
|
||||||
resp['api_version'] = API_VERSION
|
resp['api_version'] = API_VERSION
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
|
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
|
||||||
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
||||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price)
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
|
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype)
|
||||||
|
|
||||||
if trade:
|
if trade:
|
||||||
return ForceBuyResponse.parse_obj(trade.to_json())
|
return ForceBuyResponse.parse_obj(trade.to_json())
|
||||||
@ -139,7 +144,8 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
|||||||
|
|
||||||
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
||||||
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
|
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
|
||||||
return rpc._rpc_forcesell(payload.tradeid)
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
|
return rpc._rpc_forcesell(payload.tradeid, ordertype)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||||
@ -152,6 +158,13 @@ def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)):
|
|||||||
return rpc._rpc_blacklist(payload.blacklist)
|
return rpc._rpc_blacklist(payload.blacklist)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||||
|
def blacklist_delete(pairs_to_delete: List[str] = Query([]), rpc: RPC = Depends(get_rpc)):
|
||||||
|
"""Provide a list of pairs to delete from the blacklist"""
|
||||||
|
|
||||||
|
return rpc._rpc_blacklist_delete(pairs_to_delete)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/whitelist', response_model=WhitelistResponse, tags=['info', 'pairlist'])
|
@router.get('/whitelist', response_model=WhitelistResponse, tags=['info', 'pairlist'])
|
||||||
def whitelist(rpc: RPC = Depends(get_rpc)):
|
def whitelist(rpc: RPC = Depends(get_rpc)):
|
||||||
return rpc._rpc_whitelist()
|
return rpc._rpc_whitelist()
|
||||||
|
@ -7,7 +7,7 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from cachetools.ttl import TTLCache
|
from cachetools import TTLCache
|
||||||
from pycoingecko import CoinGeckoAPI
|
from pycoingecko import CoinGeckoAPI
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
|
@ -98,7 +98,8 @@ class RPC:
|
|||||||
self._fiat_converter = CryptoToFiatConverter()
|
self._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rpc_show_config(config, botstate: Union[State, str]) -> Dict[str, Any]:
|
def _rpc_show_config(config, botstate: Union[State, str],
|
||||||
|
strategy_version: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Return a dict of config options.
|
Return a dict of config options.
|
||||||
Explicitly does NOT return the full config to avoid leakage of sensitive
|
Explicitly does NOT return the full config to avoid leakage of sensitive
|
||||||
@ -106,6 +107,7 @@ class RPC:
|
|||||||
"""
|
"""
|
||||||
val = {
|
val = {
|
||||||
'version': __version__,
|
'version': __version__,
|
||||||
|
'strategy_version': strategy_version,
|
||||||
'dry_run': config['dry_run'],
|
'dry_run': config['dry_run'],
|
||||||
'stake_currency': config['stake_currency'],
|
'stake_currency': config['stake_currency'],
|
||||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||||
@ -640,7 +642,7 @@ class RPC:
|
|||||||
|
|
||||||
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||||
|
|
||||||
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
@ -664,7 +666,11 @@ class RPC:
|
|||||||
current_rate = self._freqtrade.exchange.get_rate(
|
current_rate = self._freqtrade.exchange.get_rate(
|
||||||
trade.pair, refresh=False, side="sell")
|
trade.pair, refresh=False, side="sell")
|
||||||
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||||
self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason)
|
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||||
|
"forcesell", self._freqtrade.strategy.order_types["sell"])
|
||||||
|
|
||||||
|
self._freqtrade.execute_trade_exit(
|
||||||
|
trade, current_rate, sell_reason, ordertype=order_type)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
@ -692,7 +698,8 @@ class RPC:
|
|||||||
self._freqtrade.wallets.update()
|
self._freqtrade.wallets.update()
|
||||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||||
|
|
||||||
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
|
def _rpc_forcebuy(self, pair: str, price: Optional[float],
|
||||||
|
order_type: Optional[str] = None) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Handler for forcebuy <asset> <price>
|
Handler for forcebuy <asset> <price>
|
||||||
Buys a pair trade at the given or current price
|
Buys a pair trade at the given or current price
|
||||||
@ -720,7 +727,10 @@ class RPC:
|
|||||||
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
||||||
|
|
||||||
# execute buy
|
# execute buy
|
||||||
if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True):
|
if not order_type:
|
||||||
|
order_type = self._freqtrade.strategy.order_types.get(
|
||||||
|
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||||
|
if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type):
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
return trade
|
return trade
|
||||||
@ -850,6 +860,20 @@ class RPC:
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def _rpc_blacklist_delete(self, delete: List[str]) -> Dict:
|
||||||
|
""" Removes pairs from currently active blacklist """
|
||||||
|
errors = {}
|
||||||
|
for pair in delete:
|
||||||
|
if pair in self._freqtrade.pairlists.blacklist:
|
||||||
|
self._freqtrade.pairlists.blacklist.remove(pair)
|
||||||
|
else:
|
||||||
|
errors[pair] = {
|
||||||
|
'error_msg': f"Pair {pair} is not in the current blacklist."
|
||||||
|
}
|
||||||
|
resp = self._rpc_blacklist()
|
||||||
|
resp['errors'] = errors
|
||||||
|
return resp
|
||||||
|
|
||||||
def _rpc_blacklist(self, add: List[str] = None) -> Dict:
|
def _rpc_blacklist(self, add: List[str] = None) -> Dict:
|
||||||
""" Returns the currently active blacklist"""
|
""" Returns the currently active blacklist"""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
@ -60,6 +60,10 @@ class RPCManager:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
logger.info('Sending rpc message: %s', msg)
|
logger.info('Sending rpc message: %s', msg)
|
||||||
|
if 'pair' in msg:
|
||||||
|
msg.update({
|
||||||
|
'base_currency': self._rpc._freqtrade.exchange.get_pair_base_currency(msg['pair'])
|
||||||
|
})
|
||||||
for mod in self.registered_modules:
|
for mod in self.registered_modules:
|
||||||
logger.debug('Forwarding message to rpc.%s', mod.name)
|
logger.debug('Forwarding message to rpc.%s', mod.name)
|
||||||
try:
|
try:
|
||||||
|
@ -111,8 +111,9 @@ class Telegram(RPCHandler):
|
|||||||
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
|
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
|
||||||
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
r'/stats$', r'/count$', r'/locks$', r'/balance$',
|
||||||
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
||||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
|
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
|
||||||
r'/forcebuy$', r'/help$', r'/version$']
|
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||||
|
r'/forcebuy$', r'/edge$', r'/help$', r'/version$']
|
||||||
# Create keys for generation
|
# Create keys for generation
|
||||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||||
|
|
||||||
@ -169,6 +170,7 @@ class Telegram(RPCHandler):
|
|||||||
CommandHandler('stopbuy', self._stopbuy),
|
CommandHandler('stopbuy', self._stopbuy),
|
||||||
CommandHandler('whitelist', self._whitelist),
|
CommandHandler('whitelist', self._whitelist),
|
||||||
CommandHandler('blacklist', self._blacklist),
|
CommandHandler('blacklist', self._blacklist),
|
||||||
|
CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete),
|
||||||
CommandHandler('logs', self._logs),
|
CommandHandler('logs', self._logs),
|
||||||
CommandHandler('edge', self._edge),
|
CommandHandler('edge', self._edge),
|
||||||
CommandHandler('help', self._help),
|
CommandHandler('help', self._help),
|
||||||
@ -1161,22 +1163,28 @@ class Telegram(RPCHandler):
|
|||||||
Handler for /blacklist
|
Handler for /blacklist
|
||||||
Shows the currently active blacklist
|
Shows the currently active blacklist
|
||||||
"""
|
"""
|
||||||
try:
|
self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args))
|
||||||
|
|
||||||
blacklist = self._rpc._rpc_blacklist(context.args)
|
def send_blacklist_msg(self, blacklist: Dict):
|
||||||
errmsgs = []
|
errmsgs = []
|
||||||
for pair, error in blacklist['errors'].items():
|
for pair, error in blacklist['errors'].items():
|
||||||
errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
|
errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
|
||||||
if errmsgs:
|
if errmsgs:
|
||||||
self._send_msg('\n'.join(errmsgs))
|
self._send_msg('\n'.join(errmsgs))
|
||||||
|
|
||||||
message = f"Blacklist contains {blacklist['length']} pairs\n"
|
message = f"Blacklist contains {blacklist['length']} pairs\n"
|
||||||
message += f"`{', '.join(blacklist['blacklist'])}`"
|
message += f"`{', '.join(blacklist['blacklist'])}`"
|
||||||
|
|
||||||
logger.debug(message)
|
logger.debug(message)
|
||||||
self._send_msg(message)
|
self._send_msg(message)
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
@authorized_only
|
||||||
|
def _blacklist_delete(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /bl_delete
|
||||||
|
Deletes pair(s) from current blacklist
|
||||||
|
"""
|
||||||
|
self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or []))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _logs(self, update: Update, context: CallbackContext) -> None:
|
def _logs(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1257,6 +1265,8 @@ class Telegram(RPCHandler):
|
|||||||
"*/whitelist:* `Show current whitelist` \n"
|
"*/whitelist:* `Show current whitelist` \n"
|
||||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
||||||
"to the blacklist.` \n"
|
"to the blacklist.` \n"
|
||||||
|
"*/blacklist_delete [pairs]| /bl_delete [pairs]:* "
|
||||||
|
"`Delete pair / pattern from blacklist. Will reset on reload_conf.` \n"
|
||||||
"*/reload_config:* `Reload configuration file` \n"
|
"*/reload_config:* `Reload configuration file` \n"
|
||||||
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
|
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
|
||||||
|
|
||||||
@ -1304,7 +1314,12 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._send_msg('*Version:* `{}`'.format(__version__))
|
strategy_version = self._rpc._freqtrade.strategy.version()
|
||||||
|
version_string = f'*Version:* `{__version__}`'
|
||||||
|
if strategy_version is not None:
|
||||||
|
version_string += f', *Strategy version: * `{strategy_version}`'
|
||||||
|
|
||||||
|
self._send_msg(version_string)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _show_config(self, update: Update, context: CallbackContext) -> None:
|
def _show_config(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
This module manages webhook communication
|
This module manages webhook communication
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from requests import RequestException, post
|
from requests import RequestException, post
|
||||||
@ -28,12 +29,9 @@ class Webhook(RPCHandler):
|
|||||||
super().__init__(rpc, config)
|
super().__init__(rpc, config)
|
||||||
|
|
||||||
self._url = self._config['webhook']['url']
|
self._url = self._config['webhook']['url']
|
||||||
|
|
||||||
self._format = self._config['webhook'].get('format', 'form')
|
self._format = self._config['webhook'].get('format', 'form')
|
||||||
|
self._retries = self._config['webhook'].get('retries', 0)
|
||||||
if self._format != 'form' and self._format != 'json':
|
self._retry_delay = self._config['webhook'].get('retry_delay', 0.1)
|
||||||
raise NotImplementedError('Unknown webhook format `{}`, possible values are '
|
|
||||||
'`form` (default) and `json`'.format(self._format))
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -77,13 +75,30 @@ class Webhook(RPCHandler):
|
|||||||
def _send_msg(self, payload: dict) -> None:
|
def _send_msg(self, payload: dict) -> None:
|
||||||
"""do the actual call to the webhook"""
|
"""do the actual call to the webhook"""
|
||||||
|
|
||||||
try:
|
success = False
|
||||||
if self._format == 'form':
|
attempts = 0
|
||||||
post(self._url, data=payload)
|
while not success and attempts <= self._retries:
|
||||||
elif self._format == 'json':
|
if attempts:
|
||||||
post(self._url, json=payload)
|
if self._retry_delay:
|
||||||
else:
|
time.sleep(self._retry_delay)
|
||||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
logger.info("Retrying webhook...")
|
||||||
|
|
||||||
except RequestException as exc:
|
attempts += 1
|
||||||
logger.warning("Could not call webhook url. Exception: %s", exc)
|
|
||||||
|
try:
|
||||||
|
if self._format == 'form':
|
||||||
|
response = post(self._url, data=payload)
|
||||||
|
elif self._format == 'json':
|
||||||
|
response = post(self._url, json=payload)
|
||||||
|
elif self._format == 'raw':
|
||||||
|
response = post(self._url, data=payload['data'],
|
||||||
|
headers={'Content-Type': 'text/plain'})
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
||||||
|
|
||||||
|
# Throw a RequestException if the post was not successful
|
||||||
|
response.raise_for_status()
|
||||||
|
success = True
|
||||||
|
|
||||||
|
except RequestException as exc:
|
||||||
|
logger.warning("Could not call webhook url. Exception: %s", exc)
|
||||||
|
@ -394,6 +394,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def version(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Returns version of the strategy.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
###
|
###
|
||||||
# END - Intended to be overridden by strategy
|
# END - Intended to be overridden by strategy
|
||||||
###
|
###
|
||||||
|
@ -87,6 +87,7 @@ class {{ strategy }}(IStrategy):
|
|||||||
'sell': 'gtc'
|
'sell': 'gtc'
|
||||||
}
|
}
|
||||||
{{ plot_config | indent(4) }}
|
{{ plot_config | indent(4) }}
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
"""
|
"""
|
||||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
|
@ -79,7 +79,9 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"# Load strategy using values set above\n",
|
"# Load strategy using values set above\n",
|
||||||
"from freqtrade.resolvers import StrategyResolver\n",
|
"from freqtrade.resolvers import StrategyResolver\n",
|
||||||
|
"from freqtrade.data.dataprovider import DataProvider\n",
|
||||||
"strategy = StrategyResolver.load_strategy(config)\n",
|
"strategy = StrategyResolver.load_strategy(config)\n",
|
||||||
|
"strategy.dp = DataProvider(config, None, None)\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Generate buy/sell signals using strategy\n",
|
"# Generate buy/sell signals using strategy\n",
|
||||||
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",
|
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
|
|
||||||
plot_config = {
|
@property
|
||||||
# Main plot indicators (Moving averages, ...)
|
def plot_config(self):
|
||||||
'main_plot': {
|
return {
|
||||||
'tema': {},
|
# Main plot indicators (Moving averages, ...)
|
||||||
'sar': {'color': 'white'},
|
'main_plot': {
|
||||||
},
|
'tema': {},
|
||||||
'subplots': {
|
'sar': {'color': 'white'},
|
||||||
# Subplots - each dict defines one additional plot
|
|
||||||
"MACD": {
|
|
||||||
'macd': {'color': 'blue'},
|
|
||||||
'macdsignal': {'color': 'orange'},
|
|
||||||
},
|
},
|
||||||
"RSI": {
|
'subplots': {
|
||||||
'rsi': {'color': 'red'},
|
# Subplots - each dict defines one additional plot
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
},
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -260,8 +260,8 @@ class Wallets:
|
|||||||
if self._log:
|
if self._log:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Adjusted stake amount for pair {pair} is more than 30% bigger than "
|
f"Adjusted stake amount for pair {pair} is more than 30% bigger than "
|
||||||
f"the desired stake ({stake_amount} * 1.3 > {max_stake_amount}), "
|
f"the desired stake amount of ({stake_amount:.8f} * 1.3 = "
|
||||||
f"ignoring trade."
|
f"{stake_amount * 1.3:.8f}) < {min_stake_amount}), ignoring trade."
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
stake_amount = min_stake_amount
|
stake_amount = min_stake_amount
|
||||||
|
@ -85,9 +85,12 @@ class Worker:
|
|||||||
|
|
||||||
# Log state transition
|
# Log state transition
|
||||||
if state != old_state:
|
if state != old_state:
|
||||||
self.freqtrade.notify_status(f'{state.name.lower()}')
|
|
||||||
|
|
||||||
logger.info(f"Changing state to: {state.name}")
|
if old_state != State.RELOAD_CONFIG:
|
||||||
|
self.freqtrade.notify_status(f'{state.name.lower()}')
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Changing state{f' from {old_state.name}' if old_state else ''} to: {state.name}")
|
||||||
if state == State.RUNNING:
|
if state == State.RUNNING:
|
||||||
self.freqtrade.startup()
|
self.freqtrade.startup()
|
||||||
|
|
||||||
@ -113,8 +116,12 @@ class Worker:
|
|||||||
if self._heartbeat_interval:
|
if self._heartbeat_interval:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if (now - self._heartbeat_msg) > self._heartbeat_interval:
|
if (now - self._heartbeat_msg) > self._heartbeat_interval:
|
||||||
|
version = __version__
|
||||||
|
strategy_version = self.freqtrade.strategy.version()
|
||||||
|
if (strategy_version is not None):
|
||||||
|
version += ', strategy_version: ' + strategy_version
|
||||||
logger.info(f"Bot heartbeat. PID={getpid()}, "
|
logger.info(f"Bot heartbeat. PID={getpid()}, "
|
||||||
f"version='{__version__}', state='{state.name}'")
|
f"version='{version}', state='{state.name}'")
|
||||||
self._heartbeat_msg = now
|
self._heartbeat_msg = now
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
@ -81,8 +81,10 @@ markdown_extensions:
|
|||||||
- pymdownx.snippets:
|
- pymdownx.snippets:
|
||||||
base_path: docs
|
base_path: docs
|
||||||
check_paths: true
|
check_paths: true
|
||||||
- pymdownx.tabbed
|
|
||||||
- pymdownx.superfences
|
- pymdownx.superfences
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
- pymdownx.tasklist:
|
- pymdownx.tasklist:
|
||||||
custom_checkbox: true
|
custom_checkbox: true
|
||||||
|
- pymdownx.tilde
|
||||||
- mdx_truly_sane_lists
|
- mdx_truly_sane_lists
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.5.0
|
flake8-tidy-imports==4.5.0
|
||||||
mypy==0.910
|
mypy==0.930
|
||||||
pytest==6.2.5
|
pytest==6.2.5
|
||||||
pytest-asyncio==0.16.0
|
pytest-asyncio==0.16.0
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
@ -14,16 +14,16 @@ pytest-mock==3.6.1
|
|||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
isort==5.10.1
|
isort==5.10.1
|
||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==2.4.0
|
time-machine==2.5.0
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==6.3.0
|
nbconvert==6.3.0
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==4.2.5
|
types-cachetools==4.2.6
|
||||||
types-filelock==3.2.1
|
types-filelock==3.2.1
|
||||||
types-requests==2.26.0
|
types-requests==2.26.2
|
||||||
types-tabulate==0.8.3
|
types-tabulate==0.8.3
|
||||||
|
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
types-python-dateutil==2.8.2
|
types-python-dateutil==2.8.4
|
@ -2,10 +2,10 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.7.2
|
scipy==1.7.3
|
||||||
scikit-learn==1.0.1
|
scikit-learn==1.0.2
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.4.0
|
filelock==3.4.2
|
||||||
joblib==1.1.0
|
joblib==1.1.0
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
progressbar2==3.55.0
|
progressbar2==3.55.0
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.4.0
|
plotly==5.5.0
|
||||||
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
numpy==1.21.4
|
numpy==1.21.5
|
||||||
pandas==1.3.4
|
pandas==1.3.5
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.61.92
|
ccxt==1.65.25
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==36.0.0
|
cryptography==36.0.1
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
SQLAlchemy==1.4.27
|
SQLAlchemy==1.4.29
|
||||||
python-telegram-bot==13.8.1
|
python-telegram-bot==13.9
|
||||||
arrow==1.2.1
|
arrow==1.2.1
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
urllib3==1.26.7
|
urllib3==1.26.7
|
||||||
jsonschema==4.2.1
|
jsonschema==4.3.2
|
||||||
TA-Lib==0.4.21
|
TA-Lib==0.4.22
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
tabulate==0.8.9
|
tabulate==0.8.9
|
||||||
pycoingecko==2.2.0
|
pycoingecko==2.2.0
|
||||||
@ -31,16 +31,16 @@ python-rapidjson==1.5
|
|||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.70.0
|
fastapi==0.70.1
|
||||||
uvicorn==0.15.0
|
uvicorn==0.16.0
|
||||||
pyjwt==2.3.0
|
pyjwt==2.3.0
|
||||||
aiofiles==0.7.0
|
aiofiles==0.8.0
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
|
|
||||||
# Support for colorized terminal output
|
# Support for colorized terminal output
|
||||||
colorama==0.4.4
|
colorama==0.4.4
|
||||||
# Building config files interactively
|
# Building config files interactively
|
||||||
questionary==1.10.0
|
questionary==1.10.0
|
||||||
prompt-toolkit==3.0.22
|
prompt-toolkit==3.0.24
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
|
5
setup.sh
5
setup.sh
@ -36,7 +36,8 @@ function check_installed_python() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "No usable python found. Please make sure to have python3.7 or newer installed"
|
echo "No usable python found. Please make sure to have python3.7 or newer installed."
|
||||||
|
echo "python3.10 is currently not supported."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +206,7 @@ function config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function install() {
|
function install() {
|
||||||
|
|
||||||
echo_block "Installing mandatory dependencies"
|
echo_block "Installing mandatory dependencies"
|
||||||
|
|
||||||
if [ "$(uname -s)" == "Darwin" ]; then
|
if [ "$(uname -s)" == "Darwin" ]; then
|
||||||
|
47
tests/exchange/test_bitpanda.py
Normal file
47
tests/exchange/test_bitpanda.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_trades_for_order(default_conf, mocker):
|
||||||
|
exchange_name = 'bitpanda'
|
||||||
|
order_id = 'ABCD-ABCD'
|
||||||
|
since = datetime(2018, 5, 5, 0, 0, 0)
|
||||||
|
default_conf["dry_run"] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||||
|
api_mock = MagicMock()
|
||||||
|
|
||||||
|
api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV',
|
||||||
|
'order': 'ABCD-ABCD',
|
||||||
|
'info': {'pair': 'XLTCZBTC',
|
||||||
|
'time': 1519860024.4388,
|
||||||
|
'type': 'buy',
|
||||||
|
'ordertype': 'limit',
|
||||||
|
'price': '20.00000',
|
||||||
|
'cost': '38.62000',
|
||||||
|
'fee': '0.06179',
|
||||||
|
'vol': '5',
|
||||||
|
'id': 'ABCD-ABCD'},
|
||||||
|
'timestamp': 1519860024438,
|
||||||
|
'datetime': '2018-02-28T23:20:24.438Z',
|
||||||
|
'symbol': 'LTC/BTC',
|
||||||
|
'type': 'limit',
|
||||||
|
'side': 'buy',
|
||||||
|
'price': 165.0,
|
||||||
|
'amount': 0.2340606,
|
||||||
|
'fee': {'cost': 0.06179, 'currency': 'BTC'}
|
||||||
|
}])
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
|
||||||
|
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
||||||
|
assert len(orders) == 1
|
||||||
|
assert orders[0]['price'] == 165
|
||||||
|
assert api_mock.fetch_my_trades.call_count == 1
|
||||||
|
# since argument should be
|
||||||
|
assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int)
|
||||||
|
assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC'
|
||||||
|
# Same test twice, hardcoded number and doing the same calculation
|
||||||
|
assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000
|
||||||
|
# bitpanda requires "to" argument.
|
||||||
|
assert 'to' in api_mock.fetch_my_trades.call_args[1]['params']
|
@ -1026,6 +1026,12 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
|
|||||||
assert order_closed['status'] == 'closed'
|
assert order_closed['status'] == 'closed'
|
||||||
assert order['fee']
|
assert order['fee']
|
||||||
|
|
||||||
|
# Empty orderbook test
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
|
||||||
|
return_value={'asks': [], 'bids': []})
|
||||||
|
exchange._dry_run_open_orders[order['id']]['status'] = 'open'
|
||||||
|
order_closed = exchange.fetch_dry_run_order(order['id'])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side,rate,amount,endprice", [
|
@pytest.mark.parametrize("side,rate,amount,endprice", [
|
||||||
# spread is 25.263-25.266
|
# spread is 25.263-25.266
|
||||||
@ -1667,12 +1673,21 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
assert len(res) == len(pairs)
|
assert len(res) == len(pairs)
|
||||||
|
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||||
|
exchange.required_candle_call_count = 1
|
||||||
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
|
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
|
||||||
f"timeframe {pairs[0][1]} ...",
|
f"timeframe {pairs[0][1]} ...",
|
||||||
caplog)
|
caplog)
|
||||||
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
||||||
cache=False)
|
cache=False)
|
||||||
assert len(res) == 3
|
assert len(res) == 3
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||||
|
|
||||||
|
# Test the same again, should NOT return from cache!
|
||||||
|
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||||
|
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
||||||
|
cache=False)
|
||||||
|
assert len(res) == 3
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -1768,7 +1783,7 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
|
|||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
# Test that each is in list at least once as order is not guaranteed
|
# Test that each is in list at least once as order is not guaranteed
|
||||||
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
|
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
|
||||||
assert log_has("Async code raised an exception: TypeError", caplog)
|
assert log_has("Async code raised an exception: TypeError()", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_get_next_limit_in_list():
|
def test_get_next_limit_in_list():
|
||||||
@ -2933,39 +2948,49 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
|
|||||||
assert ex.extract_cost_curr_rate(order) == expected
|
assert ex.extract_cost_curr_rate(order) == expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("order,expected", [
|
@pytest.mark.parametrize("order,unknown_fee_rate,expected", [
|
||||||
# Using base-currency
|
# Using base-currency
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.1),
|
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, None, 0.1),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.05, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.05, 'cost': 0.05,
|
||||||
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.08),
|
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, None, 0.08),
|
||||||
# Using quote currency
|
# Using quote currency
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'BTC', 'cost': 0.005}}, 0.1),
|
'fee': {'currency': 'BTC', 'cost': 0.005}}, None, 0.1),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'BTC', 'cost': 0.002, 'rate': None}}, 0.04),
|
'fee': {'currency': 'BTC', 'cost': 0.002, 'rate': None}}, None, 0.04),
|
||||||
# Using foreign currency
|
# Using foreign currency
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944),
|
'fee': {'currency': 'NEO', 'cost': 0.0012}}, None, 0.001944),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
|
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
|
||||||
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
|
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, None, 0.00074305),
|
||||||
# Rate included in return - return as is
|
# Rate included in return - return as is
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, 0.01),
|
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, None, 0.01),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
|
||||||
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005),
|
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, None, 0.005),
|
||||||
# 0.1% filled - no costs (kraken - #3431)
|
# 0.1% filled - no costs (kraken - #3431)
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
'fee': {'currency': 'BTC', 'cost': 0.0, 'rate': None}}, None),
|
'fee': {'currency': 'BTC', 'cost': 0.0, 'rate': None}}, None, None),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
'fee': {'currency': 'ETH', 'cost': 0.0, 'rate': None}}, 0.0),
|
'fee': {'currency': 'ETH', 'cost': 0.0, 'rate': None}}, None, 0.0),
|
||||||
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0,
|
||||||
'fee': {'currency': 'NEO', 'cost': 0.0, 'rate': None}}, None),
|
'fee': {'currency': 'NEO', 'cost': 0.0, 'rate': None}}, None, None),
|
||||||
|
# Invalid pair combination - POINT/BTC is not a pair
|
||||||
|
({'symbol': 'POINT/BTC', 'amount': 0.04, 'cost': 0.5,
|
||||||
|
'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, None, None),
|
||||||
|
({'symbol': 'POINT/BTC', 'amount': 0.04, 'cost': 0.5,
|
||||||
|
'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, 1, 4.0),
|
||||||
|
({'symbol': 'POINT/BTC', 'amount': 0.04, 'cost': 0.5,
|
||||||
|
'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, 2, 8.0),
|
||||||
])
|
])
|
||||||
def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
|
def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_rate) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
|
||||||
|
if unknown_fee_rate:
|
||||||
|
default_conf['exchange']['unknown_fee_rate'] = unknown_fee_rate
|
||||||
|
|
||||||
ex = get_patched_exchange(mocker, default_conf)
|
ex = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
assert ex.calculate_fee_rate(order) == expected
|
assert ex.calculate_fee_rate(order) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@ -169,6 +169,7 @@ def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_start_no_data(mocker, hyperopt_conf) -> None:
|
def test_start_no_data(mocker, hyperopt_conf) -> None:
|
||||||
|
hyperopt_conf['user_data_dir'] = Path("tests")
|
||||||
patched_configuration_load_config_file(mocker, hyperopt_conf)
|
patched_configuration_load_config_file(mocker, hyperopt_conf)
|
||||||
mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame))
|
mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame))
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -189,6 +190,12 @@ def test_start_no_data(mocker, hyperopt_conf) -> None:
|
|||||||
with pytest.raises(OperationalException, match='No data found. Terminating.'):
|
with pytest.raises(OperationalException, match='No data found. Terminating.'):
|
||||||
start_hyperopt(pargs)
|
start_hyperopt(pargs)
|
||||||
|
|
||||||
|
# Cleanup since that failed hyperopt start leaves a lockfile.
|
||||||
|
try:
|
||||||
|
Path(Hyperopt.get_lock_filename(hyperopt_conf)).unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_start_filelock(mocker, hyperopt_conf, caplog) -> None:
|
def test_start_filelock(mocker, hyperopt_conf, caplog) -> None:
|
||||||
hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf)))
|
hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf)))
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
||||||
|
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
@ -7,6 +8,7 @@ import pytest
|
|||||||
import time_machine
|
import time_machine
|
||||||
|
|
||||||
from freqtrade.constants import AVAILABLE_PAIRLISTS
|
from freqtrade.constants import AVAILABLE_PAIRLISTS
|
||||||
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
@ -216,6 +218,40 @@ def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog):
|
|||||||
log_has_re(r"Pair blacklist contains an invalid Wildcard.*", caplog)
|
log_has_re(r"Pair blacklist contains an invalid Wildcard.*", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_logs_for_pairs_already_in_blacklist(mocker, markets, static_pl_conf, caplog):
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
exchange_has=MagicMock(return_value=True),
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
)
|
||||||
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
caplog.clear()
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
# Ensure all except those in whitelist are removed.
|
||||||
|
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
|
||||||
|
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
|
||||||
|
# Ensure that log message wasn't generated.
|
||||||
|
assert not log_has('Pair BLK/BTC in your blacklist. Removing it from whitelist...', caplog)
|
||||||
|
|
||||||
|
new_whitelist = freqtrade.pairlists.verify_blacklist(whitelist + ['BLK/BTC'], logger.warning)
|
||||||
|
# Ensure that the pair is removed from the white list, and properly logged.
|
||||||
|
assert set(whitelist) == set(new_whitelist)
|
||||||
|
matches = sum(1 for message in caplog.messages
|
||||||
|
if message == 'Pair BLK/BTC in your blacklist. Removing it from whitelist...')
|
||||||
|
assert matches == 1
|
||||||
|
|
||||||
|
new_whitelist = freqtrade.pairlists.verify_blacklist(whitelist + ['BLK/BTC'], logger.warning)
|
||||||
|
# Ensure that the pair is not logged anymore when being removed from the pair list.
|
||||||
|
assert set(whitelist) == set(new_whitelist)
|
||||||
|
matches = sum(1 for message in caplog.messages
|
||||||
|
if message == 'Pair BLK/BTC in your blacklist. Removing it from whitelist...')
|
||||||
|
assert matches == 1
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
|
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -657,6 +693,22 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
|
|||||||
assert log_has("PerformanceFilter is not available in this mode.", caplog)
|
assert log_has("PerformanceFilter is not available in this mode.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None:
|
||||||
|
whitelist_conf['pairlists'] = [
|
||||||
|
{"method": "StaticPairList"},
|
||||||
|
{"method": "ShuffleFilter", "seed": 42}
|
||||||
|
]
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, whitelist_conf)
|
||||||
|
PairListManager(exchange, whitelist_conf)
|
||||||
|
assert log_has("Backtesting mode detected, applying seed value: 42", caplog)
|
||||||
|
caplog.clear()
|
||||||
|
whitelist_conf['runmode'] = RunMode.DRY_RUN
|
||||||
|
PairListManager(exchange, whitelist_conf)
|
||||||
|
assert not log_has("Backtesting mode detected, applying seed value: 42", caplog)
|
||||||
|
assert log_has("Live mode detected, not applying seed.", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
|
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
|
||||||
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
|
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
|
||||||
@ -1089,33 +1141,34 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf):
|
|||||||
# Happy path: Descending order, all values filled
|
# Happy path: Descending order, all values filled
|
||||||
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
||||||
['ETH/BTC', 'TKN/BTC'],
|
['ETH/BTC', 'TKN/BTC'],
|
||||||
[{'pair': 'TKN/BTC', 'profit': 5, 'count': 3}, {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}],
|
[{'pair': 'TKN/BTC', 'profit_ratio': 0.05, 'count': 3},
|
||||||
|
{'pair': 'ETH/BTC', 'profit_ratio': 0.04, 'count': 2}],
|
||||||
['TKN/BTC', 'ETH/BTC']),
|
['TKN/BTC', 'ETH/BTC']),
|
||||||
# Performance data outside allow list ignored
|
# Performance data outside allow list ignored
|
||||||
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
||||||
['ETH/BTC', 'TKN/BTC'],
|
['ETH/BTC', 'TKN/BTC'],
|
||||||
[{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3},
|
[{'pair': 'OTHER/BTC', 'profit_ratio': 0.05, 'count': 3},
|
||||||
{'pair': 'ETH/BTC', 'profit': 4, 'count': 2}],
|
{'pair': 'ETH/BTC', 'profit_ratio': 0.04, 'count': 2}],
|
||||||
['ETH/BTC', 'TKN/BTC']),
|
['ETH/BTC', 'TKN/BTC']),
|
||||||
# Partial performance data missing and sorted between positive and negative profit
|
# Partial performance data missing and sorted between positive and negative profit
|
||||||
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
||||||
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
|
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
|
||||||
[{'pair': 'ETH/BTC', 'profit': -5, 'count': 100},
|
[{'pair': 'ETH/BTC', 'profit_ratio': -0.05, 'count': 100},
|
||||||
{'pair': 'TKN/BTC', 'profit': 4, 'count': 2}],
|
{'pair': 'TKN/BTC', 'profit_ratio': 0.04, 'count': 2}],
|
||||||
['TKN/BTC', 'LTC/BTC', 'ETH/BTC']),
|
['TKN/BTC', 'LTC/BTC', 'ETH/BTC']),
|
||||||
# Tie in performance data broken by count (ascending)
|
# Tie in performance data broken by count (ascending)
|
||||||
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
||||||
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
|
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
|
||||||
[{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 101},
|
[{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 101},
|
||||||
{'pair': 'TKN/BTC', 'profit': -5.01, 'count': 2},
|
{'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 2},
|
||||||
{'pair': 'ETH/BTC', 'profit': -5.01, 'count': 100}],
|
{'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 100}],
|
||||||
['TKN/BTC', 'ETH/BTC', 'LTC/BTC']),
|
['TKN/BTC', 'ETH/BTC', 'LTC/BTC']),
|
||||||
# Tie in performance and count, broken by alphabetical sort
|
# Tie in performance and count, broken by alphabetical sort
|
||||||
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
|
||||||
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
|
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
|
||||||
[{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 1},
|
[{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 1},
|
||||||
{'pair': 'TKN/BTC', 'profit': -5.01, 'count': 1},
|
{'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 1},
|
||||||
{'pair': 'ETH/BTC', 'profit': -5.01, 'count': 1}],
|
{'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 1}],
|
||||||
['ETH/BTC', 'LTC/BTC', 'TKN/BTC']),
|
['ETH/BTC', 'LTC/BTC', 'TKN/BTC']),
|
||||||
])
|
])
|
||||||
def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance,
|
def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance,
|
||||||
|
@ -424,7 +424,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
assert stats['trade_count'] == 2
|
assert stats['trade_count'] == 2
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == 'just now'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == 'just now'
|
||||||
assert stats['avg_duration'] in ('0:00:00', '0:00:01')
|
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'ETH/BTC'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
|
||||||
@ -435,7 +435,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
assert stats['trade_count'] == 2
|
assert stats['trade_count'] == 2
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == 'just now'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == 'just now'
|
||||||
assert stats['avg_duration'] in ('0:00:00', '0:00:01')
|
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'ETH/BTC'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
assert isnan(stats['profit_all_coin'])
|
assert isnan(stats['profit_all_coin'])
|
||||||
@ -1093,7 +1093,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
|
|||||||
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
||||||
rpc._rpc_forcebuy(pair, 0.0001)
|
rpc._rpc_forcebuy(pair, 0.0001)
|
||||||
pair = 'XRP/BTC'
|
pair = 'XRP/BTC'
|
||||||
trade = rpc._rpc_forcebuy(pair, 0.0001)
|
trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit')
|
||||||
assert isinstance(trade, Trade)
|
assert isinstance(trade, Trade)
|
||||||
assert trade.pair == pair
|
assert trade.pair == pair
|
||||||
assert trade.open_rate == 0.0001
|
assert trade.open_rate == 0.0001
|
||||||
@ -1225,6 +1225,16 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
|
|||||||
assert 'errors' in ret
|
assert 'errors' in ret
|
||||||
assert isinstance(ret['errors'], dict)
|
assert isinstance(ret['errors'], dict)
|
||||||
|
|
||||||
|
ret = rpc._rpc_blacklist_delete(["DOGE/BTC", 'HOT/BTC'])
|
||||||
|
|
||||||
|
assert 'StaticPairList' in ret['method']
|
||||||
|
assert len(ret['blacklist']) == 2
|
||||||
|
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
|
||||||
|
assert ret['blacklist'] == ['ETH/BTC', 'XRP/.*']
|
||||||
|
assert ret['blacklist_expanded'] == ['ETH/BTC', 'XRP/BTC', 'XRP/USDT']
|
||||||
|
assert 'errors' in ret
|
||||||
|
assert isinstance(ret['errors'], dict)
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_edge_disabled(mocker, default_conf) -> None:
|
def test_rpc_edge_disabled(mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
@ -533,6 +533,7 @@ def test_api_show_config(botclient):
|
|||||||
assert rc.json()['timeframe_min'] == 5
|
assert rc.json()['timeframe_min'] == 5
|
||||||
assert rc.json()['state'] == 'running'
|
assert rc.json()['state'] == 'running'
|
||||||
assert rc.json()['bot_name'] == 'freqtrade'
|
assert rc.json()['bot_name'] == 'freqtrade'
|
||||||
|
assert rc.json()['strategy_version'] is None
|
||||||
assert not rc.json()['trailing_stop']
|
assert not rc.json()['trailing_stop']
|
||||||
assert 'bid_strategy' in rc.json()
|
assert 'bid_strategy' in rc.json()
|
||||||
assert 'ask_strategy' in rc.json()
|
assert 'ask_strategy' in rc.json()
|
||||||
@ -954,6 +955,38 @@ def test_api_blacklist(botclient, mocker):
|
|||||||
"errors": {},
|
"errors": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rc = client_delete(client, f"{BASE_URI}/blacklist?pairs_to_delete=DOGE/BTC")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json() == {"blacklist": ["HOT/BTC", "ETH/BTC", "XRP/.*"],
|
||||||
|
"blacklist_expanded": ["ETH/BTC", "XRP/BTC", "XRP/USDT"],
|
||||||
|
"length": 3,
|
||||||
|
"method": ["StaticPairList"],
|
||||||
|
"errors": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = client_delete(client, f"{BASE_URI}/blacklist?pairs_to_delete=NOTHING/BTC")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json() == {"blacklist": ["HOT/BTC", "ETH/BTC", "XRP/.*"],
|
||||||
|
"blacklist_expanded": ["ETH/BTC", "XRP/BTC", "XRP/USDT"],
|
||||||
|
"length": 3,
|
||||||
|
"method": ["StaticPairList"],
|
||||||
|
"errors": {
|
||||||
|
"NOTHING/BTC": {
|
||||||
|
"error_msg": "Pair NOTHING/BTC is not in the current blacklist."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rc = client_delete(
|
||||||
|
client,
|
||||||
|
f"{BASE_URI}/blacklist?pairs_to_delete=HOT/BTC&pairs_to_delete=ETH/BTC")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json() == {"blacklist": ["XRP/.*"],
|
||||||
|
"blacklist_expanded": ["XRP/BTC", "XRP/USDT"],
|
||||||
|
"length": 1,
|
||||||
|
"method": ["StaticPairList"],
|
||||||
|
"errors": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_api_whitelist(botclient):
|
def test_api_whitelist(botclient):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
@ -98,7 +98,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
|
|||||||
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
||||||
"['count'], ['locks'], ['unlock', 'delete_locks'], "
|
"['count'], ['locks'], ['unlock', 'delete_locks'], "
|
||||||
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
|
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
|
||||||
"['stopbuy'], ['whitelist'], ['blacklist'], "
|
"['stopbuy'], ['whitelist'], ['blacklist'], ['blacklist_delete', 'bl_delete'], "
|
||||||
"['logs'], ['edge'], ['help'], ['version']"
|
"['logs'], ['edge'], ['help'], ['version']"
|
||||||
"]")
|
"]")
|
||||||
|
|
||||||
@ -937,7 +937,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
telegram._forcesell(update=update, context=context)
|
telegram._forcesell(update=update, context=context)
|
||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert msg_mock.call_count == 4
|
||||||
last_msg = msg_mock.call_args_list[-1][0][0]
|
last_msg = msg_mock.call_args_list[-2][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
@ -952,6 +952,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
'profit_amount': 6.314e-05,
|
'profit_amount': 6.314e-05,
|
||||||
'profit_ratio': 0.0629778,
|
'profit_ratio': 0.0629778,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
|
'base_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'buy_tag': ANY,
|
'buy_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
@ -1001,7 +1002,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert msg_mock.call_count == 4
|
||||||
|
|
||||||
last_msg = msg_mock.call_args_list[-1][0][0]
|
last_msg = msg_mock.call_args_list[-2][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
@ -1016,6 +1017,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
'profit_amount': -5.497e-05,
|
'profit_amount': -5.497e-05,
|
||||||
'profit_ratio': -0.05482878,
|
'profit_ratio': -0.05482878,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
|
'base_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'buy_tag': ANY,
|
'buy_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
@ -1055,7 +1057,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
|
|
||||||
# Called for each trade 2 times
|
# Called for each trade 2 times
|
||||||
assert msg_mock.call_count == 8
|
assert msg_mock.call_count == 8
|
||||||
msg = msg_mock.call_args_list[1][0][0]
|
msg = msg_mock.call_args_list[0][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
@ -1070,6 +1072,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
'profit_amount': -4.09e-06,
|
'profit_amount': -4.09e-06,
|
||||||
'profit_ratio': -0.00408133,
|
'profit_ratio': -0.00408133,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
|
'base_currency': 'ETH',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'buy_tag': ANY,
|
'buy_tag': ANY,
|
||||||
'sell_reason': SellType.FORCE_SELL.value,
|
'sell_reason': SellType.FORCE_SELL.value,
|
||||||
@ -1470,6 +1473,13 @@ def test_blacklist_static(default_conf, update, mocker) -> None:
|
|||||||
in msg_mock.call_args_list[0][0][0])
|
in msg_mock.call_args_list[0][0][0])
|
||||||
assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"]
|
assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"]
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
context.args = ["DOGE/BTC"]
|
||||||
|
telegram._blacklist_delete(update=update, context=context)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert ("Blacklist contains 3 pairs\n`HOT/BTC, ETH/BTC, XRP/.*`"
|
||||||
|
in msg_mock.call_args_list[0][0][0])
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_logs(default_conf, update, mocker) -> None:
|
def test_telegram_logs(default_conf, update, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1597,12 +1607,20 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
def test_version_handle(default_conf, update, mocker) -> None:
|
def test_version_handle(default_conf, update, mocker) -> None:
|
||||||
|
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
telegram._version(update=update, context=MagicMock())
|
telegram._version(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
freqtradebot.strategy.version = lambda: '1.1.1'
|
||||||
|
|
||||||
|
telegram._version(update=update, context=MagicMock())
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert '*Strategy version: * `1.1.1`' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_show_config_handle(default_conf, update, mocker) -> None:
|
def test_show_config_handle(default_conf, update, mocker) -> None:
|
||||||
|
|
||||||
|
@ -292,3 +292,15 @@ def test__send_msg_with_json_format(default_conf, mocker, caplog):
|
|||||||
webhook._send_msg(msg)
|
webhook._send_msg(msg)
|
||||||
|
|
||||||
assert post.call_args[1] == {'json': msg}
|
assert post.call_args[1] == {'json': msg}
|
||||||
|
|
||||||
|
|
||||||
|
def test__send_msg_with_raw_format(default_conf, mocker, caplog):
|
||||||
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
|
default_conf["webhook"]["format"] = "raw"
|
||||||
|
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||||
|
msg = {'data': 'Hello'}
|
||||||
|
post = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||||
|
webhook._send_msg(msg)
|
||||||
|
|
||||||
|
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
|
||||||
|
@ -2171,10 +2171,20 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
|
|||||||
assert open_trade.is_open is True
|
assert open_trade.is_open is True
|
||||||
assert freqtrade.strategy.check_sell_timeout.call_count == 1
|
assert freqtrade.strategy.check_sell_timeout.call_count == 1
|
||||||
|
|
||||||
# 2nd canceled trade ...
|
# 2nd canceled trade - Fail execute sell
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
open_trade.open_order_id = 'order_id_2'
|
open_trade.open_order_id = 'order_id_2'
|
||||||
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
|
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
|
||||||
|
side_effect=DependencyException)
|
||||||
|
freqtrade.check_handle_timedout()
|
||||||
|
assert log_has_re('Unable to emergency sell .*', caplog)
|
||||||
|
|
||||||
|
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# 2nd canceled trade ...
|
||||||
|
open_trade.open_order_id = 'order_id_2'
|
||||||
freqtrade.check_handle_timedout()
|
freqtrade.check_handle_timedout()
|
||||||
assert log_has_re('Emergencyselling trade.*', caplog)
|
assert log_has_re('Emergencyselling trade.*', caplog)
|
||||||
assert et_mock.call_count == 1
|
assert et_mock.call_count == 1
|
||||||
@ -2979,7 +2989,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee,
|
|||||||
assert trade.close_profit == 0.09451372
|
assert trade.close_profit == 0.09451372
|
||||||
|
|
||||||
assert rpc_mock.call_count == 3
|
assert rpc_mock.call_count == 3
|
||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-2][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL,
|
'type': RPCMessageType.SELL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
|
@ -184,16 +184,18 @@ def test_render_template_fallback(mocker):
|
|||||||
assert 'if self.dp' in val
|
assert 'if self.dp' in val
|
||||||
|
|
||||||
|
|
||||||
def test_parse_db_uri_for_logging() -> None:
|
@pytest.mark.parametrize('conn_url,expected', [
|
||||||
postgresql_conn_uri = "postgresql+psycopg2://scott123:scott123@host/dbname"
|
("postgresql+psycopg2://scott123:scott123@host:1245/dbname",
|
||||||
mariadb_conn_uri = "mariadb+mariadbconnector://app_user:Password123!@127.0.0.1:3306/company"
|
"postgresql+psycopg2://scott123:*****@host:1245/dbname"),
|
||||||
mysql_conn_uri = "mysql+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4"
|
("postgresql+psycopg2://scott123:scott123@host.name.com/dbname",
|
||||||
sqlite_conn_uri = "sqlite:////freqtrade/user_data/tradesv3.sqlite"
|
"postgresql+psycopg2://scott123:*****@host.name.com/dbname"),
|
||||||
censored_pwd = "*****"
|
("mariadb+mariadbconnector://app_user:Password123!@127.0.0.1:3306/company",
|
||||||
|
"mariadb+mariadbconnector://app_user:*****@127.0.0.1:3306/company"),
|
||||||
|
("mysql+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4",
|
||||||
|
"mysql+pymysql://user:*****@some_mariadb/dbname?charset=utf8mb4"),
|
||||||
|
("sqlite:////freqtrade/user_data/tradesv3.sqlite",
|
||||||
|
"sqlite:////freqtrade/user_data/tradesv3.sqlite"),
|
||||||
|
])
|
||||||
|
def test_parse_db_uri_for_logging(conn_url, expected) -> None:
|
||||||
|
|
||||||
def get_pwd(x): return x.split(':')[2].split('@')[0]
|
assert parse_db_uri_for_logging(conn_url) == expected
|
||||||
|
|
||||||
assert get_pwd(parse_db_uri_for_logging(postgresql_conn_uri)) == censored_pwd
|
|
||||||
assert get_pwd(parse_db_uri_for_logging(mariadb_conn_uri)) == censored_pwd
|
|
||||||
assert get_pwd(parse_db_uri_for_logging(mysql_conn_uri)) == censored_pwd
|
|
||||||
assert sqlite_conn_uri == parse_db_uri_for_logging(sqlite_conn_uri)
|
|
||||||
|
@ -43,7 +43,7 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None:
|
|||||||
worker.freqtrade.state = State.STOPPED
|
worker.freqtrade.state = State.STOPPED
|
||||||
state = worker._worker(old_state=State.RUNNING)
|
state = worker._worker(old_state=State.RUNNING)
|
||||||
assert state is State.STOPPED
|
assert state is State.STOPPED
|
||||||
assert log_has('Changing state to: STOPPED', caplog)
|
assert log_has('Changing state from RUNNING to: STOPPED', caplog)
|
||||||
assert mock_throttle.call_count == 1
|
assert mock_throttle.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user