Merge pull request #3395 from freqtrade/new_release

New release 2020.5
This commit is contained in:
Matthias 2020-06-01 06:24:14 +02:00 committed by GitHub
commit bf53d037b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 3290 additions and 1485 deletions

View File

@ -1,33 +0,0 @@
## Step 1: Have you search for this issue before posting it?
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
If it hasn't been reported, please create a new issue.
## Step 2: Describe your environment
* Operating system: ____
* Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`)
* Branch: Master | Develop
* Last Commit ID: _____ (`git log --format="%H" -n 1`)
## Step 3: Describe the problem:
*Explain the problem you have encountered*
### Steps to reproduce:
1. _____
2. _____
3. _____
### Observed Results:
* What happened?
* What did you expect to happen?
### Relevant code exceptions or logs:
```
// paste your log here
```

48
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,48 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: "Triage Needed"
assignees: ''
---
<!--
Have you searched for similar issues before posting it?
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
If it hasn't been reported, please create a new issue.
Please do not use bug reports to request new features.
-->
## Describe your environment
* Operating system: ____
* Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`)
* Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker)
Note: All issues other than enhancement requests will be closed without further comment if the above template is deleted or not filled out.
## Describe the problem:
*Explain the problem you have encountered*
### Steps to reproduce:
1. _____
2. _____
3. _____
### Observed Results:
* What happened?
* What did you expect to happen?
### Relevant code exceptions or logs
Note: Please copy/paste text of the messages, no screenshots of logs please.
```
// paste your log here
```

View File

@ -0,0 +1,27 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
<!--
Note: this section will not show up in the issue.
Have you search for this feature before requesting it? It's highly likely that a similar request was already filed.
-->
## Describe your environment
(if applicable)
* Operating system: ____
* Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`)
* Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker)
## Describe the enhancement
*Explain the enhancement you would like*

25
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,25 @@
---
name: BQuestion
about: Ask a question you could not find an answer in the docs
title: ''
labels: "Question"
assignees: ''
---
<!--
Have you searched for similar issues before posting it?
Did you have a VERY good look at the [documentation](https://www.freqtrade.io/en/latest/) and are sure that the question is not explained there
Please do not use the question template to report bugs or to request new features.
-->
## Describe your environment
* Operating system: ____
* Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`)
* Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker)
## Your question
*Ask the question you have not been able to find an answer in our [Documentation](https://www.freqtrade.io/en/latest/)*

View File

@ -100,7 +100,7 @@ jobs:
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: homoluctus/slatify@v1.8.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
type: ${{ job.status }} type: ${{ job.status }}
job_name: '*Freqtrade CI ${{ matrix.os }}*' job_name: '*Freqtrade CI ${{ matrix.os }}*'
@ -162,7 +162,7 @@ jobs:
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: homoluctus/slatify@v1.8.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
type: ${{ job.status }} type: ${{ job.status }}
job_name: '*Freqtrade CI windows*' job_name: '*Freqtrade CI windows*'
@ -189,6 +189,29 @@ jobs:
channel: '#notifications' channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }} url: ${{ secrets.SLACK_WEBHOOK }}
cleanup-prior-runs:
runs-on: ubuntu-latest
steps:
- name: Cleanup previous runs on this branch
uses: rokroskar/workflow-run-cleanup-action@v0.2.2
if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/master' && github.repository == 'freqtrade/freqtrade'"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
# Notify on slack only once - when CI completes (and after deploy) in case it's successfull
notify-complete:
needs: [ build, build_windows, docs_check ]
runs-on: ubuntu-latest
steps:
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: always() && ( 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 }}
deploy: deploy:
needs: [ build, build_windows, docs_check ] needs: [ build, build_windows, docs_check ]
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
@ -226,25 +249,45 @@ jobs:
user: __token__ user: __token__
password: ${{ secrets.pypi_password }} password: ${{ secrets.pypi_password }}
- name: Dockerhub login
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin
- name: Build and test and push docker image - name: Build and test and push docker image
env: env:
IMAGE_NAME: freqtradeorg/freqtrade IMAGE_NAME: freqtradeorg/freqtrade
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
run: | run: |
build_helpers/publish_docker.sh build_helpers/publish_docker.sh
- name: Build raspberry image for ${{ steps.extract_branch.outputs.branch }}_pi # We need docker experimental to pull the ARM image.
uses: elgohr/Publish-Docker-Github-Action@2.7 - name: Switch docker to experimental
run: |
docker version -f '{{.Server.Experimental}}'
echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker version -f '{{.Server.Experimental}}'
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
with: with:
name: freqtradeorg/freqtrade:${{ steps.extract_branch.outputs.branch }}_pi buildx-version: latest
username: ${{ secrets.DOCKER_USERNAME }} qemu-version: latest
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile.pi - name: Available platforms
# cache: true run: echo ${{ steps.buildx.outputs.platforms }}
cache: ${{ github.event_name != 'schedule' }}
tag_names: true - name: Build Raspberry docker image
env:
IMAGE_NAME: freqtradeorg/freqtrade
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}_pi
run: |
build_helpers/publish_docker_pi.sh
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: homoluctus/slatify@v1.8.0

View File

@ -1,4 +1,4 @@
FROM python:3.8.2-slim-buster FROM python:3.8.3-slim-buster
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev \ && apt-get -y install curl build-essential libssl-dev \

29
Dockerfile.armhf Normal file
View File

