Merge pull request #6134 from freqtrade/new_release

New release 2021.12
This commit is contained in:
Matthias 2021-12-29 17:00:19 +01:00 committed by GitHub
commit b530600718
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 912 additions and 477 deletions

View File

@ -5,9 +5,17 @@ updates:
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
target-branch: develop
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: develop

View File

@ -101,16 +101,13 @@ jobs:
run: |
mypy freqtrade scripts
- name: Slack Notification
uses: lazy-actions/slatify@v3.0.0
- name: Discord notification
uses: rjstone/discord-webhook-notify@v1
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI ${{ matrix.os }}*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
severity: error
details: Freqtrade CI failed on ${{ matrix.os }}
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
build_macos:
runs-on: ${{ matrix.os }}
@ -194,17 +191,13 @@ jobs:
run: |
mypy freqtrade scripts
- name: Slack Notification
uses: lazy-actions/slatify@v3.0.0
- name: Discord notification
uses: rjstone/discord-webhook-notify@v1
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI ${{ matrix.os }}*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
severity: error
details: Test Succeeded!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
build_windows:
@ -257,16 +250,13 @@ jobs:
run: |
mypy freqtrade scripts
- name: Slack Notification
uses: lazy-actions/slatify@v3.0.0
- name: Discord notification
uses: rjstone/discord-webhook-notify@v1
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI windows*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
severity: error
details: Test Failed
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
docs_check:
runs-on: ubuntu-20.04
@ -288,14 +278,13 @@ jobs:
pip install mkdocs
mkdocs build
- name: Slack Notification
uses: lazy-actions/slatify@v3.0.0
- name: Discord notification
uses: rjstone/discord-webhook-notify@v1
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade Docs*'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
severity: error
details: Freqtrade doc test failed!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
cleanup-prior-runs:
runs-on: ubuntu-20.04
@ -306,7 +295,7 @@ jobs:
env:
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:
needs: [ build_linux, build_macos, build_windows, docs_check ]
runs-on: ubuntu-20.04
@ -320,14 +309,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Slack Notification
uses: lazy-actions/slatify@v3.0.0
- name: Discord notification
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)
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI*'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
severity: info
details: Test Completed!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
deploy:
needs: [ build_linux, build_macos, build_windows, docs_check ]
@ -385,7 +373,7 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
uses: crazy-max/ghaction-docker-buildx@v3.3.1
with:
buildx-version: latest
qemu-version: latest
@ -400,17 +388,13 @@ jobs:
run: |
build_helpers/publish_docker_multi.sh
- name: Slack Notification
uses: lazy-actions/slatify@v3.0.0
- name: Discord notification
uses: rjstone/discord-webhook-notify@v1
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI Deploy*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
severity: info
details: Deploy Succeeded!
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
deploy_arm:

View File

@ -10,7 +10,7 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2.1.0
uses: peter-evans/dockerhub-description@v2.4.3
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

View File

@ -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.

View File

@ -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}')"
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') {
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') {
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

View File

@ -13,7 +13,7 @@ A sample of this can be found below, which is identical to the Default Hyperopt
``` python
from datetime import datetime
from typing import Dict
from typing import Any, Dict
from pandas import DataFrame

View File

@ -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.
!!! 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.
Please read the [pairlists documentation](plugins.md#pairlists) for more information.

View File

@ -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).
* Loops per candle simulating entry and exit points.
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
* Call `custom_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.
* 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
!!! Note

View File

@ -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.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.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_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_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
| `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
| `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

View File

@ -324,9 +324,8 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade
This documents some decisions taken for the CI Pipeline.
* 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`.
* 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.
* Full docker image rebuilds are run once a week via schedule.
* Deployments run on ubuntu.

View File

@ -199,6 +199,11 @@ OKEX requires a passphrase for each api key, you will therefore need to add this
!!! 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.
## 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
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.

View File

@ -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).
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.
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",
"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.
!!! Warning "Backtesting"
`PrecisionFilter` does not support backtesting mode using multiple strategies.
#### PriceFilter
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.
!!! 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

View File

@ -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).
!!! 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
* [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
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"
#### Install necessary dependencies

View File

@ -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.
Additional features when using plot_config include:
Additional features when using `plot_config` include:
* Specify colors per indicator
* 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.
It also allows multiple subplots to display both MACD and RSI at the same time.
Plot type can be configured using `type` key. Possible types are:
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
* `bar` corresponding to `plotly.graph_objects.Bar` class.
@ -182,40 +183,89 @@ Extra parameters to `plotly.graph_objects.*` constructor can be specified in `pl
Sample configuration with inline comments explaining the process:
``` 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': {}
@property
def plot_config(self):
"""
There are a lot of solutions how to build the return dictionary.
The only important point is the return value.
Example:
plot_config = {'main_plot': {}, 'subplots': {}}
"""
plot_config = {}
plot_config['main_plot'] = {
# Configuration for main plot indicators.
# Assumes 2 parameters, emashort and emalong to be specified.
f'ema_{self.emashort.value}': {'color': 'red'},
f'ema_{self.emalong.value}': {'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
},
'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'}
}
# plot senkou_b, too. Not only the area to it.
'senkou_b': {}
}
plot_config['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'}
}
}
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
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.

View File

@ -1,4 +1,4 @@
mkdocs==1.2.3
mkdocs-material==7.3.6
mkdocs-material==8.1.3
mdx_truly_sane_lists==1.2
pymdown-extensions==9.1

View File

@ -127,6 +127,21 @@ The provided exit-tag is then used as sell-reason - and shown as such in backtes
!!! Note
`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
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:

View File

@ -387,8 +387,10 @@ class AwesomeStrategy(IStrategy):
**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.
!!! Warning "No backtesting support"
Custom entry-prices are currently not supported during backtesting.
!!! Warning "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

View File

@ -50,7 +50,9 @@ candles.head()
```python
# Load strategy using values set above
from freqtrade.resolvers import StrategyResolver
from freqtrade.data.dataprovider import DataProvider
strategy = StrategyResolver.load_strategy(config)
strategy.dp = DataProvider(config, None, None)
# Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair})
@ -228,7 +230,7 @@ graph = generate_candlestick_graph(pair=pair,
# Show graph inline
# graph.show()
# Render graph in a separate window
# Render graph in a seperate window
graph.show(renderer="browser")
```

View File

@ -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.
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
"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.
@ -75,11 +104,13 @@ Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `limit`
* ~~`limit` # Deprecated - should no longer be used.~~
* `open_rate`
* `amount`
* `open_date`
* `stake_amount`
* `stake_currency`
* `base_currency`
* `fiat_currency`
* `order_type`
* `current_rate`
@ -98,6 +129,7 @@ Possible parameters are:
* `open_date`
* `stake_amount`
* `stake_currency`
* `base_currency`
* `fiat_currency`
* `order_type`
* `current_rate`
@ -116,7 +148,10 @@ Possible parameters are:
* `open_date`
* `stake_amount`
* `stake_currency`
* `base_currency`
* `fiat_currency`
* `order_type`
* `current_rate`
* `buy_tag`
### Webhooksell
@ -134,6 +169,7 @@ Possible parameters are:
* `profit_amount`
* `profit_ratio`
* `stake_currency`
* `base_currency`
* `fiat_currency`
* `sell_reason`
* `order_type`
@ -156,6 +192,7 @@ Possible parameters are:
* `profit_amount`
* `profit_ratio`
* `stake_currency`
* `base_currency`
* `fiat_currency`
* `sell_reason`
* `order_type`
@ -178,6 +215,7 @@ Possible parameters are:
* `profit_amount`
* `profit_ratio`
* `stake_currency`
* `base_currency`
* `fiat_currency`
* `sell_reason`
* `order_type`

View File