@ -0,0 +1,29 @@
FROM --platform=linux/arm/v7 python:3.7.7-slim-buster
RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev libatlas3-base libgfortran5 \
&& apt-get clean \
&& pip install --upgrade pip \
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
# Prepare environment
RUN mkdir /freqtrade
WORKDIR /freqtrade
# Install TA-lib
COPY build_helpers/* /tmp/
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
ENV LD_LIBRARY_PATH /usr/local/lib
# Install dependencies
COPY requirements.txt requirements-common.txt /freqtrade/
RUN pip install numpy --no-cache-dir \
&& pip install -r requirements.txt --no-cache-dir
# Install and execute
COPY . /freqtrade/
RUN pip install -e . --no-cache-dir
ENTRYPOINT ["freqtrade"]
# Default to trade mode
CMD [ "trade" ]

View File

@ -1,41 +0,0 @@
FROM balenalib/raspberrypi3-debian:stretch
RUN [ "cross-build-start" ]
RUN apt-get update \
&& apt-get -y install wget curl build-essential libssl-dev libffi-dev \
&& apt-get clean
# Prepare environment
RUN mkdir /freqtrade
WORKDIR /freqtrade
# Install TA-lib
COPY build_helpers/ta-lib-0.4.0-src.tar.gz /freqtrade/
RUN tar -xzf /freqtrade/ta-lib-0.4.0-src.tar.gz \
&& cd /freqtrade/ta-lib/ \
&& ./configure \
&& make \
&& make install \
&& rm /freqtrade/ta-lib-0.4.0-src.tar.gz
ENV LD_LIBRARY_PATH /usr/local/lib
# Install berryconda
RUN wget -q https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh \
&& bash ./Berryconda3-2.0.0-Linux-armv7l.sh -b \
&& rm Berryconda3-2.0.0-Linux-armv7l.sh
# Install dependencies
COPY requirements-common.txt /freqtrade/
RUN ~/berryconda3/bin/conda install -y numpy pandas \
&& ~/berryconda3/bin/pip install -r requirements-common.txt --no-cache-dir
# Install and execute
COPY . /freqtrade/
RUN ~/berryconda3/bin/pip install -e . --no-cache-dir
RUN [ "cross-build-end" ]
ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"]
CMD [ "trade" ]

View File

@ -68,40 +68,42 @@ For any other type of installation please refer to [Installation doc](https://ww
### Bot commands ### Bot commands
``` ```
usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] usage: freqtrade [-h] [-V]
[-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] {trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
[--db-url PATH] [--sd-notify] ...
{backtesting,edge,hyperopt} ...
Free, open source crypto trading bot Free, open source crypto trading bot
positional arguments: positional arguments:
{backtesting,edge,hyperopt} {trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit}
trade Trade module.
create-userdir Create user-data directory.
new-config Create new config
new-hyperopt Create new hyperopt
new-strategy Create new strategy
download-data Download backtesting data.
convert-data Convert candle (OHLCV) data from one format to another.
convert-trade-data Convert trade data from one format to another.
backtesting Backtesting module. backtesting Backtesting module.
edge Edge module. edge Edge module.
hyperopt Hyperopt module. hyperopt Hyperopt module.
hyperopt-list List Hyperopt results
hyperopt-show Show details of Hyperopt results
list-exchanges Print available exchanges.
list-hyperopts Print available hyperopt classes.
list-markets Print markets on exchange.
list-pairs Print pairs on exchange.
list-strategies Print available strategies.
list-timeframes Print available ticker intervals (timeframes) for the exchange.
show-trades Show trades.
test-pairlist Test your pairlist configuration.
plot-dataframe Plot candles with indicators.
plot-profit Generate plot showing profits.
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -V, --version show program's version number and exit
--logfile FILE Log to the file specified
--version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: None). Multiple
--config options may be used.
-d PATH, --datadir PATH
Path to backtest data.
-s NAME, --strategy NAME
Specify strategy class name (default:
DefaultStrategy).
--strategy-path PATH Specify additional strategy lookup path.
--dynamic-whitelist [INT]
Dynamically generate and update whitelist based on 24h
BaseVolume (default: 20). DEPRECATED.
--db-url PATH Override trades database URL, this is useful if
dry_run is enabled or in custom deployments (default:
None).
--sd-notify Notify systemd service manager.
``` ```
### Telegram RPC commands ### Telegram RPC commands

View File

@ -1,11 +0,0 @@
#!/usr/bin/env python3
import sys
import logging
logger = logging.getLogger(__name__)
logger.error("DEPRECATED installation detected, please run `pip install -e .` again.")
sys.exit(2)

Binary file not shown.

Binary file not shown.

View File

@ -7,10 +7,10 @@ python -m pip install --upgrade pip
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
if ($pyv -eq '3.7') { if ($pyv -eq '3.7') {
pip install build_helpers\TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl pip install build_helpers\TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl
} }
if ($pyv -eq '3.8') { if ($pyv -eq '3.8') {
pip install build_helpers\TA_Lib-0.4.17-cp38-cp38-win_amd64.whl pip install build_helpers\TA_Lib-0.4.18-cp38-cp38-win_amd64.whl
} }
pip install -r requirements-dev.txt pip install -r requirements-dev.txt

View File

@ -42,14 +42,6 @@ if [ "${TAG}" = "develop" ]; then
docker tag freqtrade:$TAG ${IMAGE_NAME}:latest docker tag freqtrade:$TAG ${IMAGE_NAME}:latest
fi fi
# Login
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
if [ $? -ne 0 ]; then
echo "failed login"
return 1
fi
# Show all available images # Show all available images
docker images docker images

View File

@ -0,0 +1,36 @@
#!/bin/sh
# The below assumes a correctly setup docker buildx environment
# Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
PI_PLATFORM="linux/arm/v7"
echo "Running for ${TAG}"
CACHE_TAG=freqtradeorg/freqtrade_cache:${TAG}_cache
# Add commit and commit_message to docker container
echo "${GITHUB_SHA}" > freqtrade_commit
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache"
docker buildx build \
--cache-to=type=registry,ref=${CACHE_TAG} \
-f Dockerfile.armhf \
--platform ${PI_PLATFORM} \
-t ${IMAGE_NAME}:${TAG} --push .
else
echo "event ${GITHUB_EVENT_NAME}: building with cache"
# Pull last build to avoid rebuilding the whole image
# docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG}
docker buildx build \
--cache-from=type=registry,ref=${CACHE_TAG} \
--cache-to=type=registry,ref=${CACHE_TAG} \
-f Dockerfile.armhf \
--platform ${PI_PLATFORM} \
-t ${IMAGE_NAME}:${TAG} --push .
fi
if [ $? -ne 0 ]; then
echo "failed building image"
return 1
fi

View File

@ -6,6 +6,7 @@
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"ticker_interval": "5m", "ticker_interval": "5m",
"dry_run": false, "dry_run": false,
"cancel_open_orders_on_exit": false,
"trailing_stop": false, "trailing_stop": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,

View File

@ -6,6 +6,7 @@
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"ticker_interval": "5m", "ticker_interval": "5m",
"dry_run": true, "dry_run": true,
"cancel_open_orders_on_exit": false,
"trailing_stop": false, "trailing_stop": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,

View File

@ -8,6 +8,7 @@
"amend_last_stake_amount": false, "amend_last_stake_amount": false,
"last_stake_amount_min_ratio": 0.5, "last_stake_amount_min_ratio": 0.5,
"dry_run": false, "dry_run": false,
"cancel_open_orders_on_exit": false,
"ticker_interval": "5m", "ticker_interval": "5m",
"trailing_stop": false, "trailing_stop": false,
"trailing_stop_positive": 0.005, "trailing_stop_positive": 0.005,
@ -120,6 +121,7 @@
"enabled": false, "enabled": false,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
"listen_port": 8080, "listen_port": 8080,
"jwt_secret_key": "somethingrandom",
"username": "freqtrader", "username": "freqtrader",
"password": "SuperSecurePassword" "password": "SuperSecurePassword"
}, },

View File

@ -6,6 +6,7 @@
"fiat_display_currency": "EUR", "fiat_display_currency": "EUR",
"ticker_interval": "5m", "ticker_interval": "5m",
"dry_run": true, "dry_run": true,
"cancel_open_orders_on_exit": false,
"trailing_stop": false, "trailing_stop": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,

View File

@ -17,8 +17,12 @@ For details on downloading, please refer to the [Data Downloading](data-download
The result of backtesting will confirm if your bot has better odds of making a profit than a loss. The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
!!! Tip "Using dynamic pairlists for backtesting" !!! Warning "Using dynamic pairlists for backtesting"
While using dynamic pairlists during backtesting is not possible, a dynamic pairlist using current data can be generated via the [`test-pairlist`](utils.md#test-pairlist) command, and needs to be specified as `"pair_whitelist"` attribute in the configuration. Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed.
Please read the [pairlists documentation](configuration.md#pairlists) for more information.
To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist.
### Run a backtesting against the currencies listed in your config file ### Run a backtesting against the currencies listed in your config file
@ -198,7 +202,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- Buys happen at open-price - Buys happen at open-price
- Sell signal sells happen at open-price of the following candle - Sell signal sells happen at open-price of the following candle
- Low happens before high for stoploss, protecting capital first. - Low happens before high for stoploss, protecting capital first
- ROI - ROI
- sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%)
- sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit
@ -208,6 +212,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- High happens first - adjusting stoploss - High happens first - adjusting stoploss
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
- Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) - Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used)
- Stoploss (and trailing stoploss) is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` and/or `trailing_stop` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes.
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode. Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
Also, keep in mind that past results don't guarantee future success. Also, keep in mind that past results don't guarantee future success.

View File

@ -51,6 +51,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float | `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio) | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float (as ratio)
@ -80,14 +81,14 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)). <br> **Datatype:** List | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)). <br> **Datatype:** List | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
| `exchange.ccxt_config` | Additional CCXT parameters passed to the regular 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_config` | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `pairlists` | Define one or more pairlists to be used. [More information below](#dynamic-pairlists). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts | `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean | `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
@ -108,7 +109,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> **Datatype:** Boolean | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> **Datatype:** Boolean
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName
| `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String | `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String
| `internals.process_throttle_secs` | Set the process throttle. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Intege | `internals.process_throttle_secs` | Set the process throttle. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0 | `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean | `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String | `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
@ -544,32 +545,33 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting
Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly. Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly.
It is therefore advised to not use this setting for dry-runs. It is therefore advised to not use this setting for dry-runs.
#### Sell price without Orderbook enabled #### Sell price without Orderbook enabled
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
## Pairlists ## Pairlists and Pairlist Handlers
Pairlists define the list of pairs that the bot should trade. Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
There are [`StaticPairList`](#static-pair-list) and dynamic Whitelists available.
[`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter) act as filters, removing low-value pairs. In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler).
All pairlists can be chained, and a combination of all pairlists will become your new whitelist. Pairlists are executed in the sequence they are configured. You should always configure either `StaticPairList` or `DynamicPairList` as starting pairlists. Additionaly, [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
Inactive markets and blacklisted pairs are always removed from the resulting `pair_whitelist`. If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler.
### Available Pairlists Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
### Available Pairlist Handlers
* [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`StaticPairList`](#static-pair-list) (default, if not configured differently)
* [`VolumePairList`](#volume-pair-list) * [`VolumePairList`](#volume-pair-list)
* [`PrecisionFilter`](#precision-filter) * [`PrecisionFilter`](#precisionfilter)
* [`PriceFilter`](#price-pair-filter) * [`PriceFilter`](#pricefilter)
* [`SpreadFilter`](#spread-filter) * [`ShuffleFilter`](#shufflefilter)
* [`SpreadFilter`](#spreadfilter)
!!! Tip "Testing pairlists" !!! Tip "Testing pairlists"
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) subcommand to test your configuration quickly. Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility subcommand to test your configuration quickly.
#### Static Pair List #### Static Pair List
@ -585,16 +587,16 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis
#### Volume Pair List #### Volume Pair List
`VolumePairList` selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume` and defaults to `quoteVolume`. `VolumePairList` employs sorting/filtering of pairs by their trading volume. I selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
`VolumePairList` considers outputs of previous pairlists unless it's the first configured pairlist, it does not consider `pair_whitelist`, but selects the top assets from all available markets (with matching stake-currency) on the exchange. When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
`refresh_period` allows setting the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange.
`VolumePairList` is based on the ticker data, as reported by the ccxt library: The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library:
* The `bidVolume` is the volume (amount) of current best bid in the orderbook.
* The `askVolume` is the volume (amount) of current best ask in the orderbook.
* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours. * The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours.
```json ```json
@ -608,27 +610,39 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis
#### PrecisionFilter #### PrecisionFilter
Filters low-value coins which would not allow setting a stoploss. Filters low-value coins which would not allow setting stoplosses.
#### Price Pair Filter #### PriceFilter
The `PriceFilter` allows filtering of pairs by price. The `PriceFilter` allows filtering of pairs by price.
Currently, only `low_price_ratio` is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio.
Currently, only `low_price_ratio` setting is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to <> 0. This option is disabled by default, and will only apply if set to <> 0.
Calculation example: Calculation example:
Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value.
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. Here is what the PriceFilters takes over.
#### ShuffleFilter
Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority.
!!! Tip
You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order.
#### SpreadFilter #### SpreadFilter
Removes pairs that have a difference between asks and bids above the specified ratio (default `0.005`).
Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`).
Example: Example:
If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027 the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005`
### Full Pairlist example If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out.
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter), filtering all assets where 1 priceunit is > 1%. ### Full example of Pairlist Handlers
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 priceunit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value.
```json ```json
"exchange": { "exchange": {
@ -642,7 +656,9 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
}, },
{"method": "PrecisionFilter"}, {"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01} {"method": "PriceFilter", "low_price_ratio": 0.01},
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
{"method": "ShuffleFilter", "seed": 42}
], ],
``` ```

View File

@ -24,3 +24,13 @@ and in freqtrade 2019.7 (master branch).
`--live` in the context of backtesting allowed to download the latest tick data for backtesting. `--live` in the context of backtesting allowed to download the latest tick data for backtesting.
Did only download the latest 500 candles, so was ineffective in getting good backtest data. Did only download the latest 500 candles, so was ineffective in getting good backtest data.
Removed in 2019-7-dev (develop branch) and in freqtrade 2019-8 (master branch) Removed in 2019-7-dev (develop branch) and in freqtrade 2019-8 (master branch)
### Allow running multiple pairlists in sequence
The former `"pairlist"` section in the configuration has been removed, and is replaced by `"pairlists"` - being a list to specify a sequence of pairlists.
The old section of configuration parameters (`"pairlist"`) has been deprecated in 2019.11 and has been removed in 2020.4.
### deprecation of bidVolume and askVolume from volumepairlist
Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4.

View File

@ -22,6 +22,9 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co
!!! Note !!! Note
All below comands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. All below comands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file.
!!! Note "Docker on Raspberry"
If you're running freqtrade on a Raspberry PI, you must change the image from `freqtradeorg/freqtrade:master` to `freqtradeorg/freqtrade:master_pi` or `freqtradeorg/freqtrade:develop_pi`, otherwise the image will not work.
### Docker quick start ### Docker quick start
Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory.
@ -65,7 +68,7 @@ docker-compose up -d
#### Docker-compose logs #### Docker-compose logs
Logs will be written to `user_data/freqtrade.log`. Logs will be written to `user_data/logs/freqtrade.log`.
Alternatively, you can check the latest logs using `docker-compose logs -f`. Alternatively, you can check the latest logs using `docker-compose logs -f`.
#### Database #### Database

View File

@ -487,7 +487,7 @@ As stated in the comment, you can also use it as the values of the corresponding
If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases.
Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
## Show details of Hyperopt results ## Show details of Hyperopt results

View File

@ -248,14 +248,14 @@ git clone https://github.com/freqtrade/freqtrade.git
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.17cp36cp36mwin32.whl` (make sure to use the version matching your python version) As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.18cp38cp38win_amd64.whl` (make sure to use the version matching your python version)
```cmd ```cmd
>cd \path\freqtrade-develop >cd \path\freqtrade-develop
>python -m venv .env >python -m venv .env
>.env\Scripts\activate.bat >.env\Scripts\activate.bat
REM optionally install ta-lib from wheel REM optionally install ta-lib from wheel
REM >pip install TA_Lib0.4.17cp36cp36mwin32.whl REM >pip install TA_Lib0.4.18cp38cp38win_amd64.whl
>pip install -r requirements.txt >pip install -r requirements.txt
>pip install -e . >pip install -e .
>freqtrade >freqtrade

View File

@ -1,2 +1,2 @@
mkdocs-material==5.1.3 mkdocs-material==5.2.1
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2

View File

@ -11,6 +11,7 @@ Sample configuration:
"enabled": true, "enabled": true,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
"listen_port": 8080, "listen_port": 8080,
"jwt_secret_key": "somethingrandom",
"username": "Freqtrader", "username": "Freqtrader",
"password": "SuperSecret1!" "password": "SuperSecret1!"
}, },
@ -29,7 +30,7 @@ This should return the response:
{"status":"pong"} {"status":"pong"}
``` ```
All other endpoints return sensitive info and require authentication, so are not available through a web browser. All other endpoints return sensitive info and require authentication and are therefore not available through a web browser.
To generate a secure password, either use a password manager, or use the below code snipped. To generate a secure password, either use a password manager, or use the below code snipped.
@ -38,6 +39,9 @@ import secrets
secrets.token_hex() secrets.token_hex()
``` ```
!!! Hint
Use the same method to also generate a JWT secret key (`jwt_secret_key`).
### Configuration with docker ### Configuration with docker
If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker.
@ -202,3 +206,28 @@ whitelist
Show the current whitelist Show the current whitelist
:returns: json object :returns: json object
``` ```
## Advanced API usage using JWT tokens
!!! Note
The below should be done in an application (a Freqtrade REST API client, which fetches info via API), and is not intended to be used on a regular basis.
Freqtrade's REST API also offers JWT (JSON Web Tokens).
You can login using the following command, and subsequently use the resulting access_token.
``` bash
> curl -X POST --user Freqtrader http://localhost:8080/api/v1/token/login
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk2ODEsIm5iZiI6MTU4OTExOTY4MSwianRpIjoiMmEwYmY0NWUtMjhmOS00YTUzLTlmNzItMmM5ZWVlYThkNzc2IiwiZXhwIjoxNTg5MTIwNTgxLCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.qt6MAXYIa-l556OM7arBvYJ0SDI9J8bIk3_glDujF5g","refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk2ODEsIm5iZiI6MTU4OTExOTY4MSwianRpIjoiZWQ1ZWI3YjAtYjMwMy00YzAyLTg2N2MtNWViMjIxNWQ2YTMxIiwiZXhwIjoxNTkxNzExNjgxLCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJ0eXBlIjoicmVmcmVzaCJ9.d1AT_jYICyTAjD0fiQAr52rkRqtxCjUGEMwlNuuzgNQ"}
> access_token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk2ODEsIm5iZiI6MTU4OTExOTY4MSwianRpIjoiMmEwYmY0NWUtMjhmOS00YTUzLTlmNzItMmM5ZWVlYThkNzc2IiwiZXhwIjoxNTg5MTIwNTgxLCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.qt6MAXYIa-l556OM7arBvYJ0SDI9J8bIk3_glDujF5g"
# Use access_token for authentication
> curl -X GET --header "Authorization: Bearer ${access_token}" http://localhost:8080/api/v1/count
```
Since the access token has a short timeout (15 min) - the `token/refresh` request should be used periodically to get a fresh access token:
``` bash
> curl -X POST --header "Authorization: Bearer ${refresh_token}"http://localhost:8080/api/v1/token/refresh
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"}
```

View File

@ -1,13 +1,20 @@
# SQL Helper # SQL Helper
This page contains some help if you want to edit your sqlite db. This page contains some help if you want to edit your sqlite db.
## Install sqlite3 ## Install sqlite3
**Ubuntu/Debian installation**
Sqlite3 is a terminal based sqlite application.
Feel free to use a visual Database editor like SqliteBrowser if you feel more comfortable with that.
### Ubuntu/Debian installation
```bash ```bash
sudo apt-get install sqlite3 sudo apt-get install sqlite3
``` ```
## Open the DB ## Open the DB
```bash ```bash
sqlite3 sqlite3
.open <filepath> .open <filepath>
@ -16,45 +23,61 @@ sqlite3
## Table structure ## Table structure
### List tables ### List tables
```bash ```bash
.tables .tables
``` ```
### Display table structure ### Display table structure
```bash ```bash
.schema <table_name> .schema <table_name>
``` ```
### Trade table structure ### Trade table structure
```sql ```sql
CREATE TABLE trades ( CREATE TABLE trades
id INTEGER NOT NULL, id INTEGER NOT NULL,
exchange VARCHAR NOT NULL, exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL, pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL, is_open BOOLEAN NOT NULL,
fee_open FLOAT NOT NULL, fee_open FLOAT NOT NULL,
fee_open_cost FLOAT,
fee_open_currency VARCHAR,
fee_close FLOAT NOT NULL, fee_close FLOAT NOT NULL,
fee_close_cost FLOAT,
fee_close_currency VARCHAR,
open_rate FLOAT, open_rate FLOAT,
open_rate_requested FLOAT, open_rate_requested FLOAT,
open_trade_price FLOAT,
close_rate FLOAT, close_rate FLOAT,
close_rate_requested FLOAT, close_rate_requested FLOAT,
close_profit FLOAT, close_profit FLOAT,
close_profit_abs FLOAT,
stake_amount FLOAT NOT NULL, stake_amount FLOAT NOT NULL,
amount FLOAT, amount FLOAT,
open_date DATETIME NOT NULL, open_date DATETIME NOT NULL,
close_date DATETIME, close_date DATETIME,
open_order_id VARCHAR, open_order_id VARCHAR,
stop_loss FLOAT, stop_loss FLOAT,
stop_loss_pct FLOAT,
initial_stop_loss FLOAT, initial_stop_loss FLOAT,
initial_stop_loss_pct FLOAT,
stoploss_order_id VARCHAR, stoploss_order_id VARCHAR,
stoploss_last_update DATETIME, stoploss_last_update DATETIME,
max_rate FLOAT, max_rate FLOAT,
min_rate FLOAT,
sell_reason VARCHAR, sell_reason VARCHAR,
strategy VARCHAR, strategy VARCHAR,
ticker_interval INTEGER, ticker_interval INTEGER,
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (is_open IN (0, 1)) CHECK (is_open IN (0, 1))
); );
CREATE INDEX ix_trades_stoploss_order_id ON trades (stoploss_order_id);
CREATE INDEX ix_trades_pair ON trades (pair);
CREATE INDEX ix_trades_is_open ON trades (is_open);
``` ```
## Get all trades in the table ## Get all trades in the table

View File

@ -20,7 +20,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to
The function must return either `True` (cancel order) or `False` (keep order alive). The function must return either `True` (cancel order) or `False` (keep order alive).
``` python ``` python
from datetime import datetime, timestamp from datetime import datetime, timedelta
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class Awesomestrategy(IStrategy): class Awesomestrategy(IStrategy):
@ -59,7 +59,7 @@ class Awesomestrategy(IStrategy):
### Custom order timeout example (using additional data) ### Custom order timeout example (using additional data)
``` python ``` python
from datetime import datetime, timestamp from datetime import datetime
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class Awesomestrategy(IStrategy): class Awesomestrategy(IStrategy):

View File

@ -324,67 +324,14 @@ class Awesomestrategy(IStrategy):
!!! Note !!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
### Additional data (DataProvider) ***
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy. ### Additional data (informative_pairs)
All methods return `None` in case of failure (do not raise an exception).
Please always check the mode of operation to select the correct method to get data (samples see below).
#### Possible options for DataProvider
- `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval).
- `ohlcv(pair, timeframe)` - Currently cached candle (OHLCV) data for the pair, returns DataFrame or empty DataFrame.
- `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk.
- `get_pair_dataframe(pair, timeframe)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
- `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries.
- `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure.
- `runmode` - Property containing the current runmode.
#### Example: fetch live / historical candle (OHLCV) data for the first informative pair
``` python
if self.dp:
inf_pair, inf_timeframe = self.informative_pairs()[0]
informative = self.dp.get_pair_dataframe(pair=inf_pair,
timeframe=inf_timeframe)
```
!!! Warning "Warning about backtesting"
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
for the backtesting runmode) provides the full time-range in one go,
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
!!! Warning "Warning in hyperopt"
This option cannot currently be used during hyperopt.
#### Orderbook
``` python
if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0]
```
!!! Warning
The order book is not part of the historic data which means backtesting and hyperopt will not work if this
method is used.
#### Available Pairs
``` python
if self.dp:
for pair, timeframe in self.dp.available_pairs:
print(f"available {pair}, {timeframe}")
```
#### Get data for non-tradeable pairs #### Get data for non-tradeable pairs
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies. Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
Ohlcv data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see above). Ohlcv data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see below).
These parts will **not** be traded unless they are also specified in the pair whitelist, or have been selected by Dynamic Whitelisting. These parts will **not** be traded unless they are also specified in the pair whitelist, or have been selected by Dynamic Whitelisting.
The pairs need to be specified as tuples in the format `("pair", "interval")`, with pair as the first and time interval as the second argument. The pairs need to be specified as tuples in the format `("pair", "interval")`, with pair as the first and time interval as the second argument.
@ -404,6 +351,125 @@ def informative_pairs(self):
It is however better to use resampling to longer time-intervals when possible It is however better to use resampling to longer time-intervals when possible
to avoid hammering the exchange with too many requests and risk being blocked. to avoid hammering the exchange with too many requests and risk being blocked.
***
### Additional data (DataProvider)
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
All methods return `None` in case of failure (do not raise an exception).
Please always check the mode of operation to select the correct method to get data (samples see below).
#### Possible options for DataProvider
- [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their intervals (pair, interval).
- [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (ie. VolumePairlist)
- [`get_pair_dataframe(pair, timeframe)`](#get_pair_dataframepair-timeframe) - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
- `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk.
- `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on the Market data structure.
- `ohlcv(pair, timeframe)` - Currently cached candle (OHLCV) data for the pair, returns DataFrame or empty DataFrame.
- [`orderbook(pair, maximum)`](#orderbookpair-maximum) - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries.
- [`ticker(pair)`](#tickerpair) - Returns current ticker data for the pair. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#price-tickers) for more details on the Ticker data structure.
- `runmode` - Property containing the current runmode.
#### Example Usages:
#### *available_pairs*
``` python
if self.dp:
for pair, timeframe in self.dp.available_pairs:
print(f"available {pair}, {timeframe}")
```
#### *current_whitelist()*
Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume.
The strategy might look something like this:
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day ATR to buy and sell.*
Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day ATR. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use.
This is where calling `self.dp.current_whitelist()` comes in handy.
```python
class SampleStrategy(IStrategy):
# strategy init stuff...
ticker_interval = '5m'
# more strategy init stuff..
def informative_pairs(self):
# get access to all pairs available in whitelist.
pairs = self.dp.current_whitelist()
# Assign tf to each pair so they can be downloaded and cached for strategy.
informative_pairs = [(pair, '1d') for pair in pairs]
return informative_pairs
def populate_indicators(self, dataframe, metadata):
# Get the informative pair
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d')
# Get the 14 day ATR.
atr = ta.ATR(informative, timeperiod=14)
# Do other stuff
```
#### *get_pair_dataframe(pair, timeframe)*
``` python
# fetch live / historical candle (OHLCV) data for the first informative pair
if self.dp:
inf_pair, inf_timeframe = self.informative_pairs()[0]
informative = self.dp.get_pair_dataframe(pair=inf_pair,
timeframe=inf_timeframe)
```
!!! Warning "Warning about backtesting"
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
for the backtesting runmode) provides the full time-range in one go,
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
!!! Warning "Warning in hyperopt"
This option cannot currently be used during hyperopt.
#### *orderbook(pair, maximum)*
``` python
if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0]
```
!!! Warning
The order book is not part of the historic data which means backtesting and hyperopt will not work if this
method is used.
#### *ticker(pair)*
``` python
if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ticker = self.dp.ticker(metadata['pair'])
dataframe['last_price'] = ticker['last']
dataframe['volume24h'] = ticker['quoteVolume']
dataframe['vwap'] = ticker['vwap']
```
!!! Warning
Although the ticker data structure is a part of the ccxt Unified Interface, the values returned by this method can
vary for different exchanges. For instance, many exchanges do not return `vwap` values, the FTX exchange
does not always fills in the `last` field (so it can be None), etc. So you need to carefully verify the ticker
data returned from the exchange and add appropriate error handling / defaults.
***
### Additional data (Wallets) ### Additional data (Wallets)
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
@ -426,6 +492,7 @@ if self.wallets:
- `get_used(asset)` - currently tied up balance (open orders) - `get_used(asset)` - currently tied up balance (open orders)
- `get_total(asset)` - total available balance - sum of the 2 above - `get_total(asset)` - total available balance - sum of the 2 above
***
### Additional data (Trades) ### Additional data (Trades)
A history of Trades can be retrieved in the strategy by querying the database. A history of Trades can be retrieved in the strategy by querying the database.

View File

@ -521,3 +521,48 @@ Prints JSON data with details for the last best epoch (i.e., the best of all epo
``` ```
freqtrade hyperopt-show --best -n -1 --print-json --no-header freqtrade hyperopt-show --best -n -1 --print-json --no-header
``` ```
## Show trades
Print selected (or all) trades from database to screen.
```
usage: freqtrade show-trades [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [--db-url PATH]
[--trade-ids TRADE_IDS [TRADE_IDS ...]]
[--print-json]
optional arguments:
-h, --help show this help message and exit
--db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for
Dry Run).
--trade-ids TRADE_IDS [TRADE_IDS ...]
Specify the list of trade ids.
--print-json Print output in JSON format.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
```
### Examples
Print trades with id 2 and 3 as json
``` bash
freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json
```

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2020.4' __version__ = '2020.5'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -19,7 +19,8 @@ from freqtrade.commands.list_commands import (start_list_exchanges,
start_list_hyperopts, start_list_hyperopts,
start_list_markets, start_list_markets,
start_list_strategies, start_list_strategies,
start_list_timeframes) start_list_timeframes,
start_show_trades)
from freqtrade.commands.optimize_commands import (start_backtesting, from freqtrade.commands.optimize_commands import (start_backtesting,
start_edge, start_hyperopt) start_edge, start_hyperopt)
from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.pairlist_commands import start_test_pairlist

View File

@ -64,6 +64,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "ticker_interval"] "trade_source", "ticker_interval"]
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
"hyperopt_list_min_trades", "hyperopt_list_max_trades", "hyperopt_list_min_trades", "hyperopt_list_max_trades",
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time", "hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time",
@ -78,7 +80,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-markets", "list-pairs", "list-strategies",
"list-hyperopts", "hyperopt-list", "hyperopt-show", "list-hyperopts", "hyperopt-list", "hyperopt-show",
"plot-dataframe", "plot-profit"] "plot-dataframe", "plot-profit", "show-trades"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
@ -163,7 +165,7 @@ class Arguments:
start_list_markets, start_list_strategies, start_list_markets, start_list_strategies,
start_list_timeframes, start_new_config, start_list_timeframes, start_new_config,
start_new_hyperopt, start_new_strategy, start_new_hyperopt, start_new_strategy,
start_plot_dataframe, start_plot_profit, start_plot_dataframe, start_plot_profit, start_show_trades,
start_backtesting, start_hyperopt, start_edge, start_backtesting, start_hyperopt, start_edge,
start_test_pairlist, start_trading) start_test_pairlist, start_trading)
@ -179,25 +181,6 @@ class Arguments:
trade_cmd.set_defaults(func=start_trading) trade_cmd.set_defaults(func=start_trading)
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd) self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.',
parents=[_common_parser, _strategy_parser])
backtesting_cmd.set_defaults(func=start_backtesting)
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
# Add edge subcommand
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
parents=[_common_parser, _strategy_parser])
edge_cmd.set_defaults(func=start_edge)
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.',
parents=[_common_parser, _strategy_parser],
)
hyperopt_cmd.set_defaults(func=start_hyperopt)
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
# add create-userdir subcommand # add create-userdir subcommand
create_userdir_cmd = subparsers.add_parser('create-userdir', create_userdir_cmd = subparsers.add_parser('create-userdir',
help="Create user-data directory.", help="Create user-data directory.",
@ -211,79 +194,17 @@ class Arguments:
build_config_cmd.set_defaults(func=start_new_config) build_config_cmd.set_defaults(func=start_new_config)
self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd) self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd)
# add new-strategy subcommand
build_strategy_cmd = subparsers.add_parser('new-strategy',
help="Create new strategy")
build_strategy_cmd.set_defaults(func=start_new_strategy)
self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd)
# add new-hyperopt subcommand # add new-hyperopt subcommand
build_hyperopt_cmd = subparsers.add_parser('new-hyperopt', build_hyperopt_cmd = subparsers.add_parser('new-hyperopt',
help="Create new hyperopt") help="Create new hyperopt")
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt) build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd) self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd)
# Add list-strategies subcommand # add new-strategy subcommand
list_strategies_cmd = subparsers.add_parser( build_strategy_cmd = subparsers.add_parser('new-strategy',
'list-strategies', help="Create new strategy")
help='Print available strategies.', build_strategy_cmd.set_defaults(func=start_new_strategy)
parents=[_common_parser], self._build_args(optionlist=ARGS_BUILD_STRATEGY, parser=build_strategy_cmd)
)
list_strategies_cmd.set_defaults(func=start_list_strategies)
self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd)
# Add list-hyperopts subcommand
list_hyperopts_cmd = subparsers.add_parser(
'list-hyperopts',
help='Print available hyperopt classes.',
parents=[_common_parser],
)
list_hyperopts_cmd.set_defaults(func=start_list_hyperopts)
self._build_args(optionlist=ARGS_LIST_HYPEROPTS, parser=list_hyperopts_cmd)
# Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser(
'list-exchanges',
help='Print available exchanges.',
parents=[_common_parser],
)
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
# Add list-timeframes subcommand
list_timeframes_cmd = subparsers.add_parser(
'list-timeframes',
help='Print available ticker intervals (timeframes) for the exchange.',
parents=[_common_parser],
)
list_timeframes_cmd.set_defaults(func=start_list_timeframes)
self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd)
# Add list-markets subcommand
list_markets_cmd = subparsers.add_parser(
'list-markets',
help='Print markets on exchange.',
parents=[_common_parser],
)
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd)
# Add list-pairs subcommand
list_pairs_cmd = subparsers.add_parser(
'list-pairs',
help='Print pairs on exchange.',
parents=[_common_parser],
)
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd)
# Add test-pairlist subcommand
test_pairlist_cmd = subparsers.add_parser(
'test-pairlist',
help='Test your pairlist configuration.',
)
test_pairlist_cmd.set_defaults(func=start_test_pairlist)
self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd)
# Add download-data subcommand # Add download-data subcommand
download_data_cmd = subparsers.add_parser( download_data_cmd = subparsers.add_parser(
@ -312,23 +233,24 @@ class Arguments:
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd) self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
# Add Plotting subcommand # Add backtesting subcommand
plot_dataframe_cmd = subparsers.add_parser( backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.',
'plot-dataframe', parents=[_common_parser, _strategy_parser])
help='Plot candles with indicators.', backtesting_cmd.set_defaults(func=start_backtesting)
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
# Add edge subcommand
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
parents=[_common_parser, _strategy_parser])
edge_cmd.set_defaults(func=start_edge)
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.',
parents=[_common_parser, _strategy_parser], parents=[_common_parser, _strategy_parser],
) )
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe) hyperopt_cmd.set_defaults(func=start_hyperopt)
self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd) self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
# Plot profit
plot_profit_cmd = subparsers.add_parser(
'plot-profit',
help='Generate plot showing profits.',
parents=[_common_parser],
)
plot_profit_cmd.set_defaults(func=start_plot_profit)
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)
# Add hyperopt-list subcommand # Add hyperopt-list subcommand
hyperopt_list_cmd = subparsers.add_parser( hyperopt_list_cmd = subparsers.add_parser(
@ -347,3 +269,92 @@ class Arguments:
) )
hyperopt_show_cmd.set_defaults(func=start_hyperopt_show) hyperopt_show_cmd.set_defaults(func=start_hyperopt_show)
self._build_args(optionlist=ARGS_HYPEROPT_SHOW, parser=hyperopt_show_cmd) self._build_args(optionlist=ARGS_HYPEROPT_SHOW, parser=hyperopt_show_cmd)
# Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser(
'list-exchanges',
help='Print available exchanges.',
parents=[_common_parser],
)
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
# Add list-hyperopts subcommand
list_hyperopts_cmd = subparsers.add_parser(
'list-hyperopts',
help='Print available hyperopt classes.',
parents=[_common_parser],
)
list_hyperopts_cmd.set_defaults(func=start_list_hyperopts)
self._build_args(optionlist=ARGS_LIST_HYPEROPTS, parser=list_hyperopts_cmd)
# Add list-markets subcommand
list_markets_cmd = subparsers.add_parser(
'list-markets',
help='Print markets on exchange.',
parents=[_common_parser],
)
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd)
# Add list-pairs subcommand
list_pairs_cmd = subparsers.add_parser(
'list-pairs',
help='Print pairs on exchange.',
parents=[_common_parser],
)
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd)
# Add list-strategies subcommand
list_strategies_cmd = subparsers.add_parser(
'list-strategies',
help='Print available strategies.',
parents=[_common_parser],
)
list_strategies_cmd.set_defaults(func=start_list_strategies)
self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd)
# Add list-timeframes subcommand
list_timeframes_cmd = subparsers.add_parser(
'list-timeframes',
help='Print available ticker intervals (timeframes) for the exchange.',
parents=[_common_parser],
)
list_timeframes_cmd.set_defaults(func=start_list_timeframes)
self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd)
# Add show-trades subcommand
show_trades = subparsers.add_parser(
'show-trades',
help='Show trades.',
parents=[_common_parser],
)
show_trades.set_defaults(func=start_show_trades)
self._build_args(optionlist=ARGS_SHOW_TRADES, parser=show_trades)
# Add test-pairlist subcommand
test_pairlist_cmd = subparsers.add_parser(
'test-pairlist',
help='Test your pairlist configuration.',
)
test_pairlist_cmd.set_defaults(func=start_test_pairlist)
self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd)
# Add Plotting subcommand
plot_dataframe_cmd = subparsers.add_parser(
'plot-dataframe',
help='Plot candles with indicators.',
parents=[_common_parser, _strategy_parser],
)
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd)
# Plot profit
plot_profit_cmd = subparsers.add_parser(
'plot-profit',
help='Generate plot showing profits.',
parents=[_common_parser],
)
plot_profit_cmd.set_defaults(func=start_plot_profit)
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)

View File

@ -163,7 +163,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
) )
except TemplateNotFound: except TemplateNotFound:
selections['exchange'] = render_template( selections['exchange'] = render_template(
templatefile=f"subtemplates/exchange_generic.j2", templatefile="subtemplates/exchange_generic.j2",
arguments=selections arguments=selections
) )

View File

@ -217,7 +217,7 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"print_json": Arg( "print_json": Arg(
'--print-json', '--print-json',
help='Print best result detailization in JSON format.', help='Print output in JSON format.',
action='store_true', action='store_true',
default=False, default=False,
), ),
@ -372,8 +372,8 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"timeframes": Arg( "timeframes": Arg(
'-t', '--timeframes', '-t', '--timeframes',
help=f'Specify which tickers to download. Space-separated list. ' help='Specify which tickers to download. Space-separated list. '
f'Default: `1m 5m`.', 'Default: `1m 5m`.',
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
'6h', '8h', '12h', '1d', '3d', '1w'], '6h', '8h', '12h', '1d', '3d', '1w'],
default=['1m', '5m'], default=['1m', '5m'],
@ -425,6 +425,11 @@ AVAILABLE_CLI_OPTIONS = {
choices=["DB", "file"], choices=["DB", "file"],
default="file", default="file",
), ),
"trade_ids": Arg(
'--trade-ids',
help='Specify the list of trade ids.',
nargs='+',
),
# hyperopt-list, hyperopt-show # hyperopt-list, hyperopt-show
"hyperopt_list_profitable": Arg( "hyperopt_list_profitable": Arg(
'--profitable', '--profitable',

View File

@ -51,7 +51,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
) )
additional_methods = render_template_with_fallback( additional_methods = render_template_with_fallback(
templatefile=f"subtemplates/strategy_methods_{subtemplate}.j2", templatefile=f"subtemplates/strategy_methods_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/strategy_methods_empty.j2", templatefallbackfile="subtemplates/strategy_methods_empty.j2",
) )
strategy_text = render_template(templatefile='base_strategy.py.j2', strategy_text = render_template(templatefile='base_strategy.py.j2',

View File

@ -38,33 +38,33 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None) 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None)
} }
trials_file = (config['user_data_dir'] / results_file = (config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_results.pickle') 'hyperopt_results' / 'hyperopt_results.pickle')
# Previous evaluations # Previous evaluations
trials = Hyperopt.load_previous_results(trials_file) epochs = Hyperopt.load_previous_results(results_file)
total_epochs = len(trials) total_epochs = len(epochs)
trials = _hyperopt_filter_trials(trials, filteroptions) epochs = _hyperopt_filter_epochs(epochs, filteroptions)
if print_colorized: if print_colorized:
colorama_init(autoreset=True) colorama_init(autoreset=True)
if not export_csv: if not export_csv:
try: try:
print(Hyperopt.get_result_table(config, trials, total_epochs, print(Hyperopt.get_result_table(config, epochs, total_epochs,
not filteroptions['only_best'], print_colorized, 0)) not filteroptions['only_best'], print_colorized, 0))
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print('User interrupted..')
if trials and not no_details: if epochs and not no_details:
sorted_trials = sorted(trials, key=itemgetter('loss')) sorted_epochs = sorted(epochs, key=itemgetter('loss'))
results = sorted_trials[0] results = sorted_epochs[0]
Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header) Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header)
if trials and export_csv: if epochs and export_csv:
Hyperopt.export_csv_file( Hyperopt.export_csv_file(
config, trials, total_epochs, not filteroptions['only_best'], export_csv config, epochs, total_epochs, not filteroptions['only_best'], export_csv
) )
@ -78,7 +78,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
print_json = config.get('print_json', False) print_json = config.get('print_json', False)
no_header = config.get('hyperopt_show_no_header', False) no_header = config.get('hyperopt_show_no_header', False)
trials_file = (config['user_data_dir'] / results_file = (config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_results.pickle') 'hyperopt_results' / 'hyperopt_results.pickle')
n = config.get('hyperopt_show_index', -1) n = config.get('hyperopt_show_index', -1)
@ -96,89 +96,87 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
} }
# Previous evaluations # Previous evaluations
trials = Hyperopt.load_previous_results(trials_file) epochs = Hyperopt.load_previous_results(results_file)
total_epochs = len(trials) total_epochs = len(epochs)
trials = _hyperopt_filter_trials(trials, filteroptions) epochs = _hyperopt_filter_epochs(epochs, filteroptions)
trials_epochs = len(trials) filtered_epochs = len(epochs)
if n > trials_epochs: if n > filtered_epochs:
raise OperationalException( raise OperationalException(
f"The index of the epoch to show should be less than {trials_epochs + 1}.") f"The index of the epoch to show should be less than {filtered_epochs + 1}.")
if n < -trials_epochs: if n < -filtered_epochs:
raise OperationalException( raise OperationalException(
f"The index of the epoch to show should be greater than {-trials_epochs - 1}.") f"The index of the epoch to show should be greater than {-filtered_epochs - 1}.")
# Translate epoch index from human-readable format to pythonic # Translate epoch index from human-readable format to pythonic
if n > 0: if n > 0:
n -= 1 n -= 1
if trials: if epochs:
val = trials[n] val = epochs[n]
Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header, Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header,
header_str="Epoch details") header_str="Epoch details")
def _hyperopt_filter_trials(trials: List, filteroptions: dict) -> List: def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
""" """
Filter our items from the list of hyperopt results Filter our items from the list of hyperopt results
""" """
if filteroptions['only_best']: if filteroptions['only_best']:
trials = [x for x in trials if x['is_best']] epochs = [x for x in epochs if x['is_best']]
if filteroptions['only_profitable']: if filteroptions['only_profitable']:
trials = [x for x in trials if x['results_metrics']['profit'] > 0] epochs = [x for x in epochs if x['results_metrics']['profit'] > 0]
if filteroptions['filter_min_trades'] > 0: if filteroptions['filter_min_trades'] > 0:
trials = [ epochs = [
x for x in trials x for x in epochs
if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades'] if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades']
] ]
if filteroptions['filter_max_trades'] > 0: if filteroptions['filter_max_trades'] > 0:
trials = [ epochs = [
x for x in trials x for x in epochs
if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades'] if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades']
] ]
if filteroptions['filter_min_avg_time'] is not None: if filteroptions['filter_min_avg_time'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
trials = [ epochs = [
x for x in trials x for x in epochs
if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time'] if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time']
] ]
if filteroptions['filter_max_avg_time'] is not None: if filteroptions['filter_max_avg_time'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
trials = [ epochs = [
x for x in trials x for x in epochs
if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time'] if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time']
] ]
if filteroptions['filter_min_avg_profit'] is not None: if filteroptions['filter_min_avg_profit'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
trials = [ epochs = [
x for x in trials x for x in epochs
if x['results_metrics']['avg_profit'] if x['results_metrics']['avg_profit'] > filteroptions['filter_min_avg_profit']
> filteroptions['filter_min_avg_profit']
] ]
if filteroptions['filter_max_avg_profit'] is not None: if filteroptions['filter_max_avg_profit'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
trials = [ epochs = [
x for x in trials x for x in epochs
if x['results_metrics']['avg_profit'] if x['results_metrics']['avg_profit'] < filteroptions['filter_max_avg_profit']
< filteroptions['filter_max_avg_profit']
] ]
if filteroptions['filter_min_total_profit'] is not None: if filteroptions['filter_min_total_profit'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
trials = [ epochs = [
x for x in trials x for x in epochs
if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit'] if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit']
] ]
if filteroptions['filter_max_total_profit'] is not None: if filteroptions['filter_max_total_profit'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
trials = [ epochs = [
x for x in trials x for x in epochs
if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit'] if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
] ]
logger.info(f"{len(trials)} " + logger.info(f"{len(epochs)} " +
("best " if filteroptions['only_best'] else "") + ("best " if filteroptions['only_best'] else "") +
("profitable " if filteroptions['only_profitable'] else "") + ("profitable " if filteroptions['only_profitable'] else "") +
"epochs found.") "epochs found.")
return trials return epochs

View File

@ -197,3 +197,30 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
args.get('list_pairs_print_json', False) or args.get('list_pairs_print_json', False) or
args.get('print_csv', False)): args.get('print_csv', False)):
print(f"{summary_str}.") print(f"{summary_str}.")
def start_show_trades(args: Dict[str, Any]) -> None:
"""
Show trades
"""
from freqtrade.persistence import init, Trade
import json
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if 'db_url' not in config:
raise OperationalException("--db-url is required for this command.")
logger.info(f'Using DB: "{config["db_url"]}"')
init(config['db_url'], clean_open_orders=False)
tfilter = []
if config.get('trade_ids'):
tfilter.append(Trade.id.in_(config['trade_ids']))
trades = Trade.get_trades(tfilter).all()
logger.info(f"Printing {len(trades)} Trades: ")
if config.get('print_json', False):
print(json.dumps([trade.to_json() for trade in trades], indent=4))
else:
for trade in trades:
print(trade)

View File

@ -18,6 +18,9 @@ def start_trading(args: Dict[str, Any]) -> int:
try: try:
worker = Worker(args) worker = Worker(args)
worker.run() worker.run()
except Exception as e:
logger.error(str(e))
logger.exception("Fatal exception!")
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...') logger.info('SIGINT received, aborting ...')
finally: finally:

View File

@ -351,8 +351,12 @@ class Configuration:
self._args_to_config(config, argname='indicators2', self._args_to_config(config, argname='indicators2',
logstring='Using indicators2: {}') logstring='Using indicators2: {}')
self._args_to_config(config, argname='trade_ids',
logstring='Filtering on trade_ids: {}')
self._args_to_config(config, argname='plot_limit', self._args_to_config(config, argname='plot_limit',
logstring='Limiting plot to: {}') logstring='Limiting plot to: {}')
self._args_to_config(config, argname='trade_source', self._args_to_config(config, argname='trade_source',
logstring='Using trades from: {}') logstring='Using trades from: {}')

View File

@ -58,29 +58,6 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
'experimental', 'ignore_roi_if_buy_signal') 'experimental', 'ignore_roi_if_buy_signal')
if not config.get('pairlists') and not config.get('pairlists'):
config['pairlists'] = [{'method': 'StaticPairList'}]
logger.warning(
"DEPRECATED: "
"Pairlists must be defined explicitly in the future."
"Defaulting to StaticPairList for now.")
if config.get('pairlist', {}).get("method") == 'VolumePairList':
logger.warning(
"DEPRECATED: "
f"Using VolumePairList in pairlist is deprecated and must be moved to pairlists. "
"Please refer to the docs on configuration details")
pl = {'method': 'VolumePairList'}
pl.update(config.get('pairlist', {}).get('config'))
config['pairlists'].append(pl)
if config.get('pairlist', {}).get('config', {}).get('precision_filter'):
logger.warning(
"DEPRECATED: "
f"Using precision_filter setting is deprecated and has been replaced by"
"PrecisionFilter. Please refer to the docs on configuration details")
config['pairlists'].append({'method': 'PrecisionFilter'})
if (config.get('edge', {}).get('enabled', False) if (config.get('edge', {}).get('enabled', False)
and 'capital_available_percentage' in config.get('edge', {})): and 'capital_available_percentage' in config.get('edge', {})):
logger.warning( logger.warning(

View File

@ -3,6 +3,9 @@
""" """
bot constants bot constants
""" """
from typing import List, Tuple
DEFAULT_CONFIG = 'config.json' DEFAULT_CONFIG = 'config.json'
DEFAULT_EXCHANGE = 'bittrex' DEFAULT_EXCHANGE = 'bittrex'
PROCESS_THROTTLE_SECS = 5 # sec PROCESS_THROTTLE_SECS = 5 # sec
@ -19,11 +22,14 @@ ORDERBOOK_SIDES = ['ask', 'bid']
ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
'PrecisionFilter', 'PriceFilter', 'SpreadFilter'] 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter']
AVAILABLE_DATAHANDLERS = ['json', 'jsongz'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz']
DRY_RUN_WALLET = 1000 DRY_RUN_WALLET = 1000
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume'] DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
# it has wide consequences for stored trades files
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
USERPATH_HYPEROPTS = 'hyperopts' USERPATH_HYPEROPTS = 'hyperopts'
USERPATH_STRATEGIES = 'strategies' USERPATH_STRATEGIES = 'strategies'
@ -85,6 +91,7 @@ CONF_SCHEMA = {
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'}, 'dry_run': {'type': 'boolean'},
'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
'cancel_open_orders_on_exit': {'type': 'boolean', 'default': False},
'process_only_new_candles': {'type': 'boolean'}, 'process_only_new_candles': {'type': 'boolean'},
'minimal_roi': { 'minimal_roi': {
'type': 'object', 'type': 'object',
@ -318,3 +325,13 @@ SCHEMA_MINIMAL_REQUIRED = [
'dataformat_ohlcv', 'dataformat_ohlcv',
'dataformat_trades', 'dataformat_trades',
] ]
CANCEL_REASON = {
"TIMEOUT": "cancelled due to timeout",
"PARTIALLY_FILLED": "partially filled - keeping order open",
"ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)",
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
}
# List of pairs with their timeframes
ListPairsWithTimeframes = List[Tuple[str, str]]

View File

@ -194,7 +194,10 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
:param col_name: Column name that will be assigned the results :param col_name: Column name that will be assigned the results
:param timeframe: Timeframe used during the operations :param timeframe: Timeframe used during the operations
:return: Returns df with one additional column, col_name, containing the cumulative profit. :return: Returns df with one additional column, col_name, containing the cumulative profit.
:raise: ValueError if trade-dataframe was found empty.
""" """
if len(trades) == 0:
raise ValueError("Trade dataframe empty.")
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
timeframe_minutes = timeframe_to_minutes(timeframe) timeframe_minutes = timeframe_to_minutes(timeframe)
# Resample to timeframe to make sure trades match candles # Resample to timeframe to make sure trades match candles

View File

@ -1,14 +1,17 @@
""" """
Functions to convert data from one format to another Functions to convert data from one format to another
""" """
import itertools
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict from operator import itemgetter
from typing import Any, Dict, List
import pandas as pd import pandas as pd
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS,
DEFAULT_TRADES_COLUMNS)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -154,7 +157,27 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
return frame return frame
def trades_to_ohlcv(trades: list, timeframe: str) -> DataFrame: def trades_remove_duplicates(trades: List[List]) -> List[List]:
"""
Removes duplicates from the trades list.
Uses itertools.groupby to avoid converting to pandas.
Tests show it as being pretty efficient on lists of 4M Lists.
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
:return: same format as above, but with duplicates removed
"""
return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))]
def trades_dict_to_list(trades: List[Dict]) -> List[List]:
"""
Convert fetch_trades result into a List (to be more memory efficient).
:param trades: List of trades, as returned by ccxt.fetch_trades.
:return: List of Lists, with constants.DEFAULT_TRADES_COLUMNS as columns
"""
return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades]
def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame:
""" """
Converts trades list to OHLCV list Converts trades list to OHLCV list
TODO: This should get a dedicated test TODO: This should get a dedicated test
@ -164,9 +187,10 @@ def trades_to_ohlcv(trades: list, timeframe: str) -> DataFrame:
""" """
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
timeframe_minutes = timeframe_to_minutes(timeframe) timeframe_minutes = timeframe_to_minutes(timeframe)
df = pd.DataFrame(trades) df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
df['datetime'] = pd.to_datetime(df['datetime']) df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms',
df = df.set_index('datetime') utc=True,)
df = df.set_index('timestamp')
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc() df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum() df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()

View File

@ -5,26 +5,30 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
Common Interface for bot and strategy to access data. Common Interface for bot and strategy to access data.
""" """
import logging import logging
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional
from pandas import DataFrame from pandas import DataFrame
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.constants import ListPairsWithTimeframes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DataProvider: class DataProvider:
def __init__(self, config: dict, exchange: Exchange) -> None: def __init__(self, config: dict, exchange: Exchange, pairlists=None) -> None:
self._config = config self._config = config
self._exchange = exchange self._exchange = exchange
self._pairlists = pairlists
def refresh(self, def refresh(self,
pairlist: List[Tuple[str, str]], pairlist: ListPairsWithTimeframes,
helping_pairs: List[Tuple[str, str]] = None) -> None: helping_pairs: ListPairsWithTimeframes = None) -> None:
""" """
Refresh data, called with each cycle Refresh data, called with each cycle
""" """
@ -34,7 +38,7 @@ class DataProvider:
self._exchange.refresh_latest_ohlcv(pairlist) self._exchange.refresh_latest_ohlcv(pairlist)
@property @property
def available_pairs(self) -> List[Tuple[str, str]]: def available_pairs(self) -> ListPairsWithTimeframes:
""" """
Return a list of tuples containing (pair, timeframe) for which data is currently cached. Return a list of tuples containing (pair, timeframe) for which data is currently cached.
Should be whitelist + open trades. Should be whitelist + open trades.
@ -95,19 +99,24 @@ class DataProvider:
def ticker(self, pair: str): def ticker(self, pair: str):
""" """
Return last ticker data Return last ticker data from exchange
:param pair: Pair to get the data for
:return: Ticker dict from exchange or empty dict if ticker is not available for the pair
""" """
# TODO: Implement me try:
pass return self._exchange.fetch_ticker(pair)
except DependencyException:
return {}
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
""" """
fetch latest orderbook data Fetch latest l2 orderbook data
Warning: Does a network request - so use with common sense.
:param pair: pair to get the data for :param pair: pair to get the data for
:param maximum: Maximum number of orderbook entries to query :param maximum: Maximum number of orderbook entries to query
:return: dict including bids/asks with a total of `maximum` entries. :return: dict including bids/asks with a total of `maximum` entries.
""" """
return self._exchange.get_order_book(pair, maximum) return self._exchange.fetch_l2_order_book(pair, maximum)
@property @property
def runmode(self) -> RunMode: def runmode(self) -> RunMode:
@ -116,3 +125,17 @@ class DataProvider:
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other". can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
""" """
return RunMode(self._config.get('runmode', RunMode.OTHER)) return RunMode(self._config.get('runmode', RunMode.OTHER))
def current_whitelist(self) -> List[str]:
"""
fetch latest available whitelist.
Useful when you have a large whitelist and need to call each pair as an informative pair.
As available pairs does not show whitelist until after informative pairs have been cached.
:return: list of pairs in whitelist
"""
if self._pairlists:
return self._pairlists.whitelist
else:
raise OperationalException("Dataprovider was not initialized with a pairlist provider.")

View File

@ -9,10 +9,13 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.data.converter import ohlcv_to_dataframe, trades_to_ohlcv from freqtrade.data.converter import (ohlcv_to_dataframe,
trades_remove_duplicates,
trades_to_ohlcv)
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.misc import format_ms_time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -257,27 +260,40 @@ def _download_trades_history(exchange: Exchange,
""" """
try: try:
since = timerange.startts * 1000 if timerange and timerange.starttype == 'date' else None since = timerange.startts * 1000 if \
(timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000
trades = data_handler.trades_load(pair) trades = data_handler.trades_load(pair)
from_id = trades[-1]['id'] if trades else None # TradesList columns are defined in constants.DEFAULT_TRADES_COLUMNS
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
# DEFAULT_TRADES_COLUMNS: 1 -> id
logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None') from_id = trades[-1][1] if trades else None
logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None') if trades and since < trades[-1][0]:
# Reset since to the last available point
# - 5 seconds (to ensure we're getting all trades)
since = trades[-1][0] - (5 * 1000)
logger.info(f"Using last trade date -5s - Downloading trades for {pair} "
f"since: {format_ms_time(since)}.")
logger.debug(f"Current Start: {format_ms_time(trades[0][0]) if trades else 'None'}")
logger.debug(f"Current End: {format_ms_time(trades[-1][0]) if trades else 'None'}")
logger.info(f"Current Amount of trades: {len(trades)}")
# Default since_ms to 30 days if nothing is given # Default since_ms to 30 days if nothing is given
new_trades = exchange.get_historic_trades(pair=pair, new_trades = exchange.get_historic_trades(pair=pair,
since=since if since else since=since,
int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000,
from_id=from_id, from_id=from_id,
) )
trades.extend(new_trades[1]) trades.extend(new_trades[1])
# Remove duplicates to make sure we're not storing data we don't need
trades = trades_remove_duplicates(trades)
data_handler.trades_store(pair, data=trades) data_handler.trades_store(pair, data=trades)
logger.debug("New Start: %s", trades[0]['datetime']) logger.debug(f"New Start: {format_ms_time(trades[0][0])}")
logger.debug("New End: %s", trades[-1]['datetime']) logger.debug(f"New End: {format_ms_time(trades[-1][0])}")
logger.info(f"New Amount of trades: {len(trades)}") logger.info(f"New Amount of trades: {len(trades)}")
return True return True

View File

@ -8,16 +8,20 @@ from abc import ABC, abstractclassmethod, abstractmethod
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Type from typing import List, Optional, Type
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.converter import clean_ohlcv_dataframe, trim_dataframe from freqtrade.data.converter import (clean_ohlcv_dataframe,
trades_remove_duplicates, trim_dataframe)
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Type for trades list
TradeList = List[List]
class IDataHandler(ABC): class IDataHandler(ABC):
@ -89,23 +93,25 @@ class IDataHandler(ABC):
""" """
@abstractmethod @abstractmethod
def trades_store(self, pair: str, data: List[Dict]) -> None: def trades_store(self, pair: str, data: TradeList) -> None:
""" """
Store trades data (list of Dicts) to file Store trades data (list of Dicts) to file
:param pair: Pair - used for filename :param pair: Pair - used for filename
:param data: List of Dicts containing trade data :param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
""" """
@abstractmethod @abstractmethod
def trades_append(self, pair: str, data: List[Dict]): def trades_append(self, pair: str, data: TradeList):
""" """
Append data to existing files Append data to existing files
:param pair: Pair - used for filename :param pair: Pair - used for filename
:param data: List of Dicts containing trade data :param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
""" """
@abstractmethod @abstractmethod
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> List[Dict]: def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
:param pair: Load trades for this pair :param pair: Load trades for this pair
@ -121,6 +127,16 @@ class IDataHandler(ABC):
:return: True when deleted, false if file did not exist. :return: True when deleted, false if file did not exist.
""" """
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
"""
Load a pair from file, either .json.gz or .json
Removes duplicates in the process.
:param pair: Load trades for this pair
:param timerange: Timerange to load trades for - currently not implemented
:return: List of trades
"""
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
def ohlcv_load(self, pair, timeframe: str, def ohlcv_load(self, pair, timeframe: str,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
fill_missing: bool = True, fill_missing: bool = True,

View File

@ -1,6 +1,7 @@
import logging
import re import re
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import List, Optional
import numpy as np import numpy as np
from pandas import DataFrame, read_json, to_datetime from pandas import DataFrame, read_json, to_datetime
@ -8,8 +9,11 @@ from pandas import DataFrame, read_json, to_datetime
from freqtrade import misc from freqtrade import misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.data.converter import trades_dict_to_list
from .idatahandler import IDataHandler from .idatahandler import IDataHandler, TradeList
logger = logging.getLogger(__name__)
class JsonDataHandler(IDataHandler): class JsonDataHandler(IDataHandler):
@ -113,24 +117,26 @@ class JsonDataHandler(IDataHandler):
# Check if regex found something and only return these results to avoid exceptions. # Check if regex found something and only return these results to avoid exceptions.
return [match[0].replace('_', '/') for match in _tmp if match] return [match[0].replace('_', '/') for match in _tmp if match]
def trades_store(self, pair: str, data: List[Dict]) -> None: def trades_store(self, pair: str, data: TradeList) -> None:
""" """
Store trades data (list of Dicts) to file Store trades data (list of Dicts) to file
:param pair: Pair - used for filename :param pair: Pair - used for filename
:param data: List of Dicts containing trade data :param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
""" """
filename = self._pair_trades_filename(self._datadir, pair) filename = self._pair_trades_filename(self._datadir, pair)
misc.file_dump_json(filename, data, is_zip=self._use_zip) misc.file_dump_json(filename, data, is_zip=self._use_zip)
def trades_append(self, pair: str, data: List[Dict]): def trades_append(self, pair: str, data: TradeList):
""" """
Append data to existing files Append data to existing files
:param pair: Pair - used for filename :param pair: Pair - used for filename
:param data: List of Dicts containing trade data :param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
""" """
raise NotImplementedError() raise NotImplementedError()
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> List[Dict]: def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
# TODO: respect timerange ... # TODO: respect timerange ...
@ -140,9 +146,15 @@ class JsonDataHandler(IDataHandler):
""" """
filename = self._pair_trades_filename(self._datadir, pair) filename = self._pair_trades_filename(self._datadir, pair)
tradesdata = misc.file_load_json(filename) tradesdata = misc.file_load_json(filename)
if not tradesdata: if not tradesdata:
return [] return []
if isinstance(tradesdata[0], dict):
# Convert trades dict to list
logger.info("Old trades format detected - converting")
tradesdata = trades_dict_to_list(tradesdata)
pass
return tradesdata return tradesdata
def trades_purge(self, pair: str) -> bool: def trades_purge(self, pair: str) -> bool:

View File

@ -238,20 +238,9 @@ class Edge:
:param result Dataframe :param result Dataframe
:return: result Dataframe :return: result Dataframe
""" """
# We set stake amount to an arbitrary amount, as it doesn't change the calculation.
# stake and fees # All returned values are relative, they are defined as ratios.
# stake = 0.015
# 0.05% is 0.0005
# fee = 0.001
# we set stake amount to an arbitrary amount.
# as it doesn't change the calculation.
# all returned values are relative.
# they are defined as ratios.
stake = 0.015 stake = 0.015
fee = self.fee
open_fee = fee / 2
close_fee = fee / 2
result['trade_duration'] = result['close_time'] - result['open_time'] result['trade_duration'] = result['close_time'] - result['open_time']
@ -262,12 +251,12 @@ class Edge:
# Buy Price # Buy Price
result['buy_vol'] = stake / result['open_rate'] # How many target are we buying result['buy_vol'] = stake / result['open_rate'] # How many target are we buying
result['buy_fee'] = stake * open_fee result['buy_fee'] = stake * self.fee
result['buy_spend'] = stake + result['buy_fee'] # How much we're spending result['buy_spend'] = stake + result['buy_fee'] # How much we're spending
# Sell price # Sell price
result['sell_sum'] = result['buy_vol'] * result['close_rate'] result['sell_sum'] = result['buy_vol'] * result['close_rate']
result['sell_fee'] = result['sell_sum'] * close_fee result['sell_fee'] = result['sell_sum'] * self.fee
result['sell_take'] = result['sell_sum'] - result['sell_fee'] result['sell_take'] = result['sell_sum'] - result['sell_fee']
# profit_ratio # profit_ratio

View File

@ -21,6 +21,14 @@ class DependencyException(FreqtradeException):
""" """
class PricingError(DependencyException):
"""
Subclass of DependencyException.
Indicates that the price could not be determined.
Implicitly a buy / sell operation.
"""
class InvalidOrderException(FreqtradeException): class InvalidOrderException(FreqtradeException):
""" """
This is returned when the order is not valid. Example: This is returned when the order is not valid. Example:

View File

@ -20,7 +20,7 @@ class Binance(Exchange):
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
} }
def get_order_book(self, pair: str, limit: int = 100) -> dict: def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
""" """
get order book level 2 from exchange get order book level 2 from exchange
@ -30,7 +30,7 @@ class Binance(Exchange):
# get next-higher step in the limit_range list # get next-higher step in the limit_range list
limit = min(list(filter(lambda x: limit <= x, limit_range))) limit = min(list(filter(lambda x: limit <= x, limit_range)))
return super().get_order_book(pair, limit) return super().fetch_l2_order_book(pair, limit)
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
""" """
@ -72,7 +72,7 @@ class Binance(Exchange):
rate = self.price_to_precision(pair, rate) rate = self.price_to_precision(pair, rate)
order = self._api.create_order(symbol=pair, type=ordertype, side='sell', order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
amount=amount, price=stop_price, params=params) amount=amount, price=rate, params=params)
logger.info('stoploss limit order added for %s. ' logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate) 'stop price: %s. limit: %s', pair, stop_price, rate)
return order return order

View File

@ -1,6 +1,6 @@
import logging import logging
from freqtrade.exceptions import DependencyException, TemporaryError from freqtrade.exceptions import TemporaryError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -93,7 +93,7 @@ def retrier_async(f):
count = kwargs.pop('count', API_RETRY_COUNT) count = kwargs.pop('count', API_RETRY_COUNT)
try: try:
return await f(*args, **kwargs) return await f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex: except TemporaryError as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex) logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0: if count > 0:
count -= 1 count -= 1
@ -111,7 +111,7 @@ def retrier(f):
count = kwargs.pop('count', API_RETRY_COUNT) count = kwargs.pop('count', API_RETRY_COUNT)
try: try:
return f(*args, **kwargs) return f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex: except TemporaryError as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex) logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0: if count > 0:
count -= 1 count -= 1

View File

@ -18,12 +18,12 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
TRUNCATE, decimal_to_precision) TRUNCATE, decimal_to_precision)
from pandas import DataFrame from pandas import DataFrame
from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts, safe_value_fallback
from freqtrade.constants import ListPairsWithTimeframes
CcxtModuleType = Any CcxtModuleType = Any
@ -367,8 +367,7 @@ class Exchange:
f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}") f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
if timeframe and timeframe_to_minutes(timeframe) < 1: if timeframe and timeframe_to_minutes(timeframe) < 1:
raise OperationalException( raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.")
f"Timeframes < 1m are currently not supported by Freqtrade.")
def validate_ordertypes(self, order_types: Dict) -> None: def validate_ordertypes(self, order_types: Dict) -> None:
""" """
@ -472,26 +471,31 @@ class Exchange:
'pair': pair, 'pair': pair,
'price': rate, 'price': rate,
'amount': _amount, 'amount': _amount,
"cost": _amount * rate, 'cost': _amount * rate,
'type': ordertype, 'type': ordertype,
'side': side, 'side': side,
'remaining': _amount, 'remaining': _amount,
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'status': "closed" if ordertype == "market" else "open", 'status': "closed" if ordertype == "market" else "open",
'fee': None, 'fee': None,
"info": {} 'info': {}
} }
self._store_dry_order(dry_order) self._store_dry_order(dry_order, pair)
# Copy order and close it - so the returned order is open unless it's a market order # Copy order and close it - so the returned order is open unless it's a market order
return dry_order return dry_order
def _store_dry_order(self, dry_order: Dict) -> None: def _store_dry_order(self, dry_order: Dict, pair: str) -> None:
closed_order = dry_order.copy() closed_order = dry_order.copy()
if closed_order["type"] in ["market", "limit"]: if closed_order['type'] in ["market", "limit"]:
closed_order.update({ closed_order.update({
"status": "closed", 'status': 'closed',
"filled": closed_order["amount"], 'filled': closed_order['amount'],
"remaining": 0 'remaining': 0,
'fee': {
'currency': self.get_pair_quote_currency(pair),
'cost': dry_order['cost'] * self.get_fee(pair),
'rate': self.get_fee(pair)
}
}) })
if closed_order["type"] in ["stop_loss_limit"]: if closed_order["type"] in ["stop_loss_limit"]:
closed_order["info"].update({"stopPrice": closed_order["price"]}) closed_order["info"].update({"stopPrice": closed_order["price"]})
@ -672,7 +676,7 @@ class Exchange:
logger.info("Downloaded data for %s with length %s.", pair, len(data)) logger.info("Downloaded data for %s with length %s.", pair, len(data))
return data return data
def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]: def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes) -> List[Tuple[str, List]]:
""" """
Refresh in-memory OHLCV asynchronously and set `_klines` with the result Refresh in-memory OHLCV asynchronously and set `_klines` with the result
Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
@ -769,7 +773,7 @@ class Exchange:
@retrier_async @retrier_async
async def _async_fetch_trades(self, pair: str, async def _async_fetch_trades(self, pair: str,
since: Optional[int] = None, since: Optional[int] = None,
params: Optional[dict] = None) -> List[Dict]: params: Optional[dict] = None) -> List[List]:
""" """
Asyncronously gets trade history using fetch_trades. Asyncronously gets trade history using fetch_trades.
Handles exchange errors, does one call to the exchange. Handles exchange errors, does one call to the exchange.
@ -789,7 +793,7 @@ class Exchange:
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else '' '(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
) )
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000) trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
return trades return trades_dict_to_list(trades)
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical trade data.' f'Exchange {self._api.name} does not support fetching historical trade data.'
@ -803,7 +807,7 @@ class Exchange:
async def _async_get_trade_history_id(self, pair: str, async def _async_get_trade_history_id(self, pair: str,
until: int, until: int,
since: Optional[int] = None, since: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]: from_id: Optional[str] = None) -> Tuple[str, List[List]]:
""" """
Asyncronously gets trade history using fetch_trades Asyncronously gets trade history using fetch_trades
use this when exchange uses id-based iteration (check `self._trades_pagination`) use this when exchange uses id-based iteration (check `self._trades_pagination`)
@ -814,7 +818,7 @@ class Exchange:
returns tuple: (pair, trades-list) returns tuple: (pair, trades-list)
""" """
trades: List[Dict] = [] trades: List[List] = []
if not from_id: if not from_id:
# Fetch first elements using timebased method to get an ID to paginate on # Fetch first elements using timebased method to get an ID to paginate on
@ -823,7 +827,9 @@ class Exchange:
# e.g. Binance returns the "last 1000" candles within a 1h time interval # e.g. Binance returns the "last 1000" candles within a 1h time interval
# - so we will miss the first trades. # - so we will miss the first trades.
t = await self._async_fetch_trades(pair, since=since) t = await self._async_fetch_trades(pair, since=since)
from_id = t[-1]['id'] # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
# DEFAULT_TRADES_COLUMNS: 1 -> id
from_id = t[-1][1]
trades.extend(t[:-1]) trades.extend(t[:-1])
while True: while True:
t = await self._async_fetch_trades(pair, t = await self._async_fetch_trades(pair,
@ -831,21 +837,21 @@ class Exchange:
if len(t): if len(t):
# Skip last id since its the key for the next call # Skip last id since its the key for the next call
trades.extend(t[:-1]) trades.extend(t[:-1])
if from_id == t[-1]['id'] or t[-1]['timestamp'] > until: if from_id == t[-1][1] or t[-1][0] > until:
logger.debug(f"Stopping because from_id did not change. " logger.debug(f"Stopping because from_id did not change. "
f"Reached {t[-1]['timestamp']} > {until}") f"Reached {t[-1][0]} > {until}")
# Reached the end of the defined-download period - add last trade as well. # Reached the end of the defined-download period - add last trade as well.
trades.extend(t[-1:]) trades.extend(t[-1:])
break break
from_id = t[-1]['id'] from_id = t[-1][1]
else: else:
break break
return (pair, trades) return (pair, trades)
async def _async_get_trade_history_time(self, pair: str, until: int, async def _async_get_trade_history_time(self, pair: str, until: int,
since: Optional[int] = None) -> Tuple[str, List]: since: Optional[int] = None) -> Tuple[str, List[List]]:
""" """
Asyncronously gets trade history using fetch_trades, Asyncronously gets trade history using fetch_trades,
when the exchange uses time-based iteration (check `self._trades_pagination`) when the exchange uses time-based iteration (check `self._trades_pagination`)
@ -855,16 +861,18 @@ class Exchange:
returns tuple: (pair, trades-list) returns tuple: (pair, trades-list)
""" """
trades: List[Dict] = [] trades: List[List] = []
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
# DEFAULT_TRADES_COLUMNS: 1 -> id
while True: while True:
t = await self._async_fetch_trades(pair, since=since) t = await self._async_fetch_trades(pair, since=since)
if len(t): if len(t):
since = t[-1]['timestamp'] since = t[-1][1]
trades.extend(t) trades.extend(t)
# Reached the end of the defined-download period # Reached the end of the defined-download period
if until and t[-1]['timestamp'] > until: if until and t[-1][0] > until:
logger.debug( logger.debug(
f"Stopping because until was reached. {t[-1]['timestamp']} > {until}") f"Stopping because until was reached. {t[-1][0]} > {until}")
break break
else: else:
break break
@ -874,7 +882,7 @@ class Exchange:
async def _async_get_trade_history(self, pair: str, async def _async_get_trade_history(self, pair: str,
since: Optional[int] = None, since: Optional[int] = None,
until: Optional[int] = None, until: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]: from_id: Optional[str] = None) -> Tuple[str, List[List]]:
""" """
Async wrapper handling downloading trades using either time or id based methods. Async wrapper handling downloading trades using either time or id based methods.
""" """
@ -990,7 +998,7 @@ class Exchange:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def get_order_book(self, pair: str, limit: int = 100) -> dict: def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
""" """
get order book level 2 from exchange get order book level 2 from exchange
@ -1041,9 +1049,9 @@ class Exchange:
return matched_trades return matched_trades
except ccxt.NetworkError as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get trades due to networking error. Message: {e}') from e f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@ -1063,6 +1071,61 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@staticmethod
def order_has_fee(order: Dict) -> bool:
"""
Verifies if the passed in order dict has the needed keys to extract fees,
and that these keys (currency, cost) are not empty.
:param order: Order or trade (one trade) dict
:return: True if the fee substructure contains currency and cost, false otherwise
"""
if not isinstance(order, dict):
return False
return ('fee' in order and order['fee'] is not None
and (order['fee'].keys() >= {'currency', 'cost'})
and order['fee']['currency'] is not None
and order['fee']['cost'] is not None
)
def calculate_fee_rate(self, order: Dict) -> Optional[float]:
"""
Calculate fee rate if it's not given by the exchange.
:param order: Order or trade (one trade) dict
"""
if order['fee'].get('rate') is not None:
return order['fee'].get('rate')
fee_curr = order['fee']['currency']
# Calculate fee based on order details
if fee_curr in self.get_pair_base_currency(order['symbol']):
# Base currency - divide by amount
return round(
order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8)
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
# Quote currency - divide by cost
return round(order['fee']['cost'] / order['cost'], 8)
else:
# If Fee currency is a different currency
try:
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
tick = self.fetch_ticker(comb)
fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask')
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
except DependencyException:
return None
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
"""
Extract tuple of cost, currency, rate.
Requires order_has_fee to run first!
:param order: Order or trade (one trade) dict
:return: Tuple with cost, currency, rate of the given fee dict
"""
return (order['fee']['cost'],
order['fee']['currency'],
self.calculate_fee_rate(order))
# calculate rate ? (order['fee']['cost'] / (order['amount'] * order['price']))
def is_exchange_bad(exchange_name: str) -> bool: def is_exchange_bad(exchange_name: str) -> bool:
return exchange_name in BAD_EXCHANGES return exchange_name in BAD_EXCHANGES

View File

@ -7,7 +7,7 @@ import ccxt
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange import retrier from freqtrade.exchange.common import retrier
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -7,7 +7,7 @@ import traceback
from datetime import datetime from datetime import datetime
from math import isclose from math import isclose
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional
import arrow import arrow
from cachetools import TTLCache from cachetools import TTLCache
@ -18,7 +18,7 @@ from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exceptions import DependencyException, InvalidOrderException from freqtrade.exceptions import DependencyException, InvalidOrderException, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
@ -54,8 +54,11 @@ class FreqtradeBot:
# Init objects # Init objects
self.config = config self.config = config
self._sell_rate_cache = TTLCache(maxsize=100, ttl=5) # Cache values for 1800 to avoid frequent polling of the exchange for prices
self._buy_rate_cache = TTLCache(maxsize=100, ttl=5) # Caching only applies to RPC methods, so prices for open trades are still
# refreshed once every iteration.
self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800)
self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800)
self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
@ -68,20 +71,20 @@ class FreqtradeBot:
self.wallets = Wallets(self.config, self.exchange) self.wallets = Wallets(self.config, self.exchange)
self.dataprovider = DataProvider(self.config, self.exchange) self.pairlists = PairListManager(self.exchange, self.config)
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
# Attach Dataprovider to Strategy baseclass # Attach Dataprovider to Strategy baseclass
IStrategy.dp = self.dataprovider IStrategy.dp = self.dataprovider
# Attach Wallets to Strategy baseclass # Attach Wallets to Strategy baseclass
IStrategy.wallets = self.wallets IStrategy.wallets = self.wallets
self.pairlists = PairListManager(self.exchange, self.config)
# Initializing Edge only if enabled # Initializing Edge only if enabled
self.edge = Edge(self.config, self.exchange, self.strategy) if \ self.edge = Edge(self.config, self.exchange, self.strategy) if \
self.config.get('edge', {}).get('enabled', False) else None self.config.get('edge', {}).get('enabled', False) else None
self.active_pair_whitelist = self._refresh_whitelist() self.active_pair_whitelist = self._refresh_active_whitelist()
# Set initial bot state from config # Set initial bot state from config
initial_state = self.config.get('initial_state') initial_state = self.config.get('initial_state')
@ -113,6 +116,9 @@ class FreqtradeBot:
""" """
logger.info('Cleaning up modules ...') logger.info('Cleaning up modules ...')
if self.config['cancel_open_orders_on_exit']:
self.cancel_all_open_orders()
self.rpc.cleanup() self.rpc.cleanup()
persistence.cleanup() persistence.cleanup()
@ -139,10 +145,10 @@ class FreqtradeBot:
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
self.active_pair_whitelist = self._refresh_whitelist(trades) self.active_pair_whitelist = self._refresh_active_whitelist(trades)
# Refreshing candles # Refreshing candles
self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
self.strategy.informative_pairs()) self.strategy.informative_pairs())
with self._sell_lock: with self._sell_lock:
@ -162,9 +168,17 @@ class FreqtradeBot:
Trade.session.flush() Trade.session.flush()
def _refresh_whitelist(self, trades: List[Trade] = []) -> List[str]: def process_stopped(self) -> None:
""" """
Refresh whitelist from pairlist or edge and extend it with trades. Close all orders that were left open
"""
if self.config['cancel_open_orders_on_exit']:
self.cancel_all_open_orders()
def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]:
"""
Refresh active whitelist from pairlist or edge and extend it with
pairs that have open trades.
""" """
# Refresh whitelist # Refresh whitelist
self.pairlists.refresh_pairlist() self.pairlists.refresh_pairlist()
@ -181,12 +195,6 @@ class FreqtradeBot:
_whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist])
return _whitelist return _whitelist
def _create_pair_whitelist(self, pairs: List[str]) -> List[Tuple[str, str]]:
"""
Create pair-whitelist tuple with (pair, ticker_interval)
"""
return [(pair, self.config['ticker_interval']) for pair in pairs]
def get_free_open_trades(self): def get_free_open_trades(self):
""" """
Return the number of free open trades slots or 0 if Return the number of free open trades slots or 0 if
@ -252,12 +260,19 @@ class FreqtradeBot:
f"Getting price from order book {bid_strategy['price_side'].capitalize()} side." f"Getting price from order book {bid_strategy['price_side'].capitalize()} side."
) )
order_book_top = bid_strategy.get('order_book_top', 1) order_book_top = bid_strategy.get('order_book_top', 1)
order_book = self.exchange.get_order_book(pair, order_book_top) order_book = self.exchange.fetch_l2_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book) logger.debug('order_book %s', order_book)
# top 1 = index 0 # top 1 = index 0
order_book_rate = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0] try:
logger.info(f'...top {order_book_top} order book buy rate {order_book_rate:.8f}') rate_from_l2 = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0]
used_rate = order_book_rate except (IndexError, KeyError) as e:
logger.warning(
"Buy Price from orderbook could not be determined."
f"Orderbook: {order_book}"
)
raise PricingError from e
logger.info(f'...top {order_book_top} order book buy rate {rate_from_l2:.8f}')
used_rate = rate_from_l2
else: else:
logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price") logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price")
ticker = self.exchange.fetch_ticker(pair) ticker = self.exchange.fetch_ticker(pair)
@ -438,7 +453,7 @@ class FreqtradeBot:
""" """
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0) conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
logger.info(f"Checking depth of market for {pair} ...") logger.info(f"Checking depth of market for {pair} ...")
order_book = self.exchange.get_order_book(pair, 1000) order_book = self.exchange.fetch_l2_order_book(pair, 1000)
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
order_book_bids = order_book_data_frame['b_size'].sum() order_book_bids = order_book_data_frame['b_size'].sum()
order_book_asks = order_book_data_frame['a_size'].sum() order_book_asks = order_book_data_frame['a_size'].sum()
@ -610,7 +625,7 @@ class FreqtradeBot:
trades_closed += 1 trades_closed += 1
continue continue
# Check if we can sell our current pair # Check if we can sell our current pair
if trade.open_order_id is None and self.handle_trade(trade): if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
trades_closed += 1 trades_closed += 1
except DependencyException as exception: except DependencyException as exception:
@ -627,7 +642,7 @@ class FreqtradeBot:
""" """
Helper generator to query orderbook in loop (used for early sell-order placing) Helper generator to query orderbook in loop (used for early sell-order placing)
""" """
order_book = self.exchange.get_order_book(pair, order_book_max) order_book = self.exchange.fetch_l2_order_book(pair, order_book_max)
for i in range(order_book_min, order_book_max + 1): for i in range(order_book_min, order_book_max + 1):
yield order_book[side][i - 1][0] yield order_book[side][i - 1][0]
@ -654,8 +669,11 @@ class FreqtradeBot:
logger.info( logger.info(
f"Getting price from order book {ask_strategy['price_side'].capitalize()} side." f"Getting price from order book {ask_strategy['price_side'].capitalize()} side."
) )
try:
rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s")) rate = next(self._order_book_gen(pair, f"{ask_strategy['price_side']}s"))
except (IndexError, KeyError) as e:
logger.warning("Sell Price at location from orderbook could not be determined.")
raise PricingError from e
else: else:
rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']] rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']]
self._sell_rate_cache[pair] = rate self._sell_rate_cache[pair] = rate
@ -682,16 +700,23 @@ class FreqtradeBot:
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval)) self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
if config_ask_strategy.get('use_order_book', False): if config_ask_strategy.get('use_order_book', False):
logger.debug(f'Using order book for selling {trade.pair}...')
# logger.debug('Order book %s',orderBook) # logger.debug('Order book %s',orderBook)
order_book_min = config_ask_strategy.get('order_book_min', 1) order_book_min = config_ask_strategy.get('order_book_min', 1)
order_book_max = config_ask_strategy.get('order_book_max', 1) order_book_max = config_ask_strategy.get('order_book_max', 1)
logger.info(f'Using order book between {order_book_min} and {order_book_max} '
f'for selling {trade.pair}...')
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s", order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
order_book_min=order_book_min, order_book_min=order_book_min,
order_book_max=order_book_max) order_book_max=order_book_max)
for i in range(order_book_min, order_book_max + 1): for i in range(order_book_min, order_book_max + 1):
try:
sell_rate = next(order_book) sell_rate = next(order_book)
except (IndexError, KeyError) as e:
logger.warning(
f"Sell Price at location {i} from orderbook could not be determined."
)
raise PricingError from e
logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: " logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: "
f"{sell_rate:0.8f}") f"{sell_rate:0.8f}")
@ -752,7 +777,7 @@ class FreqtradeBot:
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] == 'closed': if stoploss_order and stoploss_order['status'] == 'closed':
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
trade.update(stoploss_order) self.update_trade_state(trade, stoploss_order, sl_order=True)
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval'])) timeframe_to_next_date(self.config['ticker_interval']))
@ -866,63 +891,80 @@ class FreqtradeBot:
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
trade_state_update = self.update_trade_state(trade, order) fully_cancelled = self.update_trade_state(trade, order)
if (order['side'] == 'buy' and ( if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
trade_state_update fully_cancelled
or self._check_timed_out('buy', order) or self._check_timed_out('buy', order)
or strategy_safe_wrapper(self.strategy.check_buy_timeout, or strategy_safe_wrapper(self.strategy.check_buy_timeout,
default_retval=False)(pair=trade.pair, default_retval=False)(pair=trade.pair,
trade=trade, trade=trade,
order=order))): order=order))):
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT'])
self.handle_timedout_limit_buy(trade, order) elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
self.wallets.update() fully_cancelled
order_type = self.strategy.order_types['buy']
self._notify_buy_cancel(trade, order_type)
elif (order['side'] == 'sell' and (
trade_state_update
or self._check_timed_out('sell', order) or self._check_timed_out('sell', order)
or strategy_safe_wrapper(self.strategy.check_sell_timeout, or strategy_safe_wrapper(self.strategy.check_sell_timeout,
default_retval=False)(pair=trade.pair, default_retval=False)(pair=trade.pair,
trade=trade, trade=trade,
order=order))): order=order))):
reason = self.handle_timedout_limit_sell(trade, order) self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT'])
self.wallets.update()
order_type = self.strategy.order_types['sell']
self._notify_sell_cancel(trade, order_type, reason)
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool: def cancel_all_open_orders(self) -> None:
""" """
Buy timeout - cancel order Cancel all orders that are currently open
:return: None
"""
for trade in Trade.get_open_order_trades():
try:
order = self.exchange.get_order(trade.open_order_id, trade.pair)
except (DependencyException, InvalidOrderException):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue
if order['side'] == 'buy':
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
elif order['side'] == 'sell':
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool:
"""
Buy cancel - cancel order
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
if order['status'] != 'canceled': was_trade_fully_canceled = False
reason = "cancelled due to timeout"
# Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in ('canceled', 'closed'):
reason = constants.CANCEL_REASON['TIMEOUT']
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount) trade.amount)
else: else:
# Order was cancelled already, so we can reuse the existing dict # Order was cancelled already, so we can reuse the existing dict
corder = order corder = order
reason = "cancelled on exchange" reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Buy order %s for %s.', reason, trade) logger.info('Buy order %s for %s.', reason, trade)
if safe_value_fallback(corder, order, 'remaining', 'remaining') == order['amount']: # Using filled to determine the filled amount
filled_amount = safe_value_fallback(corder, order, 'filled', 'filled')
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
logger.info('Buy order fully cancelled. Removing %s from database.', trade) logger.info('Buy order fully cancelled. Removing %s from database.', trade)
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
Trade.session.delete(trade) Trade.session.delete(trade)
Trade.session.flush() Trade.session.flush()
return True was_trade_fully_canceled = True
else:
# if trade is partially complete, edit the stake details for the trade # if trade is partially complete, edit the stake details for the trade
# and close the order # and close the order
# cancel_order may not contain the full order dict, so we need to fallback # cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling. # to the order dict aquired before cancelling.
# we need to fall back to the values from order if corder does not contain these keys. # we need to fall back to the values from order if corder does not contain these keys.
trade.amount = order['amount'] - safe_value_fallback(corder, order, trade.amount = filled_amount
'remaining', 'remaining')
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
self.update_trade_state(trade, corder, trade.amount) self.update_trade_state(trade, corder, trade.amount)
@ -932,22 +974,28 @@ class FreqtradeBot:
'type': RPCMessageType.STATUS_NOTIFICATION, 'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Remaining buy order for {trade.pair} cancelled due to timeout' 'status': f'Remaining buy order for {trade.pair} cancelled due to timeout'
}) })
return False
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> str: self.wallets.update()
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'])
return was_trade_fully_canceled
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str:
""" """
Sell timeout - cancel order and update trade Sell cancel - cancel order and update trade
:return: Reason for cancel :return: Reason for cancel
""" """
# if trade is not partially completed, just cancel the trade # if trade is not partially completed, just cancel the order
if order['remaining'] == order['amount'] or order.get('filled') == 0.0: if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
if not self.exchange.check_order_canceled_empty(order): if not self.exchange.check_order_canceled_empty(order):
reason = "cancelled due to timeout" try:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the order
self.exchange.cancel_order(trade.open_order_id, trade.pair) self.exchange.cancel_order(trade.open_order_id, trade.pair)
except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}")
return 'error cancelling order'
logger.info('Sell order %s for %s.', reason, trade) logger.info('Sell order %s for %s.', reason, trade)
else: else:
reason = "cancelled on exchange" reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Sell order %s for %s.', reason, trade) logger.info('Sell order %s for %s.', reason, trade)
trade.close_rate = None trade.close_rate = None
@ -957,11 +1005,17 @@ class FreqtradeBot:
trade.close_date = None trade.close_date = None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
else:
return reason
# TODO: figure out how to handle partially complete sell orders # TODO: figure out how to handle partially complete sell orders
return 'partially filled - keeping order open' reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
self.wallets.update()
self._notify_sell_cancel(
trade,
order_type=self.strategy.order_types['sell'],
reason=reason
)
return reason
def _safe_sell_amount(self, pair: str, amount: float) -> float: def _safe_sell_amount(self, pair: str, amount: float) -> float:
""" """
@ -982,7 +1036,7 @@ class FreqtradeBot:
if wallet_amount >= amount: if wallet_amount >= amount:
return amount return amount
elif wallet_amount > amount * 0.98: elif wallet_amount > amount * 0.98:
logger.info(f"{pair} - Falling back to wallet-amount.") logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.")
return wallet_amount return wallet_amount
else: else:
raise DependencyException( raise DependencyException(
@ -1032,7 +1086,7 @@ class FreqtradeBot:
trade.sell_reason = sell_reason.value trade.sell_reason = sell_reason.value
# In case of market sell orders the order can be closed immediately # In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed': if order.get('status', 'unknown') == 'closed':
trade.update(order) self.update_trade_state(trade, order)
Trade.session.flush() Trade.session.flush()
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
@ -1084,6 +1138,11 @@ class FreqtradeBot:
""" """
Sends rpc notification when a sell cancel occured. Sends rpc notification when a sell cancel occured.
""" """
if trade.sell_order_status == reason:
return
else:
trade.sell_order_status = reason
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.get_sell_rate(trade.pair, False) current_rate = self.get_sell_rate(trade.pair, False)
@ -1123,7 +1182,7 @@ class FreqtradeBot:
# #
def update_trade_state(self, trade: Trade, action_order: dict = None, def update_trade_state(self, trade: Trade, action_order: dict = None,
order_amount: float = None) -> bool: order_amount: float = None, sl_order: bool = False) -> bool:
""" """
Checks trades with open orders and updates the amount if necessary Checks trades with open orders and updates the amount if necessary
Handles closing both buy and sell orders. Handles closing both buy and sell orders.
@ -1131,12 +1190,17 @@ class FreqtradeBot:
""" """
# Get order details for actual price per unit # Get order details for actual price per unit
if trade.open_order_id: if trade.open_order_id:
order_id = trade.open_order_id
elif trade.stoploss_order_id and sl_order:
order_id = trade.stoploss_order_id
else:
return False
# Update trade with order values # Update trade with order values
logger.info('Found open order for %s', trade) logger.info('Found open order for %s', trade)
try: try:
order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) order = action_order or self.exchange.get_order(order_id, trade.pair)
except InvalidOrderException as exception: except InvalidOrderException as exception:
logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception) logger.warning('Unable to fetch order %s: %s', order_id, exception)
return False return False
# Try update amount (binance-fix) # Try update amount (binance-fix)
try: try:
@ -1144,8 +1208,6 @@ class FreqtradeBot:
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount order['amount'] = new_amount
order.pop('filled', None) order.pop('filled', None)
# Fee was applied, so set to 0
trade.fee_open = 0
trade.recalc_open_trade_price() trade.recalc_open_trade_price()
except DependencyException as exception: except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception) logger.warning("Could not update trade amount: %s", exception)
@ -1159,56 +1221,94 @@ class FreqtradeBot:
# Updating wallets when order is closed # Updating wallets when order is closed
if not trade.is_open: if not trade.is_open:
self.wallets.update() self.wallets.update()
return False return False
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
amount: float, fee_abs: float) -> float:
"""
Applies the fee to amount (either from Order or from Trades).
Can eat into dust if more than the required asset is available.
"""
self.wallets.update()
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
# Eat into dust if we own more than base currency
logger.info(f"Fee amount for {trade} was in base currency - "
f"Eating Fee {fee_abs} into dust.")
elif fee_abs != 0:
real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs)
logger.info(f"Applying fee on amount for {trade} "
f"(from {amount} to {real_amount}).")
return real_amount
return amount
def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float: def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float:
""" """
Get real amount for the trade Detect and update trade fee.
Calls trade.update_fee() uppon correct detection.
Returns modified amount if the fee was taken from the destination currency.
Necessary for exchanges which charge fees in base currency (e.g. binance) Necessary for exchanges which charge fees in base currency (e.g. binance)
:return: identical (or new) amount for the trade
""" """
# Init variables
if order_amount is None: if order_amount is None:
order_amount = order['amount'] order_amount = order['amount']
# Only run for closed orders # Only run for closed orders
if trade.fee_open == 0 or order['status'] == 'open': if trade.fee_updated(order.get('side', '')) or order['status'] == 'open':
return order_amount return order_amount
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
# use fee from order-dict if possible # use fee from order-dict if possible
if ('fee' in order and order['fee'] is not None and if self.exchange.order_has_fee(order):
(order['fee'].keys() >= {'currency', 'cost'})): fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
if (order['fee']['currency'] is not None and logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
order['fee']['cost'] is not None and f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
trade_base_currency == order['fee']['currency']):
new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order",
trade, order['amount'], new_amount)
return new_amount
# Fallback to Trades trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if trade_base_currency == fee_currency:
# Apply fee to amount
return self.apply_fee_conditional(trade, trade_base_currency,
amount=order_amount, fee_abs=fee_cost)
return order_amount
return self.fee_detection_from_trades(trade, order, order_amount)
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
"""
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
"""
trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair, trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair,
trade.open_date) trade.open_date)
if len(trades) == 0: if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
return order_amount return order_amount
fee_currency = None
amount = 0 amount = 0
fee_abs = 0 fee_abs = 0.0
fee_cost = 0.0
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
fee_rate_array: List[float] = []
for exectrade in trades: for exectrade in trades:
amount += exectrade['amount'] amount += exectrade['amount']
if ("fee" in exectrade and exectrade['fee'] is not None and if self.exchange.order_has_fee(exectrade):
(exectrade['fee'].keys() >= {'currency', 'cost'})): fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade)
fee_cost += fee_cost_
if fee_rate_ is not None:
fee_rate_array.append(fee_rate_)
# only applies if fee is in quote currency! # only applies if fee is in quote currency!
if (exectrade['fee']['currency'] is not None and if trade_base_currency == fee_currency:
exectrade['fee']['cost'] is not None and fee_abs += fee_cost_
trade_base_currency == exectrade['fee']['currency']): # Ensure at least one trade was found:
fee_abs += exectrade['fee']['cost'] if fee_currency:
# fee_rate should use mean
fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
logger.warning(f"Amount {amount} does not match amount {trade.amount}") logger.warning(f"Amount {amount} does not match amount {trade.amount}")
raise DependencyException("Half bought? Amounts don't match") raise DependencyException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0: if fee_abs != 0:
logger.info(f"Applying fee on amount for {trade} " return self.apply_fee_conditional(trade, trade_base_currency,
f"(from {order_amount} to {real_amount}) from Trades") amount=amount, fee_abs=fee_abs)
return real_amount else:
return amount

View File

@ -20,6 +20,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.optimize.optimize_reports import (show_backtest_results, from freqtrade.optimize.optimize_reports import (show_backtest_results,
store_backtest_result) store_backtest_result)
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -63,10 +64,19 @@ class Backtesting:
self.strategylist: List[IStrategy] = [] self.strategylist: List[IStrategy] = []
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
self.pairlists = PairListManager(self.exchange, self.config)
if 'VolumePairList' in self.pairlists.name_list:
raise OperationalException("VolumePairList not allowed for backtesting.")
self.pairlists.refresh_pairlist()
if len(self.pairlists.whitelist) == 0:
raise OperationalException("No pair in whitelist.")
if config.get('fee'): if config.get('fee'):
self.fee = config['fee'] self.fee = config['fee']
else: else:
self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
if self.config.get('runmode') != RunMode.HYPEROPT: if self.config.get('runmode') != RunMode.HYPEROPT:
self.dataprovider = DataProvider(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange)
@ -111,7 +121,7 @@ class Backtesting:
data = history.load_data( data = history.load_data(
datadir=self.config['datadir'], datadir=self.config['datadir'],
pairs=self.config['exchange']['pair_whitelist'], pairs=self.pairlists.whitelist,
timeframe=self.timeframe, timeframe=self.timeframe,
timerange=timerange, timerange=timerange,
startup_candles=self.required_startup, startup_candles=self.required_startup,

View File

@ -49,9 +49,9 @@ logger = logging.getLogger(__name__)
INITIAL_POINTS = 30 INITIAL_POINTS = 30
# Keep no more than 2*SKOPT_MODELS_MAX_NUM models # Keep no more than SKOPT_MODEL_QUEUE_SIZE models
# in the skopt models list # in the skopt model queue, to optimize memory consumption
SKOPT_MODELS_MAX_NUM = 10 SKOPT_MODEL_QUEUE_SIZE = 10
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
@ -75,7 +75,7 @@ class Hyperopt:
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
self.trials_file = (self.config['user_data_dir'] / self.results_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_results.pickle') 'hyperopt_results' / 'hyperopt_results.pickle')
self.data_pickle_file = (self.config['user_data_dir'] / self.data_pickle_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl') 'hyperopt_results' / 'hyperopt_tickerdata.pkl')
@ -88,10 +88,10 @@ class Hyperopt:
else: else:
logger.info("Continuing on previous hyperopt results.") logger.info("Continuing on previous hyperopt results.")
self.num_trials_saved = 0 self.num_epochs_saved = 0
# Previous evaluations # Previous evaluations
self.trials: List = [] self.epochs: List = []
# Populate functions here (hasattr is slow so should not be run during "regular" operations) # Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_indicators'): if hasattr(self.custom_hyperopt, 'populate_indicators'):
@ -132,7 +132,7 @@ class Hyperopt:
""" """
Remove hyperopt pickle files to restart hyperopt. Remove hyperopt pickle files to restart hyperopt.
""" """
for f in [self.data_pickle_file, self.trials_file]: for f in [self.data_pickle_file, self.results_file]:
p = Path(f) p = Path(f)
if p.is_file(): if p.is_file():
logger.info(f"Removing `{p}`.") logger.info(f"Removing `{p}`.")
@ -151,27 +151,26 @@ class Hyperopt:
# and the values are taken from the list of parameters. # and the values are taken from the list of parameters.
return {d.name: v for d, v in zip(dimensions, raw_params)} return {d.name: v for d, v in zip(dimensions, raw_params)}
def save_trials(self, final: bool = False) -> None: def _save_results(self) -> None:
""" """
Save hyperopt trials to file Save hyperopt results to file
""" """
num_trials = len(self.trials) num_epochs = len(self.epochs)
if num_trials > self.num_trials_saved: if num_epochs > self.num_epochs_saved:
logger.debug(f"Saving {num_trials} {plural(num_trials, 'epoch')}.") logger.debug(f"Saving {num_epochs} {plural(num_epochs, 'epoch')}.")
dump(self.trials, self.trials_file) dump(self.epochs, self.results_file)
self.num_trials_saved = num_trials self.num_epochs_saved = num_epochs
if final: logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
logger.info(f"{num_trials} {plural(num_trials, 'epoch')} " f"saved to '{self.results_file}'.")
f"saved to '{self.trials_file}'.")
@staticmethod @staticmethod
def _read_trials(trials_file: Path) -> List: def _read_results(results_file: Path) -> List:
""" """
Read hyperopt trials file Read hyperopt results from file
""" """
logger.info("Reading Trials from '%s'", trials_file) logger.info("Reading epochs from '%s'", results_file)
trials = load(trials_file) data = load(results_file)
return trials return data
def _get_params_details(self, params: Dict) -> Dict: def _get_params_details(self, params: Dict) -> Dict:
""" """
@ -376,24 +375,31 @@ class Hyperopt:
# Verification for overwrite # Verification for overwrite
if path.isfile(csv_file): if path.isfile(csv_file):
logger.error("CSV-File already exists!") logger.error(f"CSV file already exists: {csv_file}")
return return
try: try:
io.open(csv_file, 'w+').close() io.open(csv_file, 'w+').close()
except IOError: except IOError:
logger.error("Filed to create CSV-File!") logger.error(f"Failed to create CSV file: {csv_file}")
return return
trials = json_normalize(results, max_level=1) trials = json_normalize(results, max_level=1)
trials['Best'] = '' trials['Best'] = ''
trials['Stake currency'] = config['stake_currency'] trials['Stake currency'] = config['stake_currency']
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.total_profit', 'results_metrics.avg_profit', 'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration', 'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']] 'loss', 'is_initial_point', 'is_best']
trials.columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency', param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency',
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
param_columns = list(results[0]['params_dict'].keys())
trials.columns = base_columns + param_columns
trials['is_profit'] = False trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*' trials.loc[trials['is_initial_point'], 'Best'] = '*'
trials.loc[trials['is_best'], 'Best'] = 'Best' trials.loc[trials['is_best'], 'Best'] = 'Best'
@ -420,7 +426,7 @@ class Hyperopt:
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8') trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8')
print("CSV-File created!") logger.info(f"CSV file created: {csv_file}")
def has_space(self, space: str) -> bool: def has_space(self, space: str) -> bool:
""" """
@ -564,43 +570,28 @@ class Hyperopt:
n_initial_points=INITIAL_POINTS, n_initial_points=INITIAL_POINTS,
acq_optimizer_kwargs={'n_jobs': cpu_count}, acq_optimizer_kwargs={'n_jobs': cpu_count},
random_state=self.random_state, random_state=self.random_state,
model_queue_size=SKOPT_MODEL_QUEUE_SIZE,
) )
def fix_optimizer_models_list(self) -> None:
"""
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
This may cease working when skopt updates if implementation of this intrinsic
part changes.
"""
n = len(self.opt.models) - SKOPT_MODELS_MAX_NUM
# Keep no more than 2*SKOPT_MODELS_MAX_NUM models in the skopt models list,
# remove the old ones. These are actually of no use, the current model
# from the estimator is the only one used in the skopt optimizer.
# Freqtrade code also does not inspect details of the models.
if n >= SKOPT_MODELS_MAX_NUM:
logger.debug(f"Fixing skopt models list, removing {n} old items...")
del self.opt.models[0:n]
def run_optimizer_parallel(self, parallel, asked, i) -> List: def run_optimizer_parallel(self, parallel, asked, i) -> List:
return parallel(delayed( return parallel(delayed(
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked) wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
@staticmethod @staticmethod
def load_previous_results(trials_file: Path) -> List: def load_previous_results(results_file: Path) -> List:
""" """
Load data for epochs from the file if we have one Load data for epochs from the file if we have one
""" """
trials: List = [] epochs: List = []
if trials_file.is_file() and trials_file.stat().st_size > 0: if results_file.is_file() and results_file.stat().st_size > 0:
trials = Hyperopt._read_trials(trials_file) epochs = Hyperopt._read_results(results_file)
if trials[0].get('is_best') is None: # Detection of some old format, without 'is_best' field saved
if epochs[0].get('is_best') is None:
raise OperationalException( raise OperationalException(
"The file with Hyperopt results is incompatible with this version " "The file with Hyperopt results is incompatible with this version "
"of Freqtrade and cannot be loaded.") "of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(trials)} previous evaluations from disk.") logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
return trials return epochs
def _set_random_state(self, random_state: Optional[int]) -> int: def _set_random_state(self, random_state: Optional[int]) -> int:
return random_state or random.randint(1, 2**16 - 1) return random_state or random.randint(1, 2**16 - 1)
@ -626,8 +617,9 @@ class Hyperopt:
# We don't need exchange instance anymore while running hyperopt # We don't need exchange instance anymore while running hyperopt
self.backtesting.exchange = None # type: ignore self.backtesting.exchange = None # type: ignore
self.backtesting.pairlists = None # type: ignore
self.trials = self.load_previous_results(self.trials_file) self.epochs = self.load_previous_results(self.results_file)
cpus = cpu_count() cpus = cpu_count()
logger.info(f"Found {cpus} CPU cores. Let's make them scream!") logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
@ -663,7 +655,7 @@ class Hyperopt:
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
] ]
with progressbar.ProgressBar( with progressbar.ProgressBar(
maxval=self.total_epochs, redirect_stdout=False, redirect_stderr=False, max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
widgets=widgets widgets=widgets
) as pbar: ) as pbar:
EVALS = ceil(self.total_epochs / jobs) EVALS = ceil(self.total_epochs / jobs)
@ -676,7 +668,6 @@ class Hyperopt:
asked = self.opt.ask(n_points=current_jobs) asked = self.opt.ask(n_points=current_jobs)
f_val = self.run_optimizer_parallel(parallel, asked, i) f_val = self.run_optimizer_parallel(parallel, asked, i)
self.opt.tell(asked, [v['loss'] for v in f_val]) self.opt.tell(asked, [v['loss'] for v in f_val])
self.fix_optimizer_models_list()
# Calculate progressbar outputs # Calculate progressbar outputs
for j, val in enumerate(f_val): for j, val in enumerate(f_val):
@ -697,23 +688,25 @@ class Hyperopt:
if is_best: if is_best:
self.current_best_loss = val['loss'] self.current_best_loss = val['loss']
self.trials.append(val) self.epochs.append(val)
# Save results after each best epoch and every 100 epochs # Save results after each best epoch and every 100 epochs
if is_best or current % 100 == 0: if is_best or current % 100 == 0:
self.save_trials() self._save_results()
pbar.update(current) pbar.update(current)
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print('User interrupted..')
self.save_trials(final=True) self._save_results()
logger.info(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} "
f"saved to '{self.results_file}'.")
if self.trials: if self.epochs:
sorted_trials = sorted(self.trials, key=itemgetter('loss')) sorted_epochs = sorted(self.epochs, key=itemgetter('loss'))
results = sorted_trials[0] best_epoch = sorted_epochs[0]
self.print_epoch_details(results, self.total_epochs, self.print_json) self.print_epoch_details(best_epoch, self.total_epochs, self.print_json)
else: else:
# This is printed when Ctrl+C is pressed quickly, before first epochs have # This is printed when Ctrl+C is pressed quickly, before first epochs have
# a chance to be evaluated. # a chance to be evaluated.

View File

@ -1,7 +1,7 @@
import logging import logging
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Any, Dict, List
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
@ -34,118 +34,173 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame
file_dump_json(filename, records) file_dump_json(filename, records)
def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, def _get_line_floatfmt() -> List[str]:
results: DataFrame, skip_nan: bool = False) -> str:
""" """
Generates and returns a text table for the given backtest data and the results dataframe Generate floatformat (goes in line with _generate_result_line())
"""
return ['s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd']
def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
"""
Generate header lines (goes in line with _generate_result_line())
"""
return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %',
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
'Wins', 'Draws', 'Losses']
def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict:
"""
Generate one result dict, with "first_column" as key.
"""
return {
'key': first_column,
'trades': len(result.index),
'profit_mean': result.profit_percent.mean(),
'profit_mean_pct': result.profit_percent.mean() * 100.0,
'profit_sum': result.profit_percent.sum(),
'profit_sum_pct': result.profit_percent.sum() * 100.0,
'profit_total_abs': result.profit_abs.sum(),
'profit_total_pct': result.profit_percent.sum() * 100.0 / max_open_trades,
'duration_avg': str(timedelta(
minutes=round(result.trade_duration.mean()))
) if not result.empty else '0:00',
# 'duration_max': str(timedelta(
# minutes=round(result.trade_duration.max()))
# ) if not result.empty else '0:00',
# 'duration_min': str(timedelta(
# minutes=round(result.trade_duration.min()))
# ) if not result.empty else '0:00',
'wins': len(result[result.profit_abs > 0]),
'draws': len(result[result.profit_abs == 0]),
'losses': len(result[result.profit_abs < 0]),
}
def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int,
results: DataFrame, skip_nan: bool = False) -> List[Dict]:
"""
Generates and returns a list for the given backtest data and the results dataframe
:param data: Dict of <pair: dataframe> containing data that was used during backtesting. :param data: Dict of <pair: dataframe> containing data that was used during backtesting.
:param stake_currency: stake-currency - used to correctly name headers :param stake_currency: stake-currency - used to correctly name headers
:param max_open_trades: Maximum allowed open trades :param max_open_trades: Maximum allowed open trades
:param results: Dataframe containing the backtest results :param results: Dataframe containing the backtest results
:param skip_nan: Print "left open" open trades :param skip_nan: Print "left open" open trades
:return: pretty printed table with tabulate as string :return: List of Dicts containing the metrics per pair
""" """
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
tabular_data = [] tabular_data = []
headers = [
'Pair',
'Buys',
'Avg Profit %',
'Cum Profit %',
f'Tot Profit {stake_currency}',
'Tot Profit %',
'Avg Duration',
'Wins',
'Draws',
'Losses'
]
for pair in data: for pair in data:
result = results[results.pair == pair] result = results[results.pair == pair]
if skip_nan and result.profit_abs.isnull().all(): if skip_nan and result.profit_abs.isnull().all():
continue continue
tabular_data.append([ tabular_data.append(_generate_result_line(result, max_open_trades, pair))
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_percent.sum() * 100.0,
result.profit_abs.sum(),
result.profit_percent.sum() * 100.0 / max_open_trades,
str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]),
len(result[result.profit_abs == 0]),
len(result[result.profit_abs < 0])
])
# Append Total # Append Total
tabular_data.append([ tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL'))
'TOTAL', return tabular_data
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0, def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
results.profit_abs.sum(), """
results.profit_percent.sum() * 100.0 / max_open_trades, Generates and returns a text table for the given backtest data and the results dataframe
str(timedelta( :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', :param stake_currency: stake-currency - used to correctly name headers
len(results[results.profit_abs > 0]), :return: pretty printed table with tabulate as string
len(results[results.profit_abs == 0]), """
len(results[results.profit_abs < 0])
]) headers = _get_line_header('Pair', stake_currency)
floatfmt = _get_line_floatfmt()
output = [[
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
] for t in pair_results]
# Ignore type as floatfmt does allow tuples but mypy does not know that # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, return tabulate(output, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_text_table_sell_reason(stake_currency: str, max_open_trades: int, def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
results: DataFrame) -> str:
""" """
Generate small table outlining Backtest results Generate small table outlining Backtest results
:param stake_currency: Stakecurrency used
:param max_open_trades: Max_open_trades parameter :param max_open_trades: Max_open_trades parameter
:param results: Dataframe containing the backtest results :param results: Dataframe containing the backtest result for one strategy
:return: pretty printed table with tabulate as string :return: List of Dicts containing the metrics per Sell reason
""" """
tabular_data = [] tabular_data = []
headers = [
"Sell Reason",
"Sells",
"Wins",
"Draws",
"Losses",
"Avg Profit %",
"Cum Profit %",
f"Tot Profit {stake_currency}",
"Tot Profit %",
]
for reason, count in results['sell_reason'].value_counts().iteritems(): for reason, count in results['sell_reason'].value_counts().iteritems():
result = results.loc[results['sell_reason'] == reason] result = results.loc[results['sell_reason'] == reason]
wins = len(result[result['profit_abs'] > 0])
draws = len(result[result['profit_abs'] == 0]) profit_mean = result['profit_percent'].mean()
loss = len(result[result['profit_abs'] < 0]) profit_sum = result["profit_percent"].sum()
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
profit_sum = round(result["profit_percent"].sum() * 100.0, 2)
profit_tot = result['profit_abs'].sum()
profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2) profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2)
tabular_data.append( tabular_data.append(
[ {
reason.value, 'sell_reason': reason.value,
count, 'trades': count,
wins, 'wins': len(result[result['profit_abs'] > 0]),
draws, 'draws': len(result[result['profit_abs'] == 0]),
loss, 'losses': len(result[result['profit_abs'] < 0]),
profit_mean, 'profit_mean': profit_mean,
profit_sum, 'profit_mean_pct': round(profit_mean * 100, 2),
profit_tot, 'profit_sum': profit_sum,
profit_percent_tot, 'profit_sum_pct': round(profit_sum * 100, 2),
] 'profit_total_abs': result['profit_abs'].sum(),
'profit_pct_total': profit_percent_tot,
}
) )
return tabulate(tabular_data, headers=headers, tablefmt="orgtbl", stralign="right") return tabular_data
def generate_text_table_strategy(stake_currency: str, max_open_trades: str, def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]],
all_results: Dict) -> str: stake_currency: str) -> str:
"""
Generate small table outlining Backtest results
:param sell_reason_stats: Sell reason metrics
:param stake_currency: Stakecurrency used
:return: pretty printed table with tabulate as string
"""
headers = [
'Sell Reason',
'Sells',
'Wins',
'Draws',
'Losses',
'Avg Profit %',
'Cum Profit %',
f'Tot Profit {stake_currency}',
'Tot Profit %',
]
output = [[
t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'],
t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'],
] for t in sell_reason_stats]
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
all_results: Dict) -> List[Dict]:
"""
Generate summary per strategy
:param stake_currency: stake-currency - used to correctly name headers
:param max_open_trades: Maximum allowed open trades used for backtest
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
:return: List of Dicts containing the metrics per Strategy
"""
tabular_data = []
for strategy, results in all_results.items():
tabular_data.append(_generate_result_line(results, max_open_trades, strategy))
return tabular_data
def generate_text_table_strategy(strategy_results, stake_currency: str) -> str:
""" """
Generate summary table per strategy Generate summary table per strategy
:param stake_currency: stake-currency - used to correctly name headers :param stake_currency: stake-currency - used to correctly name headers
@ -153,34 +208,21 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies :param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
:return: pretty printed table with tabulate as string :return: pretty printed table with tabulate as string
""" """
floatfmt = _get_line_floatfmt()
headers = _get_line_header('Strategy', stake_currency)
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') output = [[
tabular_data = [] t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
headers = ['Strategy', 'Buys', 'Avg Profit %', 'Cum Profit %', t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', ] for t in strategy_results]
'Wins', 'Draws', 'Losses']
for strategy, results in all_results.items():
tabular_data.append([
strategy,
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0,
results.profit_abs.sum(),
results.profit_percent.sum() * 100.0 / max_open_trades,
str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]),
len(results[results.profit_abs == 0]),
len(results[results.profit_abs < 0])
])
# Ignore type as floatfmt does allow tuples but mypy does not know that # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, return tabulate(output, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_edge_table(results: dict) -> str: def generate_edge_table(results: dict) -> str:
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
tabular_data = [] tabular_data = []
headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio', headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
'Required Risk Reward', 'Expectancy', 'Total Number of Trades', 'Required Risk Reward', 'Expectancy', 'Total Number of Trades',
@ -206,38 +248,48 @@ def generate_edge_table(results: dict) -> str:
def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
all_results: Dict[str, DataFrame]): all_results: Dict[str, DataFrame]):
for strategy, results in all_results.items(): stake_currency = config['stake_currency']
max_open_trades = config['max_open_trades']
print(f"Result for strategy {strategy}") for strategy, results in all_results.items():
table = generate_text_table(btdata, stake_currency=config['stake_currency'], pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
max_open_trades=config['max_open_trades'], max_open_trades=max_open_trades,
results=results, skip_nan=False)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
results=results) results=results)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
max_open_trades=max_open_trades,
results=results.loc[results['open_at_end']],
skip_nan=True)
# Print results
print(f"Result for strategy {strategy}")
table = generate_text_table(pair_results, stake_currency=stake_currency)
if isinstance(table, str): if isinstance(table, str):
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
table = generate_text_table_sell_reason(stake_currency=config['stake_currency'], table = generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats,
max_open_trades=config['max_open_trades'], stake_currency=stake_currency,
results=results) )
if isinstance(table, str): if isinstance(table, str):
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
table = generate_text_table(btdata, table = generate_text_table(left_open_results, stake_currency=stake_currency)
stake_currency=config['stake_currency'],
max_open_trades=config['max_open_trades'],
results=results.loc[results.open_at_end], skip_nan=True)
if isinstance(table, str): if isinstance(table, str):
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
if isinstance(table, str): if isinstance(table, str):
print('=' * len(table.splitlines()[0])) print('=' * len(table.splitlines()[0]))
print() print()
if len(all_results) > 1: if len(all_results) > 1:
# Print Strategy summary table # Print Strategy summary table
table = generate_text_table_strategy(config['stake_currency'], strategy_results = generate_strategy_metrics(stake_currency=stake_currency,
config['max_open_trades'], max_open_trades=max_open_trades,
all_results=all_results) all_results=all_results)
table = generate_text_table_strategy(strategy_results, stake_currency)
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
print('=' * len(table.splitlines()[0])) print('=' * len(table.splitlines()[0]))

View File

@ -1,8 +1,5 @@
""" """
Static List provider PairList Handler base class
Provides lists as configured in config.json
""" """
import logging import logging
from abc import ABC, abstractmethod, abstractproperty from abc import ABC, abstractmethod, abstractproperty
@ -13,6 +10,7 @@ from cachetools import TTLCache, cached
from freqtrade.exchange import market_is_active from freqtrade.exchange import market_is_active
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,11 +21,13 @@ class IPairList(ABC):
pairlist_pos: int) -> None: pairlist_pos: int) -> None:
""" """
:param exchange: Exchange instance :param exchange: Exchange instance
:param pairlistmanager: Instanciating Pairlist manager :param pairlistmanager: Instantiated Pairlist manager
:param config: Global bot configuration :param config: Global bot configuration
:param pairlistconfig: Configuration for this pairlist - can be empty. :param pairlistconfig: Configuration for this Pairlist Handler - can be empty.
:param pairlist_pos: Position of the filter in the pairlist-filter-list :param pairlist_pos: Position of the Pairlist Handler in the chain
""" """
self._enabled = True
self._exchange = exchange self._exchange = exchange
self._pairlistmanager = pairlistmanager self._pairlistmanager = pairlistmanager
self._config = config self._config = config
@ -78,49 +78,50 @@ class IPairList(ABC):
-> Please overwrite in subclasses -> Please overwrite in subclasses
""" """
@abstractmethod def _validate_pair(self, ticker) -> bool:
"""
Check one pair against Pairlist Handler's specific conditions.
Either implement it in the Pairlist Handler or override the generic
filter_pairlist() method.
:param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed
"""
raise NotImplementedError()
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Filters and sorts pairlist and returns the whitelist again. Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary Called on each bot iteration - please use internal caching if necessary
-> Please overwrite in subclasses This generic implementation calls self._validate_pair() for each pair
in the pairlist.
Some Pairlist Handlers override this generic implementation and employ
own filtration.
:param pairlist: pairlist to filter or sort :param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached. :param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist :return: new whitelist
""" """
if self._enabled:
# Copy list since we're modifying this list
for p in deepcopy(pairlist):
# Filter out assets
if not self._validate_pair(tickers[p]):
pairlist.remove(p)
@staticmethod
def verify_blacklist(pairlist: List[str], blacklist: List[str],
aswarning: bool) -> List[str]:
"""
Verify and remove items from pairlist - returning a filtered pairlist.
Logs a warning or info depending on `aswarning`.
Pairlists explicitly using this method shall use `aswarning=False`!
:param pairlist: Pairlist to validate
:param blacklist: Blacklist to validate pairlist against
:param aswarning: Log message as Warning or info
:return: pairlist - blacklisted pairs
"""
for pair in deepcopy(pairlist):
if pair in blacklist:
if aswarning:
logger.warning(f"Pair {pair} in your blacklist. Removing it from whitelist...")
else:
logger.info(f"Pair {pair} in your blacklist. Removing it from whitelist...")
pairlist.remove(pair)
return pairlist return pairlist
def _verify_blacklist(self, pairlist: List[str], aswarning: bool = True) -> List[str]: def verify_blacklist(self, pairlist: List[str], logmethod) -> List[str]:
""" """
Proxy method to verify_blacklist for easy access for child classes. Proxy method to verify_blacklist for easy access for child classes.
Logs a warning or info depending on `aswarning`.
Pairlists explicitly using this method shall use aswarning=False!
:param pairlist: Pairlist to validate :param pairlist: Pairlist to validate
:param aswarning: Log message as Warning or info. :param logmethod: Function that'll be called, `logger.info` or `logger.warning`.
:return: pairlist - blacklisted pairs :return: pairlist - blacklisted pairs
""" """
return IPairList.verify_blacklist(pairlist, self._pairlistmanager.blacklist, return self._pairlistmanager.verify_blacklist(pairlist, logmethod)
aswarning=aswarning)
def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]: def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]:
""" """

View File

@ -1,14 +1,28 @@
"""
Precision pair list filter
"""
import logging import logging
from copy import deepcopy from typing import Any, Dict
from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PrecisionFilter(IPairList): class PrecisionFilter(IPairList):
def __init__(self, exchange, pairlistmanager,
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._stoploss = self._config['stoploss']
self._enabled = self._stoploss != 0
# Precalculate sanitized stoploss value to avoid recalculation for every pair
self._stoploss = 1 - abs(self._stoploss)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
""" """
@ -24,41 +38,25 @@ class PrecisionFilter(IPairList):
""" """
return f"{self.name} - Filtering untradable pairs." return f"{self.name} - Filtering untradable pairs."
def _validate_precision_filter(self, ticker: dict, stoploss: float) -> bool: def _validate_pair(self, ticker: dict) -> bool:
""" """
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
low value pairs. low value pairs.
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:param stoploss: stoploss value as set in the configuration :return: True if the pair can stay, False if it should be removed
(already cleaned to be 1 - stoploss)
:return: True if the pair can stay, false if it should be removed
""" """
stop_price = ticker['ask'] * stoploss stop_price = ticker['ask'] * self._stoploss
# Adjust stop-prices to precision # Adjust stop-prices to precision
sp = self._exchange.price_to_precision(ticker["symbol"], stop_price) sp = self._exchange.price_to_precision(ticker["symbol"], stop_price)
stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99) stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99)
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
if sp <= stop_gap_price: if sp <= stop_gap_price:
self.log_on_refresh(logger.info, self.log_on_refresh(logger.info,
f"Removed {ticker['symbol']} from whitelist, " f"Removed {ticker['symbol']} from whitelist, "
f"because stop price {sp} would be <= stop limit {stop_gap_price}") f"because stop price {sp} would be <= stop limit {stop_gap_price}")
return False return False
return True return True
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlists and assigns and returns them again.
"""
stoploss = self._config.get('stoploss')
if stoploss is not None:
# Precalculate sanitized stoploss value to avoid recalculation for every pair
stoploss = 1 - abs(stoploss)
# Copy list since we're modifying this list
for p in deepcopy(pairlist):
ticker = tickers.get(p)
# Filter out assets which would not allow setting a stoploss
if not ticker or (stoploss and not self._validate_precision_filter(ticker, stoploss)):
pairlist.remove(p)
continue
return pairlist

View File

@ -1,9 +1,12 @@
"""
Price pair list filter
"""
import logging import logging
from copy import deepcopy from typing import Any, Dict
from typing import Any, Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,6 +18,7 @@ class PriceFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0)
self._enabled = self._low_price_ratio != 0
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@ -31,43 +35,21 @@ class PriceFilter(IPairList):
""" """
return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%." return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%."
def _validate_ticker_lowprice(self, ticker) -> bool: def _validate_pair(self, ticker) -> bool:
""" """
Check if if one price-step (pip) is > than a certain barrier. Check if if one price-step (pip) is > than a certain barrier.
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if ticker['last'] is None: if ticker['last'] is None:
self.log_on_refresh(logger.info, self.log_on_refresh(logger.info,
f"Removed {ticker['symbol']} from whitelist, because " f"Removed {ticker['symbol']} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).") "ticker['last'] is empty (Usually no trade in the last 24h).")
return False return False
compare = ticker['last'] + self._exchange.price_get_one_pip(ticker['symbol'], compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
ticker['last']) changeperc = compare / ticker['last']
changeperc = (compare - ticker['last']) / ticker['last']
if changeperc > self._low_price_ratio: if changeperc > self._low_price_ratio:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%") f"because 1 unit is {changeperc * 100:.3f}%")
return False return False
return True return True
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
"""
# Copy list since we're modifying this list
for p in deepcopy(pairlist):
ticker = tickers.get(p)
if not ticker:
pairlist.remove(p)
# Filter out assets which would not allow setting a stoploss
if self._low_price_ratio and not self._validate_ticker_lowprice(ticker):
pairlist.remove(p)
return pairlist

View File

@ -0,0 +1,51 @@
"""
Shuffle pair list filter
"""
import logging
import random
from typing import Any, Dict, List
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)
class ShuffleFilter(IPairList):
def __init__(self, exchange, pairlistmanager,
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._seed = pairlistconfig.get('seed')
self._random = random.Random(self._seed)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requries tickers, an empty List is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return (f"{self.name} - Shuffling pairs" +
(f", seed = {self._seed}." if self._seed is not None else "."))
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
"""
# Shuffle is done inplace
self._random.shuffle(pairlist)
return pairlist

View File

@ -1,19 +1,24 @@
"""
Spread pair list filter
"""
import logging import logging
from copy import deepcopy from typing import Any, Dict
from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SpreadFilter(IPairList): class SpreadFilter(IPairList):
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict, def __init__(self, exchange, pairlistmanager,
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None: pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005) self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
self._enabled = self._max_spread_ratio != 0
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@ -31,29 +36,19 @@ class SpreadFilter(IPairList):
return (f"{self.name} - Filtering pairs with ask/bid diff above " return (f"{self.name} - Filtering pairs with ask/bid diff above "
f"{self._max_spread_ratio * 100}%.") f"{self._max_spread_ratio * 100}%.")
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: def _validate_pair(self, ticker: dict) -> bool:
""" """
Filters and sorts pairlist and returns the whitelist again. Validate spread for the ticker
Called on each bot iteration - please use internal caching if necessary :param ticker: ticker dict as returned from ccxt.load_markets()
:param pairlist: pairlist to filter or sort :return: True if the pair can stay, False if it should be removed
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
""" """
# Copy list since we're modifying this list
spread = None
for p in deepcopy(pairlist):
ticker = tickers.get(p)
assert ticker is not None
if 'bid' in ticker and 'ask' in ticker: if 'bid' in ticker and 'ask' in ticker:
spread = 1 - ticker['bid'] / ticker['ask'] spread = 1 - ticker['bid'] / ticker['ask']
if not ticker or spread > self._max_spread_ratio: if spread > self._max_spread_ratio:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because spread {spread * 100:.3f}% >" f"because spread {spread * 100:.3f}% >"
f"{self._max_spread_ratio * 100}%") f"{self._max_spread_ratio * 100}%")
pairlist.remove(p) return False
else: else:
pairlist.remove(p) return True
return False
return pairlist

View File

@ -1,14 +1,14 @@
""" """
Static List provider Static Pair List provider
Provides lists as configured in config.json
Provides pair white list as it configured in config
""" """
import logging import logging
from typing import Dict, List from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,8 +1,7 @@
""" """
Volume PairList provider Volume PairList provider
Provides lists as configured in config.json Provides dynamic pair list based on trade volumes
""" """
import logging import logging
from datetime import datetime from datetime import datetime
@ -11,21 +10,26 @@ from typing import Any, Dict, List
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
class VolumePairList(IPairList): class VolumePairList(IPairList):
def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: dict, def __init__(self, exchange, pairlistmanager,
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None: pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._pairlistconfig: if 'number_assets' not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
f'`number_assets` not specified. Please check your configuration ' '`number_assets` not specified. Please check your configuration '
'for "pairlist.config.number_assets"') 'for "pairlist.config.number_assets"')
self._stake_currency = config['stake_currency']
self._number_pairs = self._pairlistconfig['number_assets'] self._number_pairs = self._pairlistconfig['number_assets']
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume') self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
self._min_value = self._pairlistconfig.get('min_value', 0) self._min_value = self._pairlistconfig.get('min_value', 0)
@ -34,12 +38,18 @@ class VolumePairList(IPairList):
if not self._exchange.exchange_has('fetchTickers'): if not self._exchange.exchange_has('fetchTickers'):
raise OperationalException( raise OperationalException(
'Exchange does not support dynamic whitelist. ' 'Exchange does not support dynamic whitelist. '
'Please edit your config and restart the bot' 'Please edit your config and restart the bot.'
) )
if not self._validate_keys(self._sort_key): if not self._validate_keys(self._sort_key):
raise OperationalException( raise OperationalException(
f'key {self._sort_key} not in {SORT_VALUES}') f'key {self._sort_key} not in {SORT_VALUES}')
if self._sort_key != 'quoteVolume':
logger.warning(
"DEPRECATED: using any key other than quoteVolume for VolumePairList is deprecated."
)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
""" """
@ -72,42 +82,42 @@ class VolumePairList(IPairList):
(self._last_refresh + self.refresh_period < datetime.now().timestamp())): (self._last_refresh + self.refresh_period < datetime.now().timestamp())):
self._last_refresh = int(datetime.now().timestamp()) self._last_refresh = int(datetime.now().timestamp())
pairs = self._gen_pair_whitelist(pairlist, tickers, pairs = self._gen_pair_whitelist(pairlist, tickers)
self._config['stake_currency'],
self._sort_key, self._min_value)
else: else:
pairs = pairlist pairs = pairlist
self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}")
return pairs return pairs
def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict, def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict) -> List[str]:
base_currency: str, key: str, min_val: int) -> List[str]:
""" """
Updates the whitelist with with a dynamically generated list Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str :param pairlist: pairlist to filter or sort
:param key: sort key (defaults to 'quoteVolume')
:param tickers: Tickers (from exchange.get_tickers()). :param tickers: Tickers (from exchange.get_tickers()).
:return: List of pairs :return: List of pairs
""" """
if self._pairlist_pos == 0: if self._pairlist_pos == 0:
# If VolumePairList is the first in the list, use fresh pairlist # If VolumePairList is the first in the list, use fresh pairlist
# Check if pair quote currency equals to the stake currency. # Check if pair quote currency equals to the stake currency.
filtered_tickers = [v for k, v in tickers.items() filtered_tickers = [
if (self._exchange.get_pair_quote_currency(k) == base_currency v for k, v in tickers.items()
and v[key] is not None)] if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
and v[self._sort_key] is not None)]
else: else:
# If other pairlist is in front, use the incomming pairlist. # If other pairlist is in front, use the incoming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist] filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
if min_val > 0: if self._min_value > 0:
filtered_tickers = list(filter(lambda t: t[key] > min_val, filtered_tickers)) filtered_tickers = [
v for v in filtered_tickers if v[self._sort_key] > self._min_value]
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key]) sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key])
# Validate whitelist to only have active market pairs # Validate whitelist to only have active market pairs
pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers]) pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
pairs = self._verify_blacklist(pairs, aswarning=False) pairs = self.verify_blacklist(pairs, logger.info)
# Limit to X number of pairs # Limit pairlist to the requested number of pairs
pairs = pairs[:self._number_pairs] pairs = pairs[:self._number_pairs]
return pairs return pairs

View File

@ -1,10 +1,8 @@
""" """
Static List provider PairList manager class
Provides lists as configured in config.json
""" """
import logging import logging
from copy import deepcopy
from typing import Dict, List from typing import Dict, List
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
@ -12,6 +10,8 @@ from cachetools import TTLCache, cached
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import PairListResolver from freqtrade.resolvers import PairListResolver
from freqtrade.constants import ListPairsWithTimeframes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,24 +23,25 @@ class PairListManager():
self._config = config self._config = config
self._whitelist = self._config['exchange'].get('pair_whitelist') self._whitelist = self._config['exchange'].get('pair_whitelist')
self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._blacklist = self._config['exchange'].get('pair_blacklist', [])
self._pairlists: List[IPairList] = [] self._pairlist_handlers: List[IPairList] = []
self._tickers_needed = False self._tickers_needed = False
for pl in self._config.get('pairlists', None): for pairlist_handler_config in self._config.get('pairlists', None):
if 'method' not in pl: if 'method' not in pairlist_handler_config:
logger.warning(f"No method in {pl}") logger.warning(f"No method found in {pairlist_handler_config}, ignoring.")
continue continue
pairl = PairListResolver.load_pairlist(pl.get('method'), pairlist_handler = PairListResolver.load_pairlist(
pairlist_handler_config['method'],
exchange=exchange, exchange=exchange,
pairlistmanager=self, pairlistmanager=self,
config=config, config=config,
pairlistconfig=pl, pairlistconfig=pairlist_handler_config,
pairlist_pos=len(self._pairlists) pairlist_pos=len(self._pairlist_handlers)
) )
self._tickers_needed = pairl.needstickers or self._tickers_needed self._tickers_needed |= pairlist_handler.needstickers
self._pairlists.append(pairl) self._pairlist_handlers.append(pairlist_handler)
if not self._pairlists: if not self._pairlist_handlers:
raise OperationalException("No Pairlist defined!") raise OperationalException("No Pairlist Handlers defined")
@property @property
def whitelist(self) -> List[str]: def whitelist(self) -> List[str]:
@ -60,15 +61,15 @@ class PairListManager():
@property @property
def name_list(self) -> List[str]: def name_list(self) -> List[str]:
""" """
Get list of loaded pairlists names Get list of loaded Pairlist Handler names
""" """
return [p.name for p in self._pairlists] return [p.name for p in self._pairlist_handlers]
def short_desc(self) -> List[Dict]: def short_desc(self) -> List[Dict]:
""" """
List of short_desc for each pairlist List of short_desc for each Pairlist Handler
""" """
return [{p.name: p.short_desc()} for p in self._pairlists] return [{p.name: p.short_desc()} for p in self._pairlist_handlers]
@cached(TTLCache(maxsize=1, ttl=1800)) @cached(TTLCache(maxsize=1, ttl=1800))
def _get_cached_tickers(self): def _get_cached_tickers(self):
@ -76,21 +77,57 @@ class PairListManager():
def refresh_pairlist(self) -> None: def refresh_pairlist(self) -> None:
""" """
Run pairlist through all configured pairlists. Run pairlist through all configured Pairlist Handlers.
""" """
# Tickers should be cached to avoid calling the exchange on each call.
pairlist = self._whitelist.copy()
# tickers should be cached to avoid calling the exchange on each call.
tickers: Dict = {} tickers: Dict = {}
if self._tickers_needed: if self._tickers_needed:
tickers = self._get_cached_tickers() tickers = self._get_cached_tickers()
# Process all pairlists in chain # Adjust whitelist if filters are using tickers
for pl in self._pairlists: pairlist = self._prepare_whitelist(self._whitelist.copy(), tickers)
pairlist = pl.filter_pairlist(pairlist, tickers)
# Validation against blacklist happens after the pairlists to ensure blacklist is respected. # Process all Pairlist Handlers in the chain
pairlist = IPairList.verify_blacklist(pairlist, self.blacklist, True) for pairlist_handler in self._pairlist_handlers:
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
# Validation against blacklist happens after the chain of Pairlist Handlers
# to ensure blacklist is respected.
pairlist = self.verify_blacklist(pairlist, logger.warning)
self._whitelist = pairlist self._whitelist = pairlist
def _prepare_whitelist(self, pairlist: List[str], tickers) -> List[str]:
"""
Prepare sanitized pairlist for Pairlist Handlers that use tickers data - remove
pairs that do not have ticker available
"""
if self._tickers_needed:
# Copy list since we're modifying this list
for p in deepcopy(pairlist):
if p not in tickers:
pairlist.remove(p)
return pairlist
def verify_blacklist(self, pairlist: List[str], logmethod) -> List[str]:
"""
Verify and remove items from pairlist - returning a filtered pairlist.
Logs a warning or info depending on `aswarning`.
Pairlist Handlers explicitly using this method shall use
`logmethod=logger.info` to avoid spamming with warning messages
:param pairlist: Pairlist to validate
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`.
:return: pairlist - blacklisted pairs
"""
for pair in deepcopy(pairlist):
if pair in self._blacklist:
logmethod(f"Pair {pair} in your blacklist. Removing it from whitelist...")
pairlist.remove(pair)
return pairlist
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
"""
Create list of pair tuples with (pair, ticker_interval)
"""
return [(pair, timeframe or self._config['ticker_interval']) for pair in pairs]

View File

@ -86,11 +86,15 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}') logger.debug(f'trying {table_back_name}')
# Check for latest column # Check for latest column
if not has_column(cols, 'close_profit_abs'): if not has_column(cols, 'sell_order_status'):
logger.info(f'Running database migration - backup available as {table_back_name}') logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open = get_column_def(cols, 'fee_open', 'fee')
fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
fee_close = get_column_def(cols, 'fee_close', 'fee') fee_close = get_column_def(cols, 'fee_close', 'fee')
fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null')
fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null')
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
stop_loss = get_column_def(cols, 'stop_loss', '0.0') stop_loss = get_column_def(cols, 'stop_loss', '0.0')
@ -109,6 +113,7 @@ def check_migrate(engine) -> None:
close_profit_abs = get_column_def( close_profit_abs = get_column_def(
cols, 'close_profit_abs', cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
# Schema migration necessary # Schema migration necessary
engine.execute(f"alter table trades rename to {table_back_name}") engine.execute(f"alter table trades rename to {table_back_name}")
@ -120,12 +125,14 @@ def check_migrate(engine) -> None:
# Copy data back - following the correct schema # Copy data back - following the correct schema
engine.execute(f"""insert into trades engine.execute(f"""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate, (id, exchange, pair, is_open,
fee_open, fee_open_cost, fee_open_currency,
fee_close, fee_close_cost, fee_open_currency, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id, stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, strategy, max_rate, min_rate, sell_reason, sell_order_status, strategy,
ticker_interval, open_trade_price, close_profit_abs ticker_interval, open_trade_price, close_profit_abs
) )
select id, lower(exchange), select id, lower(exchange),
@ -136,7 +143,9 @@ def check_migrate(engine) -> None:
else pair else pair
end end
pair, pair,
is_open, {fee_open} fee_open, {fee_close} fee_close, is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
{fee_open_currency} fee_open_currency, {fee_close} fee_close,
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
open_rate, {open_rate_requested} open_rate_requested, close_rate, open_rate, {open_rate_requested} open_rate_requested, close_rate,
{close_rate_requested} close_rate_requested, close_profit, {close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id, stake_amount, amount, open_date, close_date, open_order_id,
@ -145,6 +154,7 @@ def check_migrate(engine) -> None:
{initial_stop_loss_pct} initial_stop_loss_pct, {initial_stop_loss_pct} initial_stop_loss_pct,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{sell_order_status} sell_order_status,
{strategy} strategy, {ticker_interval} ticker_interval, {strategy} strategy, {ticker_interval} ticker_interval,
{open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
from {table_back_name} from {table_back_name}
@ -185,7 +195,11 @@ class Trade(_DECL_BASE):
pair = Column(String, nullable=False, index=True) pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0) fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True)
fee_open_currency = Column(String, nullable=True)
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String, nullable=True)
open_rate = Column(Float) open_rate = Column(Float)
open_rate_requested = Column(Float) open_rate_requested = Column(Float)
# open_trade_price - calculated via _calc_open_trade_price # open_trade_price - calculated via _calc_open_trade_price
@ -216,6 +230,7 @@ class Trade(_DECL_BASE):
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate = Column(Float, nullable=True)
sell_reason = Column(String, nullable=True) sell_reason = Column(String, nullable=True)
sell_order_status = Column(String, nullable=True)
strategy = Column(String, nullable=True) strategy = Column(String, nullable=True)
ticker_interval = Column(Integer, nullable=True) ticker_interval = Column(Integer, nullable=True)
@ -235,13 +250,19 @@ class Trade(_DECL_BASE):
'pair': self.pair, 'pair': self.pair,
'is_open': self.is_open, 'is_open': self.is_open,
'fee_open': self.fee_open, 'fee_open': self.fee_open,
'fee_open_cost': self.fee_open_cost,
'fee_open_currency': self.fee_open_currency,
'fee_close': self.fee_close, 'fee_close': self.fee_close,
'fee_close_cost': self.fee_close_cost,
'fee_close_currency': self.fee_close_currency,
'open_date_hum': arrow.get(self.open_date).humanize(), 'open_date_hum': arrow.get(self.open_date).humanize(),
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'open_timestamp': int(self.open_date.timestamp() * 1000),
'close_date_hum': (arrow.get(self.close_date).humanize() 'close_date_hum': (arrow.get(self.close_date).humanize()
if self.close_date else None), if self.close_date else None),
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S") 'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S")
if self.close_date else None), if self.close_date else None),
'close_timestamp': int(self.close_date.timestamp() * 1000) if self.close_date else None,
'open_rate': self.open_rate, 'open_rate': self.open_rate,
'open_rate_requested': self.open_rate_requested, 'open_rate_requested': self.open_rate_requested,
'open_trade_price': self.open_trade_price, 'open_trade_price': self.open_trade_price,
@ -251,6 +272,7 @@ class Trade(_DECL_BASE):
'stake_amount': round(self.stake_amount, 8), 'stake_amount': round(self.stake_amount, 8),
'close_profit': self.close_profit, 'close_profit': self.close_profit,
'sell_reason': self.sell_reason, 'sell_reason': self.sell_reason,
'sell_order_status': self.sell_order_status,
'stop_loss': self.stop_loss, 'stop_loss': self.stop_loss,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
'initial_stop_loss': self.initial_stop_loss, 'initial_stop_loss': self.initial_stop_loss,
@ -354,12 +376,42 @@ class Trade(_DECL_BASE):
self.close_profit_abs = self.calc_profit() self.close_profit_abs = self.calc_profit()
self.close_date = datetime.utcnow() self.close_date = datetime.utcnow()
self.is_open = False self.is_open = False
self.sell_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
logger.info( logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.', 'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
self self
) )
def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
side: str) -> None:
"""
Update Fee parameters. Only acts once per side
"""
if side == 'buy' and self.fee_open_currency is None:
self.fee_open_cost = fee_cost
self.fee_open_currency = fee_currency
if fee_rate is not None:
self.fee_open = fee_rate
# Assume close-fee will fall into the same fee category and take an educated guess
self.fee_close = fee_rate
elif side == 'sell' and self.fee_close_currency is None:
self.fee_close_cost = fee_cost
self.fee_close_currency = fee_currency
if fee_rate is not None:
self.fee_close = fee_rate
def fee_updated(self, side: str) -> bool:
"""
Verify if this side (buy / sell) has already been updated
"""
if side == 'buy':
return self.fee_open_currency is not None
elif side == 'sell':
return self.fee_close_currency is not None
else:
return False
def _calc_open_trade_price(self) -> float: def _calc_open_trade_price(self) -> float:
""" """
Calculate the open_rate including open_fee. Calculate the open_rate including open_fee.

View File

@ -10,8 +10,9 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown,
create_cum_profit, create_cum_profit,
extract_trades_of_period, load_trades) extract_trades_of_period, load_trades)
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.misc import pair_to_filename from freqtrade.misc import pair_to_filename
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
@ -414,9 +415,12 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
for pair in pairs: for pair in pairs:
profit_col = f'cum_profit_{pair}' profit_col = f'cum_profit_{pair}'
df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col, timeframe) try:
df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col,
timeframe)
fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
except ValueError:
pass
return fig return fig
@ -504,6 +508,9 @@ def plot_profit(config: Dict[str, Any]) -> None:
trades = trades[(trades['pair'].isin(plot_elements["pairs"])) trades = trades[(trades['pair'].isin(plot_elements["pairs"]))
& (~trades['close_time'].isnull()) & (~trades['close_time'].isnull())
] ]
if len(trades) == 0:
raise OperationalException("No trades found, cannot generate Profit-plot without "
"trades from either Backtest result or database.")
# Create an average close price of all the pairs that were involved. # Create an average close price of all the pairs that were involved.
# this could be useful to gauge the overall market trend # this could be useful to gauge the overall market trend

View File

@ -2,11 +2,17 @@ import logging
import threading import threading
from datetime import date, datetime from datetime import date, datetime
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Dict, Callable, Any from typing import Any, Callable, Dict
from arrow import Arrow from arrow import Arrow
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
from flask.json import JSONEncoder from flask.json import JSONEncoder
from flask_cors import CORS
from flask_jwt_extended import (JWTManager, create_access_token,
create_refresh_token, get_jwt_identity,
jwt_refresh_token_required,
verify_jwt_in_request_optional)
from werkzeug.security import safe_str_cmp
from werkzeug.serving import make_server from werkzeug.serving import make_server
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
@ -38,9 +44,9 @@ class ArrowJSONEncoder(JSONEncoder):
def require_login(func: Callable[[Any, Any], Any]): def require_login(func: Callable[[Any, Any], Any]):
def func_wrapper(obj, *args, **kwargs): def func_wrapper(obj, *args, **kwargs):
verify_jwt_in_request_optional()
auth = request.authorization auth = request.authorization
if auth and obj.check_auth(auth.username, auth.password): if get_jwt_identity() or auth and obj.check_auth(auth.username, auth.password):
return func(obj, *args, **kwargs) return func(obj, *args, **kwargs)
else: else:
return jsonify({"error": "Unauthorized"}), 401 return jsonify({"error": "Unauthorized"}), 401
@ -70,8 +76,8 @@ class ApiServer(RPC):
""" """
def check_auth(self, username, password): def check_auth(self, username, password):
return (username == self._config['api_server'].get('username') and return (safe_str_cmp(username, self._config['api_server'].get('username')) and
password == self._config['api_server'].get('password')) safe_str_cmp(password, self._config['api_server'].get('password')))
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """
@ -83,6 +89,15 @@ class ApiServer(RPC):
self._config = freqtrade.config self._config = freqtrade.config
self.app = Flask(__name__) self.app = Flask(__name__)
self._cors = CORS(self.app,
resources={r"/api/*": {"supports_credentials": True, }}
)
# Setup the Flask-JWT-Extended extension
self.app.config['JWT_SECRET_KEY'] = self._config['api_server'].get(
'jwt_secret_key', 'super-secret')
self.jwt = JWTManager(self.app)
self.app.json_encoder = ArrowJSONEncoder self.app.json_encoder = ArrowJSONEncoder
# Register application handling # Register application handling
@ -148,6 +163,10 @@ class ApiServer(RPC):
self.app.register_error_handler(404, self.page_not_found) self.app.register_error_handler(404, self.page_not_found)
# Actions to control the bot # Actions to control the bot
self.app.add_url_rule(f'{BASE_URI}/token/login', 'login',
view_func=self._token_login, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/token/refresh', 'token_refresh',
view_func=self._token_refresh, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/start', 'start', self.app.add_url_rule(f'{BASE_URI}/start', 'start',
view_func=self._start, methods=['POST']) view_func=self._start, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST']) self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
@ -199,6 +218,37 @@ class ApiServer(RPC):
'code': 404 'code': 404
}), 404 }), 404
@require_login
@rpc_catch_errors
def _token_login(self):
"""
Handler for /token/login
Returns a JWT token
"""
auth = request.authorization
if auth and self.check_auth(auth.username, auth.password):
keystuff = {'u': auth.username}
ret = {
'access_token': create_access_token(identity=keystuff),
'refresh_token': create_refresh_token(identity=keystuff),
}
return self.rest_dump(ret)
return jsonify({"error": "Unauthorized"}), 401
@jwt_refresh_token_required
@rpc_catch_errors
def _token_refresh(self):
"""
Handler for /token/refresh
Returns a JWT token based on a JWT refresh token
"""
current_user = get_jwt_identity()
new_token = create_access_token(identity=current_user, fresh=False)
ret = {'access_token': new_token}
return self.rest_dump(ret)
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
def _start(self): def _start(self):

View File

@ -94,6 +94,7 @@ class RPC:
'dry_run': config['dry_run'], 'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'], 'stake_currency': config['stake_currency'],
'stake_amount': config['stake_amount'], 'stake_amount': config['stake_amount'],
'max_open_trades': config['max_open_trades'],
'minimal_roi': config['minimal_roi'].copy(), 'minimal_roi': config['minimal_roi'].copy(),
'stoploss': config['stoploss'], 'stoploss': config['stoploss'],
'trailing_stop': config['trailing_stop'], 'trailing_stop': config['trailing_stop'],
@ -103,6 +104,8 @@ class RPC:
'ticker_interval': config['ticker_interval'], 'ticker_interval': config['ticker_interval'],
'exchange': config['exchange']['name'], 'exchange': config['exchange']['name'],
'strategy': config['strategy'], 'strategy': config['strategy'],
'forcebuy_enabled': config.get('forcebuy_enable', False),
'state': str(self._freqtrade.state)
} }
return val return val
@ -128,13 +131,15 @@ class RPC:
current_rate = NAN current_rate = NAN
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
if trade.close_profit else None) if trade.close_profit is not None else None)
trade_dict = trade.to_json() trade_dict = trade.to_json()
trade_dict.update(dict( trade_dict.update(dict(
base_currency=self._freqtrade.config['stake_currency'], base_currency=self._freqtrade.config['stake_currency'],
close_profit=fmt_close_profit, close_profit=trade.close_profit if trade.close_profit is not None else None,
close_profit_pct=fmt_close_profit,
current_rate=current_rate, current_rate=current_rate,
current_profit=round(current_profit * 100, 2), current_profit=current_profit,
current_profit_pct=round(current_profit * 100, 2),
open_order='({} {} rem={:.8f})'.format( open_order='({} {} rem={:.8f})'.format(
order['type'], order['side'], order['remaining'] order['type'], order['side'], order['remaining']
) if order else None, ) if order else None,
@ -183,7 +188,7 @@ class RPC:
def _rpc_daily_profit( def _rpc_daily_profit(
self, timescale: int, self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> List[List[Any]]: stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
today = datetime.utcnow().date() today = datetime.utcnow().date()
profit_days: Dict[date, Dict] = {} profit_days: Dict[date, Dict] = {}
@ -203,28 +208,26 @@ class RPC:
'trades': len(trades) 'trades': len(trades)
} }
return [ data = [
[ {
key, 'date': key,
'{value:.8f} {symbol}'.format( 'abs_profit': f'{float(value["amount"]):.8f}',
value=float(value['amount']), 'fiat_value': '{value:.3f}'.format(
symbol=stake_currency
),
'{value:.3f} {symbol}'.format(
value=self._fiat_converter.convert_amount( value=self._fiat_converter.convert_amount(
value['amount'], value['amount'],
stake_currency, stake_currency,
fiat_display_currency fiat_display_currency
) if self._fiat_converter else 0, ) if self._fiat_converter else 0,
symbol=fiat_display_currency
), ),
'{value} trade{s}'.format( 'trade_count': f'{value["trades"]}',
value=value['trades'], }
s='' if value['trades'] < 2 else 's'
),
]
for key, value in profit_days.items() for key, value in profit_days.items()
] ]
return {
'stake_currency': stake_currency,
'fiat_display_currency': fiat_display_currency,
'data': data
}
def _rpc_trade_history(self, limit: int) -> Dict: def _rpc_trade_history(self, limit: int) -> Dict:
""" Returns the X last trades """ """ Returns the X last trades """
@ -311,7 +314,9 @@ class RPC:
'profit_all_fiat': profit_all_fiat, 'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades), 'trade_count': len(trades),
'first_trade_date': arrow.get(trades[0].open_date).humanize(), 'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'first_trade_timestamp': int(trades[0].open_date.timestamp() * 1000),
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(), 'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'latest_trade_timestamp': int(trades[-1].open_date.timestamp() * 1000),
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'best_pair': bp_pair, 'best_pair': bp_pair,
'best_rate': round(bp_rate * 100, 2), 'best_rate': round(bp_rate * 100, 2),
@ -544,5 +549,5 @@ class RPC:
def _rpc_edge(self) -> List[Dict[str, Any]]: def _rpc_edge(self) -> List[Dict[str, Any]]:
""" Returns information related to Edge """ """ Returns information related to Edge """
if not self._freqtrade.edge: if not self._freqtrade.edge:
raise RPCException(f'Edge is not enabled.') raise RPCException('Edge is not enabled.')
return self._freqtrade.edge.accepted_pairs() return self._freqtrade.edge.accepted_pairs()

View File

@ -215,22 +215,28 @@ class Telegram(RPC):
"*Open Rate:* `{open_rate:.8f}`", "*Open Rate:* `{open_rate:.8f}`",
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
"*Current Rate:* `{current_rate:.8f}`", "*Current Rate:* `{current_rate:.8f}`",
"*Close Profit:* `{close_profit}`" if r['close_profit'] else "", ("*Close Profit:* `{close_profit_pct}`"
"*Current Profit:* `{current_profit:.2f}%`", if r['close_profit_pct'] is not None else ""),
"*Current Profit:* `{current_profit_pct:.2f}%`",
# Adding initial stoploss only if it is different from stoploss # Adding initial stoploss only if it is different from stoploss
"*Initial Stoploss:* `{initial_stop_loss:.8f}` " + "*Initial Stoploss:* `{initial_stop_loss:.8f}` " +
("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "") ("`({initial_stop_loss_pct:.2f}%)`") if (
if r['stop_loss'] != r['initial_stop_loss'] else "", r['stop_loss'] != r['initial_stop_loss']
and r['initial_stop_loss_pct'] is not None) else "",
# Adding stoploss and stoploss percentage only if it is not None # Adding stoploss and stoploss percentage only if it is not None
"*Stoploss:* `{stop_loss:.8f}` " + "*Stoploss:* `{stop_loss:.8f}` " +
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""),
"*Open Order:* `{open_order}`" if r['open_order'] else ""
] ]
if r['open_order']:
if r['sell_order_status']:
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
else:
lines.append("*Open Order:* `{open_order}`")
# Filter empty lines using list-comprehension # Filter empty lines using list-comprehension
messages.append("\n".join([l for l in lines if l]).format(**r)) messages.append("\n".join([line for line in lines if line]).format(**r))
for msg in messages: for msg in messages:
self._send_msg(msg) self._send_msg(msg)
@ -276,12 +282,16 @@ class Telegram(RPC):
stake_cur, stake_cur,
fiat_disp_cur fiat_disp_cur
) )
stats_tab = tabulate(stats, stats_tab = tabulate(
[[day['date'],
f"{day['abs_profit']} {stats['stake_currency']}",
f"{day['fiat_value']} {stats['fiat_display_currency']}",
f"{day['trade_count']} trades"] for day in stats['data']],
headers=[ headers=[
'Day', 'Day',
f'Profit {stake_cur}', f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}', f'Profit {fiat_disp_cur}',
f'Trades' 'Trades',
], ],
tablefmt='simple') tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>' message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
@ -579,7 +589,7 @@ class Telegram(RPC):
"*/whitelist:* `Show current whitelist` \n" \ "*/whitelist:* `Show current whitelist` \n" \
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \ "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
"to the blacklist.` \n" \ "to the blacklist.` \n" \
"*/edge:* `Shows validated pairs by Edge if it is enabeld` \n" \ "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" \
"*/help:* `This help message`\n" \ "*/help:* `This help message`\n" \
"*/version:* `Show version`" "*/version:* `Show version`"
@ -621,10 +631,12 @@ class Telegram(RPC):
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
f"*Exchange:* `{val['exchange']}`\n" f"*Exchange:* `{val['exchange']}`\n"
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
f"*Max open Trades:* `{val['max_open_trades']}`\n"
f"*Minimum ROI:* `{val['minimal_roi']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n"
f"{sl_info}" f"{sl_info}"
f"*Ticker Interval:* `{val['ticker_interval']}`\n" f"*Ticker Interval:* `{val['ticker_interval']}`\n"
f"*Strategy:* `{val['strategy']}`" f"*Strategy:* `{val['strategy']}`\n"
f"*Current state:* `{val['state']}`"
) )
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:

View File

@ -14,6 +14,9 @@ class State(Enum):
STOPPED = 2 STOPPED = 2
RELOAD_CONF = 3 RELOAD_CONF = 3
def __str__(self):
return f"{self.name.lower()}"
class RunMode(Enum): class RunMode(Enum):
""" """

View File

@ -7,7 +7,7 @@ import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Tuple from typing import Dict, NamedTuple, Optional, Tuple
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
@ -17,8 +17,10 @@ from freqtrade.exceptions import StrategyError
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -185,7 +187,7 @@ class IStrategy(ABC):
""" """
return False return False
def informative_pairs(self) -> List[Tuple[str, str]]: def informative_pairs(self) -> ListPairsWithTimeframes:
""" """
Define additional, informative pair/interval combinations to be cached from the exchange. Define additional, informative pair/interval combinations to be cached from the exchange.
These pair/interval combinations are non-tradeable, unless they are part These pair/interval combinations are non-tradeable, unless they are part
@ -308,7 +310,6 @@ class IStrategy(ABC):
logger.warning('Empty candle (OHLCV) data for pair %s', pair) logger.warning('Empty candle (OHLCV) data for pair %s', pair)
return False, False return False, False
latest_date = dataframe['date'].max()
try: try:
df_len, df_close, df_date = self.preserve_df(dataframe) df_len, df_close, df_date = self.preserve_df(dataframe)
dataframe = strategy_safe_wrapper( dataframe = strategy_safe_wrapper(
@ -324,17 +325,19 @@ class IStrategy(ABC):
logger.warning('Empty dataframe for pair %s', pair) logger.warning('Empty dataframe for pair %s', pair)
return False, False return False, False
latest_date = dataframe['date'].max()
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
# Explicitly convert to arrow object to ensure the below comparison does not fail
latest_date = arrow.get(latest_date)
# Check if dataframe is out of date # Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = timeframe_to_minutes(interval) interval_minutes = timeframe_to_minutes(interval)
offset = self.config.get('exchange', {}).get('outdated_offset', 5) offset = self.config.get('exchange', {}).get('outdated_offset', 5)
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): if latest_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
logger.warning( logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old', 'Outdated history for pair %s. Last tick is %s minutes old',
pair, pair,
(arrow.utcnow() - signal_date).seconds // 60 (arrow.utcnow() - latest_date).seconds // 60
) )
return False, False return False, False

View File

@ -6,6 +6,7 @@
"fiat_display_currency": "{{ fiat_display_currency }}", "fiat_display_currency": "{{ fiat_display_currency }}",
"ticker_interval": "{{ ticker_interval }}", "ticker_interval": "{{ ticker_interval }}",
"dry_run": {{ dry_run | lower }}, "dry_run": {{ dry_run | lower }},
"cancel_open_orders_on_exit": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,
"sell": 30 "sell": 30

View File

@ -37,9 +37,7 @@ class Worker:
self._heartbeat_msg: float = 0 self._heartbeat_msg: float = 0
# Tell systemd that we completed initialization phase # Tell systemd that we completed initialization phase
if self._sd_notify: self._notify("READY=1")
logger.debug("sd_notify: READY=1")
self._sd_notify.notify("READY=1")
def _init(self, reconfig: bool) -> None: def _init(self, reconfig: bool) -> None:
""" """
@ -60,6 +58,15 @@ class Worker:
self._sd_notify = sdnotify.SystemdNotifier() if \ self._sd_notify = sdnotify.SystemdNotifier() if \
self._config.get('internals', {}).get('sd_notify', False) else None self._config.get('internals', {}).get('sd_notify', False) else None
def _notify(self, message: str) -> None:
"""
Removes the need to verify in all occurances if sd_notify is enabled
:param message: Message to send to systemd if it's enabled.
"""
if self._sd_notify:
logger.debug(f"sd_notify: {message}")
self._sd_notify.notify(message)
def run(self) -> None: def run(self) -> None:
state = None state = None
while True: while True:
@ -89,17 +96,13 @@ class Worker:
if state == State.STOPPED: if state == State.STOPPED:
# Ping systemd watchdog before sleeping in the stopped state # Ping systemd watchdog before sleeping in the stopped state
if self._sd_notify: self._notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: STOPPED.")
self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: STOPPED.")
self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs) self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs)
elif state == State.RUNNING: elif state == State.RUNNING:
# Ping systemd watchdog before throttling # Ping systemd watchdog before throttling
if self._sd_notify: self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
logger.debug("sd_notify: WATCHDOG=1\\nSTATUS=State: RUNNING.")
self._sd_notify.notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
self._throttle(func=self._process_running, throttle_secs=self._throttle_secs) self._throttle(func=self._process_running, throttle_secs=self._throttle_secs)
@ -131,8 +134,7 @@ class Worker:
return result return result
def _process_stopped(self) -> None: def _process_stopped(self) -> None:
# Maybe do here something in the future... self.freqtrade.process_stopped()
pass
def _process_running(self) -> None: def _process_running(self) -> None:
try: try:
@ -155,9 +157,7 @@ class Worker:
replaces it with the new instance replaces it with the new instance
""" """
# Tell systemd that we initiated reconfiguration # Tell systemd that we initiated reconfiguration
if self._sd_notify: self._notify("RELOADING=1")
logger.debug("sd_notify: RELOADING=1")
self._sd_notify.notify("RELOADING=1")
# Clean up current freqtrade modules # Clean up current freqtrade modules
self.freqtrade.cleanup() self.freqtrade.cleanup()
@ -168,15 +168,11 @@ class Worker:
self.freqtrade.notify_status('config reloaded') self.freqtrade.notify_status('config reloaded')
# Tell systemd that we completed reconfiguration # Tell systemd that we completed reconfiguration
if self._sd_notify: self._notify("READY=1")
logger.debug("sd_notify: READY=1")
self._sd_notify.notify("READY=1")
def exit(self) -> None: def exit(self) -> None:
# Tell systemd that we are exiting now # Tell systemd that we are exiting now
if self._sd_notify: self._notify("STOPPING=1")
logger.debug("sd_notify: STOPPING=1")
self._sd_notify.notify("STOPPING=1")
if self.freqtrade: if self.freqtrade:
self.freqtrade.notify_status('process died') self.freqtrade.notify_status('process died')