@ -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).
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_Lib0.4.22cp38cp38win_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.
``` powershell

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """
__version__ = '2021.11'
__version__ = '2021.12'
if __version__ == 'develop':

View File

@ -1,6 +1,6 @@
from datetime import datetime, timezone
from cachetools.ttl import TTLCache
from cachetools import TTLCache
class PeriodicCache(TTLCache):

View File

@ -50,6 +50,8 @@ USERPATH_STRATEGIES = 'strategies'
USERPATH_NOTEBOOKS = 'notebooks'
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
ENV_VAR_PREFIX = 'FREQTRADE__'
NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired')
@ -312,10 +314,16 @@ CONF_SCHEMA = {
'type': 'object',
'properties': {
'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'},
'webhookbuycancel': {'type': 'object'},
'webhookbuyfill': {'type': 'object'},
'webhooksell': {'type': 'object'},
'webhooksellcancel': {'type': 'object'},
'webhooksellfill': {'type': 'object'},
'webhookstatus': {'type': 'object'},
},
},
@ -387,6 +395,7 @@ CONF_SCHEMA = {
},
'uniqueItems': True
},
'unknown_fee_rate': {'type': 'number'},
'outdated_offset': {'type': 'integer', 'minimum': 1},
'markets_refresh_interval': {'type': 'integer'},
'ccxt_config': {'type': 'object'},

View File

@ -6,7 +6,6 @@ from typing import List, Optional
import numpy as np
import pandas as pd
from freqtrade import misc
from freqtrade.configuration import TimeRange
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
ListPairsWithTimeframes, TradeList)
@ -61,10 +60,10 @@ class HDF5DataHandler(IDataHandler):
filename = self._pair_data_filename(self._datadir, pair, timeframe)
ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc')
ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date'])
ds.close()
_data.loc[:, self._columns].to_hdf(
filename, key, mode='a', complevel=9, complib='blosc',
format='table', data_columns=['date']
)
def _ohlcv_load(self, pair: str, timeframe: str,
timerange: Optional[TimeRange] = None) -> pd.DataFrame:
@ -99,19 +98,6 @@ class HDF5DataHandler(IDataHandler):
'low': 'float', 'close': 'float', 'volume': 'float'})
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:
"""
Append data to existing data structures
@ -142,11 +128,11 @@ class HDF5DataHandler(IDataHandler):
"""
key = self._pair_trades_key(pair)
ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair),
mode='a', complevel=9, complib='blosc')
ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
format='table', data_columns=['timestamp'])
ds.close()
pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf(
self._pair_trades_filename(self._datadir, pair), key,
mode='a', complevel=9, complib='blosc',
format='table', data_columns=['timestamp']
)
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})
return trades.values.tolist()
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 _get_file_extension(cls):
return "h5"
@classmethod
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
@ -199,15 +177,3 @@ class HDF5DataHandler(IDataHandler):
@classmethod
def _pair_trades_key(cls, pair: str) -> str:
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

View File

@ -12,6 +12,7 @@ from typing import List, Optional, Type
from pandas import DataFrame
from freqtrade import misc
from freqtrade.configuration import TimeRange
from freqtrade.constants import ListPairsWithTimeframes, TradeList
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:
self._datadir = datadir
@classmethod
def _get_file_extension(cls) -> str:
"""
Get file extension for this particular datahandler
"""
raise NotImplementedError()
@abstractclassmethod
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
"""
@ -70,7 +78,6 @@ class IDataHandler(ABC):
:return: DataFrame with ohlcv data, or empty DataFrame
"""
@abstractmethod
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
"""
Remove data for this pair
@ -78,6 +85,11 @@ class IDataHandler(ABC):
: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
@abstractmethod
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
@ -123,13 +135,17 @@ class IDataHandler(ABC):
:return: List of trades
"""
@abstractmethod
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
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))
@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,
timerange: Optional[TimeRange] = None,
fill_missing: bool = True,

View File

@ -174,34 +174,10 @@ class JsonDataHandler(IDataHandler):
pass
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
def _get_file_extension(cls):
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):

View File

@ -1,5 +1,6 @@
# flake8: noqa: F401
from freqtrade.enums.backteststate import BacktestState
from freqtrade.enums.ordertypevalue import OrderTypeValues
from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType

View File

@ -0,0 +1,6 @@
from enum import Enum
class OrderTypeValues(str, Enum):
limit = 'limit'
market = 'market'

View File

@ -5,6 +5,7 @@ from freqtrade.exchange.exchange import Exchange
# isort: on
from freqtrade.exchange.bibox import Bibox
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.bybit import Bybit
from freqtrade.exchange.coinbasepro import Coinbasepro

View 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)

View File

@ -685,16 +685,20 @@ class Exchange:
if not self.exchange_has('fetchL2OrderBook'):
return True
ob = self.fetch_l2_order_book(pair, 1)
if side == 'buy':
price = ob['asks'][0][0]
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
if limit >= price:
return True
else:
price = ob['bids'][0][0]
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
if limit <= price:
return True
try:
if side == 'buy':
price = ob['asks'][0][0]
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
if limit >= price:
return True
else:
price = ob['bids'][0][0]
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
if limit <= price:
return True
except IndexError:
# Ignore empty orderbooks when filling - can be filled with the next iteration.
pass
return False
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
@ -1087,7 +1091,8 @@ class Exchange:
# Fee handling
@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.
The "since" argument passed in is coming from the database and is in UTC,
@ -1111,8 +1116,10 @@ class Exchange:
try:
# Allow 5s offset to catch slight time offsets (discovered in #1185)
# since needs to be int in milliseconds
_params = params if params else {}
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]
self._log_exchange_response('get_trades_for_order', matched_trades)
@ -1190,9 +1197,11 @@ class Exchange:
tick = self.fetch_ticker(comb)
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
except ExchangeError:
return None
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]]:
"""
@ -1263,7 +1272,7 @@ class Exchange:
results = await asyncio.gather(*input_coro, return_exceptions=True)
for res in results:
if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
logger.warning(f"Async code raised an exception: {repr(res)}")
if raise_:
raise
continue
@ -1294,7 +1303,7 @@ class Exchange:
cached_pairs = []
# Gather coroutines to run
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)):
if not since_ms and self.required_candle_call_count > 1:
# Multiple calls for one pair - to get more history
@ -1317,27 +1326,30 @@ class Exchange:
)
cached_pairs.append((pair, timeframe))
results = asyncio.get_event_loop().run_until_complete(
asyncio.gather(*input_coroutines, return_exceptions=True))
results_df = {}
# handle caching
for res in results:
if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
continue
# Deconstruct tuple (has 3 elements)
pair, timeframe, ticks = res
# keeping last candle time as last refreshed time of the pair
if ticks:
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
# keeping parsed dataframe in cache
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
# Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
for input_coro in chunks(input_coroutines, 100):
results = asyncio.get_event_loop().run_until_complete(
asyncio.gather(*input_coro, return_exceptions=True))
# handle caching
for res in results:
if isinstance(res, Exception):
logger.warning(f"Async code raised an exception: {repr(res)}")
continue
# Deconstruct tuple (has 3 elements)
pair, timeframe, ticks = res
# keeping last candle time as last refreshed time of the pair
if ticks:
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
# keeping parsed dataframe in cache
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
for pair, timeframe in cached_pairs:
results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False)

View File

@ -278,7 +278,8 @@ class FreqtradeBot(LoggingMixin):
if order:
logger.info(f"Updating sell-fee on trade {trade} for order {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()
for trade in trades:
@ -286,7 +287,7 @@ class FreqtradeBot(LoggingMixin):
order = trade.select_order('buy', False)
if order:
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):
"""
@ -308,7 +309,7 @@ class FreqtradeBot(LoggingMixin):
order = trade.select_order('buy', False)
if order:
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):
"""
@ -466,8 +467,8 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
return False
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *,
ordertype: Optional[str] = None, buy_tag: Optional[str] = None) -> bool:
"""
Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY
@ -510,10 +511,7 @@ class FreqtradeBot(LoggingMixin):
f"{stake_amount} ...")
amount = stake_amount / enter_limit_requested
order_type = self.strategy.order_types['buy']
if forcebuy:
# Forcebuy can define a different ordertype
order_type = self.strategy.order_types.get('forcebuy', order_type)
order_type = ordertype or self.strategy.order_types['buy']
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,
@ -581,10 +579,6 @@ class FreqtradeBot(LoggingMixin):
)
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.commit()
@ -593,19 +587,25 @@ class FreqtradeBot(LoggingMixin):
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
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.
"""
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY,
'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY,
'buy_tag': trade.buy_tag,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
'limit': trade.open_rate,
'limit': trade.open_rate, # Deprecated (?)
'open_rate': trade.open_rate,
'order_type': order_type,
'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'],
@ -644,22 +644,6 @@ class FreqtradeBot(LoggingMixin):
# Send the message
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
#
@ -682,7 +666,7 @@ class FreqtradeBot(LoggingMixin):
trades_closed += 1
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
if trades_closed:
@ -868,7 +852,7 @@ class FreqtradeBot(LoggingMixin):
logger.info(
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
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 False
@ -926,8 +910,12 @@ class FreqtradeBot(LoggingMixin):
if max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
f'timed out {max_timeouts} times.')
self.execute_trade_exit(trade, order.get('price'), sell_reason=SellCheckTuple(
sell_type=SellType.EMERGENCY_SELL))
try:
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:
"""
@ -1081,7 +1069,10 @@ class FreqtradeBot(LoggingMixin):
trade: Trade,
limit: float,
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
:param trade: Trade instance
@ -1119,14 +1110,10 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException:
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:
# Emergency sells (default to 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)
time_in_force = self.strategy.order_time_in_force['sell']
@ -1158,16 +1145,16 @@ class FreqtradeBot(LoggingMixin):
trade.sell_order_status = ''
trade.close_rate_requested = limit
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
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock')
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
@ -1264,13 +1251,14 @@ class FreqtradeBot(LoggingMixin):
#
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
Handles closing both buy and sell orders.
:param trade: Trade object of the trade we're analyzing
:param order_id: Order-id of the order we're analyzing
: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
"""
if not order_id:
@ -1310,13 +1298,13 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets when order is closed
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.handle_protections(trade.pair)
self.wallets.update()
elif not trade.open_order_id:
elif send_msg and not trade.open_order_id:
# Buy fill
self._notify_enter_fill(trade)
self._notify_enter(trade, fill=True)
return False

View File

@ -342,10 +342,7 @@ class Backtesting:
# use Open rate if open_rate > calculated sell rate
return sell_row[OPEN_IDX]
# Use the maximum between close_rate and low as we
# cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that.
return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
return close_rate
else:
# 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)
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:
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)
except DependencyException:
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()
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
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)
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']
# Confirm trade entry:
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()):
return None
@ -450,10 +466,10 @@ class Backtesting:
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
trade = LocalTrade(
pair=pair,
open_rate=row[OPEN_IDX],
open_rate=propose_rate,
open_date=row[DATE_IDX].to_pydatetime(),
stake_amount=stake_amount,
amount=round(stake_amount / row[OPEN_IDX], 8),
amount=round(stake_amount / propose_rate, 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,

View File

@ -68,14 +68,14 @@ class PerformanceFilter(IPairList):
# - then pair name alphametically
sorted_df = list_df.merge(performance, on='pair', how='left')\
.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:
removed = sorted_df[sorted_df['profit'] < self._min_profit]
removed = sorted_df[sorted_df['profit_ratio'] < self._min_profit]
for _, row in removed.iterrows():
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)
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()

View File

@ -5,6 +5,7 @@ import logging
import random
from typing import Any, Dict, List
from freqtrade.enums import RunMode
from freqtrade.plugins.pairlist.IPairList import IPairList
@ -18,7 +19,15 @@ class ShuffleFilter(IPairList):
pairlist_pos: int) -> None:
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)
@property

View File

@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
import arrow
import numpy as np
from cachetools.ttl import TTLCache
from cachetools import TTLCache
from pandas import DataFrame
from freqtrade.exceptions import OperationalException

View File

@ -8,7 +8,7 @@ from functools import partial
from typing import Any, Dict, List
import arrow
from cachetools.ttl import TTLCache
from cachetools import TTLCache
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes

View File

@ -6,7 +6,7 @@ from copy import deepcopy
from typing import Any, Dict, List, Optional
import arrow
from cachetools.ttl import TTLCache
from cachetools import TTLCache
from pandas import DataFrame
from freqtrade.exceptions import OperationalException

View File

@ -2,13 +2,14 @@
PairList manager class
"""
import logging
from copy import deepcopy
from functools import partial
from typing import Dict, List
from cachetools import TTLCache, cached
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException
from freqtrade.mixins import LoggingMixin
from freqtrade.plugins.pairlist.IPairList import IPairList
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import PairListResolver
@ -17,7 +18,7 @@ from freqtrade.resolvers import PairListResolver
logger = logging.getLogger(__name__)
class PairListManager():
class PairListManager(LoggingMixin):
def __init__(self, exchange, config: dict) -> None:
self._exchange = exchange
@ -41,6 +42,9 @@ class PairListManager():
if not self._pairlist_handlers:
raise OperationalException("No Pairlist Handlers defined")
refresh_period = config.get('pairlist_refresh_period', 3600)
LoggingMixin.__init__(self, logger, refresh_period)
@property
def whitelist(self) -> List[str]:
"""The current whitelist"""
@ -108,9 +112,10 @@ class PairListManager():
except ValueError as err:
logger.error(f"Pair blacklist contains an invalid Wildcard: {err}")
return []
for pair in deepcopy(pairlist):
log_once = partial(self.log_once, logmethod=logmethod)
for pair in pairlist.copy():
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)
return pairlist

View File

@ -33,6 +33,9 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
if settings[setting] is not None:
btconfig[setting] = settings[setting]
# Force dry-run for backtesting
btconfig['dry_run'] = True
# Start backtesting
# Initialize backtesting object
def run_backtest():

View File

@ -4,6 +4,7 @@ from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.enums import OrderTypeValues
class Ping(BaseModel):
@ -125,25 +126,26 @@ class Daily(BaseModel):
class UnfilledTimeout(BaseModel):
buy: int
sell: int
unit: str
buy: Optional[int]
sell: Optional[int]
unit: Optional[str]
exit_timeout_count: Optional[int]
class OrderTypes(BaseModel):
buy: str
sell: str
emergencysell: Optional[str]
forcesell: Optional[str]
forcebuy: Optional[str]
stoploss: str
buy: OrderTypeValues
sell: OrderTypeValues
emergencysell: Optional[OrderTypeValues]
forcesell: Optional[OrderTypeValues]
forcebuy: Optional[OrderTypeValues]
stoploss: OrderTypeValues
stoploss_on_exchange: bool
stoploss_on_exchange_interval: Optional[int]
class ShowConfig(BaseModel):
version: str
strategy_version: Optional[str]
api_version: float
dry_run: bool
stake_currency: str
@ -158,7 +160,7 @@ class ShowConfig(BaseModel):
trailing_stop_positive_offset: Optional[float]
trailing_only_offset_is_reached: Optional[bool]
unfilledtimeout: UnfilledTimeout
order_types: OrderTypes
order_types: Optional[OrderTypes]
use_custom_stoploss: Optional[bool]
timeframe: Optional[str]
timeframe_ms: int
@ -274,10 +276,12 @@ class Logs(BaseModel):
class ForceBuyPayload(BaseModel):
pair: str
price: Optional[float]
ordertype: Optional[OrderTypeValues]
class ForceSellPayload(BaseModel):
tradeid: str
ordertype: Optional[OrderTypeValues]
class BlacklistPayload(BaseModel):

View File

@ -3,7 +3,7 @@ from copy import deepcopy
from pathlib import Path
from typing import List, Optional
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Query
from fastapi.exceptions import HTTPException
from freqtrade import __version__
@ -29,7 +29,9 @@ logger = logging.getLogger(__name__)
# API version
# Pre-1.1, no version was provided
# 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.
router_public = APIRouter()
@ -120,16 +122,19 @@ def edge(rpc: RPC = Depends(get_rpc)):
@router.get('/show_config', response_model=ShowConfig, tags=['info'])
def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)):
state = ''
strategy_version = None
if rpc:
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
return resp
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
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:
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'])
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'])
@ -152,6 +158,13 @@ def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)):
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'])
def whitelist(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_whitelist()

View File

@ -7,7 +7,7 @@ import datetime
import logging
from typing import Dict, List
from cachetools.ttl import TTLCache
from cachetools import TTLCache
from pycoingecko import CoinGeckoAPI
from requests.exceptions import RequestException

View File

@ -98,7 +98,8 @@ class RPC:
self._fiat_converter = CryptoToFiatConverter()
@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.
Explicitly does NOT return the full config to avoid leakage of sensitive
@ -106,6 +107,7 @@ class RPC:
"""
val = {
'version': __version__,
'strategy_version': strategy_version,
'dry_run': config['dry_run'],
'stake_currency': 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.'}
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>.
Sells the given trade at current price
@ -664,7 +666,11 @@ class RPC:
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side="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 ----
if self._freqtrade.state != State.RUNNING:
@ -692,7 +698,8 @@ class RPC:
self._freqtrade.wallets.update()
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>
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)
# 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 = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade
@ -850,6 +860,20 @@ class RPC:
}
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:
""" Returns the currently active blacklist"""
errors = {}

View File

@ -60,6 +60,10 @@ class RPCManager:
}
"""
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:
logger.debug('Forwarding message to rpc.%s', mod.name)
try:

View File

@ -111,8 +111,9 @@ class Telegram(RPCHandler):
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
r'/stats$', r'/count$', r'/locks$', r'/balance$',
r'/stopbuy$', r'/reload_config$', r'/show_config$',
r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$',
r'/forcebuy$', r'/help$', r'/version$']
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
r'/forcebuy$', r'/edge$', r'/help$', r'/version$']
# Create keys for generation
valid_keys_print = [k.replace('$', '') for k in valid_keys]
@ -169,6 +170,7 @@ class Telegram(RPCHandler):
CommandHandler('stopbuy', self._stopbuy),
CommandHandler('whitelist', self._whitelist),
CommandHandler('blacklist', self._blacklist),
CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete),
CommandHandler('logs', self._logs),
CommandHandler('edge', self._edge),
CommandHandler('help', self._help),
@ -1161,22 +1163,28 @@ class Telegram(RPCHandler):
Handler for /blacklist
Shows the currently active blacklist
"""
try:
self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args))
blacklist = self._rpc._rpc_blacklist(context.args)
errmsgs = []
for pair, error in blacklist['errors'].items():
errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
if errmsgs:
self._send_msg('\n'.join(errmsgs))
def send_blacklist_msg(self, blacklist: Dict):
errmsgs = []
for pair, error in blacklist['errors'].items():
errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
if errmsgs:
self._send_msg('\n'.join(errmsgs))
message = f"Blacklist contains {blacklist['length']} pairs\n"
message += f"`{', '.join(blacklist['blacklist'])}`"
message = f"Blacklist contains {blacklist['length']} pairs\n"
message += f"`{', '.join(blacklist['blacklist'])}`"
logger.debug(message)
self._send_msg(message)
except RPCException as e:
self._send_msg(str(e))
logger.debug(message)
self._send_msg(message)
@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
def _logs(self, update: Update, context: CallbackContext) -> None:
@ -1257,6 +1265,8 @@ class Telegram(RPCHandler):
"*/whitelist:* `Show current whitelist` \n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
"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"
"*/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
: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
def _show_config(self, update: Update, context: CallbackContext) -> None:

View File

@ -2,6 +2,7 @@
This module manages webhook communication
"""
import logging
import time
from typing import Any, Dict
from requests import RequestException, post
@ -28,12 +29,9 @@ class Webhook(RPCHandler):
super().__init__(rpc, config)
self._url = self._config['webhook']['url']
self._format = self._config['webhook'].get('format', 'form')
if self._format != 'form' and self._format != 'json':
raise NotImplementedError('Unknown webhook format `{}`, possible values are '
'`form` (default) and `json`'.format(self._format))
self._retries = self._config['webhook'].get('retries', 0)
self._retry_delay = self._config['webhook'].get('retry_delay', 0.1)
def cleanup(self) -> None:
"""
@ -77,13 +75,30 @@ class Webhook(RPCHandler):
def _send_msg(self, payload: dict) -> None:
"""do the actual call to the webhook"""
try:
if self._format == 'form':
post(self._url, data=payload)
elif self._format == 'json':
post(self._url, json=payload)
else:
raise NotImplementedError('Unknown format: {}'.format(self._format))
success = False
attempts = 0
while not success and attempts <= self._retries:
if attempts:
if self._retry_delay:
time.sleep(self._retry_delay)
logger.info("Retrying webhook...")
except RequestException as exc:
logger.warning("Could not call webhook url. Exception: %s", exc)
attempts += 1
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)

View File

@ -394,6 +394,12 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return []
def version(self) -> Optional[str]:
"""
Returns version of the strategy.
"""
return None
###
# END - Intended to be overridden by strategy
###

View File

@ -87,6 +87,7 @@ class {{ strategy }}(IStrategy):
'sell': 'gtc'
}
{{ plot_config | indent(4) }}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.

View File

@ -79,7 +79,9 @@
"source": [
"# Load strategy using values set above\n",
"from freqtrade.resolvers import StrategyResolver\n",
"from freqtrade.data.dataprovider import DataProvider\n",
"strategy = StrategyResolver.load_strategy(config)\n",
"strategy.dp = DataProvider(config, None, None)\n",
"\n",
"# Generate buy/sell signals using strategy\n",
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",

View File

@ -1,18 +1,20 @@
plot_config = {
# Main plot indicators (Moving averages, ...)
'main_plot': {
'tema': {},
'sar': {'color': 'white'},
},
'subplots': {
# Subplots - each dict defines one additional plot
"MACD": {
'macd': {'color': 'blue'},
'macdsignal': {'color': 'orange'},
@property
def plot_config(self):
return {
# Main plot indicators (Moving averages, ...)
'main_plot': {
'tema': {},
'sar': {'color': 'white'},
},
"RSI": {
'rsi': {'color': 'red'},
'subplots': {
# Subplots - each dict defines one additional plot
"MACD": {
'macd': {'color': 'blue'},
'macdsignal': {'color': 'orange'},
},
"RSI": {
'rsi': {'color': 'red'},
}
}
}
}

View File

@ -260,8 +260,8 @@ class Wallets:
if self._log:
logger.info(
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"ignoring trade."
f"the desired stake amount of ({stake_amount:.8f} * 1.3 = "
f"{stake_amount * 1.3:.8f}) < {min_stake_amount}), ignoring trade."
)
return 0
stake_amount = min_stake_amount

View File

@ -85,9 +85,12 @@ class Worker:
# Log state transition
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:
self.freqtrade.startup()
@ -113,8 +116,12 @@ class Worker:
if self._heartbeat_interval:
now = time.time()
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()}, "
f"version='{__version__}', state='{state.name}'")
f"version='{version}', state='{state.name}'")
self._heartbeat_msg = now
return state

View File

@ -81,8 +81,10 @@ markdown_extensions:
- pymdownx.snippets:
base_path: docs
check_paths: true
- pymdownx.tabbed
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde
- mdx_truly_sane_lists

View File

@ -6,7 +6,7 @@
coveralls==3.3.1
flake8==4.0.1
flake8-tidy-imports==4.5.0
mypy==0.910
mypy==0.930
pytest==6.2.5
pytest-asyncio==0.16.0
pytest-cov==3.0.0
@ -14,16 +14,16 @@ pytest-mock==3.6.1
pytest-random-order==1.0.4
isort==5.10.1
# For datetime mocking
time-machine==2.4.0
time-machine==2.5.0
# Convert jupyter notebooks to markdown documents
nbconvert==6.3.0
# mypy types
types-cachetools==4.2.5
types-cachetools==4.2.6
types-filelock==3.2.1
types-requests==2.26.0
types-requests==2.26.2
types-tabulate==0.8.3
# Extensions to datetime library
types-python-dateutil==2.8.2
types-python-dateutil==2.8.4

View File

@ -2,10 +2,10 @@
-r requirements.txt
# Required for hyperopt
scipy==1.7.2
scikit-learn==1.0.1
scipy==1.7.3
scikit-learn==1.0.2
scikit-optimize==0.9.0
filelock==3.4.0
filelock==3.4.2
joblib==1.1.0
psutil==5.8.0
progressbar2==3.55.0

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==5.4.0
plotly==5.5.0

View File

@ -1,19 +1,19 @@
numpy==1.21.4
pandas==1.3.4
numpy==1.21.5
pandas==1.3.5
pandas-ta==0.3.14b
ccxt==1.61.92
ccxt==1.65.25
# Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.0
cryptography==36.0.1
aiohttp==3.8.1
SQLAlchemy==1.4.27
python-telegram-bot==13.8.1
SQLAlchemy==1.4.29
python-telegram-bot==13.9
arrow==1.2.1
cachetools==4.2.2
requests==2.26.0
urllib3==1.26.7
jsonschema==4.2.1
TA-Lib==0.4.21
jsonschema==4.3.2
TA-Lib==0.4.22
technical==1.3.0
tabulate==0.8.9
pycoingecko==2.2.0
@ -31,16 +31,16 @@ python-rapidjson==1.5
sdnotify==0.3.2
# API Server
fastapi==0.70.0
uvicorn==0.15.0
fastapi==0.70.1
uvicorn==0.16.0
pyjwt==2.3.0
aiofiles==0.7.0
aiofiles==0.8.0
psutil==5.8.0
# Support for colorized terminal output
colorama==0.4.4
# Building config files interactively
questionary==1.10.0
prompt-toolkit==3.0.22
prompt-toolkit==3.0.24
# Extensions to datetime library
python-dateutil==2.8.2

View File

@ -36,7 +36,8 @@ function check_installed_python() {
fi
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
}
@ -205,7 +206,7 @@ function config() {
}
function install() {
echo_block "Installing mandatory dependencies"
if [ "$(uname -s)" == "Darwin" ]; then

View 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']

View File

@ -1026,6 +1026,12 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
assert order_closed['status'] == 'closed'
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", [
# 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 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]}, "
f"timeframe {pairs[0][1]} ...",
caplog)
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
# 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
@ -1768,7 +1783,7 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
assert len(res) == 1
# 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("Async code raised an exception: TypeError", caplog)
assert log_has("Async code raised an exception: TypeError()", caplog)
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
@pytest.mark.parametrize("order,expected", [
@pytest.mark.parametrize("order,unknown_fee_rate,expected", [
# Using base-currency
({'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,
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.08),
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, None, 0.08),
# Using quote currency
({'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,
'fee': {'currency': 'BTC', 'cost': 0.002, 'rate': None}}, 0.04),
'fee': {'currency': 'BTC', 'cost': 0.002, 'rate': None}}, None, 0.04),
# Using foreign currency
({'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,
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, None, 0.00074305),
# Rate included in return - return as is
({'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,
'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)
({'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,
'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,
'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})
if unknown_fee_rate:
default_conf['exchange']['unknown_fee_rate'] = unknown_fee_rate
ex = get_patched_exchange(mocker, default_conf)
assert ex.calculate_fee_rate(order) == expected

View File

@ -169,6 +169,7 @@ def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> 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)
mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame))
mocker.patch(
@ -189,6 +190,12 @@ def test_start_no_data(mocker, hyperopt_conf) -> None:
with pytest.raises(OperationalException, match='No data found. Terminating.'):
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:
hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf)))

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring,C0103,protected-access
import logging
import time
from unittest.mock import MagicMock, PropertyMock
@ -7,6 +8,7 @@ import pytest
import time_machine
from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade
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)
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):
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)
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")
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
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
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
['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']),
# Performance data outside allow list ignored
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
['ETH/BTC', 'TKN/BTC'],
[{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3},
{'pair': 'ETH/BTC', 'profit': 4, 'count': 2}],
[{'pair': 'OTHER/BTC', 'profit_ratio': 0.05, 'count': 3},
{'pair': 'ETH/BTC', 'profit_ratio': 0.04, 'count': 2}],
['ETH/BTC', 'TKN/BTC']),
# Partial performance data missing and sorted between positive and negative profit
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
[{'pair': 'ETH/BTC', 'profit': -5, 'count': 100},
{'pair': 'TKN/BTC', 'profit': 4, 'count': 2}],
[{'pair': 'ETH/BTC', 'profit_ratio': -0.05, 'count': 100},
{'pair': 'TKN/BTC', 'profit_ratio': 0.04, 'count': 2}],
['TKN/BTC', 'LTC/BTC', 'ETH/BTC']),
# Tie in performance data broken by count (ascending)
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
[{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 101},
{'pair': 'TKN/BTC', 'profit': -5.01, 'count': 2},
{'pair': 'ETH/BTC', 'profit': -5.01, 'count': 100}],
[{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 101},
{'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 2},
{'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 100}],
['TKN/BTC', 'ETH/BTC', 'LTC/BTC']),
# Tie in performance and count, broken by alphabetical sort
([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}],
['ETH/BTC', 'TKN/BTC', 'LTC/BTC'],
[{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 1},
{'pair': 'TKN/BTC', 'profit': -5.01, 'count': 1},
{'pair': 'ETH/BTC', 'profit': -5.01, 'count': 1}],
[{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 1},
{'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 1},
{'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 1}],
['ETH/BTC', 'LTC/BTC', 'TKN/BTC']),
])
def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance,

View File

@ -424,7 +424,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
assert stats['trade_count'] == 2
assert stats['first_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 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['first_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 prec_satoshi(stats['best_rate'], 6.2)
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'):
rpc._rpc_forcebuy(pair, 0.0001)
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 trade.pair == pair
assert trade.open_rate == 0.0001
@ -1225,6 +1225,16 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
assert 'errors' in ret
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:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())

View File

@ -533,6 +533,7 @@ def test_api_show_config(botclient):
assert rc.json()['timeframe_min'] == 5
assert rc.json()['state'] == 'running'
assert rc.json()['bot_name'] == 'freqtrade'
assert rc.json()['strategy_version'] is None
assert not rc.json()['trailing_stop']
assert 'bid_strategy' in rc.json()
assert 'ask_strategy' in rc.json()
@ -954,6 +955,38 @@ def test_api_blacklist(botclient, mocker):
"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):
ftbot, client = botclient

View File

@ -98,7 +98,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
"['stats'], ['daily'], ['weekly'], ['monthly'], "
"['count'], ['locks'], ['unlock', 'delete_locks'], "
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
"['stopbuy'], ['whitelist'], ['blacklist'], "
"['stopbuy'], ['whitelist'], ['blacklist'], ['blacklist_delete', 'bl_delete'], "
"['logs'], ['edge'], ['help'], ['version']"
"]")
@ -937,7 +937,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
telegram._forcesell(update=update, context=context)
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 {
'type': RPCMessageType.SELL,
'trade_id': 1,
@ -952,6 +952,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
'profit_amount': 6.314e-05,
'profit_ratio': 0.0629778,
'stake_currency': 'BTC',
'base_currency': 'ETH',
'fiat_currency': 'USD',
'buy_tag': ANY,
'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
last_msg = msg_mock.call_args_list[-1][0][0]
last_msg = msg_mock.call_args_list[-2][0][0]
assert {
'type': RPCMessageType.SELL,
'trade_id': 1,
@ -1016,6 +1017,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
'profit_amount': -5.497e-05,
'profit_ratio': -0.05482878,
'stake_currency': 'BTC',
'base_currency': 'ETH',
'fiat_currency': 'USD',
'buy_tag': ANY,
'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
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 {
'type': RPCMessageType.SELL,
'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_ratio': -0.00408133,
'stake_currency': 'BTC',
'base_currency': 'ETH',
'fiat_currency': 'USD',
'buy_tag': ANY,
'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])
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:
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:
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())
assert msg_mock.call_count == 1
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:

View File

@ -292,3 +292,15 @@ def test__send_msg_with_json_format(default_conf, mocker, caplog):
webhook._send_msg(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'}}

View File

@ -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 freqtrade.strategy.check_sell_timeout.call_count == 1
# 2nd canceled trade ...
# 2nd canceled trade - Fail execute sell
caplog.clear()
open_trade.open_order_id = 'order_id_2'
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()
assert log_has_re('Emergencyselling trade.*', caplog)
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 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 {
'type': RPCMessageType.SELL,
'trade_id': 1,

View File

@ -184,16 +184,18 @@ def test_render_template_fallback(mocker):
assert 'if self.dp' in val
def test_parse_db_uri_for_logging() -> None:
postgresql_conn_uri = "postgresql+psycopg2://scott123:scott123@host/dbname"
mariadb_conn_uri = "mariadb+mariadbconnector://app_user:Password123!@127.0.0.1:3306/company"
mysql_conn_uri = "mysql+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4"
sqlite_conn_uri = "sqlite:////freqtrade/user_data/tradesv3.sqlite"
censored_pwd = "*****"
@pytest.mark.parametrize('conn_url,expected', [
("postgresql+psycopg2://scott123:scott123@host:1245/dbname",
"postgresql+psycopg2://scott123:*****@host:1245/dbname"),
("postgresql+psycopg2://scott123:scott123@host.name.com/dbname",
"postgresql+psycopg2://scott123:*****@host.name.com/dbname"),
("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 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)
assert parse_db_uri_for_logging(conn_url) == expected

View File

@ -43,7 +43,7 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None:
worker.freqtrade.state = State.STOPPED
state = worker._worker(old_state=State.RUNNING)
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