View File

@ -1,15 +1,15 @@
# requirements without requirements installable via conda # requirements without requirements installable via conda
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.27.1 ccxt==1.28.49
SQLAlchemy==1.3.16 SQLAlchemy==1.3.17
python-telegram-bot==12.6.1 python-telegram-bot==12.7
arrow==0.15.5 arrow==0.15.6
cachetools==4.1.0 cachetools==4.1.0
requests==2.23.0 requests==2.23.0
urllib3==1.25.9 urllib3==1.25.9
wrapt==1.12.1 wrapt==1.12.1
jsonschema==3.2.0 jsonschema==3.2.0
TA-Lib==0.4.17 TA-Lib==0.4.18
tabulate==0.8.7 tabulate==0.8.7
pycoingecko==1.2.0 pycoingecko==1.2.0
jinja2==2.11.2 jinja2==2.11.2
@ -25,6 +25,8 @@ sdnotify==0.3.2
# Api server # Api server
flask==1.1.2 flask==1.1.2
flask-jwt-extended==3.24.1
flask-cors==3.0.8
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.3 colorama==0.4.3

View File

@ -4,13 +4,13 @@
-r requirements-hyperopt.txt -r requirements-hyperopt.txt
coveralls==2.0.0 coveralls==2.0.0
flake8==3.7.9 flake8==3.8.2
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==4.1.0 flake8-tidy-imports==4.1.0
mypy==0.770 mypy==0.770
pytest==5.4.1 pytest==5.4.2
pytest-asyncio==0.11.0 pytest-asyncio==0.12.0
pytest-cov==2.8.1 pytest-cov==2.9.0
pytest-mock==3.1.0 pytest-mock==3.1.0
pytest-random-order==1.0.4 pytest-random-order==1.0.4

View File

@ -3,8 +3,8 @@
# Required for hyperopt # Required for hyperopt
scipy==1.4.1 scipy==1.4.1
scikit-learn==0.22.2.post1 scikit-learn==0.23.1
scikit-optimize==0.7.4 scikit-optimize==0.7.4
filelock==3.0.12 filelock==3.0.12
joblib==0.14.1 joblib==0.15.1
progressbar2==3.51.0 progressbar2==3.51.3

View File

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

View File

@ -1,5 +1,5 @@
# Load common requirements # Load common requirements
-r requirements-common.txt -r requirements-common.txt
numpy==1.18.3 numpy==1.18.4
pandas==1.0.3 pandas==1.0.4

View File

@ -16,12 +16,12 @@ if readme_file.is_file():
readme_long = (Path(__file__).parent / "README.md").read_text() readme_long = (Path(__file__).parent / "README.md").read_text()
# Requirements used for submodules # Requirements used for submodules
api = ['flask'] api = ['flask', 'flask-jwt-extended', 'flask-cors']
plot = ['plotly>=4.0'] plot = ['plotly>=4.0']
hyperopt = [ hyperopt = [
'scipy', 'scipy',
'scikit-learn', 'scikit-learn',
'scikit-optimize', 'scikit-optimize>=0.7.0',
'filelock', 'filelock',
'joblib', 'joblib',
'progressbar2', 'progressbar2',

View File

@ -10,11 +10,13 @@ from freqtrade.commands import (start_convert_data, start_create_userdir,
start_list_hyperopts, start_list_markets, start_list_hyperopts, start_list_markets,
start_list_strategies, start_list_timeframes, start_list_strategies, start_list_timeframes,
start_new_hyperopt, start_new_strategy, start_new_hyperopt, start_new_strategy,
start_test_pairlist, start_trading) start_show_trades, start_test_pairlist,
start_trading)
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re,
patch_exchange,
patched_configuration_load_config_file) patched_configuration_load_config_file)
@ -30,7 +32,7 @@ def test_setup_utils_configuration():
assert config['exchange']['secret'] == '' assert config['exchange']['secret'] == ''
def test_start_trading_fail(mocker): def test_start_trading_fail(mocker, caplog):
mocker.patch("freqtrade.worker.Worker.run", MagicMock(side_effect=OperationalException)) mocker.patch("freqtrade.worker.Worker.run", MagicMock(side_effect=OperationalException))
@ -41,16 +43,15 @@ def test_start_trading_fail(mocker):
'trade', 'trade',
'-c', 'config.json.example' '-c', 'config.json.example'
] ]
with pytest.raises(OperationalException):
start_trading(get_args(args)) start_trading(get_args(args))
assert exitmock.call_count == 1 assert exitmock.call_count == 1
exitmock.reset_mock() exitmock.reset_mock()
caplog.clear()
mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(side_effect=OperationalException)) mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(side_effect=OperationalException))
with pytest.raises(OperationalException):
start_trading(get_args(args)) start_trading(get_args(args))
assert exitmock.call_count == 0 assert exitmock.call_count == 0
assert log_has('Fatal exception!', caplog)
def test_list_exchanges(capsys): def test_list_exchanges(capsys):
@ -727,7 +728,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
assert re.match("['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', 'XRP/BTC']", captured.out) assert re.match("['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', 'XRP/BTC']", captured.out)
def test_hyperopt_list(mocker, capsys, hyperopt_results): def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', 'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results',
MagicMock(return_value=hyperopt_results) MagicMock(return_value=hyperopt_results)
@ -911,8 +912,7 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
pargs['config'] = None pargs['config'] = None
start_hyperopt_list(pargs) start_hyperopt_list(pargs)
captured = capsys.readouterr() captured = capsys.readouterr()
assert all(x in captured.out log_has("CSV file created: test_file.csv", caplog)
for x in ["CSV-File created!"])
f = Path("test_file.csv") f = Path("test_file.csv")
assert 'Best,1,2,-1.25%,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() assert 'Best,1,2,-1.25%,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text()
assert f.is_file() assert f.is_file()
@ -1041,3 +1041,46 @@ def test_convert_data_trades(mocker, testdatadir):
assert trades_mock.call_args[1]['convert_from'] == 'jsongz' assert trades_mock.call_args[1]['convert_from'] == 'jsongz'
assert trades_mock.call_args[1]['convert_to'] == 'json' assert trades_mock.call_args[1]['convert_to'] == 'json'
assert trades_mock.call_args[1]['erase'] is False assert trades_mock.call_args[1]['erase'] is False
@pytest.mark.usefixtures("init_persistence")
def test_show_trades(mocker, fee, capsys, caplog):
mocker.patch("freqtrade.persistence.init")
create_mock_trades(fee)
args = [
"show-trades",
"--db-url",
"sqlite:///"
]
pargs = get_args(args)
pargs['config'] = None
start_show_trades(pargs)
assert log_has("Printing 3 Trades: ", caplog)
captured = capsys.readouterr()
assert "Trade(id=1" in captured.out
assert "Trade(id=2" in captured.out
assert "Trade(id=3" in captured.out
args = [
"show-trades",
"--db-url",
"sqlite:///",
"--print-json",
"--trade-ids", "1", "2"
]
pargs = get_args(args)
pargs['config'] = None
start_show_trades(pargs)
captured = capsys.readouterr()
assert log_has("Printing 2 Trades: ", caplog)
assert '"trade_id": 1' in captured.out
assert '"trade_id": 2' in captured.out
assert '"trade_id": 3' not in captured.out
args = [
"show-trades",
]
pargs = get_args(args)
pargs['config'] = None
with pytest.raises(OperationalException, match=r"--db-url is required for this command."):
start_show_trades(pargs)

View File

@ -92,7 +92,7 @@ def patch_wallet(mocker, free=999.9) -> None:
def patch_whitelist(mocker, conf) -> None: def patch_whitelist(mocker, conf) -> None:
mocker.patch('freqtrade.freqtradebot.FreqtradeBot._refresh_whitelist', mocker.patch('freqtrade.freqtradebot.FreqtradeBot._refresh_active_whitelist',
MagicMock(return_value=conf['exchange']['pair_whitelist'])) MagicMock(return_value=conf['exchange']['pair_whitelist']))
@ -249,6 +249,7 @@ def default_conf(testdatadir):
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"ticker_interval": '5m', "ticker_interval": '5m',
"dry_run": True, "dry_run": True,
"cancel_open_orders_on_exit": False,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,
@ -304,7 +305,8 @@ def default_conf(testdatadir):
"user_data_dir": Path("user_data"), "user_data_dir": Path("user_data"),
"verbosity": 3, "verbosity": 3,
"strategy_path": str(Path(__file__).parent / "strategy" / "strats"), "strategy_path": str(Path(__file__).parent / "strategy" / "strats"),
"strategy": "DefaultStrategy" "strategy": "DefaultStrategy",
"internals": {},
} }
return configuration return configuration
@ -779,7 +781,7 @@ def limit_buy_order():
'id': 'mocked_limit_buy', 'id': 'mocked_limit_buy',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
@ -795,7 +797,7 @@ def market_buy_order():
'id': 'mocked_market_buy', 'id': 'mocked_market_buy',
'type': 'market', 'type': 'market',
'side': 'buy', 'side': 'buy',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00004099, 'price': 0.00004099,
'amount': 91.99181073, 'amount': 91.99181073,
@ -811,7 +813,7 @@ def market_sell_order():
'id': 'mocked_limit_sell', 'id': 'mocked_limit_sell',
'type': 'market', 'type': 'market',
'side': 'sell', 'side': 'sell',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'price': 0.00004173, 'price': 0.00004173,
'amount': 91.99181073, 'amount': 91.99181073,
@ -827,7 +829,7 @@ def limit_buy_order_old():
'id': 'mocked_limit_buy_old', 'id': 'mocked_limit_buy_old',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
@ -843,7 +845,7 @@ def limit_sell_order_old():
'id': 'mocked_limit_sell_old', 'id': 'mocked_limit_sell_old',
'type': 'limit', 'type': 'limit',
'side': 'sell', 'side': 'sell',
'pair': 'ETH/BTC', 'symbol': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
@ -859,7 +861,7 @@ def limit_buy_order_old_partial():
'id': 'mocked_limit_buy_old_partial', 'id': 'mocked_limit_buy_old_partial',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'pair': 'ETH/BTC', 'symbol': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
@ -873,10 +875,103 @@ def limit_buy_order_old_partial():
def limit_buy_order_old_partial_canceled(limit_buy_order_old_partial): def limit_buy_order_old_partial_canceled(limit_buy_order_old_partial):
res = deepcopy(limit_buy_order_old_partial) res = deepcopy(limit_buy_order_old_partial)
res['status'] = 'canceled' res['status'] = 'canceled'
res['fee'] = {'cost': 0.0001, 'currency': 'ETH'} res['fee'] = {'cost': 0.023, 'currency': 'ETH'}
return res return res
@pytest.fixture(scope='function')
def limit_buy_order_canceled_empty(request):
# Indirect fixture
# Documentation:
# https://docs.pytest.org/en/latest/example/parametrize.html#apply-indirect-on-particular-arguments
exchange_name = request.param
if exchange_name == 'ftx':
return {
'info': {},
'id': '1234512345',
'clientOrderId': None,
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 34.3225,
'amount': 0.55,
'cost': 0.0,
'average': None,
'filled': 0.0,
'remaining': 0.0,
'status': 'closed',
'fee': None,
'trades': None
}
elif exchange_name == 'kraken':
return {
'info': {},
'id': 'AZNPFF-4AC4N-7MKTAT',
'clientOrderId': None,
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'status': 'canceled',
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 34.3225,
'cost': 0.0,
'amount': 0.55,
'filled': 0.0,
'average': 0.0,
'remaining': 0.55,
'fee': {'cost': 0.0, 'rate': None, 'currency': 'USDT'},
'trades': []
}
elif exchange_name == 'binance':
return {
'info': {},
'id': '1234512345',
'clientOrderId': 'alb1234123',
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 0.016804,
'amount': 0.55,
'cost': 0.0,
'average': None,
'filled': 0.0,
'remaining': 0.55,
'status': 'canceled',
'fee': None,
'trades': None
}
else:
return {
'info': {},
'id': '1234512345',
'clientOrderId': 'alb1234123',
'timestamp': arrow.utcnow().shift(minutes=-601).timestamp,
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'lastTradeTimestamp': None,
'symbol': 'LTC/USDT',
'type': 'limit',
'side': 'buy',
'price': 0.016804,
'amount': 0.55,
'cost': 0.0,
'average': None,
'filled': 0.0,
'remaining': 0.55,
'status': 'canceled',
'fee': None,
'trades': None
}
@pytest.fixture @pytest.fixture
def limit_sell_order(): def limit_sell_order():
return { return {
@ -1328,6 +1423,15 @@ def trades_for_order():
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def trades_history(): def trades_history():
return [[1565798399463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508],
[1565798399629, '126181330', None, 'buy', 0.019627, 0.244, 0.004788987999999999],
[1565798399752, '126181331', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
[1565798399862, '126181332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
[1565798399872, '126181333', None, 'sell', 0.019626, 0.011, 0.00021588599999999999]]
@pytest.fixture(scope="function")
def fetch_trades_result():
return [{'info': {'a': 126181329, return [{'info': {'a': 126181329,
'p': '0.01962700', 'p': '0.01962700',
'q': '0.04000000', 'q': '0.04000000',
@ -1482,7 +1586,7 @@ def buy_order_fee():
'id': 'mocked_limit_buy_old', 'id': 'mocked_limit_buy_old',
'type': 'limit', 'type': 'limit',
'side': 'buy', 'side': 'buy',
'pair': 'mocked', 'symbol': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.245441, 'price': 0.245441,
'amount': 8.0, 'amount': 8.0,
@ -1613,7 +1717,8 @@ def hyperopt_results():
'loss': 20.0, 'loss': 20.0,
'params_dict': { 'params_dict': {
'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501 'params_details': {
'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
'stoploss': {'stoploss': -0.338070047333259}}, 'stoploss': {'stoploss': -0.338070047333259}},
@ -1663,7 +1768,8 @@ def hyperopt_results():
}, { }, {
'loss': 4.713497421432944, 'loss': 4.713497421432944,
'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 'params_details': {
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501

View File

@ -178,6 +178,10 @@ def test_create_cum_profit1(testdatadir):
assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[0]['cum_profits'] == 0
assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005
with pytest.raises(ValueError, match='Trade dataframe empty.'):
create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'],
"cum_profits", timeframe="5m")
def test_calculate_max_drawdown(testdatadir): def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest-result_test.json" filename = testdatadir / "backtest-result_test.json"

View File

@ -5,12 +5,10 @@ from freqtrade.configuration.timerange import TimeRange
from freqtrade.data.converter import (convert_ohlcv_format, from freqtrade.data.converter import (convert_ohlcv_format,
convert_trades_format, convert_trades_format,
ohlcv_fill_up_missing_data, ohlcv_fill_up_missing_data,
ohlcv_to_dataframe, ohlcv_to_dataframe, trades_dict_to_list,
trim_dataframe) trades_remove_duplicates, trim_dataframe)
from freqtrade.data.history import (get_timerange, from freqtrade.data.history import (get_timerange, load_data,
load_data, load_pair_history, validate_backtest_data)
load_pair_history,
validate_backtest_data)
from tests.conftest import log_has from tests.conftest import log_has
from tests.data.test_history import _backup_file, _clean_test_file from tests.data.test_history import _backup_file, _clean_test_file
@ -197,32 +195,60 @@ def test_trim_dataframe(testdatadir) -> None:
assert all(data_modify.iloc[0] == data.iloc[25]) assert all(data_modify.iloc[0] == data.iloc[25])
def test_convert_trades_format(mocker, default_conf, testdatadir): def test_trades_remove_duplicates(trades_history):
file = testdatadir / "XRP_ETH-trades.json.gz" trades_history1 = trades_history * 3
file_new = testdatadir / "XRP_ETH-trades.json" assert len(trades_history1) == len(trades_history) * 3
_backup_file(file, copy_file=True) res = trades_remove_duplicates(trades_history1)
default_conf['datadir'] = testdatadir assert len(res) == len(trades_history)
for i, t in enumerate(res):
assert t == trades_history[i]
assert not file_new.exists()
def test_trades_dict_to_list(fetch_trades_result):
res = trades_dict_to_list(fetch_trades_result)
assert isinstance(res, list)
assert isinstance(res[0], list)
for i, t in enumerate(res):
assert t[0] == fetch_trades_result[i]['timestamp']
assert t[1] == fetch_trades_result[i]['id']
assert t[2] == fetch_trades_result[i]['type']
assert t[3] == fetch_trades_result[i]['side']
assert t[4] == fetch_trades_result[i]['price']
assert t[5] == fetch_trades_result[i]['amount']
assert t[6] == fetch_trades_result[i]['cost']
def test_convert_trades_format(mocker, default_conf, testdatadir):
files = [{'old': testdatadir / "XRP_ETH-trades.json.gz",
'new': testdatadir / "XRP_ETH-trades.json"},
{'old': testdatadir / "XRP_OLD-trades.json.gz",
'new': testdatadir / "XRP_OLD-trades.json"},
]
for file in files:
_backup_file(file['old'], copy_file=True)
assert not file['new'].exists()
default_conf['datadir'] = testdatadir
convert_trades_format(default_conf, convert_from='jsongz', convert_trades_format(default_conf, convert_from='jsongz',
convert_to='json', erase=False) convert_to='json', erase=False)
assert file_new.exists() for file in files:
assert file.exists() assert file['new'].exists()
assert file['old'].exists()
# Remove original file # Remove original file
file.unlink() file['old'].unlink()
# Convert back # Convert back
convert_trades_format(default_conf, convert_from='json', convert_trades_format(default_conf, convert_from='json',
convert_to='jsongz', erase=True) convert_to='jsongz', erase=True)
for file in files:
assert file['old'].exists()
assert not file['new'].exists()
assert file.exists() _clean_test_file(file['old'])
assert not file_new.exists() if file['new'].exists():
file['new'].unlink()
_clean_test_file(file)
if file_new.exists():
file_new.unlink()
def test_convert_ohlcv_format(mocker, default_conf, testdatadir): def test_convert_ohlcv_format(mocker, default_conf, testdatadir):

View File

@ -1,8 +1,11 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from pandas import DataFrame from pandas import DataFrame
import pytest
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
@ -64,8 +67,8 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history):
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
# Test with and without parameter # Test with and without parameter
assert dp.get_pair_dataframe("UNITTEST/BTC", assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)\
ticker_interval).equals(dp.get_pair_dataframe("UNITTEST/BTC")) .equals(dp.get_pair_dataframe("UNITTEST/BTC"))
default_conf["runmode"] = RunMode.LIVE default_conf["runmode"] = RunMode.LIVE
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
@ -90,10 +93,7 @@ def test_available_pairs(mocker, default_conf, ohlcv_history):
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
assert len(dp.available_pairs) == 2 assert len(dp.available_pairs) == 2
assert dp.available_pairs == [ assert dp.available_pairs == [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval), ]
("XRP/BTC", ticker_interval),
("UNITTEST/BTC", ticker_interval),
]
def test_refresh(mocker, default_conf, ohlcv_history): def test_refresh(mocker, default_conf, ohlcv_history):
@ -152,3 +152,45 @@ def test_market(mocker, default_conf, markets):
res = dp.market('UNITTEST/BTC') res = dp.market('UNITTEST/BTC')
assert res is None assert res is None
def test_ticker(mocker, default_conf, tickers):
ticker_mock = MagicMock(return_value=tickers()['ETH/BTC'])
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, exchange)
res = dp.ticker('ETH/BTC')
assert type(res) is dict
assert 'symbol' in res
assert res['symbol'] == 'ETH/BTC'
ticker_mock = MagicMock(side_effect=DependencyException('Pair not found'))
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, exchange)
res = dp.ticker('UNITTEST/BTC')
assert res == {}
def test_current_whitelist(mocker, default_conf, tickers):
# patch default conf to volumepairlist
default_conf['pairlists'][0] = {'method': 'VolumePairList', "number_assets": 5}
mocker.patch.multiple('freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
get_tickers=tickers)
exchange = get_patched_exchange(mocker, default_conf)
pairlist = PairListManager(exchange, default_conf)
dp = DataProvider(default_conf, exchange, pairlist)
# Simulate volumepairs from exchange.
pairlist.refresh_pairlist()
assert dp.current_whitelist() == pairlist._whitelist
# The identity of the 2 lists should be identical
assert dp.current_whitelist() is pairlist._whitelist
with pytest.raises(OperationalException):
dp = DataProvider(default_conf, exchange)
dp.current_whitelist()

View File

@ -547,6 +547,17 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
assert log_has("New Amount of trades: 5", caplog) assert log_has("New Amount of trades: 5", caplog)
assert file1.is_file() assert file1.is_file()
ght_mock.reset_mock()
since_time = int(trades_history[-3][0] // 1000)
since_time2 = int(trades_history[-1][0] // 1000)
timerange = TimeRange('date', None, since_time, 0)
assert _download_trades_history(data_handler=data_handler, exchange=exchange,
pair='ETH/BTC', timerange=timerange)
assert ght_mock.call_count == 1
# Check this in seconds - since we had to convert to seconds above too.
assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time2 - 5
# clean files freshly downloaded # clean files freshly downloaded
_clean_test_file(file1) _clean_test_file(file1)
@ -601,7 +612,7 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir):
def test_jsondatahandler_trades_get_pairs(testdatadir): def test_jsondatahandler_trades_get_pairs(testdatadir):
pairs = JsonGzDataHandler.trades_get_pairs(testdatadir) pairs = JsonGzDataHandler.trades_get_pairs(testdatadir)
# Convert to set to avoid failures due to sorting # Convert to set to avoid failures due to sorting
assert set(pairs) == {'XRP/ETH'} assert set(pairs) == {'XRP/ETH', 'XRP/OLD'}
def test_jsondatahandler_ohlcv_purge(mocker, testdatadir): def test_jsondatahandler_ohlcv_purge(mocker, testdatadir):
@ -614,6 +625,17 @@ def test_jsondatahandler_ohlcv_purge(mocker, testdatadir):
assert dh.ohlcv_purge('UNITTEST/NONEXIST', '5m') assert dh.ohlcv_purge('UNITTEST/NONEXIST', '5m')
def test_jsondatahandler_trades_load(mocker, testdatadir, caplog):
dh = JsonGzDataHandler(testdatadir)
logmsg = "Old trades format detected - converting"
dh.trades_load('XRP/ETH')
assert not log_has(logmsg, caplog)
# Test conversation is happening
dh.trades_load('XRP/OLD')
assert log_has(logmsg, caplog)
def test_jsondatahandler_trades_purge(mocker, testdatadir): def test_jsondatahandler_trades_purge(mocker, testdatadir):
mocker.patch.object(Path, "exists", MagicMock(return_value=False)) mocker.patch.object(Path, "exists", MagicMock(return_value=False))
mocker.patch.object(Path, "unlink", MagicMock()) mocker.patch.object(Path, "unlink", MagicMock())

View File

@ -335,12 +335,16 @@ def test_edge_init_error(mocker, edge_conf,):
get_patched_freqtradebot(mocker, edge_conf) get_patched_freqtradebot(mocker, edge_conf)
def test_process_expectancy(mocker, edge_conf): @pytest.mark.parametrize("fee,risk_reward_ratio,expectancy", [
(0.0005, 306.5384615384, 101.5128205128),
(0.001, 152.6923076923, 50.2307692308),
])
def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectancy):
edge_conf['edge']['min_trade_number'] = 2 edge_conf['edge']['min_trade_number'] = 2
freqtrade = get_patched_freqtradebot(mocker, edge_conf) freqtrade = get_patched_freqtradebot(mocker, edge_conf)
def get_fee(*args, **kwargs): def get_fee(*args, **kwargs):
return 0.001 return fee
freqtrade.exchange.get_fee = get_fee freqtrade.exchange.get_fee = get_fee
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
@ -394,9 +398,9 @@ def test_process_expectancy(mocker, edge_conf):
assert 'TEST/BTC' in final assert 'TEST/BTC' in final
assert final['TEST/BTC'].stoploss == -0.9 assert final['TEST/BTC'].stoploss == -0.9
assert round(final['TEST/BTC'].winrate, 10) == 0.3333333333 assert round(final['TEST/BTC'].winrate, 10) == 0.3333333333
assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384 assert round(final['TEST/BTC'].risk_reward_ratio, 10) == risk_reward_ratio
assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0 assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0
assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128 assert round(final['TEST/BTC'].expectancy, 10) == expectancy
# Pop last item so no trade is profitable # Pop last item so no trade is profitable
trades.pop() trades.pop()

View File

@ -9,7 +9,12 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
def test_stoploss_order_binance(default_conf, mocker): @pytest.mark.parametrize('limitratio,expected', [
(None, 220 * 0.99),
(0.99, 220 * 0.99),
(0.98, 220 * 0.98),
])
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit' order_type = 'stop_loss_limit'
@ -20,7 +25,6 @@ def test_stoploss_order_binance(default_conf, mocker):
'foo': 'bar' 'foo': 'bar'
} }
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
@ -32,8 +36,8 @@ def test_stoploss_order_binance(default_conf, mocker):
order_types={'stoploss_on_exchange_limit_ratio': 1.05}) order_types={'stoploss_on_exchange_limit_ratio': 1.05})
api_mock.create_order.reset_mock() api_mock.create_order.reset_mock()
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
@ -42,7 +46,8 @@ def test_stoploss_order_binance(default_conf, mocker):
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
assert api_mock.create_order.call_args_list[0][1]['price'] == 220 # Price should be 1% below stopprice
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220} assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220}
# test exception handling # test exception handling

View File

@ -517,9 +517,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf) Exchange(default_conf)
assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange." assert log_has("Pair XRP/BTC is restricted for some users on this exchange."
f"Please check if you are impacted by this restriction " "Please check if you are impacted by this restriction "
f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog) "on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
@ -1413,13 +1413,13 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name): def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name):
default_conf['exchange']['name'] = exchange_name default_conf['exchange']['name'] = exchange_name
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_l2_order_book = order_book_l2 api_mock.fetch_l2_order_book = order_book_l2
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
order_book = exchange.get_order_book(pair='ETH/BTC', limit=10) order_book = exchange.fetch_l2_order_book(pair='ETH/BTC', limit=10)
assert 'bids' in order_book assert 'bids' in order_book
assert 'asks' in order_book assert 'asks' in order_book
assert len(order_book['bids']) == 10 assert len(order_book['bids']) == 10
@ -1427,20 +1427,20 @@ def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_order_book_exception(default_conf, mocker, exchange_name): def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported")) api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50)
def make_fetch_ohlcv_mock(data): def make_fetch_ohlcv_mock(data):
@ -1537,18 +1537,18 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
trades_history): fetch_trades_result):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
# Monkey-patch async function # Monkey-patch async function
exchange._api_async.fetch_trades = get_mock_coro(trades_history) exchange._api_async.fetch_trades = get_mock_coro(fetch_trades_result)
pair = 'ETH/BTC' pair = 'ETH/BTC'
res = await exchange._async_fetch_trades(pair, since=None, params=None) res = await exchange._async_fetch_trades(pair, since=None, params=None)
assert type(res) is list assert type(res) is list
assert isinstance(res[0], dict) assert isinstance(res[0], list)
assert isinstance(res[1], dict) assert isinstance(res[1], list)
assert exchange._api_async.fetch_trades.call_count == 1 assert exchange._api_async.fetch_trades.call_count == 1
assert exchange._api_async.fetch_trades.call_args[0][0] == pair assert exchange._api_async.fetch_trades.call_args[0][0] == pair
@ -1594,7 +1594,7 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang
if 'since' in kwargs: if 'since' in kwargs:
# Return first 3 # Return first 3
return trades_history[:-2] return trades_history[:-2]
elif kwargs.get('params', {}).get(pagination_arg) == trades_history[-3]['id']: elif kwargs.get('params', {}).get(pagination_arg) == trades_history[-3][1]:
# Return 2 # Return 2
return trades_history[-3:-1] return trades_history[-3:-1]
else: else:
@ -1604,8 +1604,8 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC' pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_id(pair, since=trades_history[0]["timestamp"], ret = await exchange._async_get_trade_history_id(pair, since=trades_history[0][0],
until=trades_history[-1]["timestamp"]-1) until=trades_history[-1][0]-1)
assert type(ret) is tuple assert type(ret) is tuple
assert ret[0] == pair assert ret[0] == pair
assert type(ret[1]) is list assert type(ret[1]) is list
@ -1614,7 +1614,7 @@ async def test__async_get_trade_history_id(default_conf, mocker, caplog, exchang
fetch_trades_cal = exchange._async_fetch_trades.call_args_list fetch_trades_cal = exchange._async_fetch_trades.call_args_list
# first call (using since, not fromId) # first call (using since, not fromId)
assert fetch_trades_cal[0][0][0] == pair assert fetch_trades_cal[0][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0]["timestamp"] assert fetch_trades_cal[0][1]['since'] == trades_history[0][0]
# 2nd call # 2nd call
assert fetch_trades_cal[1][0][0] == pair assert fetch_trades_cal[1][0][0] == pair
@ -1630,7 +1630,7 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
async def mock_get_trade_hist(pair, *args, **kwargs): async def mock_get_trade_hist(pair, *args, **kwargs):
if kwargs['since'] == trades_history[0]["timestamp"]: if kwargs['since'] == trades_history[0][0]:
return trades_history[:-1] return trades_history[:-1]
else: else:
return trades_history[-1:] return trades_history[-1:]
@ -1640,8 +1640,8 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
# Monkey-patch async function # Monkey-patch async function
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC' pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0]["timestamp"], ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0],
until=trades_history[-1]["timestamp"]-1) until=trades_history[-1][0]-1)
assert type(ret) is tuple assert type(ret) is tuple
assert ret[0] == pair assert ret[0] == pair
assert type(ret[1]) is list assert type(ret[1]) is list
@ -1650,11 +1650,11 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
fetch_trades_cal = exchange._async_fetch_trades.call_args_list fetch_trades_cal = exchange._async_fetch_trades.call_args_list
# first call (using since, not fromId) # first call (using since, not fromId)
assert fetch_trades_cal[0][0][0] == pair assert fetch_trades_cal[0][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0]["timestamp"] assert fetch_trades_cal[0][1]['since'] == trades_history[0][0]
# 2nd call # 2nd call
assert fetch_trades_cal[1][0][0] == pair assert fetch_trades_cal[1][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0]["timestamp"] assert fetch_trades_cal[0][1]['since'] == trades_history[0][0]
assert log_has_re(r"Stopping because until was reached.*", caplog) assert log_has_re(r"Stopping because until was reached.*", caplog)
@ -1666,7 +1666,7 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog,
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
async def mock_get_trade_hist(pair, *args, **kwargs): async def mock_get_trade_hist(pair, *args, **kwargs):
if kwargs['since'] == trades_history[0]["timestamp"]: if kwargs['since'] == trades_history[0][0]:
return trades_history[:-1] return trades_history[:-1]
else: else:
return [] return []
@ -1676,8 +1676,8 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog,
# Monkey-patch async function # Monkey-patch async function
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC' pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0]["timestamp"], ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0],
until=trades_history[-1]["timestamp"]-1) until=trades_history[-1][0]-1)
assert type(ret) is tuple assert type(ret) is tuple
assert ret[0] == pair assert ret[0] == pair
assert type(ret[1]) is list assert type(ret[1]) is list
@ -1686,7 +1686,7 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog,
fetch_trades_cal = exchange._async_fetch_trades.call_args_list fetch_trades_cal = exchange._async_fetch_trades.call_args_list
# first call (using since, not fromId) # first call (using since, not fromId)
assert fetch_trades_cal[0][0][0] == pair assert fetch_trades_cal[0][0][0] == pair
assert fetch_trades_cal[0][1]['since'] == trades_history[0]["timestamp"] assert fetch_trades_cal[0][1]['since'] == trades_history[0][0]
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
@ -1698,8 +1698,8 @@ def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades
exchange._async_get_trade_history_id = get_mock_coro((pair, trades_history)) exchange._async_get_trade_history_id = get_mock_coro((pair, trades_history))
exchange._async_get_trade_history_time = get_mock_coro((pair, trades_history)) exchange._async_get_trade_history_time = get_mock_coro((pair, trades_history))
ret = exchange.get_historic_trades(pair, since=trades_history[0]["timestamp"], ret = exchange.get_historic_trades(pair, since=trades_history[0][0],
until=trades_history[-1]["timestamp"]) until=trades_history[-1][0])
# Depending on the exchange, one or the other method should be called # Depending on the exchange, one or the other method should be called
assert sum([exchange._async_get_trade_history_id.call_count, assert sum([exchange._async_get_trade_history_id.call_count,
@ -1720,8 +1720,8 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match="This exchange does not suport downloading Trades."): match="This exchange does not suport downloading Trades."):
exchange.get_historic_trades(pair, since=trades_history[0]["timestamp"], exchange.get_historic_trades(pair, since=trades_history[0][0],
until=trades_history[-1]["timestamp"]) until=trades_history[-1][0])
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
@ -2145,3 +2145,58 @@ def test_symbol_is_pair(market_symbol, base_currency, quote_currency, expected_r
]) ])
def test_market_is_active(market, expected_result) -> None: def test_market_is_active(market, expected_result) -> None:
assert market_is_active(market) == expected_result assert market_is_active(market) == expected_result
@pytest.mark.parametrize("order,expected", [
([{'fee'}], False),
({'fee': None}, False),
({'fee': {'currency': 'ETH/BTC'}}, False),
({'fee': {'currency': 'ETH/BTC', 'cost': None}}, False),
({'fee': {'currency': 'ETH/BTC', 'cost': 0.01}}, True),
])
def test_order_has_fee(order, expected) -> None:
assert Exchange.order_has_fee(order) == expected
@pytest.mark.parametrize("order,expected", [
({'symbol': 'ETH/BTC', 'fee': {'currency': 'ETH', 'cost': 0.43}},
(0.43, 'ETH', 0.01)),
({'symbol': 'ETH/USDT', 'fee': {'currency': 'USDT', 'cost': 0.01}},
(0.01, 'USDT', 0.01)),
({'symbol': 'BTC/USDT', 'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}},
(0.34, 'USDT', 0.01)),
])
def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
mocker.patch('freqtrade.exchange.Exchange.calculate_fee_rate', MagicMock(return_value=0.01))
ex = get_patched_exchange(mocker, default_conf)
assert ex.extract_cost_curr_rate(order) == expected
@pytest.mark.parametrize("order,expected", [
# Using base-currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.1),
({'symbol': 'ETH/BTC', 'amount': 0.05, 'cost': 0.05,
'fee': {'currency': 'ETH', 'cost': 0.004, 'rate': None}}, 0.08),
# Using quote currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'BTC', 'cost': 0.005}}, 0.1),
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'BTC', 'cost': 0.002, 'rate': None}}, 0.04),
# Using foreign currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944),
({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561,
'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305),
# TODO: More tests here!
# 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),
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005),
])
def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
ex = get_patched_exchange(mocker, default_conf)
assert ex.calculate_fee_rate(order) == expected

View File

@ -2,7 +2,7 @@
import random import random
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock, PropertyMock
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -10,8 +10,9 @@ import pytest
from arrow import Arrow from arrow import Arrow
from freqtrade import constants from freqtrade import constants
from freqtrade.commands.optimize_commands import (setup_optimize_configuration,
start_backtesting)
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.btanalysis import evaluate_result_multi
from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.converter import clean_ohlcv_dataframe
@ -333,8 +334,9 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
default_conf['ticker_interval'] = '1m' default_conf['ticker_interval'] = '1m'
default_conf['datadir'] = testdatadir default_conf['datadir'] = testdatadir
default_conf['export'] = None default_conf['export'] = None
@ -362,9 +364,9 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
default_conf['ticker_interval'] = "1m" default_conf['ticker_interval'] = "1m"
default_conf['datadir'] = testdatadir default_conf['datadir'] = testdatadir
default_conf['export'] = None default_conf['export'] = None
@ -375,6 +377,29 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
backtesting.start() backtesting.start()
def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
mocker.patch('freqtrade.data.history.history_utils.load_pair_history',
MagicMock(return_value=pd.DataFrame()))
mocker.patch('freqtrade.data.history.get_timerange', get_timerange)
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=[]))
default_conf['ticker_interval'] = "1m"
default_conf['datadir'] = testdatadir
default_conf['export'] = None
default_conf['timerange'] = '20180101-20180102'
with pytest.raises(OperationalException, match='No pair in whitelist.'):
Backtesting(default_conf)
default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}]
with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'):
Backtesting(default_conf)
def test_backtest(default_conf, fee, mocker, testdatadir) -> None: def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False default_conf['ask_strategy']['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
@ -585,12 +610,12 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock())
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
@ -624,17 +649,26 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
assert log_has(line, caplog) assert log_has(line, caplog)
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
patch_exchange(mocker) patch_exchange(mocker)
backtestmock = MagicMock() backtestmock = MagicMock()
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
gen_table_mock = MagicMock() gen_table_mock = MagicMock()
mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table', gen_table_mock) sell_reason_mock = MagicMock()
gen_strattable_mock = MagicMock() gen_strattable_mock = MagicMock()
mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table_strategy', gen_strat_summary = MagicMock()
gen_strattable_mock)
mocker.patch.multiple('freqtrade.optimize.optimize_reports',
generate_text_table=gen_table_mock,
generate_text_table_strategy=gen_strattable_mock,
generate_pair_metrics=MagicMock(),
generate_sell_reason_stats=sell_reason_mock,
generate_strategy_metrics=gen_strat_summary,
)
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
@ -656,6 +690,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
assert backtestmock.call_count == 2 assert backtestmock.call_count == 2
assert gen_table_mock.call_count == 4 assert gen_table_mock.call_count == 4
assert gen_strattable_mock.call_count == 1 assert gen_strattable_mock.call_count == 1
assert sell_reason_mock.call_count == 2
assert gen_strat_summary.call_count == 1
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
exists = [ exists = [
@ -676,3 +712,92 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
for line in exists: for line in exists:
assert log_has(line, caplog) assert log_has(line, caplog)
@pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys):
patch_exchange(mocker)
backtestmock = MagicMock(side_effect=[
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
'profit_percent': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'open_time': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00', ], utc=True
),
'close_time': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00', ], utc=True),
'open_index': [78, 184],
'close_index': [125, 192],
'trade_duration': [235, 40],
'open_at_end': [False, False],
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI]
}),
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
'profit_percent': [0.03, 0.01, 0.1],
'profit_abs': [0.01, 0.02, 0.2],
'open_time': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00',
'2018-01-30 05:30:00'], utc=True
),
'close_time': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00',
'2018-01-30 08:30:00'], utc=True),
'open_index': [78, 184, 185],
'close_index': [125, 224, 205],
'trade_duration': [47, 40, 20],
'open_at_end': [False, False, False],
'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}),
])
mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
patched_configuration_load_config_file(mocker, default_conf)
args = [
'backtesting',
'--config', 'config.json',
'--datadir', str(testdatadir),
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
'--ticker-interval', '1m',
'--timerange', '1510694220-1510700340',
'--enable-position-stacking',
'--disable-max-market-positions',
'--strategy-list',
'DefaultStrategy',
'TestStrategyLegacy',
]
args = get_args(args)
start_backtesting(args)
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Loading data from 2017-11-14T20:57:00+00:00 '
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Backtesting with data from 2017-11-14T21:17:00+00:00 '
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy',
'Running backtesting for Strategy TestStrategyLegacy',
]
for line in exists:
assert log_has(line, caplog)
captured = capsys.readouterr()
assert 'BACKTESTING REPORT' in captured.out
assert 'SELL REASON STATS' in captured.out
assert 'LEFT OPEN TRADES REPORT' in captured.out
assert 'STRATEGY SUMMARY' in captured.out

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import locale import locale
import logging
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List from typing import Dict, List
@ -56,14 +57,14 @@ def hyperopt_results():
# Functions for recurrent object patching # Functions for recurrent object patching
def create_trials(mocker, hyperopt, testdatadir) -> List[Dict]: def create_results(mocker, hyperopt, testdatadir) -> List[Dict]:
""" """
When creating trials, mock the hyperopt Trials so that *by default* When creating results, mock the hyperopt so that *by default*
- we don't create any pickle'd files in the filesystem - we don't create any pickle'd files in the filesystem
- we might have a pickle'd file so make sure that we return - we might have a pickle'd file so make sure that we return
false when looking for it false when looking for it
""" """
hyperopt.trials_file = testdatadir / 'optimize/ut_trials.pickle' hyperopt.results_file = testdatadir / 'optimize/ut_results.pickle'
mocker.patch.object(Path, "is_file", MagicMock(return_value=False)) mocker.patch.object(Path, "is_file", MagicMock(return_value=False))
stat_mock = MagicMock() stat_mock = MagicMock()
@ -477,28 +478,30 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
assert caplog.record_tuples == [] assert caplog.record_tuples == []
def test_save_trials_saves_trials(mocker, hyperopt, testdatadir, caplog) -> None: def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
trials = create_trials(mocker, hyperopt, testdatadir) epochs = create_results(mocker, hyperopt, testdatadir)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
trials_file = testdatadir / 'optimize' / 'ut_trials.pickle' results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt.trials = trials caplog.set_level(logging.DEBUG)
hyperopt.save_trials(final=True)
assert log_has(f"1 epoch saved to '{trials_file}'.", caplog) hyperopt.epochs = epochs
hyperopt._save_results()
assert log_has(f"1 epoch saved to '{results_file}'.", caplog)
mock_dump.assert_called_once() mock_dump.assert_called_once()
hyperopt.trials = trials + trials hyperopt.epochs = epochs + epochs
hyperopt.save_trials(final=True) hyperopt._save_results()
assert log_has(f"2 epochs saved to '{trials_file}'.", caplog) assert log_has(f"2 epochs saved to '{results_file}'.", caplog)
def test_read_trials_returns_trials_file(mocker, hyperopt, testdatadir, caplog) -> None: def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
trials = create_trials(mocker, hyperopt, testdatadir) epochs = create_results(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials) mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs)
trials_file = testdatadir / 'optimize' / 'ut_trials.pickle' results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt_trial = hyperopt._read_trials(trials_file) hyperopt_epochs = hyperopt._read_results(results_file)
assert log_has(f"Reading Trials from '{trials_file}'", caplog) assert log_has(f"Reading epochs from '{results_file}'", caplog)
assert hyperopt_trial == trials assert hyperopt_epochs == epochs
mock_load.assert_called_once() mock_load.assert_called_once()
@ -817,7 +820,7 @@ def test_continue_hyperopt(mocker, default_conf, caplog):
Hyperopt(default_conf) Hyperopt(default_conf)
assert unlinkmock.call_count == 0 assert unlinkmock.call_count == 0
assert log_has(f"Continuing on previous hyperopt results.", caplog) assert log_has("Continuing on previous hyperopt results.", caplog)
def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:

View File

@ -1,11 +1,13 @@
from pathlib import Path from pathlib import Path
import pandas as pd import pandas as pd
import pytest
from arrow import Arrow from arrow import Arrow
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.optimize.optimize_reports import ( from freqtrade.optimize.optimize_reports import (
generate_edge_table, generate_text_table, generate_text_table_sell_reason, generate_pair_metrics, generate_edge_table, generate_sell_reason_stats,
generate_text_table, generate_text_table_sell_reason, generate_strategy_metrics,
generate_text_table_strategy, store_backtest_result) generate_text_table_strategy, store_backtest_result)
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from tests.conftest import patch_exchange from tests.conftest import patch_exchange
@ -35,12 +37,39 @@ def test_generate_text_table(default_conf, mocker):
'| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |' '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |'
' 15.00 | 0:20:00 | 2 | 0 | 0 |' ' 15.00 | 0:20:00 | 2 | 0 | 0 |'
) )
assert generate_text_table(data={'ETH/BTC': {}},
stake_currency='BTC', max_open_trades=2, pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
results=results) == result_str max_open_trades=2, results=results)
assert generate_text_table(pair_results,
stake_currency='BTC') == result_str
def test_generate_text_table_sell_reason(default_conf, mocker): def test_generate_pair_metrics(default_conf, mocker):
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2],
'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30],
'wins': [2, 0],
'draws': [0, 0],
'losses': [0, 0]
}
)
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
max_open_trades=2, results=results)
assert isinstance(pair_results, list)
assert len(pair_results) == 2
assert pair_results[-1]['key'] == 'TOTAL'
assert (
pytest.approx(pair_results[-1]['profit_mean_pct']) == pair_results[-1]['profit_mean'] * 100)
assert (
pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100)
def test_generate_text_table_sell_reason(default_conf):
results = pd.DataFrame( results = pd.DataFrame(
{ {
@ -65,8 +94,46 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
'| stop_loss | 1 | 0 | 0 | 1 |' '| stop_loss | 1 | 0 | 0 | 1 |'
' -10 | -10 | -0.2 | -5 |' ' -10 | -10 | -0.2 | -5 |'
) )
assert generate_text_table_sell_reason(stake_currency='BTC', max_open_trades=2,
results=results) == result_str sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
results=results)
assert generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats,
stake_currency='BTC') == result_str
def test_generate_sell_reason_stats(default_conf):
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2, -0.1],
'profit_abs': [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10],
'wins': [2, 0, 0],
'draws': [0, 0, 0],
'losses': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}
)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=2,
results=results)
roi_result = sell_reason_stats[0]
assert roi_result['sell_reason'] == 'roi'
assert roi_result['trades'] == 2
assert pytest.approx(roi_result['profit_mean']) == 0.15
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2)
assert pytest.approx(roi_result['profit_mean']) == 0.15
assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2)
stop_result = sell_reason_stats[1]
assert stop_result['sell_reason'] == 'stop_loss'
assert stop_result['trades'] == 1
assert pytest.approx(stop_result['profit_mean']) == -0.1
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
assert pytest.approx(stop_result['profit_mean']) == -0.1
assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2)
def test_generate_text_table_strategy(default_conf, mocker): def test_generate_text_table_strategy(default_conf, mocker):
@ -106,7 +173,12 @@ def test_generate_text_table_strategy(default_conf, mocker):
'| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |'
' 45.00 | 0:20:00 | 3 | 0 | 0 |' ' 45.00 | 0:20:00 | 3 | 0 | 0 |'
) )
assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str
strategy_results = generate_strategy_metrics(stake_currency='BTC',
max_open_trades=2,
all_results=results)
assert generate_text_table_strategy(strategy_results, 'BTC') == result_str
def test_generate_edge_table(edge_conf, mocker): def test_generate_edge_table(edge_conf, mocker):

View File

@ -4,13 +4,11 @@ from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
from freqtrade.exceptions import OperationalException
from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.resolvers import PairListResolver from freqtrade.exceptions import OperationalException
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
from tests.conftest import get_patched_freqtradebot, log_has_re from freqtrade.resolvers import PairListResolver
from tests.conftest import get_patched_freqtradebot, log_has, log_has_re
# whitelist, blacklist
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -36,6 +34,28 @@ def whitelist_conf(default_conf):
return default_conf return default_conf
@pytest.fixture(scope="function")
def whitelist_conf_2(default_conf):
default_conf['stake_currency'] = 'BTC'
default_conf['exchange']['pair_whitelist'] = [
'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC',
'BTT/BTC', 'HOT/BTC', 'FUEL/BTC', 'XRP/BTC'
]
default_conf['exchange']['pair_blacklist'] = [
'BLK/BTC'
]
default_conf['pairlists'] = [
# { "method": "StaticPairList"},
{
"method": "VolumePairList",
"number_assets": 5,
"sort_key": "quoteVolume",
"refresh_period": 0,
},
]
return default_conf
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def static_pl_conf(whitelist_conf): def static_pl_conf(whitelist_conf):
whitelist_conf['pairlists'] = [ whitelist_conf['pairlists'] = [
@ -55,7 +75,7 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
logmock = MagicMock() logmock = MagicMock()
# Assign starting whitelist # Assign starting whitelist
pl = freqtrade.pairlists._pairlists[0] pl = freqtrade.pairlists._pairlist_handlers[0]
pl.log_on_refresh(logmock, 'Hello world') pl.log_on_refresh(logmock, 'Hello world')
assert logmock.call_count == 1 assert logmock.call_count == 1
pl.log_on_refresh(logmock, 'Hello world') pl.log_on_refresh(logmock, 'Hello world')
@ -69,44 +89,44 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers):
def test_load_pairlist_noexist(mocker, markets, default_conf): def test_load_pairlist_noexist(mocker, markets, default_conf):
bot = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
plm = PairListManager(bot.exchange, default_conf) plm = PairListManager(freqtrade.exchange, default_conf)
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r"Impossible to load Pairlist 'NonexistingPairList'. " match=r"Impossible to load Pairlist 'NonexistingPairList'. "
r"This class does not exist or contains Python code errors."): r"This class does not exist or contains Python code errors."):
PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm, PairListResolver.load_pairlist('NonexistingPairList', freqtrade.exchange, plm,
default_conf, {}, 1) default_conf, {}, 1)
def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf) freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
freqtradebot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume # List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed # Ensure all except those in whitelist are removed
assert set(whitelist) == set(freqtradebot.pairlists.whitelist) assert set(whitelist) == set(freqtrade.pairlists.whitelist)
# Ensure config dict hasn't been changed # Ensure config dict hasn't been changed
assert (static_pl_conf['exchange']['pair_whitelist'] == assert (static_pl_conf['exchange']['pair_whitelist'] ==
freqtradebot.config['exchange']['pair_whitelist']) freqtrade.config['exchange']['pair_whitelist'])
def test_refresh_static_pairlist(mocker, markets, static_pl_conf): def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf) freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
markets=PropertyMock(return_value=markets), markets=PropertyMock(return_value=markets),
) )
freqtradebot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume # List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed # Ensure all except those in whitelist are removed
assert set(whitelist) == set(freqtradebot.pairlists.whitelist) assert set(whitelist) == set(freqtrade.pairlists.whitelist)
assert static_pl_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
@ -116,7 +136,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
get_tickers=tickers, get_tickers=tickers,
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
) )
bot = get_patched_freqtradebot(mocker, whitelist_conf) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
# Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture # Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -124,19 +144,44 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co
) )
# argument: use the whitelist dynamically by exchange-volume # argument: use the whitelist dynamically by exchange-volume
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']
bot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert whitelist == freqtrade.pairlists.whitelist
assert whitelist == bot.pairlists.whitelist
whitelist_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {}
}
]
whitelist_conf['pairlists'] = [{'method': 'VolumePairList'}]
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r'`number_assets` not specified. Please check your configuration ' match=r'`number_assets` not specified. Please check your configuration '
r'for "pairlist.config.number_assets"'): r'for "pairlist.config.number_assets"'):
PairListManager(bot.exchange, whitelist_conf) PairListManager(freqtrade.exchange, whitelist_conf)
def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_conf_2):
tickers_dict = tickers()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
)
# Remove caching of ticker data to emulate changing volume by the time of second call
mocker.patch.multiple(
'freqtrade.pairlist.pairlistmanager.PairListManager',
_get_cached_tickers=MagicMock(return_value=tickers_dict),
)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_2)
# Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=shitcoinmarkets),
)
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']
freqtrade.pairlists.refresh_pairlist()
assert whitelist == freqtrade.pairlists.whitelist
whitelist = ['FUEL/BTC', 'ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']
tickers_dict['FUEL/BTC']['quoteVolume'] = 10000.0
freqtrade.pairlists.refresh_pairlist()
assert whitelist == freqtrade.pairlists.whitelist
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
@ -144,19 +189,20 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
) )
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty)) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty))
# argument: use the whitelist dynamically by exchange-volume # argument: use the whitelist dynamically by exchange-volume
whitelist = [] whitelist = []
whitelist_conf['exchange']['pair_whitelist'] = [] whitelist_conf['exchange']['pair_whitelist'] = []
freqtradebot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
pairslist = whitelist_conf['exchange']['pair_whitelist'] pairslist = whitelist_conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist) assert set(whitelist) == set(pairslist)
@pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [ @pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [
# VolumePairList only
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']),
# Different sorting depending on quote or bid volume # Different sorting depending on quote or bid volume
@ -164,9 +210,20 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']),
# No pair for ETH ... # No pair for ETH, VolumePairList
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"ETH", []), "ETH", []),
# No pair for ETH, StaticPairList
([{"method": "StaticPairList"}],
"ETH", []),
# No pair for ETH, all handlers
([{"method": "StaticPairList"},
{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.03},
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
{"method": "ShuffleFilter"}],
"ETH", []),
# Precisionfilter and quote volume # Precisionfilter and quote volume
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), {"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
@ -184,28 +241,41 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
# Hot is removed by precision_filter, Fuel by low_price_filter. # Hot is removed by precision_filter, Fuel by low_price_filter.
([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}, {"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.02} {"method": "PriceFilter", "low_price_ratio": 0.02}],
], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
# HOT and XRP are removed because below 1250 quoteVolume # HOT and XRP are removed because below 1250 quoteVolume
([{"method": "VolumePairList", "number_assets": 5, ([{"method": "VolumePairList", "number_assets": 5,
"sort_key": "quoteVolume", "min_value": 1250}], "sort_key": "quoteVolume", "min_value": 1250}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
# StaticPairlist Only # StaticPairlist only
([{"method": "StaticPairList"}, ([{"method": "StaticPairList"}],
], "BTC", ['ETH/BTC', 'TKN/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC']),
# Static Pairlist before VolumePairList - sorting changes # Static Pairlist before VolumePairList - sorting changes
([{"method": "StaticPairList"}, ([{"method": "StaticPairList"},
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, {"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
], "BTC", ['TKN/BTC', 'ETH/BTC']), "BTC", ['TKN/BTC', 'ETH/BTC']),
# SpreadFilter # SpreadFilter
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "SpreadFilter", "max_spread": 0.005} {"method": "SpreadFilter", "max_spread_ratio": 0.005}],
], "USDT", ['ETH/USDT']), "USDT", ['ETH/USDT']),
# ShuffleFilter
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter", "seed": 77}],
"USDT", ['ETH/USDT', 'ADAHALF/USDT', 'NANO/USDT']),
# ShuffleFilter, other seed
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter", "seed": 42}],
"USDT", ['NANO/USDT', 'ETH/USDT', 'ADAHALF/USDT']),
# ShuffleFilter, no seed
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter"}],
"USDT", 3),
]) ])
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
pairlists, base_currency, whitelist_result, pairlists, base_currency, whitelist_result,
caplog) -> None: caplog) -> None:
whitelist_conf['pairlists'] = pairlists whitelist_conf['pairlists'] = pairlists
whitelist_conf['stake_currency'] = base_currency
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
@ -215,32 +285,44 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
markets=PropertyMock(return_value=shitcoinmarkets), markets=PropertyMock(return_value=shitcoinmarkets),
) )
freqtrade.config['stake_currency'] = base_currency
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
whitelist = freqtrade.pairlists.whitelist whitelist = freqtrade.pairlists.whitelist
assert isinstance(whitelist, list)
# Verify length of pairlist matches (used for ShuffleFilter without seed)
if type(whitelist_result) is list:
assert whitelist == whitelist_result assert whitelist == whitelist_result
else:
len(whitelist) == whitelist_result
for pairlist in pairlists: for pairlist in pairlists:
if pairlist['method'] == 'PrecisionFilter': if pairlist['method'] == 'PrecisionFilter' and whitelist_result:
assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' assert log_has_re(r'^Removed .* from whitelist, because stop price .* '
r'would be <= stop limit.*', caplog) r'would be <= stop limit.*', caplog)
if pairlist['method'] == 'PriceFilter': if pairlist['method'] == 'PriceFilter' and whitelist_result:
assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] is empty.*", log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] is empty.*",
caplog)) caplog))
if pairlist['method'] == 'VolumePairList':
logmsg = ("DEPRECATED: using any key other than quoteVolume for "
"VolumePairList is deprecated.")
if pairlist['sort_key'] != 'quoteVolume':
assert log_has(logmsg, caplog)
else:
assert not log_has(logmsg, caplog)
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
default_conf['pairlists'] = [{'method': 'VolumePairList', default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
'config': {'number_assets': 10}
}]
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
get_tickers=tickers, get_tickers=tickers,
exchange_has=MagicMock(return_value=False), exchange_has=MagicMock(return_value=False),
) )
with pytest.raises(OperationalException): with pytest.raises(OperationalException,
match=r'Exchange does not support dynamic whitelist.*'):
get_patched_freqtradebot(mocker, default_conf) get_patched_freqtradebot(mocker, default_conf)
@ -283,7 +365,8 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist
caplog.clear() caplog.clear()
# Assign starting whitelist # Assign starting whitelist
new_whitelist = freqtrade.pairlists._pairlists[0]._whitelist_for_active_markets(whitelist) pairlist_handler = freqtrade.pairlists._pairlist_handlers[0]
new_whitelist = pairlist_handler._whitelist_for_active_markets(whitelist)
assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC']) assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC'])
assert log_message in caplog.text assert log_message in caplog.text
@ -305,18 +388,18 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
get_tickers=tickers get_tickers=tickers
) )
bot = get_patched_freqtradebot(mocker, whitelist_conf) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
assert bot.pairlists._pairlists[0]._last_refresh == 0 assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == 0
assert tickers.call_count == 0 assert tickers.call_count == 0
bot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert tickers.call_count == 1 assert tickers.call_count == 1
assert bot.pairlists._pairlists[0]._last_refresh != 0 assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh != 0
lrf = bot.pairlists._pairlists[0]._last_refresh lrf = freqtrade.pairlists._pairlist_handlers[0]._last_refresh
bot.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert tickers.call_count == 1 assert tickers.call_count == 1
# Time should not be updated. # Time should not be updated.
assert bot.pairlists._pairlists[0]._last_refresh == lrf assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
@ -325,5 +408,5 @@ def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
whitelist_conf['pairlists'] = [] whitelist_conf['pairlists'] = []
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r"No Pairlist defined!"): match=r"No Pairlist Handlers defined"):
get_patched_freqtradebot(mocker, whitelist_conf) get_patched_freqtradebot(mocker, whitelist_conf)

View File

@ -49,13 +49,19 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'base_currency': 'BTC', 'base_currency': 'BTC',
'open_date': ANY, 'open_date': ANY,
'open_date_hum': ANY, 'open_date_hum': ANY,
'open_timestamp': ANY,
'is_open': ANY, 'is_open': ANY,
'fee_open': ANY, 'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': ANY, 'fee_close': ANY,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY, 'open_rate_requested': ANY,
'open_trade_price': ANY, 'open_trade_price': ANY,
'close_rate_requested': ANY, 'close_rate_requested': ANY,
'sell_reason': ANY, 'sell_reason': ANY,
'sell_order_status': ANY,
'min_rate': ANY, 'min_rate': ANY,
'max_rate': ANY, 'max_rate': ANY,
'strategy': ANY, 'strategy': ANY,
@ -63,13 +69,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_order_id': ANY, 'open_order_id': ANY,
'close_date': None, 'close_date': None,
'close_date_hum': None, 'close_date_hum': None,
'close_timestamp': None,
'open_rate': 1.098e-05, 'open_rate': 1.098e-05,
'close_rate': None, 'close_rate': None,
'current_rate': 1.099e-05, 'current_rate': 1.099e-05,
'amount': 91.07468124, 'amount': 91.07468124,
'stake_amount': 0.001, 'stake_amount': 0.001,
'close_profit': None, 'close_profit': None,
'current_profit': -0.41, 'close_profit_pct': None,
'current_profit': -0.00408133,
'current_profit_pct': -0.41,
'stop_loss': 0.0, 'stop_loss': 0.0,
'initial_stop_loss': 0.0, 'initial_stop_loss': 0.0,
'initial_stop_loss_pct': None, 'initial_stop_loss_pct': None,
@ -78,7 +87,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
} == results[0] } == results[0]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
results = rpc._rpc_trade_status() results = rpc._rpc_trade_status()
assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_profit'])
assert isnan(results[0]['current_rate']) assert isnan(results[0]['current_rate'])
@ -88,13 +97,19 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'base_currency': 'BTC', 'base_currency': 'BTC',
'open_date': ANY, 'open_date': ANY,
'open_date_hum': ANY, 'open_date_hum': ANY,
'open_timestamp': ANY,
'is_open': ANY, 'is_open': ANY,
'fee_open': ANY, 'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': ANY, 'fee_close': ANY,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY, 'open_rate_requested': ANY,
'open_trade_price': ANY, 'open_trade_price': ANY,
'close_rate_requested': ANY, 'close_rate_requested': ANY,
'sell_reason': ANY, 'sell_reason': ANY,
'sell_order_status': ANY,
'min_rate': ANY, 'min_rate': ANY,
'max_rate': ANY, 'max_rate': ANY,
'strategy': ANY, 'strategy': ANY,
@ -102,13 +117,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_order_id': ANY, 'open_order_id': ANY,
'close_date': None, 'close_date': None,
'close_date_hum': None, 'close_date_hum': None,
'close_timestamp': None,
'open_rate': 1.098e-05, 'open_rate': 1.098e-05,
'close_rate': None, 'close_rate': None,
'current_rate': ANY, 'current_rate': ANY,
'amount': 91.07468124, 'amount': 91.07468124,
'stake_amount': 0.001, 'stake_amount': 0.001,
'close_profit': None, 'close_profit': None,
'close_profit_pct': None,
'current_profit': ANY, 'current_profit': ANY,
'current_profit_pct': ANY,
'stop_loss': 0.0, 'stop_loss': 0.0,
'initial_stop_loss': 0.0, 'initial_stop_loss': 0.0,
'initial_stop_loss_pct': None, 'initial_stop_loss_pct': None,
@ -157,7 +175,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert '-0.41% (-0.06)' == result[0][3] assert '-0.41% (-0.06)' == result[0][3]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' == result[0][2] assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1] assert 'ETH/BTC' in result[0][1]
@ -195,16 +213,18 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
# Try valid data # Try valid data
update.message.text = '/daily 2' update.message.text = '/daily 2'
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency) days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert len(days) == 7 assert len(days['data']) == 7
for day in days: assert days['stake_currency'] == default_conf['stake_currency']
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
for day in days['data']:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD'] # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
assert (day[1] == '0.00000000 BTC' or assert (day['abs_profit'] == '0.00000000' or
day[1] == '0.00006217 BTC') day['abs_profit'] == '0.00006217')
assert (day[2] == '0.000 USD' or assert (day['fiat_value'] == '0.000' or
day[2] == '0.767 USD') day['fiat_value'] == '0.767')
# ensure first day is current date # ensure first day is current date
assert str(days[0][0]) == str(datetime.utcnow().date()) assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
# Try invalid data # Try invalid data
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'): with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
@ -307,7 +327,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
# Test non-available pair # Test non-available pair
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available")))
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats['trade_count'] == 2 assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now' assert stats['first_trade_date'] == 'just now'

View File

@ -39,16 +39,21 @@ def client_post(client, url, data={}):
return client.post(url, return client.post(url,
content_type="application/json", content_type="application/json",
data=data, data=data,
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)}) headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'example.com'})
def client_get(client, url): def client_get(client, url):
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)}) # Add fake Origin to ensure CORS kicks in
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'example.com'})
def assert_response(response, expected_code=200): def assert_response(response, expected_code=200, needs_cors=True):
assert response.status_code == expected_code assert response.status_code == expected_code
assert response.content_type == "application/json" assert response.content_type == "application/json"
if needs_cors:
assert ('Access-Control-Allow-Credentials', 'true') in response.headers._list
def test_api_not_found(botclient): def test_api_not_found(botclient):
@ -65,12 +70,12 @@ def test_api_not_found(botclient):
def test_api_unauthorized(botclient): def test_api_unauthorized(botclient):
ftbot, client = botclient ftbot, client = botclient
rc = client.get(f"{BASE_URI}/ping") rc = client.get(f"{BASE_URI}/ping")
assert_response(rc) assert_response(rc, needs_cors=False)
assert rc.json == {'status': 'pong'} assert rc.json == {'status': 'pong'}
# Don't send user/pass information # Don't send user/pass information
rc = client.get(f"{BASE_URI}/version") rc = client.get(f"{BASE_URI}/version")
assert_response(rc, 401) assert_response(rc, 401, needs_cors=False)
assert rc.json == {'error': 'Unauthorized'} assert rc.json == {'error': 'Unauthorized'}
# Change only username # Change only username
@ -94,6 +99,35 @@ def test_api_unauthorized(botclient):
assert rc.json == {'error': 'Unauthorized'} assert rc.json == {'error': 'Unauthorized'}
def test_api_token_login(botclient):
ftbot, client = botclient
rc = client_post(client, f"{BASE_URI}/token/login")
assert_response(rc)
assert 'access_token' in rc.json
assert 'refresh_token' in rc.json
# test Authentication is working with JWT tokens too
rc = client.get(f"{BASE_URI}/count",
content_type="application/json",
headers={'Authorization': f'Bearer {rc.json["access_token"]}',
'Origin': 'example.com'})
assert_response(rc)
def test_api_token_refresh(botclient):
ftbot, client = botclient
rc = client_post(client, f"{BASE_URI}/token/login")
assert_response(rc)
rc = client.post(f"{BASE_URI}/token/refresh",
content_type="application/json",
data=None,
headers={'Authorization': f'Bearer {rc.json["refresh_token"]}',
'Origin': 'example.com'})
assert_response(rc)
assert 'access_token' in rc.json
assert 'refresh_token' not in rc.json
def test_api_stop_workflow(botclient): def test_api_stop_workflow(botclient):
ftbot, client = botclient ftbot, client = botclient
assert ftbot.state == State.RUNNING assert ftbot.state == State.RUNNING
@ -123,6 +157,12 @@ def test_api__init__(default_conf, mocker):
""" """
Test __init__() method Test __init__() method
""" """
default_conf.update({"api_server": {"enabled": True,
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"username": "TestUser",
"password": "testPass",
}})
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock()) mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock())
@ -283,6 +323,7 @@ def test_api_show_config(botclient, mocker):
assert 'dry_run' in rc.json assert 'dry_run' in rc.json
assert rc.json['exchange'] == 'bittrex' assert rc.json['exchange'] == 'bittrex'
assert rc.json['ticker_interval'] == '5m' assert rc.json['ticker_interval'] == '5m'
assert rc.json['state'] == 'running'
assert not rc.json['trailing_stop'] assert not rc.json['trailing_stop']
@ -298,8 +339,10 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
) )
rc = client_get(client, f"{BASE_URI}/daily") rc = client_get(client, f"{BASE_URI}/daily")
assert_response(rc) assert_response(rc)
assert len(rc.json) == 7 assert len(rc.json['data']) == 7
assert rc.json[0][0] == str(datetime.utcnow().date()) assert rc.json['stake_currency'] == 'BTC'
assert rc.json['fiat_display_currency'] == 'USD'
assert rc.json['data'][0]['date'] == str(datetime.utcnow().date())
def test_api_trades(botclient, mocker, ticker, fee, markets): def test_api_trades(botclient, mocker, ticker, fee, markets):
@ -377,7 +420,9 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
'best_pair': 'ETH/BTC', 'best_pair': 'ETH/BTC',
'best_rate': 6.2, 'best_rate': 6.2,
'first_trade_date': 'just now', 'first_trade_date': 'just now',
'first_trade_timestamp': ANY,
'latest_trade_date': 'just now', 'latest_trade_date': 'just now',
'latest_trade_timestamp': ANY,
'profit_all_coin': 6.217e-05, 'profit_all_coin': 6.217e-05,
'profit_all_fiat': 0, 'profit_all_fiat': 0,
'profit_all_percent': 6.2, 'profit_all_percent': 6.2,
@ -454,14 +499,18 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'base_currency': 'BTC', 'base_currency': 'BTC',
'close_date': None, 'close_date': None,
'close_date_hum': None, 'close_date_hum': None,
'close_timestamp': None,
'close_profit': None, 'close_profit': None,
'close_profit_pct': None,
'close_rate': None, 'close_rate': None,
'current_profit': -0.41, 'current_profit': -0.00408133,
'current_profit_pct': -0.41,
'current_rate': 1.099e-05, 'current_rate': 1.099e-05,
'initial_stop_loss': 0.0, 'initial_stop_loss': 0.0,
'initial_stop_loss_pct': None, 'initial_stop_loss_pct': None,
'open_date': ANY, 'open_date': ANY,
'open_date_hum': 'just now', 'open_date_hum': 'just now',
'open_timestamp': ANY,
'open_order': '(limit buy rem=0.00000000)', 'open_order': '(limit buy rem=0.00000000)',
'open_rate': 1.098e-05, 'open_rate': 1.098e-05,
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -472,7 +521,11 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'close_rate_requested': None, 'close_rate_requested': None,
'current_rate': 1.099e-05, 'current_rate': 1.099e-05,
'fee_close': 0.0025, 'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025, 'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'open_date': ANY, 'open_date': ANY,
'is_open': True, 'is_open': True,
'max_rate': 0.0, 'max_rate': 0.0,
@ -481,6 +534,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'open_rate_requested': 1.098e-05, 'open_rate_requested': 1.098e-05,
'open_trade_price': 0.0010025, 'open_trade_price': 0.0010025,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None,
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'ticker_interval': 5}] 'ticker_interval': 5}]
@ -561,11 +615,13 @@ def test_api_forcebuy(botclient, mocker, fee):
assert rc.json == {'amount': 1, assert rc.json == {'amount': 1,
'close_date': None, 'close_date': None,
'close_date_hum': None, 'close_date_hum': None,
'close_timestamp': None,
'close_rate': 0.265441, 'close_rate': 0.265441,
'initial_stop_loss': None, 'initial_stop_loss': None,
'initial_stop_loss_pct': None, 'initial_stop_loss_pct': None,
'open_date': ANY, 'open_date': ANY,
'open_date_hum': 'just now', 'open_date_hum': 'just now',
'open_timestamp': ANY,
'open_rate': 0.245441, 'open_rate': 0.245441,
'pair': 'ETH/ETH', 'pair': 'ETH/ETH',
'stake_amount': 1, 'stake_amount': 1,
@ -575,7 +631,11 @@ def test_api_forcebuy(botclient, mocker, fee):
'close_profit': None, 'close_profit': None,
'close_rate_requested': None, 'close_rate_requested': None,
'fee_close': 0.0025, 'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025, 'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': False, 'is_open': False,
'max_rate': None, 'max_rate': None,
'min_rate': None, 'min_rate': None,
@ -583,6 +643,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'open_rate_requested': None, 'open_rate_requested': None,
'open_trade_price': 0.2460546025, 'open_trade_price': 0.2460546025,
'sell_reason': None, 'sell_reason': None,
'sell_order_status': None,
'strategy': None, 'strategy': None,
'ticker_interval': None 'ticker_interval': None
} }

View File

@ -166,10 +166,12 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None:
'current_rate': 1.098e-05, 'current_rate': 1.098e-05,
'amount': 90.99181074, 'amount': 90.99181074,
'stake_amount': 90.99181074, 'stake_amount': 90.99181074,
'close_profit': None, 'close_profit_pct': None,
'current_profit': -0.59, 'current_profit': -0.0059,
'current_profit_pct': -0.59,
'initial_stop_loss': 1.098e-05, 'initial_stop_loss': 1.098e-05,
'stop_loss': 1.099e-05, 'stop_loss': 1.099e-05,
'sell_order_status': None,
'initial_stop_loss_pct': -0.05, 'initial_stop_loss_pct': -0.05,
'stop_loss_pct': -0.01, 'stop_loss_pct': -0.01,
'open_order': '(limit buy rem=0.00000000)' 'open_order': '(limit buy rem=0.00000000)'

Some files were not shown because too many files have changed in this diff Show More