Merge branch 'develop' into hyperopt-trailing-space

This commit is contained in:
hroff-1902 2019-11-23 03:42:58 +03:00 committed by GitHub
commit e7ddd81251
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 2364 additions and 892 deletions

230
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,230 @@
name: Freqtrade CI
on:
push:
branches:
- master
- develop
- github_actions_tests
tags:
pull_request:
schedule:
- cron: '0 5 * * 4'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-18.04, macos-latest ]
python-version: [3.7]
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Cache_dependencies
uses: actions/cache@v1
id: cache
with:
path: ~/dependencies/
key: ${{ runner.os }}-dependencies
- name: pip cache (linux)
uses: actions/cache@preview
if: startsWith(matrix.os, 'ubuntu')
with:
path: ~/.cache/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
- name: pip cache (macOS)
uses: actions/cache@preview
if: startsWith(matrix.os, 'macOS')
with:
path: ~/Library/Caches/pip
key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip
- name: TA binary *nix
if: steps.cache.outputs.cache-hit != 'true'
run: |
cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- name: Installation - *nix
run: |
python -m pip install --upgrade pip
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include
pip install -r requirements-dev.txt
pip install -e .
- name: Tests
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
COVERALLS_SERVICE_NAME: travis-ci
TRAVIS: "true"
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
# Allow failure for coveralls
# Fake travis environment to get coveralls working correctly
export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)"
export CI_BRANCH=${GITHUB_REF#"ref/heads"}
echo "${CI_BRANCH}"
coveralls || true
- name: Backtesting
run: |
cp config.json.example config.json
freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy
- name: Hyperopt
run: |
cp config.json.example config.json
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt
- name: Flake8
run: |
flake8
- name: Mypy
run: |
mypy freqtrade scripts
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: always() && github.repository.fork == true
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI ${{ matrix.os }}*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
build_windows:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-latest ]
python-version: [3.7]
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Pip cache (Windows)
uses: actions/cache@preview
if: startsWith(runner.os, 'Windows')
with:
path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip
restore-keys: ${{ runner.os }}-pip
- name: Installation
run: |
./build_helpers/install_windows.ps1
- name: Tests
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
- name: Backtesting
run: |
cp config.json.example config.json
freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy
- name: Hyperopt
run: |
cp config.json.example config.json
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt
- name: Flake8
run: |
flake8
- name: Mypy
run: |
mypy freqtrade scripts
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: always() && github.repository.fork == true
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI windows*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
docs_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Documentation syntax
run: |
./tests/test_docs.sh
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: failure() && github.repository.fork == true
with:
type: ${{ job.status }}
job_name: '*Freqtrade Docs*'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}
deploy:
needs: [ build, build_windows, docs_check ]
runs-on: ubuntu-18.04
if: (github.event_name == 'push' || github.event_name == 'schedule') && github.repository == 'freqtrade/freqtrade'
steps:
- uses: actions/checkout@v1
- name: Extract branch name
shell: bash
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
id: extract_branch
- name: Build and test and push docker image
env:
IMAGE_NAME: freqtradeorg/freqtrade
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
run: |
build_helpers/publish_docker.sh
- name: Build raspberry image for ${{ steps.extract_branch.outputs.branch }}_pi
uses: elgohr/Publish-Docker-Github-Action@2.7
with:
name: freqtradeorg/freqtrade:${{ steps.extract_branch.outputs.branch }}_pi
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile.pi
# cache: true
cache: ${{ github.event_name != 'schedule' }}
tag_names: true
- name: Slack Notification
uses: homoluctus/slatify@v1.8.0
if: always() && github.repository.fork == true
with:
type: ${{ job.status }}
job_name: '*Freqtrade CI Deploy*'
mention: 'here'
mention_if: 'failure'
channel: '#notifications'
url: ${{ secrets.SLACK_WEBHOOK }}

View File

@ -0,0 +1,18 @@
name: Update Docker Hub Description
on:
push:
branches:
- master
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2.1.0
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade

View File

@ -24,15 +24,15 @@ jobs:
script: script:
- pytest --random-order --cov=freqtrade --cov-config=.coveragerc - pytest --random-order --cov=freqtrade --cov-config=.coveragerc
# Allow failure for coveralls # Allow failure for coveralls
- coveralls || true # - coveralls || true
name: pytest name: pytest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- freqtrade --datadir tests/testdata backtesting - freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy
name: backtest name: backtest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- freqtrade --datadir tests/testdata --strategy SampleStrategy hyperopt --customhyperopt SampleHyperOpts -e 5 - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt
name: hyperopt name: hyperopt
- script: flake8 - script: flake8
name: flake8 name: flake8
@ -45,11 +45,11 @@ jobs:
- script: mypy freqtrade scripts - script: mypy freqtrade scripts
name: mypy name: mypy
- stage: docker # - stage: docker
if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron)) # if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron))
script: # script:
- build_helpers/publish_docker.sh # - build_helpers/publish_docker.sh
name: "Build and test and push docker image" # name: "Build and test and push docker image"
notifications: notifications:
slack: slack:

View File

@ -24,3 +24,5 @@ RUN pip install numpy --no-cache-dir \
COPY . /freqtrade/ COPY . /freqtrade/
RUN pip install -e . --no-cache-dir RUN pip install -e . --no-cache-dir
ENTRYPOINT ["freqtrade"] ENTRYPOINT ["freqtrade"]
# Default to trade mode
CMD [ "trade" ]

View File

@ -38,3 +38,4 @@ RUN ~/berryconda3/bin/pip install -e . --no-cache-dir
RUN [ "cross-build-end" ] RUN [ "cross-build-end" ]
ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"] ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"]
CMD [ "trade" ]

View File

@ -62,7 +62,6 @@ git checkout develop
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/). For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/).
## Basic Usage ## Basic Usage
### Bot commands ### Bot commands
@ -106,7 +105,7 @@ optional arguments:
### Telegram RPC commands ### Telegram RPC commands
Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/) Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
- `/start`: Starts the trader - `/start`: Starts the trader
- `/stop`: Stops the trader - `/stop`: Stops the trader
@ -129,11 +128,6 @@ The project is currently setup in two main branches:
- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested.
- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. - `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature.
## A note on Binance
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
## Support ## Support
### Help / Slack ### Help / Slack

Binary file not shown.

View File

@ -0,0 +1,8 @@
# Downloads don't work automatically, since the URL is regenerated via javascript.
# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib
# Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl"
pip install build_helpers\TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl
pip install -r requirements-dev.txt
pip install -e .

View File

@ -1,17 +1,17 @@
#!/bin/sh #!/bin/sh
# - export TAG=`if [ "$TRAVIS_BRANCH" == "develop" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi`
# Replace / with _ to create a valid tag
TAG=$(echo "${TRAVIS_BRANCH}" | sed -e "s/\//_/")
# Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
echo "Running for ${TAG}"
# Add commit and commit_message to docker container # Add commit and commit_message to docker container
echo "${TRAVIS_COMMIT} ${TRAVIS_COMMIT_MESSAGE}" > freqtrade_commit echo "${GITHUB_SHA}" > freqtrade_commit
if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
echo "event ${TRAVIS_EVENT_TYPE}: full rebuild - skipping cache" echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache"
docker build -t freqtrade:${TAG} . docker build -t freqtrade:${TAG} .
else else
echo "event ${TRAVIS_EVENT_TYPE}: building with cache" echo "event ${GITHUB_EVENT_NAME}: building with cache"
# Pull last build to avoid rebuilding the whole image # Pull last build to avoid rebuilding the whole image
docker pull ${IMAGE_NAME}:${TAG} docker pull ${IMAGE_NAME}:${TAG}
docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} .
@ -23,7 +23,7 @@ if [ $? -ne 0 ]; then
fi fi
# Run backtest # Run backtest
docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} --datadir /tests/testdata backtesting docker run --rm -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy DefaultStrategy
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed running backtest" echo "failed running backtest"
@ -38,12 +38,12 @@ if [ $? -ne 0 ]; then
fi fi
# Tag as latest for develop builds # Tag as latest for develop builds
if [ "${TRAVIS_BRANCH}" = "develop" ]; then if [ "${GITHUB_REF}" = "develop" ]; then
docker tag freqtrade:$TAG ${IMAGE_NAME}:latest docker tag freqtrade:$TAG ${IMAGE_NAME}:latest
fi fi
# Login # Login
echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed login" echo "failed login"

View File

@ -52,6 +52,9 @@
"DOGE/BTC" "DOGE/BTC"
] ]
}, },
"pairlists": [
{"method": "StaticPairList"}
],
"edge": { "edge": {
"enabled": false, "enabled": false,
"process_throttle_secs": 3600, "process_throttle_secs": 3600,
@ -68,7 +71,7 @@
"remove_pumps": false "remove_pumps": false
}, },
"telegram": { "telegram": {
"enabled": true, "enabled": false,
"token": "your_telegram_token", "token": "your_telegram_token",
"chat_id": "your_telegram_chat_id" "chat_id": "your_telegram_chat_id"
}, },

View File

@ -54,6 +54,9 @@
"BNB/BTC" "BNB/BTC"
] ]
}, },
"pairlists": [
{"method": "StaticPairList"}
],
"edge": { "edge": {
"enabled": false, "enabled": false,
"process_throttle_secs": 3600, "process_throttle_secs": 3600,

View File

@ -50,14 +50,18 @@
"buy": "gtc", "buy": "gtc",
"sell": "gtc" "sell": "gtc"
}, },
"pairlist": { "pairlists": [
"method": "VolumePairList", {"method": "StaticPairList"},
"config": { {
"method": "VolumePairList",
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"precision_filter": false "refresh_period": 1800
},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01
} }
}, ],
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
"sandbox": false, "sandbox": false,

View File

@ -46,6 +46,9 @@
] ]
}, },
"pairlists": [
{"method": "StaticPairList"}
],
"edge": { "edge": {
"enabled": false, "enabled": false,
"process_throttle_secs": 3600, "process_throttle_secs": 3600,

View File

@ -8,6 +8,9 @@ If you do not know what things mentioned here mean, you probably do not need it.
Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
!!! Note
Certain systems (like Raspbian) don't load service unit files from the user directory. In this case, copy `freqtrade.service` into `/etc/systemd/user/` (requires superuser permissions).
After that you can start the daemon with: After that you can start the daemon with:
```bash ```bash

View File

@ -45,7 +45,7 @@ freqtrade --datadir user_data/data/bittrex-20180101 backtesting
#### With a (custom) strategy file #### With a (custom) strategy file
```bash ```bash
freqtrade -s SampleStrategy backtesting freqtrade backtesting -s SampleStrategy
``` ```
Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory. Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory.

View File

@ -5,20 +5,18 @@ This page explains the different parameters of the bot and how to run it.
!!! Note !!! Note
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
## Bot commands ## Bot commands
``` ```
usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] usage: freqtrade [-h] [-V]
[--userdir PATH] [-s NAME] [--strategy-path PATH] {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
[--db-url PATH] [--sd-notify]
{backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
... ...
Free, open source crypto trading bot Free, open source crypto trading bot
positional arguments: positional arguments:
{backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit} {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
trade Trade module.
backtesting Backtesting module. backtesting Backtesting module.
edge Edge module. edge Edge module.
hyperopt Hyperopt module. hyperopt Hyperopt module.
@ -32,6 +30,27 @@ positional arguments:
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-V, --version show program's version number and exit
```
### Bot trading commands
```
usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[--db-url PATH] [--sd-notify] [--dry-run]
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://` for Dry Run).
--sd-notify Notify systemd service manager.
--dry-run Enforce dry-run for trading (removes Exchange secrets
and simulates trades).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. --logfile FILE Log to the file specified.
-V, --version show program's version number and exit -V, --version show program's version number and exit
@ -43,15 +62,12 @@ optional arguments:
Path to directory with historical backtesting data. Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH --userdir PATH, --user-data-dir PATH
Path to userdata directory. Path to userdata directory.
-s NAME, --strategy NAME
Specify strategy class name (default:
`DefaultStrategy`).
--strategy-path PATH Specify additional strategy lookup path.
--db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite://` for Dry Run).
--sd-notify Notify systemd service manager.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
``` ```
### How to specify which configuration file be used? ### How to specify which configuration file be used?
@ -60,7 +76,7 @@ The bot allows you to select which configuration file you want to use by means o
the `-c/--config` command line option: the `-c/--config` command line option:
```bash ```bash
freqtrade -c path/far/far/away/config.json freqtrade trade -c path/far/far/away/config.json
``` ```
Per default, the bot loads the `config.json` configuration file from the current Per default, the bot loads the `config.json` configuration file from the current
@ -79,13 +95,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua
require them): require them):
```bash ```bash
freqtrade -c ./config.json freqtrade trade -c ./config.json
``` ```
and specify both configuration files when running in the normal Live Trade Mode: and specify both configuration files when running in the normal Live Trade Mode:
```bash ```bash
freqtrade -c ./config.json -c path/to/secrets/keys.config.json freqtrade trade -c ./config.json -c path/to/secrets/keys.config.json
``` ```
This could help you hide your private Exchange key and Exchange secrete on you local machine This could help you hide your private Exchange key and Exchange secrete on you local machine
@ -134,7 +150,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
a strategy class called `AwesomeStrategy` to load it: a strategy class called `AwesomeStrategy` to load it:
```bash ```bash
freqtrade --strategy AwesomeStrategy freqtrade trade --strategy AwesomeStrategy
``` ```
If the bot does not find your strategy file, it will display in an error If the bot does not find your strategy file, it will display in an error
@ -149,7 +165,7 @@ This parameter allows you to add an additional strategy lookup path, which gets
checked before the default locations (The passed path must be a directory!): checked before the default locations (The passed path must be a directory!):
```bash ```bash
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
#### How to install a strategy? #### How to install a strategy?
@ -165,7 +181,7 @@ using `--db-url`. This can also be used to specify a custom database
in production mode. Example command: in production mode. Example command:
```bash ```bash
freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite freqtrade trade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
``` ```
## Backtesting commands ## Backtesting commands
@ -173,8 +189,10 @@ freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
Backtesting also uses the config specified via `-c/--config`. Backtesting also uses the config specified via `-c/--config`.
``` ```
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--max_open_trades INT] [-d PATH] [--userdir PATH] [-s NAME]
[--strategy-path PATH] [-i TICKER_INTERVAL]
[--timerange TIMERANGE] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [--fee FLOAT] [--stake_amount STAKE_AMOUNT] [--fee FLOAT]
[--eps] [--dmmp] [--eps] [--dmmp]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
@ -211,11 +229,29 @@ optional arguments:
--export EXPORT Export backtest results, argument are: trades. --export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades` Example: `--export=trades`
--export-filename PATH --export-filename PATH
Save backtest results to the file with this filename Save backtest results to the file with this filename.
(default: `user_data/backtest_results/backtest- Requires `--export` to be set as well. Example:
result.json`). Requires `--export` to be set as well. `--export-filename=user_data/backtest_results/backtest
Example: `--export-filename=user_data/backtest_results _today.json`
/backtest_today.json`
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
``` ```
@ -223,7 +259,7 @@ optional arguments:
The first time your run Backtesting, you will need to download some historic data first. The first time your run Backtesting, you will need to download some historic data first.
This can be accomplished by using `freqtrade download-data`. This can be accomplished by using `freqtrade download-data`.
Check the corresponding [help page section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) for more details Check the corresponding [Data Downloading](data-download.md) section for more details
## Hyperopt commands ## Hyperopt commands
@ -231,12 +267,14 @@ To optimize your strategy, you can use hyperopt parameter hyperoptimization
to find optimal parameter values for your stategy. to find optimal parameter values for your stategy.
``` ```
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades INT] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [--fee FLOAT] [--stake_amount STAKE_AMOUNT] [--fee FLOAT]
[--customhyperopt NAME] [--hyperopt-path PATH] [--hyperopt NAME] [--hyperopt-path PATH] [--eps]
[--eps] [-e INT] [-e INT]
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
[--dmmp] [--print-all] [--no-color] [--print-json] [--dmmp] [--print-all] [--no-color] [--print-json]
[-j JOBS] [--random-state INT] [--min-trades INT] [-j JOBS] [--random-state INT] [--min-trades INT]
[--continue] [--hyperopt-loss NAME] [--continue] [--hyperopt-loss NAME]
@ -254,16 +292,15 @@ optional arguments:
Specify stake_amount. Specify stake_amount.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade --fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit). entry and exit).
--customhyperopt NAME --hyperopt NAME Specify hyperopt class name which will be used by the
Specify hyperopt class name (default: bot.
`DefaultHyperOpt`). --hyperopt-path PATH Specify additional lookup path for Hyperopt and
--hyperopt-path PATH Specify additional lookup path for Hyperopts and
Hyperopt Loss functions. Hyperopt Loss functions.
--eps, --enable-position-stacking --eps, --enable-position-stacking
Allow buying the same pair multiple times (position Allow buying the same pair multiple times (position
stacking). stacking).
-e INT, --epochs INT Specify number of epochs (default: 100). -e INT, --epochs INT Specify number of epochs (default: 100).
-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...] --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]
Specify which parameters to hyperopt. Space-separated Specify which parameters to hyperopt. Space-separated
list. Default: `all`. list. Default: `all`.
--dmmp, --disable-max-market-positions --dmmp, --disable-max-market-positions
@ -292,8 +329,27 @@ optional arguments:
generate completely different results, since the generate completely different results, since the
target for optimization is different. Built-in target for optimization is different. Built-in
Hyperopt-loss-functions are: DefaultHyperOptLoss, Hyperopt-loss-functions are: DefaultHyperOptLoss,
OnlyProfitHyperOptLoss, SharpeHyperOptLoss.(default: OnlyProfitHyperOptLoss, SharpeHyperOptLoss (default:
`DefaultHyperOptLoss`). `DefaultHyperOptLoss`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
``` ```
## Edge commands ## Edge commands
@ -301,7 +357,9 @@ optional arguments:
To know your trade expectancy and winrate against historical data, you can use Edge. To know your trade expectancy and winrate against historical data, you can use Edge.
``` ```
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades INT] [--stake_amount STAKE_AMOUNT] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT]
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE] [--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
@ -324,6 +382,24 @@ optional arguments:
(without any space). Example: (without any space). Example:
`--stoplosses=-0.01,-0.1,-0.001` `--stoplosses=-0.01,-0.1,-0.001`
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
``` ```
To understand edge and how to read the results, please read the [edge documentation](edge.md). To understand edge and how to read the results, please read the [edge documentation](edge.md).

View File

@ -82,8 +82,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded.
| `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation. | `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. | `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists). | `pairlists` | StaticPairList | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.***
| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.*** | `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.***
@ -95,7 +94,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. | `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
| `initial_state` | running | Defines the initial application state. More information below. | `initial_state` | running | Defines the initial application state. More information below.
| `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below.
| `strategy` | DefaultStrategy | Defines Strategy class to use. | `strategy` | None | **Required** Defines Strategy class to use. Recommended to set via `--strategy NAME`.
| `strategy_path` | null | Adds an additional strategy lookup path (must be a directory). | `strategy_path` | null | Adds an additional strategy lookup path (must be a directory).
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
| `internals.heartbeat_interval` | 60 | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages. | `internals.heartbeat_interval` | 60 | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages.
@ -357,13 +356,6 @@ For example, to test the order type `FOK` with Kraken, and modify candle_limit t
!!! Warning !!! Warning
Please make sure to fully understand the impacts of these settings before modifying them. Please make sure to fully understand the impacts of these settings before modifying them.
#### Random notes for other exchanges
* The Ocean (ccxt id: 'theocean') exchange uses Web3 functionality and requires web3 package to be installed:
```shell
$ pip3 install web3
```
### What values can be used for fiat_display_currency? ### What values can be used for fiat_display_currency?
The `fiat_display_currency` configuration parameter sets the base currency to use for the The `fiat_display_currency` configuration parameter sets the base currency to use for the
@ -383,6 +375,88 @@ The valid values are:
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT" "BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
``` ```
## Pairlists
Pairlists define the list of pairs that the bot should trade.
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.
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.
Inactive markets and blacklisted pairs are always removed from the resulting `pair_whitelist`.
### Available Pairlists
* [`StaticPairList`](#static-pair-list) (default, if not configured differently)
* [`VolumePairList`](#volume-pair-list)
* [`PrecisionFilter`](#precision-filter)
* [`PriceFilter`](#price-pair-filter)
#### Static Pair List
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration.
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
```json
"pairlists": [
{"method": "StaticPairList"}
],
```
#### Volume Pair List
`VolumePairList` selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume` and defaults to `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.
`refresh_period` allows setting the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
```json
"pairlists": [{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
"refresh_period": 1800,
],
```
#### Precision Filter
Filters low-value coins which would not allow setting a stoploss.
#### Price Pair Filter
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.
This option is disabled by default, and will only apply if set to <> 0.
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.
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses.
### Full Pairlist example
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%.
```json
"exchange": {
"pair_whitelist": [],
"pair_blacklist": ["BNB/BTC"]
},
"pairlists": [
{
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01}
],
```
## Switch to Dry-run mode ## Switch to Dry-run mode
We recommend starting the bot in the Dry-run mode to see how your bot will We recommend starting the bot in the Dry-run mode to see how your bot will
@ -412,45 +486,6 @@ creating trades on the exchange.
Once you will be happy with your bot performance running in the Dry-run mode, Once you will be happy with your bot performance running in the Dry-run mode,
you can switch it to production mode. you can switch it to production mode.
### Dynamic Pairlists
Dynamic pairlists select pairs for you based on the logic configured.
The bot runs against all pairs (with that stake) on the exchange, and a number of assets
(`number_assets`) is selected based on the selected criteria.
By default, the `StaticPairList` method is used.
The Pairlist method is configured as `pair_whitelist` parameter under the `exchange`
section of the configuration.
**Available Pairlist methods:**
* `StaticPairList`
* It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
* `VolumePairList`
* It selects `number_assets` top pairs based on `sort_key`, which can be one of
`askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`.
* There is a possibility to filter low-value coins that would not allow setting a stop loss
(set `precision_filter` parameter to `true` for this).
* `VolumePairList` does not consider `pair_whitelist`, but builds this automatically based the pairlist configuration.
* Pairs in `pair_blacklist` are not considered for VolumePairList, even if all other filters would match.
Example:
```json
"exchange": {
"pair_whitelist": [],
"pair_blacklist": ["BNB/BTC"]
},
"pairlist": {
"method": "VolumePairList",
"config": {
"number_assets": 20,
"sort_key": "quoteVolume",
"precision_filter": false
}
},
```
## Switch to production mode ## Switch to production mode
In production mode, the bot will engage your money. Be careful, since a wrong In production mode, the bot will engage your money. Be careful, since a wrong
@ -476,12 +511,14 @@ you run it in production mode.
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
... ...
} }
``` ```
!!! Note !!! Note
If you have an exchange API key yet, [see our tutorial](/pre-requisite). If you have an exchange API key yet, [see our tutorial](/pre-requisite).
### Using proxy with FreqTrade You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange.
### Using proxy with Freqtrade
To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration. To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration.
@ -501,14 +538,13 @@ export HTTPS_PROXY="http://addr:port"
freqtrade freqtrade
``` ```
## Embedding Strategies
### Embedding Strategies
FreqTrade provides you with with an easy way to embed the strategy into your configuration file. FreqTrade provides you with with an easy way to embed the strategy into your configuration file.
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field, This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
in your chosen config file. in your chosen config file.
#### Encoding a string as BASE64 ### Encoding a string as BASE64
This is a quick example, how to generate the BASE64 string in python This is a quick example, how to generate the BASE64 string in python

View File

@ -78,10 +78,8 @@ freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --d
!!! Warning !!! Warning
The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading. The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading.
### Historic Kraken data !!! Note "Kraken user"
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
The Kraken API does only provide 720 historic candles, which is sufficient for FreqTrade dry-run and live trade modes, but is a problem for backtesting.
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
## Next step ## Next step

View File

@ -46,15 +46,18 @@ def test_method_to_test(caplog):
The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine. The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine.
#### Install #### Install
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [docker](https://docs.docker.com/install/) * [docker](https://docs.docker.com/install/)
* [docker-compose](https://docs.docker.com/compose/install/) * [docker-compose](https://docs.docker.com/compose/install/)
#### Starting the bot #### Starting the bot
##### Use the develop dockerfile ##### Use the develop dockerfile
``` bash ``` bash
rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml
``` ```
#### Docker Compose #### Docker Compose
##### Starting ##### Starting
@ -62,9 +65,11 @@ rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml
``` bash ``` bash
docker-compose up docker-compose up
``` ```
![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png) ![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png)
##### Rebuilding ##### Rebuilding
``` bash ``` bash
docker-compose build docker-compose build
``` ```
@ -77,8 +82,8 @@ that can be effected by `docker-compose up` or `docker-compose run freqtrade_dev
``` bash ``` bash
docker-compose exec freqtrade_develop /bin/bash docker-compose exec freqtrade_develop /bin/bash
``` ```
![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png)
![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png)
## Modules ## Modules
@ -95,22 +100,22 @@ This is a simple provider, which however serves as a good example on how to star
Next, modify the classname of the provider (ideally align this with the Filename). Next, modify the classname of the provider (ideally align this with the Filename).
The base-class provides the an instance of the bot (`self._freqtrade`), as well as the configuration (`self._config`), and initiates both `_blacklist` and `_whitelist`. The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`), the pairlist dedicated configuration (`self._pairlistconfig`) and the absolute position within the list of pairlists.
```python ```python
self._freqtrade = freqtrade self._exchange = exchange
self._pairlistmanager = pairlistmanager
self._config = config self._config = config
self._whitelist = self._config['exchange']['pair_whitelist'] self._pairlistconfig = pairlistconfig
self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlist_pos = pairlist_pos
``` ```
Now, let's step through the methods which require actions: Now, let's step through the methods which require actions:
#### configuration #### Pairlist configuration
Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`. Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`.
This Pairlist-object may contain a `"config"` dict with additional configurations for the configured pairlist. This Pairlist-object may contain configurations with additional configurations for the configured pairlist.
By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the whitelist. Please follow this to ensure a consistent user experience. By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the whitelist. Please follow this to ensure a consistent user experience.
Additional elements can be configured as needed. `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic. Additional elements can be configured as needed. `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic.
@ -120,29 +125,30 @@ Additional elements can be configured as needed. `VolumePairList` uses `"sort_ke
Returns a description used for Telegram messages. Returns a description used for Telegram messages.
This should contain the name of the Provider, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`. This should contain the name of the Provider, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`.
#### refresh_pairlist #### filter_pairlist
Override this method and run all calculations needed in this method. Override this method and run all calculations needed in this method.
This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations. This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations.
Assign the resulting whiteslist to `self._whitelist` and `self._blacklist` respectively. These will then be used to run the bot in this iteration. Pairs with open trades will be added to the whitelist to have the sell-methods run correctly. It get's passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`.
Please also run `self._validate_whitelist(pairs)` and to check and remove pairs with inactive markets. This function is available in the Parent class (`StaticPairList`) and should ideally not be overwritten. It must return the resulting pairlist (which may then be passed into the next pairlist filter).
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected.
##### sample ##### sample
``` python ``` python
def refresh_pairlist(self) -> None: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
# Generate dynamic whitelist # Generate dynamic whitelist
pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key) pairs = self._calculate_pairlist(pairlist, tickers)
# Validate whitelist to only have active market pairs return pairs
self._whitelist = self._validate_whitelist(pairs)[:self._number_pairs]
``` ```
#### _gen_pair_whitelist #### _gen_pair_whitelist
This is a simple method used by `VolumePairList` - however serves as a good example. This is a simple method used by `VolumePairList` - however serves as a good example.
It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider. In VolumePairList, this implements different methods of sorting, does early validation so only the expected number of pairs is returned.
## Implement a new Exchange (WIP) ## Implement a new Exchange (WIP)
@ -198,6 +204,19 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace user_data/not
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md
``` ```
## Continuous integration
This documents some decisions taken for the CI Pipeline.
* CI runs on all OS variants, Linux (ubuntu), macOS and Windows.
* Docker images are build for the branches `master` and `develop`.
* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:master_pi` and `develop_pi`.
* Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of.
* Full docker image rebuilds are run once a week via schedule.
* Deployments run on ubuntu.
* ta-lib binaries are contained in the build_helpers directory to avoid fails related to external unavailability.
* All tests must pass for a PR to be merged to `master` or `develop`.
## Creating a release ## Creating a release
This part of the documentation is aimed at maintainers, and shows how to create a release. This part of the documentation is aimed at maintainers, and shows how to create a release.

View File

@ -160,7 +160,7 @@ docker run -d \
-v ~/.freqtrade/config.json:/freqtrade/config.json \ -v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/user_data/:/freqtrade/user_data \ -v ~/.freqtrade/user_data/:/freqtrade/user_data \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
``` ```
!!! Note !!! Note
@ -202,7 +202,7 @@ docker run -d \
-v ~/.freqtrade/config.json:/freqtrade/config.json \ -v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
freqtrade --strategy AwsomelyProfitableStrategy backtesting freqtrade backtesting --strategy AwsomelyProfitableStrategy
``` ```
Head over to the [Backtesting Documentation](backtesting.md) for more details. Head over to the [Backtesting Documentation](backtesting.md) for more details.

View File

@ -235,7 +235,7 @@ An example of its output:
### Update cached pairs with the latest data ### Update cached pairs with the latest data
Edge requires historic data the same way as backtesting does. Edge requires historic data the same way as backtesting does.
Please refer to the [download section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) of the documentation for details. Please refer to the [Data Downloading](data-download.md) section of the documentation for details.
### Precising stoploss range ### Precising stoploss range

63
docs/exchanges.md Normal file
View File

@ -0,0 +1,63 @@
# Exchange-specific Notes
This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges.
## Binance
!!! Tip "Stoploss on Exchange"
Binance is currently the only exchange supporting `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it.
### Blacklists
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
### Binance sites
Binance has been split into 3, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized.
* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`.
* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`.
* [binance.je](https://www.binance.je/) - Binance Jersey, trading fiat currencies. Use exchange id: `binanceje`.
## Kraken
### Historic Kraken data
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
## Bittrex
### Restricted markets
Bittrex split its exchange into US and International versions.
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
If you have restricted pairs in your whitelist, you'll get a warning message in the log on Freqtrade startup for each restricted pair.
The warning message will look similar to the following:
``` output
[...] Message: bittrex {"success":false,"message":"RESTRICTED_MARKET","result":null,"explanation":null}"
```
If you're an "International" customer on the Bittrex exchange, then this warning will probably not impact you.
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your whitelist.
You can get a list of restricted markets by using the following snippet:
``` python
import ccxt
ct = ccxt.bittrex()
_ = ct.load_markets()
res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarkets()['result'] if x['IsRestricted']]
print(res)
```
## Random notes for other exchanges
* The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed:
```shell
$ pip3 install web3
```

View File

@ -4,7 +4,7 @@
### The bot does not start ### The bot does not start
Running the bot with `freqtrade --config config.json` does show the output `freqtrade: command not found`. Running the bot with `freqtrade trade --config config.json` does show the output `freqtrade: command not found`.
This could have the following reasons: This could have the following reasons:
@ -48,12 +48,8 @@ You can use the `/forcesell all` command from Telegram.
### I get the message "RESTRICTED_MARKET" ### I get the message "RESTRICTED_MARKET"
Currently known to happen for US Bittrex users. Currently known to happen for US Bittrex users.
Bittrex split its exchange into US and International versions.
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair. Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you.
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist.
### How do I search the bot logs for something? ### How do I search the bot logs for something?

View File

@ -245,7 +245,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will
We strongly recommend to use `screen` or `tmux` to prevent any connection loss. We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
```bash ```bash
freqtrade -c config.json hyperopt --customhyperopt <hyperoptname> -e 5000 --spaces all freqtrade hyperopt --config config.json --hyperopt <hyperoptname> -e 5000 --spaces all
``` ```
Use `<hyperoptname>` as the name of the custom hyperopt used. Use `<hyperoptname>` as the name of the custom hyperopt used.
@ -281,7 +281,7 @@ freqtrade hyperopt --timerange 20180401-20180501
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
```bash ```bash
freqtrade --strategy SampleStrategy hyperopt --customhyperopt SampleHyperopt freqtrade hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt
``` ```
### Running Hyperopt with Smaller Search Space ### Running Hyperopt with Smaller Search Space

View File

@ -26,24 +26,32 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E
## Quick start ## Quick start
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot.
!!! Note
Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case.
```bash
git clone git@github.com:freqtrade/freqtrade.git
cd freqtrade
git checkout develop
./setup.sh --install
```
!!! Note !!! Note
Windows installation is explained [here](#windows). Windows installation is explained [here](#windows).
## Easy Installation - Linux Script The easiest way to install and run Freqtrade is to clone the bot GitHub repository and then run the Easy Installation script, if it's available for your platform.
If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot. !!! Note "Version considerations"
When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `master` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
!!! Note
Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
This can be achieved with the following commands:
```bash
git clone git@github.com:freqtrade/freqtrade.git
cd freqtrade
git checkout master # Optional, see (1)
./setup.sh --install
```
(1) This command switches the cloned repository to the use of the `master` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout master`/`git checkout develop` commands.
## Easy Installation Script (Linux/MacOS)
If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot.
```bash ```bash
$ ./setup.sh $ ./setup.sh
@ -56,25 +64,25 @@ usage:
** --install ** ** --install **
This script will install everything you need to run the bot: With this option, the script will install everything you need to run the bot:
* Mandatory software as: `ta-lib` * Mandatory software as: `ta-lib`
* Setup your virtualenv * Setup your virtualenv
* Configure your `config.json` file * Configure your `config.json` file
This script is a combination of `install script` `--reset`, `--config` This option is a combination of installation tasks, `--reset` and `--config`.
** --update ** ** --update **
Update parameter will pull the last version of your current branch and update your virtualenv. This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot.
** --reset ** ** --reset **
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv. This option will hard reset your branch (only if you are on either `master` or `develop`) and recreate your virtualenv.
** --config ** ** --config **
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. Use this option to configure the `config.json` configuration file. The script will interactively ask you questions to setup your bot and create your `config.json`.
------ ------
@ -184,7 +192,7 @@ python3 -m pip install -e .
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
```bash ```bash
freqtrade -c config.json freqtrade trade -c config.json
``` ```
*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.

View File

@ -23,13 +23,15 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three
Possible arguments: Possible arguments:
``` ```
usage: freqtrade plot-dataframe [-h] [-p PAIRS [PAIRS ...]] usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-s NAME]
[--strategy-path PATH] [-p PAIRS [PAIRS ...]]
[--indicators1 INDICATORS1 [INDICATORS1 ...]] [--indicators1 INDICATORS1 [INDICATORS1 ...]]
[--indicators2 INDICATORS2 [INDICATORS2 ...]] [--indicators2 INDICATORS2 [INDICATORS2 ...]]
[--plot-limit INT] [--db-url PATH] [--plot-limit INT] [--db-url PATH]
[--trade-source {DB,file}] [--export EXPORT] [--trade-source {DB,file}] [--export EXPORT]
[--export-filename PATH] [--export-filename PATH]
[--timerange TIMERANGE] [--timerange TIMERANGE] [-i TICKER_INTERVAL]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -62,6 +64,28 @@ optional arguments:
/backtest_today.json` /backtest_today.json`
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
Multiple --config options may be used. Can be set to
`-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name (default:
`DefaultStrategy`).
--strategy-path PATH Specify additional strategy lookup path.
``` ```
@ -83,7 +107,7 @@ Use `--indicators1` for the main plot and `--indicators2` for the subplot below
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command. You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma ema --indicators2 macd freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --indicators1 sma ema --indicators2 macd
``` ```
### Further usage examples ### Further usage examples
@ -91,25 +115,25 @@ freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma
To plot multiple pairs, separate them with a space: To plot multiple pairs, separate them with a space:
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH XRP/ETH freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH XRP/ETH
``` ```
To plot a timerange (to zoom in) To plot a timerange (to zoom in)
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --timerange=20180801-20180805 freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805
``` ```
To plot trades stored in a database use `--db-url` in combination with `--trade-source DB`: To plot trades stored in a database use `--db-url` in combination with `--trade-source DB`:
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB freqtrade plot-dataframe --strategy AwesomeStrategy --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB
``` ```
To plot trades from a backtesting result, use `--export-filename <filename>` To plot trades from a backtesting result, use `--export-filename <filename>`
``` bash ``` bash
freqtrade --strategy AwesomeStrategy plot-dataframe --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
``` ```
## Plot profit ## Plot profit
@ -133,10 +157,11 @@ The third graph can be useful to spot outliers, events in pairs that cause profi
Possible options for the `freqtrade plot-profit` subcommand: Possible options for the `freqtrade plot-profit` subcommand:
``` ```
usage: freqtrade plot-profit [-h] [-p PAIRS [PAIRS ...]] usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]]
[--timerange TIMERANGE] [--export EXPORT] [--timerange TIMERANGE] [--export EXPORT]
[--export-filename PATH] [--db-url PATH] [--export-filename PATH] [--db-url PATH]
[--trade-source {DB,file}] [--trade-source {DB,file}] [-i TICKER_INTERVAL]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -159,6 +184,22 @@ optional arguments:
--trade-source {DB,file} --trade-source {DB,file}
Specify the source for trades (Can be DB or file Specify the source for trades (Can be DB or file
(backtest file)) Default: file (backtest file)) Default: file
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default: `config.json`).
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.
``` ```

View File

@ -1,2 +1,2 @@
mkdocs-material==4.4.3 mkdocs-material==4.5.0
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2

View File

@ -22,7 +22,14 @@ Sample configuration:
!!! Danger "Password selection" !!! Danger "Password selection"
Please make sure to select a very strong, unique password to protect your bot from unauthorized access. Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly.
This should return the response:
``` output
{"status":"pong"}
```
All other endpoints return sensitive info and require authentication, so are 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.
@ -58,7 +65,7 @@ docker run -d \
-v ~/.freqtrade/user_data/:/freqtrade/user_data \ -v ~/.freqtrade/user_data/:/freqtrade/user_data \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
-p 127.0.0.1:8080:8080 \ -p 127.0.0.1:8080:8080 \
freqtrade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
``` ```
!!! Danger "Security warning" !!! Danger "Security warning"
@ -99,6 +106,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `stop` | | Stops the trader | `stop` | | Stops the trader
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `reload_conf` | | Reloads the configuration file | `reload_conf` | | Reloads the configuration file
| `show_config` | | Shows part of the current configuration with relevant settings to operation
| `status` | | Lists all open trades | `status` | | Lists all open trades
| `count` | | Displays number of trades used and available | `count` | | Displays number of trades used and available
| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance | `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
@ -165,6 +173,10 @@ reload_conf
Reload configuration Reload configuration
:returns: json object :returns: json object
show_config
Returns part of the configuration, relevant for trading operations.
:return: json object containing the version
start start
Start the bot if it's in stopped state. Start the bot if it's in stopped state.
:returns: json object :returns: json object

View File

@ -13,7 +13,7 @@ Let assume you have a class called `AwesomeStrategy` in the file `awesome-strate
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
```bash ```bash
freqtrade --strategy AwesomeStrategy freqtrade trade --strategy AwesomeStrategy
``` ```
## Change your strategy ## Change your strategy
@ -45,7 +45,7 @@ The current version is 2 - which is also the default when it's not set explicitl
Future versions will require this to be set. Future versions will require this to be set.
```bash ```bash
freqtrade --strategy AwesomeStrategy freqtrade trade --strategy AwesomeStrategy
``` ```
**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py) **For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py)
@ -314,9 +314,9 @@ Please always check the mode of operation to select the correct method to get da
#### Possible options for DataProvider #### Possible options for DataProvider
- `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval). - `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval).
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame. - `ohlcv(pair, timeframe)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame.
- `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk. - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk.
- `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - `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. - `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. - `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. - `runmode` - Property containing the current runmode.
@ -327,7 +327,7 @@ Please always check the mode of operation to select the correct method to get da
if self.dp: if self.dp:
inf_pair, inf_timeframe = self.informative_pairs()[0] inf_pair, inf_timeframe = self.informative_pairs()[0]
informative = self.dp.get_pair_dataframe(pair=inf_pair, informative = self.dp.get_pair_dataframe(pair=inf_pair,
ticker_interval=inf_timeframe) timeframe=inf_timeframe)
``` ```
!!! Warning "Warning about backtesting" !!! Warning "Warning about backtesting"
@ -485,7 +485,7 @@ The strategy template is located in the file
If you want to use a strategy from a different directory you can pass `--strategy-path` If you want to use a strategy from a different directory you can pass `--strategy-path`
```bash ```bash
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
### Common mistakes when developing strategies ### Common mistakes when developing strategies

View File

@ -10,7 +10,7 @@ from pathlib import Path
# Customize these according to your needs. # Customize these according to your needs.
# Define some constants # Define some constants
ticker_interval = "5m" timeframe = "5m"
# Name of the strategy class # Name of the strategy class
strategy_name = 'SampleStrategy' strategy_name = 'SampleStrategy'
# Path to user data # Path to user data
@ -29,7 +29,7 @@ pair = "BTC_USDT"
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
candles = load_pair_history(datadir=data_location, candles = load_pair_history(datadir=data_location,
ticker_interval=ticker_interval, timeframe=timeframe,
pair=pair) pair=pair)
# Confirm success # Confirm success

View File

@ -53,6 +53,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/stop` | | Stops the trader | `/stop` | | Stops the trader
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_conf` | | Reloads the configuration file | `/reload_conf` | | Reloads the configuration file
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
| `/status` | | Lists all open trades | `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format | `/status table` | | List all open trades in a table format
| `/count` | | Displays number of trades used and available | `/count` | | Displays number of trades used and available

View File

@ -6,7 +6,7 @@ After=network.target
# Set WorkingDirectory and ExecStart to your file paths accordingly # Set WorkingDirectory and ExecStart to your file paths accordingly
# NOTE: %h will be resolved to /home/<username> # NOTE: %h will be resolved to /home/<username>
WorkingDirectory=%h/freqtrade WorkingDirectory=%h/freqtrade
ExecStart=/usr/bin/freqtrade ExecStart=/usr/bin/freqtrade trade
Restart=on-failure Restart=on-failure
[Install] [Install]

View File

@ -6,7 +6,7 @@ After=network.target
# Set WorkingDirectory and ExecStart to your file paths accordingly # Set WorkingDirectory and ExecStart to your file paths accordingly
# NOTE: %h will be resolved to /home/<username> # NOTE: %h will be resolved to /home/<username>
WorkingDirectory=%h/freqtrade WorkingDirectory=%h/freqtrade
ExecStart=/usr/bin/freqtrade --sd-notify ExecStart=/usr/bin/freqtrade trade --sd-notify
Restart=always Restart=always
#Restart=on-failure #Restart=on-failure

View File

@ -13,7 +13,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat
ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
"max_open_trades", "stake_amount", "fee"] "max_open_trades", "stake_amount", "fee"]
@ -42,8 +42,9 @@ ARGS_CREATE_USERDIR = ["user_data_dir"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange",
"timeframes", "erase"] "timeframes", "erase"]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"trade_source", "export", "exportfilename", "timerange", "ticker_interval"] "db_url", "trade_source", "export", "exportfilename",
"timerange", "ticker_interval"]
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"]
@ -61,11 +62,6 @@ class Arguments:
def __init__(self, args: Optional[List[str]]) -> None: def __init__(self, args: Optional[List[str]]) -> None:
self.args = args self.args = args
self._parsed_arg: Optional[argparse.Namespace] = None self._parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
def _load_args(self) -> None:
self._build_args(optionlist=ARGS_MAIN)
self._build_subcommands()
def get_parsed_arg(self) -> Dict[str, Any]: def get_parsed_arg(self) -> Dict[str, Any]:
""" """
@ -73,7 +69,7 @@ class Arguments:
:return: List[str] List of arguments :return: List[str] List of arguments
""" """
if self._parsed_arg is None: if self._parsed_arg is None:
self._load_args() self._build_subcommands()
self._parsed_arg = self._parse_args() self._parsed_arg = self._parse_args()
return vars(self._parsed_arg) return vars(self._parsed_arg)
@ -84,22 +80,17 @@ class Arguments:
""" """
parsed_arg = self.parser.parse_args(self.args) parsed_arg = self.parser.parse_args(self.args)
# When no config is provided, but a config exists, use that configuration!
subparser = parsed_arg.subparser if 'subparser' in parsed_arg else None
# Workaround issue in argparse with action='append' and default value # Workaround issue in argparse with action='append' and default value
# (see https://bugs.python.org/issue16399) # (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting) # Allow no-config for certain commands (like downloading / plotting)
if (parsed_arg.config is None if ('config' in parsed_arg and parsed_arg.config is None and
and subparser not in NO_CONF_ALLOWED ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or
and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file() not ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED))):
or (subparser not in NO_CONF_REQURIED))):
parsed_arg.config = [constants.DEFAULT_CONFIG] parsed_arg.config = [constants.DEFAULT_CONFIG]
return parsed_arg return parsed_arg
def _build_args(self, optionlist, parser=None): def _build_args(self, optionlist, parser):
parser = parser or self.parser
for val in optionlist: for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val] opt = AVAILABLE_CLI_OPTIONS[val]
@ -110,38 +101,68 @@ class Arguments:
Builds and attaches all subcommands. Builds and attaches all subcommands.
:return: None :return: None
""" """
# Build shared arguments (as group Common Options)
_common_parser = argparse.ArgumentParser(add_help=False)
group = _common_parser.add_argument_group("Common arguments")
self._build_args(optionlist=ARGS_COMMON, parser=group)
_strategy_parser = argparse.ArgumentParser(add_help=False)
strategy_group = _strategy_parser.add_argument_group("Strategy arguments")
self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group)
# Build main command
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
self._build_args(optionlist=['version'], parser=self.parser)
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
from freqtrade.utils import (start_create_userdir, start_download_data, from freqtrade.utils import (start_create_userdir, start_download_data,
start_list_exchanges, start_list_timeframes, start_list_exchanges, start_list_markets,
start_list_markets) start_list_timeframes, start_trading)
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
subparsers = self.parser.add_subparsers(dest='subparser') subparsers = self.parser.add_subparsers(dest='command',
# Use custom message when no subhandler is added
# shown from `main.py`
# required=True
)
# Add trade subcommand
trade_cmd = subparsers.add_parser('trade', help='Trade module.',
parents=[_common_parser, _strategy_parser])
trade_cmd.set_defaults(func=start_trading)
self._build_args(optionlist=ARGS_TRADE, parser=trade_cmd)
# Add backtesting subcommand # Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.') backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.',
parents=[_common_parser, _strategy_parser])
backtesting_cmd.set_defaults(func=start_backtesting) backtesting_cmd.set_defaults(func=start_backtesting)
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
# Add edge subcommand # Add edge subcommand
edge_cmd = subparsers.add_parser('edge', help='Edge module.') edge_cmd = subparsers.add_parser('edge', help='Edge module.',
parents=[_common_parser, _strategy_parser])
edge_cmd.set_defaults(func=start_edge) edge_cmd.set_defaults(func=start_edge)
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd) self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
# Add hyperopt subcommand # Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.') hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.',
parents=[_common_parser, _strategy_parser],
)
hyperopt_cmd.set_defaults(func=start_hyperopt) hyperopt_cmd.set_defaults(func=start_hyperopt)
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) 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.",
)
create_userdir_cmd.set_defaults(func=start_create_userdir) create_userdir_cmd.set_defaults(func=start_create_userdir)
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd) self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
# Add list-exchanges subcommand # Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser( list_exchanges_cmd = subparsers.add_parser(
'list-exchanges', 'list-exchanges',
help='Print available exchanges.' help='Print available exchanges.',
parents=[_common_parser],
) )
list_exchanges_cmd.set_defaults(func=start_list_exchanges) list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
@ -149,7 +170,8 @@ class Arguments:
# Add list-timeframes subcommand # Add list-timeframes subcommand
list_timeframes_cmd = subparsers.add_parser( list_timeframes_cmd = subparsers.add_parser(
'list-timeframes', 'list-timeframes',
help='Print available ticker intervals (timeframes) for the exchange.' help='Print available ticker intervals (timeframes) for the exchange.',
parents=[_common_parser],
) )
list_timeframes_cmd.set_defaults(func=start_list_timeframes) list_timeframes_cmd.set_defaults(func=start_list_timeframes)
self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd) self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd)
@ -157,7 +179,8 @@ class Arguments:
# Add list-markets subcommand # Add list-markets subcommand
list_markets_cmd = subparsers.add_parser( list_markets_cmd = subparsers.add_parser(
'list-markets', 'list-markets',
help='Print markets on exchange.' help='Print markets on exchange.',
parents=[_common_parser],
) )
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False)) list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd) self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd)
@ -165,7 +188,8 @@ class Arguments:
# Add list-pairs subcommand # Add list-pairs subcommand
list_pairs_cmd = subparsers.add_parser( list_pairs_cmd = subparsers.add_parser(
'list-pairs', 'list-pairs',
help='Print pairs on exchange.' help='Print pairs on exchange.',
parents=[_common_parser],
) )
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True)) list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd) self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd)
@ -173,16 +197,17 @@ class Arguments:
# Add download-data subcommand # Add download-data subcommand
download_data_cmd = subparsers.add_parser( download_data_cmd = subparsers.add_parser(
'download-data', 'download-data',
help='Download backtesting data.' help='Download backtesting data.',
parents=[_common_parser],
) )
download_data_cmd.set_defaults(func=start_download_data) download_data_cmd.set_defaults(func=start_download_data)
self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd) self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd)
# Add Plotting subcommand # Add Plotting subcommand
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
plot_dataframe_cmd = subparsers.add_parser( plot_dataframe_cmd = subparsers.add_parser(
'plot-dataframe', 'plot-dataframe',
help='Plot candles with indicators.' help='Plot candles with indicators.',
parents=[_common_parser, _strategy_parser],
) )
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe) plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd) self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd)
@ -190,7 +215,8 @@ class Arguments:
# Plot profit # Plot profit
plot_profit_cmd = subparsers.add_parser( plot_profit_cmd = subparsers.add_parser(
'plot-profit', 'plot-profit',
help='Generate plot showing profits.' help='Generate plot showing profits.',
parents=[_common_parser],
) )
plot_profit_cmd.set_defaults(func=start_plot_profit) plot_profit_cmd.set_defaults(func=start_plot_profit)
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd) self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)

View File

@ -65,9 +65,8 @@ AVAILABLE_CLI_OPTIONS = {
# Main options # Main options
"strategy": Arg( "strategy": Arg(
'-s', '--strategy', '-s', '--strategy',
help='Specify strategy class name (default: `%(default)s`).', help='Specify strategy class name which will be used by the bot.',
metavar='NAME', metavar='NAME',
default='DefaultStrategy',
), ),
"strategy_path": Arg( "strategy_path": Arg(
'--strategy-path', '--strategy-path',
@ -86,6 +85,11 @@ AVAILABLE_CLI_OPTIONS = {
help='Notify systemd service manager.', help='Notify systemd service manager.',
action='store_true', action='store_true',
), ),
"dry_run": Arg(
'--dry-run',
help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).',
action='store_true',
),
# Optimize common # Optimize common
"ticker_interval": Arg( "ticker_interval": Arg(
'-i', '--ticker-interval', '-i', '--ticker-interval',
@ -136,7 +140,7 @@ AVAILABLE_CLI_OPTIONS = {
), ),
"exportfilename": Arg( "exportfilename": Arg(
'--export-filename', '--export-filename',
help='Save backtest results to the file with this filename (default: `%(default)s`). ' help='Save backtest results to the file with this filename. '
'Requires `--export` to be set as well. ' 'Requires `--export` to be set as well. '
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`', 'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
metavar='PATH', metavar='PATH',
@ -156,14 +160,13 @@ AVAILABLE_CLI_OPTIONS = {
), ),
# Hyperopt # Hyperopt
"hyperopt": Arg( "hyperopt": Arg(
'--customhyperopt', '--hyperopt',
help='Specify hyperopt class name (default: `%(default)s`).', help='Specify hyperopt class name which will be used by the bot.',
metavar='NAME', metavar='NAME',
default=constants.DEFAULT_HYPEROPT,
), ),
"hyperopt_path": Arg( "hyperopt_path": Arg(
'--hyperopt-path', '--hyperopt-path',
help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.', help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.',
metavar='PATH', metavar='PATH',
), ),
"epochs": Arg( "epochs": Arg(

View File

@ -122,6 +122,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]: RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]:
return return
if (conf.get('pairlist', {}).get('method', 'StaticPairList') == 'StaticPairList' for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]):
and not conf.get('exchange', {}).get('pair_whitelist')): if (pl.get('method') == 'StaticPairList'
raise OperationalException("StaticPairList requires pair_whitelist to be set.") and not conf.get('exchange', {}).get('pair_whitelist')):
raise OperationalException("StaticPairList requires pair_whitelist to be set.")

View File

@ -81,6 +81,9 @@ class Configuration:
if 'ask_strategy' not in config: if 'ask_strategy' not in config:
config['ask_strategy'] = {} config['ask_strategy'] = {}
if 'pairlists' not in config:
config['pairlists'] = []
# validate configuration before returning # validate configuration before returning
logger.info('Validating configuration ...') logger.info('Validating configuration ...')
validate_config_schema(config) validate_config_schema(config)
@ -93,7 +96,7 @@ class Configuration:
:return: Configuration dictionary :return: Configuration dictionary
""" """
# Load all configs # Load all configs
config: Dict[str, Any] = self.load_from_files(self.args["config"]) config: Dict[str, Any] = self.load_from_files(self.args.get("config", []))
# Keep a copy of the original configuration file # Keep a copy of the original configuration file
config['original_config'] = deepcopy(config) config['original_config'] = deepcopy(config)
@ -153,7 +156,7 @@ class Configuration:
self._process_logging_options(config) self._process_logging_options(config)
# Set strategy if not specified in config and or if it's non default # Set strategy if not specified in config and or if it's non default
if self.args.get("strategy") != constants.DEFAULT_STRATEGY or not config.get('strategy'): if self.args.get("strategy") or not config.get('strategy'):
config.update({'strategy': self.args.get("strategy")}) config.update({'strategy': self.args.get("strategy")})
self._args_to_config(config, argname='strategy_path', self._args_to_config(config, argname='strategy_path',
@ -171,6 +174,10 @@ class Configuration:
if 'sd_notify' in self.args and self.args["sd_notify"]: if 'sd_notify' in self.args and self.args["sd_notify"]:
config['internals'].update({'sd_notify': True}) config['internals'].update({'sd_notify': True})
self._args_to_config(config, argname='dry_run',
logstring='Parameter --dry-run detected, '
'overriding dry_run to: {} ...')
def _process_datadir_options(self, config: Dict[str, Any]) -> None: def _process_datadir_options(self, config: Dict[str, Any]) -> None:
""" """
Extract information for sys.argv and load directory configurations Extract information for sys.argv and load directory configurations

View File

@ -57,3 +57,19 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
'experimental', 'sell_profit_only') 'experimental', 'sell_profit_only')
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 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'})

View File

@ -39,12 +39,12 @@ class TimeRange:
if self.startts: if self.startts:
self.startts = self.startts - seconds self.startts = self.startts - seconds
def adjust_start_if_necessary(self, ticker_interval_secs: int, startup_candles: int, def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int,
min_date: arrow.Arrow) -> None: min_date: arrow.Arrow) -> None:
""" """
Adjust startts by <startup_candles> candles. Adjust startts by <startup_candles> candles.
Applies only if no startup-candles have been available. Applies only if no startup-candles have been available.
:param ticker_interval_secs: Ticker interval in seconds e.g. `timeframe_to_seconds('5m')` :param timeframe_secs: Ticker timeframe in seconds e.g. `timeframe_to_seconds('5m')`
:param startup_candles: Number of candles to move start-date forward :param startup_candles: Number of candles to move start-date forward
:param min_date: Minimum data date loaded. Key kriterium to decide if start-time :param min_date: Minimum data date loaded. Key kriterium to decide if start-time
has to be moved has to be moved
@ -55,7 +55,7 @@ class TimeRange:
# If no startts was defined, or backtest-data starts at the defined backtest-date # If no startts was defined, or backtest-data starts at the defined backtest-date
logger.warning("Moving start-date by %s candles to account for startup time.", logger.warning("Moving start-date by %s candles to account for startup time.",
startup_candles) startup_candles)
self.startts = (min_date.timestamp + ticker_interval_secs * startup_candles) self.startts = (min_date.timestamp + timeframe_secs * startup_candles)
self.starttype = 'date' self.starttype = 'date'
@staticmethod @staticmethod

View File

@ -9,8 +9,6 @@ PROCESS_THROTTLE_SECS = 5 # sec
DEFAULT_TICKER_INTERVAL = 5 # min DEFAULT_TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_HYPEROPT = 'DefaultHyperOpt'
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://' DEFAULT_DB_DRYRUN_URL = 'sqlite://'
@ -20,11 +18,11 @@ REQUIRED_ORDERTIF = ['buy', 'sell']
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
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']
DRY_RUN_WALLET = 999.9 DRY_RUN_WALLET = 999.9
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
TICKER_INTERVALS = [ TIMEFRAMES = [
'1m', '3m', '5m', '15m', '30m', '1m', '3m', '5m', '15m', '30m',
'1h', '2h', '4h', '6h', '8h', '12h', '1h', '2h', '4h', '6h', '8h', '12h',
'1d', '3d', '1w', '1d', '3d', '1w',
@ -57,7 +55,7 @@ CONF_SCHEMA = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'max_open_trades': {'type': 'integer', 'minimum': -1}, 'max_open_trades': {'type': 'integer', 'minimum': -1},
'ticker_interval': {'type': 'string', 'enum': TICKER_INTERVALS}, 'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': { 'stake_amount': {
"type": ["number", "string"], "type": ["number", "string"],
@ -151,13 +149,16 @@ CONF_SCHEMA = {
'block_bad_exchanges': {'type': 'boolean'} 'block_bad_exchanges': {'type': 'boolean'}
} }
}, },
'pairlist': { 'pairlists': {
'type': 'object', 'type': 'array',
'properties': { 'items': {
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, 'type': 'object',
'config': {'type': 'object'} 'properties': {
}, 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
'required': ['method'] 'config': {'type': 'object'}
},
'required': ['method'],
}
}, },
'telegram': { 'telegram': {
'type': 'object', 'type': 'object',

View File

@ -7,7 +7,7 @@ from typing import Dict
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytz from datetime import timezone
from freqtrade import persistence from freqtrade import persistence
from freqtrade.misc import json_load from freqtrade.misc import json_load
@ -106,8 +106,8 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
"stop_loss", "initial_stop_loss", "strategy", "ticker_interval"] "stop_loss", "initial_stop_loss", "strategy", "ticker_interval"]
trades = pd.DataFrame([(t.pair, trades = pd.DataFrame([(t.pair,
t.open_date.replace(tzinfo=pytz.UTC), t.open_date.replace(tzinfo=timezone.utc),
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None,
t.calc_profit(), t.calc_profit_percent(), t.calc_profit(), t.calc_profit_percent(),
t.open_rate, t.close_rate, t.amount, t.open_rate, t.close_rate, t.amount,
(round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2) (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2)
@ -178,9 +178,9 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
: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.
""" """
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
ticker_minutes = timeframe_to_minutes(timeframe) timeframe_minutes = timeframe_to_minutes(timeframe)
# Resample to ticker_interval to make sure trades match candles # Resample to timeframe to make sure trades match candles
_trades_sum = trades.resample(f'{ticker_minutes}min', on='close_time')[['profitperc']].sum() _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time')[['profitperc']].sum()
df.loc[:, col_name] = _trades_sum.cumsum() df.loc[:, col_name] = _trades_sum.cumsum()
# Set first value to 0 # Set first value to 0
df.loc[df.iloc[0].name, col_name] = 0 df.loc[df.iloc[0].name, col_name] = 0

View File

@ -10,13 +10,13 @@ from pandas import DataFrame, to_datetime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *, def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *,
fill_missing: bool = True, fill_missing: bool = True,
drop_incomplete: bool = True) -> DataFrame: drop_incomplete: bool = True) -> DataFrame:
""" """
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe
:param ticker: ticker list, as returned by exchange.async_get_candle_history :param ticker: ticker list, as returned by exchange.async_get_candle_history
:param ticker_interval: ticker_interval (e.g. 5m). Used to fill up eventual missing data :param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data
:param pair: Pair this data is for (used to warn if fillup was necessary) :param pair: Pair this data is for (used to warn if fillup was necessary)
:param fill_missing: fill up missing candles with 0 candles :param fill_missing: fill up missing candles with 0 candles
(see ohlcv_fill_up_missing_data for details) (see ohlcv_fill_up_missing_data for details)
@ -52,12 +52,12 @@ def parse_ticker_dataframe(ticker: list, ticker_interval: str, pair: str, *,
logger.debug('Dropping last candle') logger.debug('Dropping last candle')
if fill_missing: if fill_missing:
return ohlcv_fill_up_missing_data(frame, ticker_interval, pair) return ohlcv_fill_up_missing_data(frame, timeframe, pair)
else: else:
return frame return frame
def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str, pair: str) -> DataFrame: def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) -> DataFrame:
""" """
Fills up missing data with 0 volume rows, Fills up missing data with 0 volume rows,
using the previous close as price for "open", "high" "low" and "close", volume is set to 0 using the previous close as price for "open", "high" "low" and "close", volume is set to 0
@ -72,7 +72,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str, pair:
'close': 'last', 'close': 'last',
'volume': 'sum' 'volume': 'sum'
} }
ticker_minutes = timeframe_to_minutes(ticker_interval) ticker_minutes = timeframe_to_minutes(timeframe)
# Resample to create "NAN" values # Resample to create "NAN" values
df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict) df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict)

View File

@ -37,52 +37,53 @@ class DataProvider:
@property @property
def available_pairs(self) -> List[Tuple[str, str]]: def available_pairs(self) -> List[Tuple[str, str]]:
""" """
Return a list of tuples containing pair, ticker_interval 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.
""" """
return list(self._exchange._klines.keys()) return list(self._exchange._klines.keys())
def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame:
""" """
Get ohlcv data for the given pair as DataFrame Get ohlcv data for the given pair as DataFrame
Please use the `available_pairs` method to verify which pairs are currently cached. Please use the `available_pairs` method to verify which pairs are currently cached.
:param pair: pair to get the data for :param pair: pair to get the data for
:param ticker_interval: ticker interval to get data for :param timeframe: Ticker timeframe to get data for
:param copy: copy dataframe before returning if True. :param copy: copy dataframe before returning if True.
Use False only for read-only operations (where the dataframe is not modified) Use False only for read-only operations (where the dataframe is not modified)
""" """
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
return self._exchange.klines((pair, ticker_interval or self._config['ticker_interval']), return self._exchange.klines((pair, timeframe or self._config['ticker_interval']),
copy=copy) copy=copy)
else: else:
return DataFrame() return DataFrame()
def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame: def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame:
""" """
Get stored historic ohlcv data Get stored historic ohlcv data
:param pair: pair to get the data for :param pair: pair to get the data for
:param ticker_interval: ticker interval to get data for :param timeframe: timeframe to get data for
""" """
return load_pair_history(pair=pair, return load_pair_history(pair=pair,
ticker_interval=ticker_interval or self._config['ticker_interval'], timeframe=timeframe or self._config['ticker_interval'],
datadir=Path(self._config['datadir']) datadir=Path(self._config['datadir'])
) )
def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame: def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
""" """
Return pair ohlcv data, either live or cached historical -- depending Return pair ohlcv data, either live or cached historical -- depending
on the runmode. on the runmode.
:param pair: pair to get the data for :param pair: pair to get the data for
:param ticker_interval: ticker interval to get data for :param timeframe: timeframe to get data for
:return: Dataframe for this pair
""" """
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
# Get live ohlcv data. # Get live ohlcv data.
data = self.ohlcv(pair=pair, ticker_interval=ticker_interval) data = self.ohlcv(pair=pair, timeframe=timeframe)
else: else:
# Get historic ohlcv data (cached on disk). # Get historic ohlcv data (cached on disk).
data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval) data = self.historic_ohlcv(pair=pair, timeframe=timeframe)
if len(data) == 0: if len(data) == 0:
logger.warning(f"No data found for ({pair}, {ticker_interval}).") logger.warning(f"No data found for ({pair}, {timeframe}).")
return data return data
def market(self, pair: str) -> Optional[Dict[str, Any]]: def market(self, pair: str) -> Optional[Dict[str, Any]]:

View File

@ -9,12 +9,11 @@ Includes:
import logging import logging
import operator import operator
from copy import deepcopy from copy import deepcopy
from datetime import datetime from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
import pytz
from pandas import DataFrame from pandas import DataFrame
from freqtrade import OperationalException, misc from freqtrade import OperationalException, misc
@ -51,26 +50,30 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
return tickerlist[start_index:stop_index] return tickerlist[start_index:stop_index]
def trim_dataframe(df: DataFrame, timerange: TimeRange) -> DataFrame: def trim_dataframe(df: DataFrame, timerange: TimeRange, df_date_col: str = 'date') -> DataFrame:
""" """
Trim dataframe based on given timerange Trim dataframe based on given timerange
:param df: Dataframe to trim
:param timerange: timerange (use start and end date if available)
:param: df_date_col: Column in the dataframe to use as Date column
:return: trimmed dataframe
""" """
if timerange.starttype == 'date': if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=pytz.utc) start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
df = df.loc[df['date'] >= start, :] df = df.loc[df[df_date_col] >= start, :]
if timerange.stoptype == 'date': if timerange.stoptype == 'date':
stop = datetime.fromtimestamp(timerange.stopts, tz=pytz.utc) stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
df = df.loc[df['date'] <= stop, :] df = df.loc[df[df_date_col] <= stop, :]
return df return df
def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str, def load_tickerdata_file(datadir: Path, pair: str, timeframe: str,
timerange: Optional[TimeRange] = None) -> Optional[list]: timerange: Optional[TimeRange] = None) -> Optional[list]:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
:return: tickerlist or None if unsuccessful :return: tickerlist or None if unsuccessful
""" """
filename = pair_data_filename(datadir, pair, ticker_interval) filename = pair_data_filename(datadir, pair, timeframe)
pairdata = misc.file_load_json(filename) pairdata = misc.file_load_json(filename)
if not pairdata: if not pairdata:
return [] return []
@ -81,11 +84,11 @@ def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str,
def store_tickerdata_file(datadir: Path, pair: str, def store_tickerdata_file(datadir: Path, pair: str,
ticker_interval: str, data: list, is_zip: bool = False): timeframe: str, data: list, is_zip: bool = False):
""" """
Stores tickerdata to file Stores tickerdata to file
""" """
filename = pair_data_filename(datadir, pair, ticker_interval) filename = pair_data_filename(datadir, pair, timeframe)
misc.file_dump_json(filename, data, is_zip=is_zip) misc.file_dump_json(filename, data, is_zip=is_zip)
@ -122,7 +125,7 @@ def _validate_pairdata(pair, pairdata, timerange: TimeRange):
def load_pair_history(pair: str, def load_pair_history(pair: str,
ticker_interval: str, timeframe: str,
datadir: Path, datadir: Path,
timerange: Optional[TimeRange] = None, timerange: Optional[TimeRange] = None,
refresh_pairs: bool = False, refresh_pairs: bool = False,
@ -134,7 +137,7 @@ def load_pair_history(pair: str,
""" """
Loads cached ticker history for the given pair. Loads cached ticker history for the given pair.
:param pair: Pair to load data for :param pair: Pair to load data for
:param ticker_interval: Ticker-interval (e.g. "5m") :param timeframe: Ticker timeframe (e.g. "5m")
:param datadir: Path to the data storage location. :param datadir: Path to the data storage location.
:param timerange: Limit data to be loaded to this timerange :param timerange: Limit data to be loaded to this timerange
:param refresh_pairs: Refresh pairs from exchange. :param refresh_pairs: Refresh pairs from exchange.
@ -148,34 +151,34 @@ def load_pair_history(pair: str,
timerange_startup = deepcopy(timerange) timerange_startup = deepcopy(timerange)
if startup_candles > 0 and timerange_startup: if startup_candles > 0 and timerange_startup:
timerange_startup.subtract_start(timeframe_to_seconds(ticker_interval) * startup_candles) timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
# The user forced the refresh of pairs # The user forced the refresh of pairs
if refresh_pairs: if refresh_pairs:
download_pair_history(datadir=datadir, download_pair_history(datadir=datadir,
exchange=exchange, exchange=exchange,
pair=pair, pair=pair,
ticker_interval=ticker_interval, timeframe=timeframe,
timerange=timerange) timerange=timerange)
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange_startup) pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup)
if pairdata: if pairdata:
if timerange_startup: if timerange_startup:
_validate_pairdata(pair, pairdata, timerange_startup) _validate_pairdata(pair, pairdata, timerange_startup)
return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair, return parse_ticker_dataframe(pairdata, timeframe, pair=pair,
fill_missing=fill_up_missing, fill_missing=fill_up_missing,
drop_incomplete=drop_incomplete) drop_incomplete=drop_incomplete)
else: else:
logger.warning( logger.warning(
f'No history data for pair: "{pair}", interval: {ticker_interval}. ' f'No history data for pair: "{pair}", timeframe: {timeframe}. '
'Use `freqtrade download-data` to download the data' 'Use `freqtrade download-data` to download the data'
) )
return None return None
def load_data(datadir: Path, def load_data(datadir: Path,
ticker_interval: str, timeframe: str,
pairs: List[str], pairs: List[str],
refresh_pairs: bool = False, refresh_pairs: bool = False,
exchange: Optional[Exchange] = None, exchange: Optional[Exchange] = None,
@ -187,7 +190,7 @@ def load_data(datadir: Path,
""" """
Loads ticker history data for a list of pairs Loads ticker history data for a list of pairs
:param datadir: Path to the data storage location. :param datadir: Path to the data storage location.
:param ticker_interval: Ticker-interval (e.g. "5m") :param timeframe: Ticker Timeframe (e.g. "5m")
:param pairs: List of pairs to load :param pairs: List of pairs to load
:param refresh_pairs: Refresh pairs from exchange. :param refresh_pairs: Refresh pairs from exchange.
(Note: Requires exchange to be passed as well.) (Note: Requires exchange to be passed as well.)
@ -207,7 +210,7 @@ def load_data(datadir: Path,
logger.info(f'Using indicator startup period: {startup_candles} ...') logger.info(f'Using indicator startup period: {startup_candles} ...')
for pair in pairs: for pair in pairs:
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, hist = load_pair_history(pair=pair, timeframe=timeframe,
datadir=datadir, timerange=timerange, datadir=datadir, timerange=timerange,
refresh_pairs=refresh_pairs, refresh_pairs=refresh_pairs,
exchange=exchange, exchange=exchange,
@ -221,9 +224,9 @@ def load_data(datadir: Path,
return result return result
def pair_data_filename(datadir: Path, pair: str, ticker_interval: str) -> Path: def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path:
pair_s = pair.replace("/", "_") pair_s = pair.replace("/", "_")
filename = datadir.joinpath(f'{pair_s}-{ticker_interval}.json') filename = datadir.joinpath(f'{pair_s}-{timeframe}.json')
return filename return filename
@ -233,7 +236,7 @@ def pair_trades_filename(datadir: Path, pair: str) -> Path:
return filename return filename
def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str, def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str,
timerange: Optional[TimeRange]) -> Tuple[List[Any], timerange: Optional[TimeRange]) -> Tuple[List[Any],
Optional[int]]: Optional[int]]:
""" """
@ -251,12 +254,12 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: st
if timerange.starttype == 'date': if timerange.starttype == 'date':
since_ms = timerange.startts * 1000 since_ms = timerange.startts * 1000
elif timerange.stoptype == 'line': elif timerange.stoptype == 'line':
num_minutes = timerange.stopts * timeframe_to_minutes(ticker_interval) num_minutes = timerange.stopts * timeframe_to_minutes(timeframe)
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
# read the cached file # read the cached file
# Intentionally don't pass timerange in - since we need to load the full dataset. # Intentionally don't pass timerange in - since we need to load the full dataset.
data = load_tickerdata_file(datadir, pair, ticker_interval) data = load_tickerdata_file(datadir, pair, timeframe)
# remove the last item, could be incomplete candle # remove the last item, could be incomplete candle
if data: if data:
data.pop() data.pop()
@ -277,18 +280,18 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: st
def download_pair_history(datadir: Path, def download_pair_history(datadir: Path,
exchange: Optional[Exchange], exchange: Optional[Exchange],
pair: str, pair: str,
ticker_interval: str = '5m', timeframe: str = '5m',
timerange: Optional[TimeRange] = None) -> bool: timerange: Optional[TimeRange] = None) -> bool:
""" """
Download the latest ticker intervals from the exchange for the pair passed in parameters Download latest candles from the exchange for the pair and timeframe passed in parameters
The data is downloaded starting from the last correct ticker interval data that The data is downloaded starting from the last correct data that
exists in a cache. If timerange starts earlier than the data in the cache, exists in a cache. If timerange starts earlier than the data in the cache,
the full data will be redownloaded the full data will be redownloaded
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
:param pair: pair to download :param pair: pair to download
:param ticker_interval: ticker interval :param timeframe: Ticker Timeframe (e.g 5m)
:param timerange: range of time to download :param timerange: range of time to download
:return: bool with success state :return: bool with success state
""" """
@ -299,17 +302,17 @@ def download_pair_history(datadir: Path,
try: try:
logger.info( logger.info(
f'Download history data for pair: "{pair}", interval: {ticker_interval} ' f'Download history data for pair: "{pair}", timeframe: {timeframe} '
f'and store in {datadir}.' f'and store in {datadir}.'
) )
data, since_ms = _load_cached_data_for_updating(datadir, pair, ticker_interval, timerange) data, since_ms = _load_cached_data_for_updating(datadir, pair, timeframe, timerange)
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
# Default since_ms to 30 days if nothing is given # Default since_ms to 30 days if nothing is given
new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, new_data = exchange.get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms if since_ms since_ms=since_ms if since_ms
else else
int(arrow.utcnow().shift( int(arrow.utcnow().shift(
@ -319,12 +322,12 @@ def download_pair_history(datadir: Path,
logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
logger.debug("New End: %s", misc.format_ms_time(data[-1][0])) logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
store_tickerdata_file(datadir, pair, ticker_interval, data=data) store_tickerdata_file(datadir, pair, timeframe, data=data)
return True return True
except Exception as e: except Exception as e:
logger.error( logger.error(
f'Failed to download history data for pair: "{pair}", interval: {ticker_interval}. ' f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}. '
f'Error: {e}' f'Error: {e}'
) )
return False return False
@ -344,17 +347,17 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
pairs_not_available.append(pair) pairs_not_available.append(pair)
logger.info(f"Skipping pair {pair}...") logger.info(f"Skipping pair {pair}...")
continue continue
for ticker_interval in timeframes: for timeframe in timeframes:
dl_file = pair_data_filename(dl_path, pair, ticker_interval) dl_file = pair_data_filename(dl_path, pair, timeframe)
if erase and dl_file.exists(): if erase and dl_file.exists():
logger.info( logger.info(
f'Deleting existing data for pair {pair}, interval {ticker_interval}.') f'Deleting existing data for pair {pair}, interval {timeframe}.')
dl_file.unlink() dl_file.unlink()
logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') logger.info(f'Downloading pair {pair}, interval {timeframe}.')
download_pair_history(datadir=dl_path, exchange=exchange, download_pair_history(datadir=dl_path, exchange=exchange,
pair=pair, ticker_interval=str(ticker_interval), pair=pair, timeframe=str(timeframe),
timerange=timerange) timerange=timerange)
return pairs_not_available return pairs_not_available
@ -460,7 +463,7 @@ def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]
def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
max_date: datetime, ticker_interval_mins: int) -> bool: max_date: datetime, timeframe_mins: int) -> bool:
""" """
Validates preprocessed backtesting data for missing values and shows warnings about it that. Validates preprocessed backtesting data for missing values and shows warnings about it that.
@ -468,10 +471,10 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
:param pair: pair used for log output. :param pair: pair used for log output.
:param min_date: start-date of the data :param min_date: start-date of the data
:param max_date: end-date of the data :param max_date: end-date of the data
:param ticker_interval_mins: ticker interval in minutes :param timeframe_mins: ticker Timeframe in minutes
""" """
# total difference in minutes / interval-minutes # total difference in minutes / timeframe-minutes
expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins) expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_mins)
found_missing = False found_missing = False
dflen = len(data) dflen = len(data)
if dflen < expected_frames: if dflen < expected_frames:

View File

@ -97,7 +97,7 @@ class Edge:
data = history.load_data( data = history.load_data(
datadir=Path(self.config['datadir']), datadir=Path(self.config['datadir']),
pairs=pairs, pairs=pairs,
ticker_interval=self.strategy.ticker_interval, timeframe=self.strategy.ticker_interval,
refresh_pairs=self._refresh_pairs, refresh_pairs=self._refresh_pairs,
exchange=self.exchange, exchange=self.exchange,
timerange=self._timerange, timerange=self._timerange,

View File

@ -15,3 +15,4 @@ from freqtrade.exchange.exchange import (market_is_active, # noqa: F401
symbol_is_pair) symbol_is_pair)
from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.kraken import Kraken # noqa: F401
from freqtrade.exchange.binance import Binance # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401
from freqtrade.exchange.bibox import Bibox # noqa: F401

View File

@ -0,0 +1,22 @@
""" Bibox exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Bibox(Exchange):
"""
Bibox exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
# fetchCurrencies API point requires authentication for Bibox,
# so switch it off for Freqtrade load_markets()
_ccxt_config: Dict = {"has": {"fetchCurrencies": False}}

View File

@ -30,6 +30,9 @@ class Exchange:
_config: Dict = {} _config: Dict = {}
# Parameters to add directly to ccxt sync/async initialization.
_ccxt_config: Dict = {}
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement) # Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
_params: Dict = {} _params: Dict = {}
@ -91,10 +94,17 @@ class Exchange:
self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
# Initialize ccxt objects # Initialize ccxt objects
ccxt_config = self._ccxt_config.copy()
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
ccxt_config)
self._api = self._init_ccxt( self._api = self._init_ccxt(
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) exchange_config, ccxt_kwargs=ccxt_config)
ccxt_async_config = self._ccxt_config.copy()
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
ccxt_async_config)
self._api_async = self._init_ccxt( self._api_async = self._init_ccxt(
exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config')) exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
logger.info('Using Exchange "%s"', self.name) logger.info('Using Exchange "%s"', self.name)
@ -536,40 +546,40 @@ class Exchange:
logger.info("returning cached ticker-data for %s", pair) logger.info("returning cached ticker-data for %s", pair)
return self._cached_ticker[pair] return self._cached_ticker[pair]
def get_historic_ohlcv(self, pair: str, ticker_interval: str, def get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int) -> List: since_ms: int) -> List:
""" """
Gets candle history using asyncio and returns the list of candles. Gets candle history using asyncio and returns the list of candles.
Handles all async doing. Handles all async doing.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
:param pair: Pair to download :param pair: Pair to download
:param ticker_interval: Interval to get :param timeframe: Ticker Timeframe to get
:param since_ms: Timestamp in milliseconds to get history from :param since_ms: Timestamp in milliseconds to get history from
:returns List of tickers :returns List of tickers
""" """
return asyncio.get_event_loop().run_until_complete( return asyncio.get_event_loop().run_until_complete(
self._async_get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval, self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms)) since_ms=since_ms))
async def _async_get_historic_ohlcv(self, pair: str, async def _async_get_historic_ohlcv(self, pair: str,
ticker_interval: str, timeframe: str,
since_ms: int) -> List: since_ms: int) -> List:
one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit
logger.debug( logger.debug(
"one_call: %s msecs (%s)", "one_call: %s msecs (%s)",
one_call, one_call,
arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True) arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True)
) )
input_coroutines = [self._async_get_candle_history( input_coroutines = [self._async_get_candle_history(
pair, ticker_interval, since) for since in pair, timeframe, since) for since in
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
# Combine tickers # Combine tickers
data: List = [] data: List = []
for p, ticker_interval, ticker in tickers: for p, timeframe, ticker in tickers:
if p == pair: if p == pair:
data.extend(ticker) data.extend(ticker)
# Sort data again after extending the result - above calls return in "async order" # Sort data again after extending the result - above calls return in "async order"
@ -589,14 +599,14 @@ class Exchange:
input_coroutines = [] input_coroutines = []
# Gather coroutines to run # Gather coroutines to run
for pair, ticker_interval in set(pair_list): for pair, timeframe in set(pair_list):
if (not ((pair, ticker_interval) in self._klines) if (not ((pair, timeframe) in self._klines)
or self._now_is_time_to_refresh(pair, ticker_interval)): or self._now_is_time_to_refresh(pair, timeframe)):
input_coroutines.append(self._async_get_candle_history(pair, ticker_interval)) input_coroutines.append(self._async_get_candle_history(pair, timeframe))
else: else:
logger.debug( logger.debug(
"Using cached ohlcv data for pair %s, interval %s ...", "Using cached ohlcv data for pair %s, timeframe %s ...",
pair, ticker_interval pair, timeframe
) )
tickers = asyncio.get_event_loop().run_until_complete( tickers = asyncio.get_event_loop().run_until_complete(
@ -608,40 +618,40 @@ class Exchange:
logger.warning("Async code raised an exception: %s", res.__class__.__name__) logger.warning("Async code raised an exception: %s", res.__class__.__name__)
continue continue
pair = res[0] pair = res[0]
ticker_interval = res[1] timeframe = res[1]
ticks = res[2] ticks = res[2]
# keeping last candle time as last refreshed time of the pair # keeping last candle time as last refreshed time of the pair
if ticks: if ticks:
self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
# keeping parsed dataframe in cache # keeping parsed dataframe in cache
self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( self._klines[(pair, timeframe)] = parse_ticker_dataframe(
ticks, ticker_interval, pair=pair, fill_missing=True, ticks, timeframe, pair=pair, fill_missing=True,
drop_incomplete=self._ohlcv_partial_candle) drop_incomplete=self._ohlcv_partial_candle)
return tickers return tickers
def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:
# Calculating ticker interval in seconds # Calculating ticker interval in seconds
interval_in_sec = timeframe_to_seconds(ticker_interval) interval_in_sec = timeframe_to_seconds(timeframe)
return not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0) return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0)
+ interval_in_sec) >= arrow.utcnow().timestamp) + interval_in_sec) >= arrow.utcnow().timestamp)
@retrier_async @retrier_async
async def _async_get_candle_history(self, pair: str, ticker_interval: str, async def _async_get_candle_history(self, pair: str, timeframe: str,
since_ms: Optional[int] = None) -> Tuple[str, str, List]: since_ms: Optional[int] = None) -> Tuple[str, str, List]:
""" """
Asynchronously gets candle histories using fetch_ohlcv Asynchronously gets candle histories using fetch_ohlcv
returns tuple: (pair, ticker_interval, ohlcv_list) returns tuple: (pair, timeframe, ohlcv_list)
""" """
try: try:
# fetch ohlcv asynchronously # fetch ohlcv asynchronously
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
logger.debug( logger.debug(
"Fetching pair %s, interval %s, since %s %s...", "Fetching pair %s, interval %s, since %s %s...",
pair, ticker_interval, since_ms, s pair, timeframe, since_ms, s
) )
data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe,
since=since_ms) since=since_ms)
# Because some exchange sort Tickers ASC and other DESC. # Because some exchange sort Tickers ASC and other DESC.
@ -653,9 +663,9 @@ class Exchange:
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
except IndexError: except IndexError:
logger.exception("Error loading %s. Result was %s.", pair, data) logger.exception("Error loading %s. Result was %s.", pair, data)
return pair, ticker_interval, [] return pair, timeframe, []
logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval) logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
return pair, ticker_interval, data return pair, timeframe, data
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
@ -802,7 +812,6 @@ class Exchange:
Handles all async doing. Handles all async doing.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
:param pair: Pair to download :param pair: Pair to download
:param ticker_interval: Interval to get
:param since: Timestamp in milliseconds to get history from :param since: Timestamp in milliseconds to get history from
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined. :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
:param from_id: Download data starting with ID (if id is known) :param from_id: Download data starting with ID (if id is known)
@ -875,6 +884,22 @@ class Exchange:
@retrier @retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
"""
Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
The "since" argument passed in is coming from the database and is in UTC,
as timezone-native datetime object.
From the python documentation:
> Naive datetime instances are assumed to represent local time
Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
transformation from local timezone to UTC.
This works for timezones UTC+ since then the result will contain trades from a few hours
instead of from the last 5 seconds, however fails for UTC- timezones,
since we're then asking for trades with a "since" argument in the future.
:param order_id order_id: Order-id as given when creating the order
:param pair: Pair the order is for
:param since: datetime object of the order creation time. Assumes object is in UTC.
"""
if self._config['dry_run']: if self._config['dry_run']:
return [] return []
if not self.exchange_has('fetchMyTrades'): if not self.exchange_has('fetchMyTrades'):
@ -882,7 +907,8 @@ class Exchange:
try: try:
# Allow 5s offset to catch slight time offsets (discovered in #1185) # Allow 5s offset to catch slight time offsets (discovered in #1185)
# since needs to be int in milliseconds # since needs to be int in milliseconds
my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000)) my_trades = self._api.fetch_my_trades(
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
matched_trades = [trade for trade in my_trades if trade['order'] == order_id] matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
return matched_trades return matched_trades
@ -941,27 +967,27 @@ def available_exchanges(ccxt_module=None) -> List[str]:
return [x for x in exchanges if not is_exchange_bad(x)] return [x for x in exchanges if not is_exchange_bad(x)]
def timeframe_to_seconds(ticker_interval: str) -> int: def timeframe_to_seconds(timeframe: str) -> int:
""" """
Translates the timeframe interval value written in the human readable Translates the timeframe interval value written in the human readable
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
of seconds for one timeframe interval. of seconds for one timeframe interval.
""" """
return ccxt.Exchange.parse_timeframe(ticker_interval) return ccxt.Exchange.parse_timeframe(timeframe)
def timeframe_to_minutes(ticker_interval: str) -> int: def timeframe_to_minutes(timeframe: str) -> int:
""" """
Same as timeframe_to_seconds, but returns minutes. Same as timeframe_to_seconds, but returns minutes.
""" """
return ccxt.Exchange.parse_timeframe(ticker_interval) // 60 return ccxt.Exchange.parse_timeframe(timeframe) // 60
def timeframe_to_msecs(ticker_interval: str) -> int: def timeframe_to_msecs(timeframe: str) -> int:
""" """
Same as timeframe_to_seconds, but returns milliseconds. Same as timeframe_to_seconds, but returns milliseconds.
""" """
return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 return ccxt.Exchange.parse_timeframe(timeframe) * 1000
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:

View File

@ -20,9 +20,9 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import (ExchangeResolver, PairListResolver, from freqtrade.resolvers import ExchangeResolver, StrategyResolver
StrategyResolver)
from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.strategy.interface import IStrategy, SellType
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@ -70,8 +70,7 @@ class FreqtradeBot:
# Attach Wallets to Strategy baseclass # Attach Wallets to Strategy baseclass
IStrategy.wallets = self.wallets IStrategy.wallets = self.wallets
pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList') self.pairlists = PairListManager(self.exchange, self.config)
self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist
# 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 \
@ -139,10 +138,9 @@ class FreqtradeBot:
if len(trades) < self.config['max_open_trades']: if len(trades) < self.config['max_open_trades']:
self.process_maybe_execute_buys() self.process_maybe_execute_buys()
if 'unfilledtimeout' in self.config: # Check and handle any timed out open orders
# Check and handle any timed out open orders self.check_handle_timedout()
self.check_handle_timedout() Trade.session.flush()
Trade.session.flush()
if (self.heartbeat_interval if (self.heartbeat_interval
and (arrow.utcnow().timestamp - self._heartbeat_msg > self.heartbeat_interval)): and (arrow.utcnow().timestamp - self._heartbeat_msg > self.heartbeat_interval)):
@ -756,23 +754,28 @@ class FreqtradeBot:
return True return True
return False return False
def _check_timed_out(self, side: str, order: dict) -> bool:
"""
Check if timeout is active, and if the order is still open and timed out
"""
timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime
if timeout is not None:
timeout_threshold = arrow.utcnow().shift(minutes=-timeout).datetime
return (order['status'] == 'open' and order['side'] == side
and ordertime < timeout_threshold)
return False
def check_handle_timedout(self) -> None: def check_handle_timedout(self) -> None:
""" """
Check if any orders are timed out and cancel if neccessary Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out :param timeoutvalue: Number of minutes until order is considered timed out
:return: None :return: None
""" """
buy_timeout = self.config['unfilledtimeout']['buy']
sell_timeout = self.config['unfilledtimeout']['sell']
buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
for trade in Trade.get_open_order_trades(): for trade in Trade.get_open_order_trades():
try: try:
# FIXME: Somehow the query above returns results
# where the open_order_id is in fact None.
# This is probably because the record got
# updated via /forcesell in a different thread.
if not trade.open_order_id: if not trade.open_order_id:
continue continue
order = self.exchange.get_order(trade.open_order_id, trade.pair) order = self.exchange.get_order(trade.open_order_id, trade.pair)
@ -782,23 +785,20 @@ class FreqtradeBot:
trade, trade,
traceback.format_exc()) traceback.format_exc())
continue continue
ordertime = arrow.get(order['datetime']).datetime
# Check if trade is still actually open # Check if trade is still actually open
if float(order['remaining']) == 0.0: if float(order.get('remaining', 0.0)) == 0.0:
self.wallets.update() self.wallets.update()
continue continue
if ((order['side'] == 'buy' and order['status'] == 'canceled') if ((order['side'] == 'buy' and order['status'] == 'canceled')
or (order['status'] == 'open' or (self._check_timed_out('buy', order))):
and order['side'] == 'buy' and ordertime < buy_timeout_threshold)):
self.handle_timedout_limit_buy(trade, order) self.handle_timedout_limit_buy(trade, order)
self.wallets.update() self.wallets.update()
elif ((order['side'] == 'sell' and order['status'] == 'canceled') elif ((order['side'] == 'sell' and order['status'] == 'canceled')
or (order['status'] == 'open' or (self._check_timed_out('sell', order))):
and order['side'] == 'sell' and ordertime < sell_timeout_threshold)):
self.handle_timedout_limit_sell(trade, order) self.handle_timedout_limit_sell(trade, order)
self.wallets.update() self.wallets.update()
@ -813,7 +813,8 @@ class FreqtradeBot:
}) })
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool: def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
"""Buy timeout - cancel order """
Buy timeout - cancel order
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
reason = "cancelled due to timeout" reason = "cancelled due to timeout"
@ -824,18 +825,22 @@ class FreqtradeBot:
corder = order corder = order
reason = "canceled on Exchange" reason = "canceled on Exchange"
if corder['remaining'] == corder['amount']: if corder.get('remaining', order['remaining']) == order['amount']:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
self.handle_buy_order_full_cancel(trade, reason) self.handle_buy_order_full_cancel(trade, reason)
return True return True
# 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
trade.amount = corder['amount'] - corder['remaining'] # cancel_order may not contain the full order dict, so we need to fallback
# to the order dict aquired before cancelling.
# we need to fall back to the values from order if corder does not contain these keys.
trade.amount = order['amount'] - corder.get('remaining', order['remaining'])
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
# verify if fees were taken from amount to avoid problems during selling # verify if fees were taken from amount to avoid problems during selling
try: try:
new_amount = self.get_real_amount(trade, corder, trade.amount) new_amount = self.get_real_amount(trade, corder if 'fee' in corder else order,
trade.amount)
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):
trade.amount = new_amount trade.amount = new_amount
# Fee was applied, so set to 0 # Fee was applied, so set to 0

View File

@ -15,7 +15,6 @@ from typing import Any, List
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.configuration import Arguments from freqtrade.configuration import Arguments
from freqtrade.worker import Worker
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
@ -28,21 +27,23 @@ def main(sysargv: List[str] = None) -> None:
""" """
return_code: Any = 1 return_code: Any = 1
worker = None
try: try:
arguments = Arguments(sysargv) arguments = Arguments(sysargv)
args = arguments.get_parsed_arg() args = arguments.get_parsed_arg()
# A subcommand has been issued. # Call subcommand.
# Means if Backtesting or Hyperopt have been called we exit the bot
if 'func' in args: if 'func' in args:
args['func'](args) return_code = args['func'](args)
# TODO: fetch return_code as returned by the command function here
return_code = 0
else: else:
# Load and run worker # No subcommand was issued.
worker = Worker(args) raise OperationalException(
worker.run() "Usage of Freqtrade requires a subcommand to be specified.\n"
"To have the previous behavior (bot executing trades in live/dry-run modes, "
"depending on the value of the `dry_run` setting in the config), run freqtrade "
"as `freqtrade trade [options...]`.\n"
"To see the full list of options available, please use "
"`freqtrade --help` or `freqtrade <command> --help`."
)
except SystemExit as e: except SystemExit as e:
return_code = e return_code = e
@ -55,8 +56,6 @@ def main(sysargv: List[str] = None) -> None:
except Exception: except Exception:
logger.exception('Fatal exception!') logger.exception('Fatal exception!')
finally: finally:
if worker:
worker.exit()
sys.exit(return_code) sys.exit(return_code)

View File

@ -78,7 +78,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
except Timeout: except Timeout:
logger.info("Another running instance of freqtrade Hyperopt detected.") logger.info("Another running instance of freqtrade Hyperopt detected.")
logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. " logger.info("Simultaneous execution of multiple Hyperopt commands is not supported. "
"Hyperopt module is resource hungry. Please run your Hyperopts sequentially " "Hyperopt module is resource hungry. Please run your Hyperopt sequentially "
"or on separate machines.") "or on separate machines.")
logger.info("Quitting now.") logger.info("Quitting now.")
# TODO: return False here in order to help freqtrade to exit # TODO: return False here in order to help freqtrade to exit

View File

@ -83,8 +83,8 @@ class Backtesting:
if "ticker_interval" not in self.config: if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration " raise OperationalException("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`") "or as cli argument `--ticker-interval 5m`")
self.ticker_interval = str(self.config.get('ticker_interval')) self.timeframe = str(self.config.get('ticker_interval'))
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) self.timeframe_mins = timeframe_to_minutes(self.timeframe)
# Get maximum required startup period # Get maximum required startup period
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
@ -108,7 +108,7 @@ class Backtesting:
data = history.load_data( data = history.load_data(
datadir=Path(self.config['datadir']), datadir=Path(self.config['datadir']),
pairs=self.config['exchange']['pair_whitelist'], pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.ticker_interval, timeframe=self.timeframe,
timerange=timerange, timerange=timerange,
startup_candles=self.required_startup, startup_candles=self.required_startup,
fail_without_data=True, fail_without_data=True,
@ -121,7 +121,7 @@ class Backtesting:
min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days
) )
# Adjust startts forward if not enough data is available # Adjust startts forward if not enough data is available
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.ticker_interval), timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
self.required_startup, min_date) self.required_startup, min_date)
return data, timerange return data, timerange
@ -375,7 +375,7 @@ class Backtesting:
lock_pair_until: Dict = {} lock_pair_until: Dict = {}
# Indexes per pair, so some pairs are allowed to have a missing start. # Indexes per pair, so some pairs are allowed to have a missing start.
indexes: Dict = {} indexes: Dict = {}
tmp = start_date + timedelta(minutes=self.ticker_interval_mins) tmp = start_date + timedelta(minutes=self.timeframe_mins)
# Loop timerange and get candle for each pair at that point in time # Loop timerange and get candle for each pair at that point in time
while tmp < end_date: while tmp < end_date:
@ -427,7 +427,7 @@ class Backtesting:
lock_pair_until[pair] = end_date.datetime lock_pair_until[pair] = end_date.datetime
# Move time one configured time_interval ahead. # Move time one configured time_interval ahead.
tmp += timedelta(minutes=self.ticker_interval_mins) tmp += timedelta(minutes=self.timeframe_mins)
return DataFrame.from_records(trades, columns=BacktestResult._fields) return DataFrame.from_records(trades, columns=BacktestResult._fields)
def start(self) -> None: def start(self) -> None:

View File

@ -1,6 +1,6 @@
""" """
IHyperOpt interface IHyperOpt interface
This module defines the interface to apply for hyperopts This module defines the interface to apply for hyperopt
""" """
import logging import logging
import math import math
@ -27,8 +27,8 @@ def _format_exception_message(method: str, space: str) -> str:
class IHyperOpt(ABC): class IHyperOpt(ABC):
""" """
Interface for freqtrade hyperopts Interface for freqtrade hyperopt
Defines the mandatory structure must follow any custom hyperopts Defines the mandatory structure must follow any custom hyperopt
Class attributes you can use: Class attributes you can use:
ticker_interval -> int: value of the ticker interval to use for the strategy ticker_interval -> int: value of the ticker interval to use for the strategy
@ -106,10 +106,10 @@ class IHyperOpt(ABC):
roi_t_alpha = 1.0 roi_t_alpha = 1.0
roi_p_alpha = 1.0 roi_p_alpha = 1.0
ticker_interval_mins = timeframe_to_minutes(IHyperOpt.ticker_interval) timeframe_mins = timeframe_to_minutes(IHyperOpt.ticker_interval)
# We define here limits for the ROI space parameters automagically adapted to the # We define here limits for the ROI space parameters automagically adapted to the
# ticker_interval used by the bot: # timeframe used by the bot:
# #
# * 'roi_t' (limits for the time intervals in the ROI tables) components # * 'roi_t' (limits for the time intervals in the ROI tables) components
# are scaled linearly. # are scaled linearly.
@ -117,8 +117,8 @@ class IHyperOpt(ABC):
# #
# The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space()
# method for the 5m ticker interval. # method for the 5m ticker interval.
roi_t_scale = ticker_interval_mins / 5 roi_t_scale = timeframe_mins / 5
roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5) roi_p_scale = math.log1p(timeframe_mins) / math.log1p(5)
roi_limits = { roi_limits = {
'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha), 'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha),
'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha), 'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha),

View File

@ -1,6 +1,6 @@
""" """
IHyperOptLoss interface IHyperOptLoss interface
This module defines the interface for the loss-function for hyperopts This module defines the interface for the loss-function for hyperopt
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@ -11,7 +11,7 @@ from pandas import DataFrame
class IHyperOptLoss(ABC): class IHyperOptLoss(ABC):
""" """
Interface for freqtrade hyperopts Loss functions. Interface for freqtrade hyperopt Loss functions.
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
""" """
ticker_interval: str ticker_interval: str

View File

@ -5,22 +5,31 @@ Provides lists as configured in config.json
""" """
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod, abstractproperty
from typing import List from copy import deepcopy
from typing import Dict, List
from freqtrade.exchange import market_is_active from freqtrade.exchange import market_is_active
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class IPairList(ABC): class IPairList(ABC):
def __init__(self, freqtrade, config: dict) -> None: def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
self._freqtrade = freqtrade pairlist_pos: int) -> None:
"""
:param exchange: Exchange instance
:param pairlistmanager: Instanciating Pairlist manager
:param config: Global bot configuration
:param pairlistconfig: Configuration for this pairlist - can be empty.
:param pairlist_pos: Position of the filter in the pairlist-filter-list
"""
self._exchange = exchange
self._pairlistmanager = pairlistmanager
self._config = config self._config = config
self._whitelist = self._config['exchange']['pair_whitelist'] self._pairlistconfig = pairlistconfig
self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlist_pos = pairlist_pos
@property @property
def name(self) -> str: def name(self) -> str:
@ -30,21 +39,13 @@ class IPairList(ABC):
""" """
return self.__class__.__name__ return self.__class__.__name__
@property @abstractproperty
def whitelist(self) -> List[str]: def needstickers(self) -> bool:
""" """
Has the current whitelist Boolean property defining if tickers are necessary.
-> no need to overwrite in subclasses If no Pairlist requries tickers, an empty List is passed
as tickers argument to filter_pairlist
""" """
return self._whitelist
@property
def blacklist(self) -> List[str]:
"""
Has the current blacklist
-> no need to overwrite in subclasses
"""
return self._blacklist
@abstractmethod @abstractmethod
def short_desc(self) -> str: def short_desc(self) -> str:
@ -54,36 +55,62 @@ class IPairList(ABC):
""" """
@abstractmethod @abstractmethod
def refresh_pairlist(self) -> None: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
-> Please overwrite in subclasses -> Please overwrite in subclasses
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
""" """
def _validate_whitelist(self, whitelist: List[str]) -> List[str]: @staticmethod
def verify_blacklist(pairlist: List[str], blacklist: List[str]) -> List[str]:
"""
Verify and remove items from pairlist - returning a filtered pairlist.
"""
for pair in deepcopy(pairlist):
if pair in blacklist:
logger.warning(f"Pair {pair} in your blacklist. Removing it from whitelist...")
pairlist.remove(pair)
return pairlist
def _verify_blacklist(self, pairlist: List[str]) -> List[str]:
"""
Proxy method to verify_blacklist for easy access for child classes.
"""
return IPairList.verify_blacklist(pairlist, self._pairlistmanager.blacklist)
def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]:
""" """
Check available markets and remove pair from whitelist if necessary Check available markets and remove pair from whitelist if necessary
:param whitelist: the sorted list of pairs the user might want to trade :param whitelist: the sorted list of pairs the user might want to trade
:return: the list of pairs the user wants to trade without those unavailable or :return: the list of pairs the user wants to trade without those unavailable or
black_listed black_listed
""" """
markets = self._freqtrade.exchange.markets markets = self._exchange.markets
sanitized_whitelist = set() sanitized_whitelist: List[str] = []
for pair in whitelist: for pair in pairlist:
# pair is not in the generated dynamic market, or in the blacklist ... ignore it # pair is not in the generated dynamic market or has the wrong stake currency
if (pair in self.blacklist or pair not in markets if pair not in markets:
or not pair.endswith(self._config['stake_currency'])):
logger.warning(f"Pair {pair} is not compatible with exchange " logger.warning(f"Pair {pair} is not compatible with exchange "
f"{self._freqtrade.exchange.name} or contained in " f"{self._exchange.name}. Removing it from whitelist..")
f"your blacklist. Removing it from whitelist..")
continue continue
if not pair.endswith(self._config['stake_currency']):
logger.warning(f"Pair {pair} is not compatible with your stake currency "
f"{self._config['stake_currency']}. Removing it from whitelist..")
continue
# Check if market is active # Check if market is active
market = markets[pair] market = markets[pair]
if not market_is_active(market): if not market_is_active(market):
logger.info(f"Ignoring {pair} from whitelist. Market is not active.") logger.info(f"Ignoring {pair} from whitelist. Market is not active.")
continue continue
sanitized_whitelist.add(pair) if pair not in sanitized_whitelist:
sanitized_whitelist.append(pair)
sanitized_whitelist = self._verify_blacklist(sanitized_whitelist)
# We need to remove pairs that are unknown # We need to remove pairs that are unknown
return list(sanitized_whitelist) return sanitized_whitelist

View File

@ -0,0 +1,62 @@
import logging
from copy import deepcopy
from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)
class PrecisionFilter(IPairList):
@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 True
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - Filtering untradable pairs."
def _validate_precision_filter(self, ticker: dict, stoploss: float) -> bool:
"""
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
low value pairs.
:param ticker: ticker dict as returned from ccxt.load_markets()
:param stoploss: stoploss value as set in the configuration
(already cleaned to be 1 - stoploss)
:return: True if the pair can stay, false if it should be removed
"""
stop_price = ticker['ask'] * stoploss
# Adjust stop-prices to precision
sp = self._exchange.symbol_price_prec(ticker["symbol"], stop_price)
stop_gap_price = self._exchange.symbol_price_prec(ticker["symbol"], stop_price * 0.99)
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
if sp <= stop_gap_price:
logger.info(f"Removed {ticker['symbol']} from whitelist, "
f"because stop price {sp} would be <= stop limit {stop_gap_price}")
return False
return True
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlists and assigns and returns them again.
"""
if self._config.get('stoploss') is not None:
# Precalculate sanitized stoploss value to avoid recalculation for every pair
stoploss = 1 - abs(self._config.get('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

@ -0,0 +1,69 @@
import logging
from copy import deepcopy
from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)
class PriceFilter(IPairList):
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0)
@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 True
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%."
def _validate_ticker_lowprice(self, ticker) -> bool:
"""
Check if if one price-step (pip) is > than a certain barrier.
:param ticker: ticker dict as returned from ccxt.load_markets()
:param precision: Precision
:return: True if the pair can stay, false if it should be removed
"""
precision = self._exchange.markets[ticker['symbol']]['precision']['price']
compare = ticker['last'] + 1 / pow(10, precision)
changeperc = (compare - ticker['last']) / ticker['last']
if changeperc > self._low_price_ratio:
logger.info(f"Removed {ticker['symbol']} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%")
return False
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

@ -5,6 +5,7 @@ Provides lists as configured in config.json
""" """
import logging import logging
from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
@ -13,18 +14,28 @@ logger = logging.getLogger(__name__)
class StaticPairList(IPairList): class StaticPairList(IPairList):
def __init__(self, freqtrade, config: dict) -> None: @property
super().__init__(freqtrade, config) 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: def short_desc(self) -> str:
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
-> Please overwrite in subclasses -> Please overwrite in subclasses
""" """
return f"{self.name}: {self.whitelist}" return f"{self.name}"
def refresh_pairlist(self) -> None: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively 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
""" """
self._whitelist = self._validate_whitelist(self._config['exchange']['pair_whitelist']) return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist'])

View File

@ -5,11 +5,12 @@ Provides lists as configured in config.json
""" """
import logging import logging
from typing import List from datetime import datetime
from cachetools import TTLCache, cached from typing import Dict, List
from freqtrade.pairlist.IPairList import IPairList
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
@ -17,18 +18,19 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
class VolumePairList(IPairList): class VolumePairList(IPairList):
def __init__(self, freqtrade, config: dict) -> None: def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
super().__init__(freqtrade, config) pairlist_pos: int) -> None:
self._whitelistconf = self._config.get('pairlist', {}).get('config') super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._whitelistconf:
if 'number_assets' not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
f'`number_assets` not specified. Please check your configuration ' f'`number_assets` not specified. Please check your configuration '
'for "pairlist.config.number_assets"') 'for "pairlist.config.number_assets"')
self._number_pairs = self._whitelistconf['number_assets'] self._number_pairs = self._pairlistconfig['number_assets']
self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume') self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
self._precision_filter = self._whitelistconf.get('precision_filter', False) self.refresh_period = self._pairlistconfig.get('refresh_period', 1800)
if not self._freqtrade.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'
@ -36,6 +38,16 @@ class VolumePairList(IPairList):
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}')
self._last_refresh = 0
@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 True
def _validate_keys(self, key): def _validate_keys(self, key):
return key in SORT_VALUES return key in SORT_VALUES
@ -43,54 +55,54 @@ class VolumePairList(IPairList):
def short_desc(self) -> str: def short_desc(self) -> str:
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
-> Please overwrite in subclasses
""" """
return f"{self.name} - top {self._whitelistconf['number_assets']} volume pairs." return f"{self.name} - top {self._pairlistconfig['number_assets']} volume pairs."
def refresh_pairlist(self) -> None: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively Filters and sorts pairlist and returns the whitelist again.
-> Please overwrite in subclasses 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
""" """
# Generate dynamic whitelist # Generate dynamic whitelist
self._whitelist = self._gen_pair_whitelist( if self._last_refresh + self.refresh_period < datetime.now().timestamp():
self._config['stake_currency'], self._sort_key) self._last_refresh = int(datetime.now().timestamp())
return self._gen_pair_whitelist(pairlist,
tickers,
self._config['stake_currency'],
self._sort_key,
)
else:
return pairlist
@cached(TTLCache(maxsize=1, ttl=1800)) def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str, key: str) -> List[str]:
def _gen_pair_whitelist(self, base_currency: str, key: str) -> 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 base_currency: base currency as str
:param key: sort key (defaults to 'quoteVolume') :param key: sort key (defaults to 'quoteVolume')
:param tickers: Tickers (from exchange.get_tickers()).
:return: List of pairs :return: List of pairs
""" """
tickers = self._freqtrade.exchange.get_tickers() if self._pairlist_pos == 0:
# check length so that we make sure that '/' is actually in the string # If VolumePairList is the first in the list, use fresh pairlist
tickers = [v for k, v in tickers.items() # check length so that we make sure that '/' is actually in the string
if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency filtered_tickers = [v for k, v in tickers.items()
and v[key] is not None)] if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency
sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key]) and v[key] is not None)]
else:
# If other pairlist is in front, use the incomming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key])
# Validate whitelist to only have active market pairs # Validate whitelist to only have active market pairs
valid_pairs = self._validate_whitelist([s['symbol'] for s in sorted_tickers]) pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
valid_tickers = [t for t in sorted_tickers if t["symbol"] in valid_pairs] pairs = self._verify_blacklist(pairs)
# Limit to X number of pairs
if self._freqtrade.strategy.stoploss is not None and self._precision_filter: pairs = pairs[:self._number_pairs]
logger.info(f"Searching {self._number_pairs} pairs: {pairs}")
stop_prices = [self._freqtrade.get_target_bid(t["symbol"], t)
* (1 - abs(self._freqtrade.strategy.stoploss)) for t in valid_tickers]
rates = [sp * 0.99 for sp in stop_prices]
logger.debug("\n".join([f"{sp} : {r}" for sp, r in zip(stop_prices[:10], rates[:10])]))
for i, t in enumerate(valid_tickers):
sp = self._freqtrade.exchange.symbol_price_prec(t["symbol"], stop_prices[i])
r = self._freqtrade.exchange.symbol_price_prec(t["symbol"], rates[i])
logger.debug(f"{t['symbol']} - {sp} : {r}")
if sp <= r:
logger.info(f"Removed {t['symbol']} from whitelist, "
f"because stop price {sp} would be <= stop limit {r}")
valid_tickers.remove(t)
pairs = [s['symbol'] for s in valid_tickers]
logger.info(f"Searching pairs: {pairs[:self._number_pairs]}")
return pairs return pairs

View File

@ -0,0 +1,95 @@
"""
Static List provider
Provides lists as configured in config.json
"""
from cachetools import TTLCache, cached
import logging
from typing import Dict, List
from freqtrade import OperationalException
from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import PairListResolver
logger = logging.getLogger(__name__)
class PairListManager():
def __init__(self, exchange, config: dict) -> None:
self._exchange = exchange
self._config = config
self._whitelist = self._config['exchange'].get('pair_whitelist')
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
self._pairlists: List[IPairList] = []
self._tickers_needed = False
for pl in self._config.get('pairlists', None):
if 'method' not in pl:
logger.warning(f"No method in {pl}")
continue
pairl = PairListResolver(pl.get('method'),
exchange=exchange,
pairlistmanager=self,
config=config,
pairlistconfig=pl,
pairlist_pos=len(self._pairlists)
).pairlist
self._tickers_needed = pairl.needstickers or self._tickers_needed
self._pairlists.append(pairl)
if not self._pairlists:
raise OperationalException("No Pairlist defined!")
@property
def whitelist(self) -> List[str]:
"""
Has the current whitelist
"""
return self._whitelist
@property
def blacklist(self) -> List[str]:
"""
Has the current blacklist
-> no need to overwrite in subclasses
"""
return self._blacklist
@property
def name_list(self) -> List[str]:
"""
Get list of loaded pairlists names
"""
return [p.name for p in self._pairlists]
def short_desc(self) -> List[Dict]:
"""
List of short_desc for each pairlist
"""
return [{p.name: p.short_desc()} for p in self._pairlists]
@cached(TTLCache(maxsize=1, ttl=1800))
def _get_cached_tickers(self):
return self._exchange.get_tickers()
def refresh_pairlist(self) -> None:
"""
Run pairlist through all configured pairlists.
"""
pairlist = self._whitelist.copy()
# tickers should be cached to avoid calling the exchange on each call.
tickers: Dict = {}
if self._tickers_needed:
tickers = self._get_cached_tickers()
# Process all pairlists in chain
for pl in self._pairlists:
pairlist = pl.filter_pairlist(pairlist, tickers)
# Validation against blacklist happens after the pairlists to ensure blacklist is respected.
pairlist = IPairList.verify_blacklist(pairlist, self.blacklist)
self._whitelist = pairlist

View File

@ -39,7 +39,7 @@ def init_plotscript(config):
tickers = history.load_data( tickers = history.load_data(
datadir=Path(str(config.get("datadir"))), datadir=Path(str(config.get("datadir"))),
pairs=pairs, pairs=pairs,
ticker_interval=config.get('ticker_interval', '5m'), timeframe=config.get('ticker_interval', '5m'),
timerange=timerange, timerange=timerange,
) )
@ -47,7 +47,7 @@ def init_plotscript(config):
db_url=config.get('db_url'), db_url=config.get('db_url'),
exportfilename=config.get('exportfilename'), exportfilename=config.get('exportfilename'),
) )
trades = history.trim_dataframe(trades, timerange, 'open_time')
return {"tickers": tickers, return {"tickers": tickers,
"trades": trades, "trades": trades,
"pairs": pairs, "pairs": pairs,
@ -300,12 +300,12 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
return fig return fig
def generate_plot_filename(pair, ticker_interval) -> str: def generate_plot_filename(pair, timeframe) -> str:
""" """
Generate filenames per pair/ticker_interval to be used for storing plots Generate filenames per pair/timeframe to be used for storing plots
""" """
pair_name = pair.replace("/", "_") pair_name = pair.replace("/", "_")
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' file_name = 'freqtrade-plot-' + pair_name + '-' + timeframe + '.html'
logger.info('Generate plot file for %s', pair) logger.info('Generate plot file for %s', pair)
@ -316,8 +316,9 @@ def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False
""" """
Generate a plot html file from pre populated fig plotly object Generate a plot html file from pre populated fig plotly object
:param fig: Plotly Figure to plot :param fig: Plotly Figure to plot
:param pair: Pair to plot (used as filename and Plot title) :param filename: Name to store the file as
:param ticker_interval: Used as part of the filename :param directory: Directory to store the file in
:param auto_open: Automatically open files saved
:return: None :return: None
""" """
directory.mkdir(parents=True, exist_ok=True) directory.mkdir(parents=True, exist_ok=True)
@ -376,12 +377,14 @@ def plot_profit(config: Dict[str, Any]) -> None:
in helping out to find a good algorithm. in helping out to find a good algorithm.
""" """
plot_elements = init_plotscript(config) plot_elements = init_plotscript(config)
trades = load_trades(config['trade_source'], trades = plot_elements['trades']
db_url=str(config.get('db_url')),
exportfilename=str(config.get('exportfilename')),
)
# Filter trades to relevant pairs # Filter trades to relevant pairs
trades = trades[trades['pair'].isin(plot_elements["pairs"])] # Remove open pairs - we don't know the profit yet so can't calculate profit for these.
# Also, If only one open pair is left, then the profit-generation would fail.
trades = trades[(trades['pair'].isin(plot_elements["pairs"]))
& (~trades['close_time'].isnull())
]
# 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
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"],

View File

@ -1,14 +1,14 @@
# pragma pylint: disable=attribute-defined-outside-init # pragma pylint: disable=attribute-defined-outside-init
""" """
This module load custom hyperopts This module load custom hyperopt
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Dict from typing import Optional, Dict
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_HYPEROPT_LOSS from freqtrade.constants import DEFAULT_HYPEROPT_LOSS
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
@ -20,7 +20,6 @@ class HyperOptResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt class This class contains all the logic to load custom hyperopt class
""" """
__slots__ = ['hyperopt'] __slots__ = ['hyperopt']
def __init__(self, config: Dict) -> None: def __init__(self, config: Dict) -> None:
@ -28,9 +27,12 @@ class HyperOptResolver(IResolver):
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary :param config: configuration dictionary
""" """
if not config.get('hyperopt'):
raise OperationalException("No Hyperopt set. Please use `--hyperopt` to specify "
"the Hyperopt class to use.")
hyperopt_name = config['hyperopt']
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
self.hyperopt = self._load_hyperopt(hyperopt_name, config, self.hyperopt = self._load_hyperopt(hyperopt_name, config,
extra_dir=config.get('hyperopt_path')) extra_dir=config.get('hyperopt_path'))
@ -72,27 +74,28 @@ class HyperOptLossResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt loss class This class contains all the logic to load custom hyperopt loss class
""" """
__slots__ = ['hyperoptloss'] __slots__ = ['hyperoptloss']
def __init__(self, config: Dict = None) -> None: def __init__(self, config: Dict) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary
""" """
config = config or {}
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt # Verify the hyperopt_loss is in the configuration, otherwise fallback to the
hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS # default hyperopt loss
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
self.hyperoptloss = self._load_hyperoptloss( self.hyperoptloss = self._load_hyperoptloss(
hyperopt_name, config, extra_dir=config.get('hyperopt_path')) hyperoptloss_name, config, extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt # Assign ticker_interval to be used in hyperopt
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'):
raise OperationalException( raise OperationalException(
f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.") f"Found HyperoptLoss class {hyperoptloss_name} does not "
"implement `hyperopt_loss_function`.")
def _load_hyperoptloss( def _load_hyperoptloss(
self, hyper_loss_name: str, config: Dict, self, hyper_loss_name: str, config: Dict,

View File

@ -17,13 +17,13 @@ class IResolver:
This class contains all the logic to load custom classes This class contains all the logic to load custom classes
""" """
def build_search_paths(self, config, current_path: Path, user_subdir: str, def build_search_paths(self, config, current_path: Path, user_subdir: Optional[str] = None,
extra_dir: Optional[str] = None) -> List[Path]: extra_dir: Optional[str] = None) -> List[Path]:
abs_paths = [ abs_paths: List[Path] = [current_path]
config['user_data_dir'].joinpath(user_subdir),
current_path, if user_subdir:
] abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir))
if extra_dir: if extra_dir:
# Add extra directory to the top of the search paths # Add extra directory to the top of the search paths

View File

@ -20,13 +20,18 @@ class PairListResolver(IResolver):
__slots__ = ['pairlist'] __slots__ = ['pairlist']
def __init__(self, pairlist_name: str, freqtrade, config: dict) -> None: def __init__(self, pairlist_name: str, exchange, pairlistmanager,
config: dict, pairlistconfig: dict, pairlist_pos: int) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary or None
""" """
self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'freqtrade': freqtrade, self.pairlist = self._load_pairlist(pairlist_name, config,
'config': config}) kwargs={'exchange': exchange,
'pairlistmanager': pairlistmanager,
'config': config,
'pairlistconfig': pairlistconfig,
'pairlist_pos': pairlist_pos})
def _load_pairlist( def _load_pairlist(
self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList: self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
@ -40,7 +45,7 @@ class PairListResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = self.build_search_paths(config, current_path=current_path, abs_paths = self.build_search_paths(config, current_path=current_path,
user_subdir='pairlist', extra_dir=None) user_subdir=None, extra_dir=None)
pairlist = self._load_object(paths=abs_paths, object_type=IPairList, pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs) object_name=pairlist_name, kwargs=kwargs)

View File

@ -32,8 +32,11 @@ class StrategyResolver(IResolver):
""" """
config = config or {} config = config or {}
# Verify the strategy is in the configuration, otherwise fallback to the default strategy if not config.get('strategy'):
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY raise OperationalException("No strategy set. Please use `--strategy` to specify "
"the strategy class to use.")
strategy_name = config['strategy']
self.strategy: IStrategy = self._load_strategy(strategy_name, self.strategy: IStrategy = self._load_strategy(strategy_name,
config=config, config=config,
extra_dir=config.get('strategy_path')) extra_dir=config.get('strategy_path'))

View File

@ -169,6 +169,10 @@ class ApiServer(RPC):
view_func=self._status, methods=['GET']) view_func=self._status, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/version', 'version', self.app.add_url_rule(f'{BASE_URI}/version', 'version',
view_func=self._version, methods=['GET']) view_func=self._version, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/show_config', 'show_config',
view_func=self._show_config, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/ping', 'ping',
view_func=self._ping, methods=['GET'])
# Combined actions and infos # Combined actions and infos
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
@ -224,6 +228,13 @@ class ApiServer(RPC):
msg = self._rpc_stopbuy() msg = self._rpc_stopbuy()
return self.rest_dump(msg) return self.rest_dump(msg)
@rpc_catch_errors
def _ping(self):
"""
simple poing version
"""
return self.rest_dump({"status": "pong"})
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
def _version(self): def _version(self):
@ -232,6 +243,14 @@ class ApiServer(RPC):
""" """
return self.rest_dump({"version": __version__}) return self.rest_dump({"version": __version__})
@require_login
@rpc_catch_errors
def _show_config(self):
"""
Prints the bot's version
"""
return self.rest_dump(self._rpc_show_config())
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors
def _reload_conf(self): def _reload_conf(self):
@ -265,7 +284,7 @@ class ApiServer(RPC):
stats = self._rpc_daily_profit(timescale, stats = self._rpc_daily_profit(timescale,
self._config['stake_currency'], self._config['stake_currency'],
self._config['fiat_display_currency'] self._config.get('fiat_display_currency', '')
) )
return self.rest_dump(stats) return self.rest_dump(stats)
@ -321,8 +340,11 @@ class ApiServer(RPC):
Returns the current status of the trades in json format Returns the current status of the trades in json format
""" """
results = self._rpc_trade_status() try:
return self.rest_dump(results) results = self._rpc_trade_status()
return self.rest_dump(results)
except RPCException:
return self.rest_dump([])
@require_login @require_login
@rpc_catch_errors @rpc_catch_errors

View File

@ -3,16 +3,15 @@ This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import timedelta, datetime, date from datetime import date, datetime, timedelta
from decimal import Decimal
from enum import Enum from enum import Enum
from typing import Dict, Any, List, Optional from math import isnan
from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
from numpy import mean, NAN from numpy import NAN, mean
from pandas import DataFrame
from freqtrade import TemporaryError, DependencyException from freqtrade import DependencyException, TemporaryError
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@ -81,6 +80,29 @@ class RPC:
def send_msg(self, msg: Dict[str, str]) -> None: def send_msg(self, msg: Dict[str, str]) -> None:
""" Sends a message to all registered rpc modules """ """ Sends a message to all registered rpc modules """
def _rpc_show_config(self) -> Dict[str, Any]:
"""
Return a dict of config options.
Explicitly does NOT return the full config to avoid leakage of sensitive
information via rpc.
"""
config = self._freqtrade.config
val = {
'dry_run': config.get('dry_run', False),
'stake_currency': config['stake_currency'],
'stake_amount': config['stake_amount'],
'minimal_roi': config['minimal_roi'].copy(),
'stoploss': config['stoploss'],
'trailing_stop': config['trailing_stop'],
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'ticker_interval': config['ticker_interval'],
'exchange': config['exchange']['name'],
'strategy': config['strategy'],
}
return val
def _rpc_trade_status(self) -> List[Dict[str, Any]]: def _rpc_trade_status(self) -> List[Dict[str, Any]]:
""" """
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
@ -117,7 +139,7 @@ class RPC:
results.append(trade_dict) results.append(trade_dict)
return results return results
def _rpc_status_table(self) -> DataFrame: def _rpc_status_table(self, stake_currency, fiat_display_currency: str) -> Tuple[List, List]:
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
if not trades: if not trades:
raise RPCException('no active order') raise RPCException('no active order')
@ -130,17 +152,28 @@ class RPC:
except DependencyException: except DependencyException:
current_rate = NAN current_rate = NAN
trade_perc = (100 * trade.calc_profit_percent(current_rate)) trade_perc = (100 * trade.calc_profit_percent(current_rate))
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade_perc:.2f}%'
if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount(
trade_profit,
stake_currency,
fiat_display_currency
)
if fiat_profit and not isnan(fiat_profit):
profit_str += f" ({fiat_profit:.2f})"
trades_list.append([ trades_list.append([
trade.id, trade.id,
trade.pair, trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
f'{trade_perc:.2f}%' profit_str
]) ])
profitcol = "Profit"
if self._fiat_converter:
profitcol += " (" + fiat_display_currency + ")"
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', profitcol]
df_statuses = DataFrame.from_records(trades_list, columns=columns) return trades_list, columns
df_statuses = df_statuses.set_index(columns[0])
return df_statuses
def _rpc_daily_profit( def _rpc_daily_profit(
self, timescale: int, self, timescale: int,
@ -219,7 +252,7 @@ class RPC:
profit_percent = trade.calc_profit_percent(rate=current_rate) profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append( profit_all_coin.append(
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)) trade.calc_profit(rate=trade.close_rate or current_rate)
) )
profit_all_perc.append(profit_percent) profit_all_perc.append(profit_percent)
@ -452,7 +485,7 @@ class RPC:
def _rpc_whitelist(self) -> Dict: def _rpc_whitelist(self) -> Dict:
""" Returns the currently active whitelist""" """ Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name, res = {'method': self._freqtrade.pairlists.name_list,
'length': len(self._freqtrade.active_pair_whitelist), 'length': len(self._freqtrade.active_pair_whitelist),
'whitelist': self._freqtrade.active_pair_whitelist 'whitelist': self._freqtrade.active_pair_whitelist
} }
@ -467,7 +500,7 @@ class RPC:
and pair not in self._freqtrade.pairlists.blacklist): and pair not in self._freqtrade.pairlists.blacklist):
self._freqtrade.pairlists.blacklist.append(pair) self._freqtrade.pairlists.blacklist.append(pair)
res = {'method': self._freqtrade.pairlists.name, res = {'method': self._freqtrade.pairlists.name_list,
'length': len(self._freqtrade.pairlists.blacklist), 'length': len(self._freqtrade.pairlists.blacklist),
'blacklist': self._freqtrade.pairlists.blacklist, 'blacklist': self._freqtrade.pairlists.blacklist,
} }

View File

@ -95,6 +95,7 @@ class Telegram(RPC):
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf), CommandHandler('reload_conf', self._reload_conf),
CommandHandler('show_config', self._show_config),
CommandHandler('stopbuy', self._stopbuy), CommandHandler('stopbuy', self._stopbuy),
CommandHandler('whitelist', self._whitelist), CommandHandler('whitelist', self._whitelist),
CommandHandler('blacklist', self._blacklist), CommandHandler('blacklist', self._blacklist),
@ -234,8 +235,9 @@ class Telegram(RPC):
:return: None :return: None
""" """
try: try:
df_statuses = self._rpc_status_table() statlist, head = self._rpc_status_table(self._config['stake_currency'],
message = tabulate(df_statuses, headers='keys', tablefmt='simple') self._config.get('fiat_display_currency', ''))
message = tabulate(statlist, headers=head, tablefmt='simple')
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML) self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@ -549,6 +551,7 @@ class Telegram(RPC):
"*/balance:* `Show account balance per currency`\n" \ "*/balance:* `Show account balance per currency`\n" \
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
"*/reload_conf:* `Reload configuration file` \n" \ "*/reload_conf:* `Reload configuration file` \n" \
"*/show_config:* `Show running configuration` \n" \
"*/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" \
@ -569,6 +572,26 @@ class Telegram(RPC):
""" """
self._send_msg('*Version:* `{}`'.format(__version__)) self._send_msg('*Version:* `{}`'.format(__version__))
@authorized_only
def _show_config(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /show_config.
Show config information information
:param bot: telegram bot
:param update: message update
:return: None
"""
val = self._rpc_show_config()
self._send_msg(
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
f"*Exchange:* `{val['exchange']}`\n"
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
f"*{'Trailing ' if val['trailing_stop'] else ''}Stoploss:* `{val['stoploss']}`\n"
f"*Ticker Interval:* `{val['ticker_interval']}`\n"
f"*Strategy:* `{val['strategy']}`'"
)
def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message

View File

@ -109,8 +109,8 @@ class IStrategy(ABC):
# Class level variables (intentional) containing # Class level variables (intentional) containing
# the dataprovider (dp) (access to other candles, historic data, ...) # the dataprovider (dp) (access to other candles, historic data, ...)
# and wallets - access to the current balance. # and wallets - access to the current balance.
dp: DataProvider dp: Optional[DataProvider] = None
wallets: Wallets wallets: Optional[Wallets] = None
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
self.config = config self.config = config

View File

@ -39,6 +39,25 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
return config return config
def start_trading(args: Dict[str, Any]) -> int:
"""
Main entry point for trading mode
"""
from freqtrade.worker import Worker
# Load and run worker
worker = None
try:
worker = Worker(args)
worker.run()
except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...')
finally:
if worker:
logger.info("worker found ... calling exit")
worker.exit()
return 0
def start_list_exchanges(args: Dict[str, Any]) -> None: def start_list_exchanges(args: Dict[str, Any]) -> None:
""" """
Print available exchanges Print available exchanges
@ -57,7 +76,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
def start_create_userdir(args: Dict[str, Any]) -> None: def start_create_userdir(args: Dict[str, Any]) -> None:
""" """
Create "user_data" directory to contain user data strategies, hyperopts, ...) Create "user_data" directory to contain user data strategies, hyperopt, ...)
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
:return: None :return: None
""" """

View File

@ -16,6 +16,7 @@ nav:
- Hyperopt: hyperopt.md - Hyperopt: hyperopt.md
- Edge Positioning: edge.md - Edge Positioning: edge.md
- Utility Subcommands: utils.md - Utility Subcommands: utils.md
- Exchange-specific Notes: exchanges.md
- FAQ: faq.md - FAQ: faq.md
- Data Analysis: - Data Analysis:
- Jupyter Notebooks: data-analysis.md - Jupyter Notebooks: data-analysis.md

View File

@ -1,23 +1,23 @@
# 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.19.14 ccxt==1.19.54
SQLAlchemy==1.3.10 SQLAlchemy==1.3.11
python-telegram-bot==12.2.0 python-telegram-bot==12.2.0
arrow==0.15.4 arrow==0.15.4
cachetools==3.1.1 cachetools==3.1.1
requests==2.22.0 requests==2.22.0
urllib3==1.25.6 urllib3==1.25.7
wrapt==1.11.2 wrapt==1.11.2
jsonschema==3.1.1 jsonschema==3.1.1
TA-Lib==0.4.17 TA-Lib==0.4.17
tabulate==0.8.5 tabulate==0.8.6
coinmarketcap==5.0.3 coinmarketcap==5.0.3
# find first, C search in arrays # find first, C search in arrays
py_find_1st==1.1.4 py_find_1st==1.1.4
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==0.8.0 python-rapidjson==0.9.1
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2

View File

@ -6,9 +6,9 @@
coveralls==1.8.2 coveralls==1.8.2
flake8==3.7.9 flake8==3.7.9
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==3.0.0 flake8-tidy-imports==3.1.0
mypy==0.740 mypy==0.740
pytest==5.2.2 pytest==5.2.4
pytest-asyncio==0.10.0 pytest-asyncio==0.10.0
pytest-cov==2.8.1 pytest-cov==2.8.1
pytest-mock==1.11.2 pytest-mock==1.11.2

View File

@ -2,7 +2,7 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.3.1 scipy==1.3.2
scikit-learn==0.21.3 scikit-learn==0.21.3
scikit-optimize==0.5.2 scikit-optimize==0.5.2
filelock==3.0.12 filelock==3.0.12

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.2.1 plotly==4.3.0

View File

@ -1,5 +1,5 @@
# Load common requirements # Load common requirements
-r requirements-common.txt -r requirements-common.txt
numpy==1.17.3 numpy==1.17.4
pandas==0.25.3 pandas==0.25.3

View File

@ -8,12 +8,15 @@ so it can be used as a standalone script.
""" """
import argparse import argparse
import json
import logging
import inspect import inspect
from urllib.parse import urlencode, urlparse, urlunparse import json
import re
import logging
import sys
from pathlib import Path from pathlib import Path
from urllib.parse import urlencode, urlparse, urlunparse
import rapidjson
import requests import requests
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
@ -63,100 +66,106 @@ class FtRestClient():
return self._call("POST", apipath, params=params, data=data) return self._call("POST", apipath, params=params, data=data)
def start(self): def start(self):
""" """Start the bot if it's in the stopped state.
Start the bot if it's in stopped state.
:return: json object :return: json object
""" """
return self._post("start") return self._post("start")
def stop(self): def stop(self):
""" """Stop the bot. Use `start` to restart.
Stop the bot. Use start to restart
:return: json object :return: json object
""" """
return self._post("stop") return self._post("stop")
def stopbuy(self): def stopbuy(self):
""" """Stop buying (but handle sells gracefully). Use `reload_conf` to reset.
Stop buying (but handle sells gracefully).
use reload_conf to reset
:return: json object :return: json object
""" """
return self._post("stopbuy") return self._post("stopbuy")
def reload_conf(self): def reload_conf(self):
""" """Reload configuration.
Reload configuration
:return: json object :return: json object
""" """
return self._post("reload_conf") return self._post("reload_conf")
def balance(self): def balance(self):
""" """Get the account balance.
Get the account balance
:return: json object :return: json object
""" """
return self._get("balance") return self._get("balance")
def count(self): def count(self):
""" """Return the amount of open trades.
Returns the amount of open trades
:return: json object :return: json object
""" """
return self._get("count") return self._get("count")
def daily(self, days=None): def daily(self, days=None):
""" """Return the amount of open trades.
Returns the amount of open trades
:return: json object :return: json object
""" """
return self._get("daily", params={"timescale": days} if days else None) return self._get("daily", params={"timescale": days} if days else None)
def edge(self): def edge(self):
""" """Return information about edge.
Returns information about edge
:return: json object :return: json object
""" """
return self._get("edge") return self._get("edge")
def profit(self): def profit(self):
""" """Return the profit summary.
Returns the profit summary
:return: json object :return: json object
""" """
return self._get("profit") return self._get("profit")
def performance(self): def performance(self):
""" """Return the performance of the different coins.
Returns the performance of the different coins
:return: json object :return: json object
""" """
return self._get("performance") return self._get("performance")
def status(self): def status(self):
""" """Get the status of open trades.
Get the status of open trades
:return: json object :return: json object
""" """
return self._get("status") return self._get("status")
def version(self): def version(self):
""" """Return the version of the bot.
Returns the version of the bot
:return: json object containing the version :return: json object containing the version
""" """
return self._get("version") return self._get("version")
def whitelist(self): def show_config(self):
""" """
Show the current whitelist Returns part of the configuration, relevant for trading operations.
:return: json object containing the version
"""
return self._get("show_config")
def whitelist(self):
"""Show the current whitelist.
:return: json object :return: json object
""" """
return self._get("whitelist") return self._get("whitelist")
def blacklist(self, *args): def blacklist(self, *args):
""" """Show the current blacklist.
Show the current blacklist
:param add: List of coins to add (example: "BNB/BTC") :param add: List of coins to add (example: "BNB/BTC")
:return: json object :return: json object
""" """
@ -166,8 +175,8 @@ class FtRestClient():
return self._post("blacklist", data={"blacklist": args}) return self._post("blacklist", data={"blacklist": args})
def forcebuy(self, pair, price=None): def forcebuy(self, pair, price=None):
""" """Buy an asset.
Buy an asset
:param pair: Pair to buy (ETH/BTC) :param pair: Pair to buy (ETH/BTC)
:param price: Optional - price to buy :param price: Optional - price to buy
:return: json object of the trade :return: json object of the trade
@ -178,8 +187,8 @@ class FtRestClient():
return self._post("forcebuy", data=data) return self._post("forcebuy", data=data)
def forcesell(self, tradeid): def forcesell(self, tradeid):
""" """Force-sell a trade.
Force-sell a trade
:param tradeid: Id of the trade (can be received via status command) :param tradeid: Id of the trade (can be received via status command)
:return: json object :return: json object
""" """
@ -190,7 +199,9 @@ class FtRestClient():
def add_arguments(): def add_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("command", parser.add_argument("command",
help="Positional argument defining the command to execute.") help="Positional argument defining the command to execute.",
nargs="?"
)
parser.add_argument('--show', parser.add_argument('--show',
help='Show possible methods with this client', help='Show possible methods with this client',
@ -221,24 +232,29 @@ def load_config(configfile):
file = Path(configfile) file = Path(configfile)
if file.is_file(): if file.is_file():
with file.open("r") as f: with file.open("r") as f:
config = json.load(f) config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS |
rapidjson.PM_TRAILING_COMMAS)
return config return config
return {} else:
logger.warning(f"Could not load config file {file}.")
sys.exit(1)
def print_commands(): def print_commands():
# Print dynamic help for the different commands using the commands doc-strings # Print dynamic help for the different commands using the commands doc-strings
client = FtRestClient(None) client = FtRestClient(None)
print("Possible commands:") print("Possible commands:\n")
for x, y in inspect.getmembers(client): for x, y in inspect.getmembers(client):
if not x.startswith('_'): if not x.startswith('_'):
print(f"{x} {getattr(client, x).__doc__}") doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip()
print(f"{x}\n\t{doc}\n")
def main(args): def main(args):
if args.get("help"): if args.get("show"):
print_commands() print_commands()
sys.exit()
config = load_config(args["config"]) config = load_config(args["config"])
url = config.get("api_server", {}).get("server_url", "127.0.0.1") url = config.get("api_server", {}).get("server_url", "127.0.0.1")

View File

@ -242,6 +242,9 @@ def default_conf(testdatadir):
"HOT/BTC", "HOT/BTC",
] ]
}, },
"pairlists": [
{"method": "StaticPairList"}
],
"telegram": { "telegram": {
"enabled": True, "enabled": True,
"token": "token", "token": "token",
@ -252,6 +255,7 @@ def default_conf(testdatadir):
"db_url": "sqlite://", "db_url": "sqlite://",
"user_data_dir": Path("user_data"), "user_data_dir": Path("user_data"),
"verbosity": 3, "verbosity": 3,
"strategy": "DefaultStrategy"
} }
return configuration return configuration
@ -572,6 +576,72 @@ def get_markets():
} }
@pytest.fixture
def shitcoinmarkets(markets):
"""
Fixture with shitcoin markets - used to test filters in pairlists
"""
shitmarkets = deepcopy(markets)
shitmarkets.update({'HOT/BTC': {
'id': 'HOTBTC',
'symbol': 'HOT/BTC',
'base': 'HOT',
'quote': 'BTC',
'active': True,
'precision': {
'base': 8,
'quote': 8,
'amount': 0,
'price': 8
},
'limits': {
'amount': {
'min': 1.0,
'max': 90000000.0
},
'price': {
'min': None,
'max': None
},
'cost': {
'min': 0.001,
'max': None
}
},
'info': {},
},
'FUEL/BTC': {
'id': 'FUELBTC',
'symbol': 'FUEL/BTC',
'base': 'FUEL',
'quote': 'BTC',
'active': True,
'precision': {
'base': 8,
'quote': 8,
'amount': 0,
'price': 8
},
'limits': {
'amount': {
'min': 1.0,
'max': 90000000.0
},
'price': {
'min': 1e-08,
'max': 1000.0
},
'cost': {
'min': 0.001,
'max': None
}
},
'info': {},
},
})
return shitmarkets
@pytest.fixture @pytest.fixture
def markets_empty(): def markets_empty():
return MagicMock(return_value=[]) return MagicMock(return_value=[])
@ -866,6 +936,50 @@ def tickers():
'quoteVolume': 1215.14489611, 'quoteVolume': 1215.14489611,
'info': {} 'info': {}
}, },
'HOT/BTC': {
'symbol': 'HOT/BTC',
'timestamp': 1572273518661,
'datetime': '2019-10-28T14:38:38.661Z',
'high': 0.00000011,
'low': 0.00000009,
'bid': 0.0000001,
'bidVolume': 1476027288.0,
'ask': 0.00000011,
'askVolume': 820153831.0,
'vwap': 0.0000001,
'open': 0.00000009,
'close': 0.00000011,
'last': 0.00000011,
'previousClose': 0.00000009,
'change': 0.00000002,
'percentage': 22.222,
'average': None,
'baseVolume': 1442290324.0,
'quoteVolume': 143.78311994,
'info': {}
},
'FUEL/BTC': {
'symbol': 'FUEL/BTC',
'timestamp': 1572340250771,
'datetime': '2019-10-29T09:10:50.771Z',
'high': 0.00000040,
'low': 0.00000035,
'bid': 0.00000036,
'bidVolume': 8932318.0,
'ask': 0.00000037,
'askVolume': 10140774.0,
'vwap': 0.00000037,
'open': 0.00000039,
'close': 0.00000037,
'last': 0.00000037,
'previousClose': 0.00000038,
'change': -0.00000002,
'percentage': -5.128,
'average': None,
'baseVolume': 168927742.0,
'quoteVolume': 62.68220262,
'info': {}
},
'ETH/USDT': { 'ETH/USDT': {
'symbol': 'ETH/USDT', 'symbol': 'ETH/USDT',
'timestamp': 1522014804118, 'timestamp': 1522014804118,

View File

@ -56,7 +56,7 @@ def test_extract_trades_of_period(testdatadir):
# 2018-11-14 06:07:00 # 2018-11-14 06:07:00
timerange = TimeRange('date', None, 1510639620, 0) timerange = TimeRange('date', None, 1510639620, 0)
data = load_pair_history(pair=pair, ticker_interval='1m', data = load_pair_history(pair=pair, timeframe='1m',
datadir=testdatadir, timerange=timerange) datadir=testdatadir, timerange=timerange)
trades = DataFrame( trades = DataFrame(
@ -122,7 +122,7 @@ def test_combine_tickers_with_mean(testdatadir):
pairs = ["ETH/BTC", "ADA/BTC"] pairs = ["ETH/BTC", "ADA/BTC"]
tickers = load_data(datadir=testdatadir, tickers = load_data(datadir=testdatadir,
pairs=pairs, pairs=pairs,
ticker_interval='5m' timeframe='5m'
) )
df = combine_tickers_with_mean(tickers) df = combine_tickers_with_mean(tickers)
assert isinstance(df, DataFrame) assert isinstance(df, DataFrame)
@ -136,7 +136,7 @@ def test_create_cum_profit(testdatadir):
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112") timerange = TimeRange.parse_timerange("20180110-20180112")
df = load_pair_history(pair="TRX/BTC", ticker_interval='5m', df = load_pair_history(pair="TRX/BTC", timeframe='5m',
datadir=testdatadir, timerange=timerange) datadir=testdatadir, timerange=timerange)
cum_profits = create_cum_profit(df.set_index('date'), cum_profits = create_cum_profit(df.set_index('date'),
@ -154,7 +154,7 @@ def test_create_cum_profit1(testdatadir):
bt_data.loc[:, 'close_time'] = bt_data.loc[:, 'close_time'] + DateOffset(seconds=20) bt_data.loc[:, 'close_time'] = bt_data.loc[:, 'close_time'] + DateOffset(seconds=20)
timerange = TimeRange.parse_timerange("20180110-20180112") timerange = TimeRange.parse_timerange("20180110-20180112")
df = load_pair_history(pair="TRX/BTC", ticker_interval='5m', df = load_pair_history(pair="TRX/BTC", timeframe='5m',
datadir=testdatadir, timerange=timerange) datadir=testdatadir, timerange=timerange)
cum_profits = create_cum_profit(df.set_index('date'), cum_profits = create_cum_profit(df.set_index('date'),

View File

@ -23,7 +23,7 @@ def test_parse_ticker_dataframe(ticker_history_list, caplog):
def test_ohlcv_fill_up_missing_data(testdatadir, caplog): def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
data = load_pair_history(datadir=testdatadir, data = load_pair_history(datadir=testdatadir,
ticker_interval='1m', timeframe='1m',
pair='UNITTEST/BTC', pair='UNITTEST/BTC',
fill_up_missing=False) fill_up_missing=False)
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
@ -42,7 +42,7 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
def test_ohlcv_fill_up_missing_data2(caplog): def test_ohlcv_fill_up_missing_data2(caplog):
ticker_interval = '5m' timeframe = '5m'
ticks = [[ ticks = [[
1511686200000, # 8:50:00 1511686200000, # 8:50:00
8.794e-05, # open 8.794e-05, # open
@ -78,10 +78,10 @@ def test_ohlcv_fill_up_missing_data2(caplog):
] ]
# Generate test-data without filling missing # Generate test-data without filling missing
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", fill_missing=False) data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", fill_missing=False)
assert len(data) == 3 assert len(data) == 3
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
data2 = ohlcv_fill_up_missing_data(data, ticker_interval, "UNITTEST/BTC") data2 = ohlcv_fill_up_missing_data(data, timeframe, "UNITTEST/BTC")
assert len(data2) == 4 assert len(data2) == 4
# 3rd candle has been filled # 3rd candle has been filled
row = data2.loc[2, :] row = data2.loc[2, :]
@ -99,7 +99,7 @@ def test_ohlcv_fill_up_missing_data2(caplog):
def test_ohlcv_drop_incomplete(caplog): def test_ohlcv_drop_incomplete(caplog):
ticker_interval = '1d' timeframe = '1d'
ticks = [[ ticks = [[
1559750400000, # 2019-06-04 1559750400000, # 2019-06-04
8.794e-05, # open 8.794e-05, # open
@ -134,13 +134,13 @@ def test_ohlcv_drop_incomplete(caplog):
] ]
] ]
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=False) fill_missing=False, drop_incomplete=False)
assert len(data) == 4 assert len(data) == 4
assert not log_has("Dropping last candle", caplog) assert not log_has("Dropping last candle", caplog)
# Drop last candle # Drop last candle
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=True) fill_missing=False, drop_incomplete=True)
assert len(data) == 3 assert len(data) == 3

View File

@ -9,32 +9,32 @@ from tests.conftest import get_patched_exchange
def test_ohlcv(mocker, default_conf, ticker_history): def test_ohlcv(mocker, default_conf, ticker_history):
default_conf["runmode"] = RunMode.DRY_RUN default_conf["runmode"] = RunMode.DRY_RUN
ticker_interval = default_conf["ticker_interval"] timeframe = default_conf["ticker_interval"]
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("XRP/BTC", timeframe)] = ticker_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", timeframe)] = ticker_history
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN assert dp.runmode == RunMode.DRY_RUN
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe))
assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame)
assert dp.ohlcv("UNITTEST/BTC", ticker_interval) is not ticker_history assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ticker_history
assert dp.ohlcv("UNITTEST/BTC", ticker_interval, copy=False) is ticker_history assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ticker_history
assert not dp.ohlcv("UNITTEST/BTC", ticker_interval).empty assert not dp.ohlcv("UNITTEST/BTC", timeframe).empty
assert dp.ohlcv("NONESENSE/AAA", ticker_interval).empty assert dp.ohlcv("NONESENSE/AAA", timeframe).empty
# Test with and without parameter # Test with and without parameter
assert dp.ohlcv("UNITTEST/BTC", ticker_interval).equals(dp.ohlcv("UNITTEST/BTC")) assert dp.ohlcv("UNITTEST/BTC", timeframe).equals(dp.ohlcv("UNITTEST/BTC"))
default_conf["runmode"] = RunMode.LIVE default_conf["runmode"] = RunMode.LIVE
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.LIVE assert dp.runmode == RunMode.LIVE
assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame)
default_conf["runmode"] = RunMode.BACKTEST default_conf["runmode"] = RunMode.BACKTEST
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.BACKTEST assert dp.runmode == RunMode.BACKTEST
assert dp.ohlcv("UNITTEST/BTC", ticker_interval).empty assert dp.ohlcv("UNITTEST/BTC", timeframe).empty
def test_historic_ohlcv(mocker, default_conf, ticker_history): def test_historic_ohlcv(mocker, default_conf, ticker_history):
@ -45,7 +45,7 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history):
data = dp.historic_ohlcv("UNITTEST/BTC", "5m") data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
assert isinstance(data, DataFrame) assert isinstance(data, DataFrame)
assert historymock.call_count == 1 assert historymock.call_count == 1
assert historymock.call_args_list[0][1]["ticker_interval"] == "5m" assert historymock.call_args_list[0][1]["timeframe"] == "5m"
def test_get_pair_dataframe(mocker, default_conf, ticker_history): def test_get_pair_dataframe(mocker, default_conf, ticker_history):

View File

@ -64,20 +64,20 @@ def _clean_test_file(file: Path) -> None:
def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None: def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None:
ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='30m', datadir=testdatadir) ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir)
assert isinstance(ld, DataFrame) assert isinstance(ld, DataFrame)
assert not log_has( assert not log_has(
'Download history data for pair: "UNITTEST/BTC", interval: 30m ' 'Download history data for pair: "UNITTEST/BTC", timeframe: 30m '
'and store in None.', caplog 'and store in None.', caplog
) )
def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None: def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None:
ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='7m', datadir=testdatadir) ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir)
assert not isinstance(ld, DataFrame) assert not isinstance(ld, DataFrame)
assert ld is None assert ld is None
assert log_has( assert log_has(
'No history data for pair: "UNITTEST/BTC", interval: 7m. ' 'No history data for pair: "UNITTEST/BTC", timeframe: 7m. '
'Use `freqtrade download-data` to download the data', caplog 'Use `freqtrade download-data` to download the data', caplog
) )
@ -86,7 +86,7 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> N
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history)
file = testdatadir / 'UNITTEST_BTC-1m.json' file = testdatadir / 'UNITTEST_BTC-1m.json'
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
history.load_data(datadir=testdatadir, ticker_interval='1m', pairs=['UNITTEST/BTC']) history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'])
assert file.is_file() assert file.is_file()
assert not log_has( assert not log_has(
'Download history data for pair: "UNITTEST/BTC", interval: 1m ' 'Download history data for pair: "UNITTEST/BTC", interval: 1m '
@ -99,7 +99,7 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) ->
ltfmock = mocker.patch('freqtrade.data.history.load_tickerdata_file', ltfmock = mocker.patch('freqtrade.data.history.load_tickerdata_file',
MagicMock(return_value=None)) MagicMock(return_value=None))
timerange = TimeRange('date', None, 1510639620, 0) timerange = TimeRange('date', None, 1510639620, 0)
history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='1m', history.load_pair_history(pair='UNITTEST/BTC', timeframe='1m',
datadir=testdatadir, timerange=timerange, datadir=testdatadir, timerange=timerange,
startup_candles=20, startup_candles=20,
) )
@ -122,28 +122,28 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog,
_backup_file(file) _backup_file(file)
# do not download a new pair if refresh_pairs isn't set # do not download a new pair if refresh_pairs isn't set
history.load_pair_history(datadir=testdatadir, history.load_pair_history(datadir=testdatadir,
ticker_interval='1m', timeframe='1m',
pair='MEME/BTC') pair='MEME/BTC')
assert not file.is_file() assert not file.is_file()
assert log_has( assert log_has(
'No history data for pair: "MEME/BTC", interval: 1m. ' 'No history data for pair: "MEME/BTC", timeframe: 1m. '
'Use `freqtrade download-data` to download the data', caplog 'Use `freqtrade download-data` to download the data', caplog
) )
# download a new pair if refresh_pairs is set # download a new pair if refresh_pairs is set
history.load_pair_history(datadir=testdatadir, history.load_pair_history(datadir=testdatadir,
ticker_interval='1m', timeframe='1m',
refresh_pairs=True, refresh_pairs=True,
exchange=exchange, exchange=exchange,
pair='MEME/BTC') pair='MEME/BTC')
assert file.is_file() assert file.is_file()
assert log_has_re( assert log_has_re(
'Download history data for pair: "MEME/BTC", interval: 1m ' 'Download history data for pair: "MEME/BTC", timeframe: 1m '
'and store in .*', caplog 'and store in .*', caplog
) )
with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'):
history.load_pair_history(datadir=testdatadir, history.load_pair_history(datadir=testdatadir,
ticker_interval='1m', timeframe='1m',
refresh_pairs=True, refresh_pairs=True,
exchange=None, exchange=None,
pair='MEME/BTC') pair='MEME/BTC')
@ -269,10 +269,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda
assert download_pair_history(datadir=testdatadir, exchange=exchange, assert download_pair_history(datadir=testdatadir, exchange=exchange,
pair='MEME/BTC', pair='MEME/BTC',
ticker_interval='1m') timeframe='1m')
assert download_pair_history(datadir=testdatadir, exchange=exchange, assert download_pair_history(datadir=testdatadir, exchange=exchange,
pair='CFI/BTC', pair='CFI/BTC',
ticker_interval='1m') timeframe='1m')
assert not exchange._pairs_last_refresh_time assert not exchange._pairs_last_refresh_time
assert file1_1.is_file() assert file1_1.is_file()
assert file2_1.is_file() assert file2_1.is_file()
@ -286,10 +286,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda
assert download_pair_history(datadir=testdatadir, exchange=exchange, assert download_pair_history(datadir=testdatadir, exchange=exchange,
pair='MEME/BTC', pair='MEME/BTC',
ticker_interval='5m') timeframe='5m')
assert download_pair_history(datadir=testdatadir, exchange=exchange, assert download_pair_history(datadir=testdatadir, exchange=exchange,
pair='CFI/BTC', pair='CFI/BTC',
ticker_interval='5m') timeframe='5m')
assert not exchange._pairs_last_refresh_time assert not exchange._pairs_last_refresh_time
assert file1_5.is_file() assert file1_5.is_file()
assert file2_5.is_file() assert file2_5.is_file()
@ -307,8 +307,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", ticker_interval='1m') download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m')
download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", ticker_interval='3m') download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m')
assert json_dump_mock.call_count == 2 assert json_dump_mock.call_count == 2
@ -326,12 +326,12 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog,
assert not download_pair_history(datadir=testdatadir, exchange=exchange, assert not download_pair_history(datadir=testdatadir, exchange=exchange,
pair='MEME/BTC', pair='MEME/BTC',
ticker_interval='1m') timeframe='1m')
# clean files freshly downloaded # clean files freshly downloaded
_clean_test_file(file1_1) _clean_test_file(file1_1)
_clean_test_file(file1_5) _clean_test_file(file1_5)
assert log_has( assert log_has(
'Failed to download history data for pair: "MEME/BTC", interval: 1m. ' 'Failed to download history data for pair: "MEME/BTC", timeframe: 1m. '
'Error: File Error', caplog 'Error: File Error', caplog
) )
@ -369,7 +369,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
caplog.clear() caplog.clear()
start = arrow.get('2018-01-10T00:00:00') start = arrow.get('2018-01-10T00:00:00')
end = arrow.get('2018-02-20T00:00:00') end = arrow.get('2018-02-20T00:00:00')
tickerdata = history.load_data(datadir=testdatadir, ticker_interval='5m', tickerdata = history.load_data(datadir=testdatadir, timeframe='5m',
pairs=['UNITTEST/BTC'], pairs=['UNITTEST/BTC'],
timerange=TimeRange('date', 'date', timerange=TimeRange('date', 'date',
start.timestamp, end.timestamp)) start.timestamp, end.timestamp))
@ -390,7 +390,7 @@ def test_init(default_conf, mocker) -> None:
exchange=exchange, exchange=exchange,
pairs=[], pairs=[],
refresh_pairs=True, refresh_pairs=True,
ticker_interval=default_conf['ticker_interval'] timeframe=default_conf['ticker_interval']
) )
@ -449,7 +449,7 @@ def test_trim_tickerlist(testdatadir) -> None:
def test_trim_dataframe(testdatadir) -> None: def test_trim_dataframe(testdatadir) -> None:
data = history.load_data( data = history.load_data(
datadir=testdatadir, datadir=testdatadir,
ticker_interval='1m', timeframe='1m',
pairs=['UNITTEST/BTC'] pairs=['UNITTEST/BTC']
)['UNITTEST/BTC'] )['UNITTEST/BTC']
min_date = int(data.iloc[0]['date'].timestamp()) min_date = int(data.iloc[0]['date'].timestamp())
@ -517,7 +517,7 @@ def test_get_timeframe(default_conf, mocker, testdatadir) -> None:
data = strategy.tickerdata_to_dataframe( data = strategy.tickerdata_to_dataframe(
history.load_data( history.load_data(
datadir=testdatadir, datadir=testdatadir,
ticker_interval='1m', timeframe='1m',
pairs=['UNITTEST/BTC'] pairs=['UNITTEST/BTC']
) )
) )
@ -533,7 +533,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
data = strategy.tickerdata_to_dataframe( data = strategy.tickerdata_to_dataframe(
history.load_data( history.load_data(
datadir=testdatadir, datadir=testdatadir,
ticker_interval='1m', timeframe='1m',
pairs=['UNITTEST/BTC'], pairs=['UNITTEST/BTC'],
fill_up_missing=False fill_up_missing=False
) )
@ -556,7 +556,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
data = strategy.tickerdata_to_dataframe( data = strategy.tickerdata_to_dataframe(
history.load_data( history.load_data(
datadir=testdatadir, datadir=testdatadir,
ticker_interval='5m', timeframe='5m',
pairs=['UNITTEST/BTC'], pairs=['UNITTEST/BTC'],
timerange=timerange timerange=timerange
) )
@ -669,10 +669,10 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
file5 = testdatadir / 'XRP_ETH-5m.json' file5 = testdatadir / 'XRP_ETH-5m.json'
# Compare downloaded dataset with converted dataset # Compare downloaded dataset with converted dataset
dfbak_1m = history.load_pair_history(datadir=testdatadir, dfbak_1m = history.load_pair_history(datadir=testdatadir,
ticker_interval="1m", timeframe="1m",
pair=pair) pair=pair)
dfbak_5m = history.load_pair_history(datadir=testdatadir, dfbak_5m = history.load_pair_history(datadir=testdatadir,
ticker_interval="5m", timeframe="5m",
pair=pair) pair=pair)
_backup_file(file1, copy_file=True) _backup_file(file1, copy_file=True)
@ -686,10 +686,10 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog)
# Load new data # Load new data
df_1m = history.load_pair_history(datadir=testdatadir, df_1m = history.load_pair_history(datadir=testdatadir,
ticker_interval="1m", timeframe="1m",
pair=pair) pair=pair)
df_5m = history.load_pair_history(datadir=testdatadir, df_5m = history.load_pair_history(datadir=testdatadir,
ticker_interval="5m", timeframe="5m",
pair=pair) pair=pair)
assert df_1m.equals(dfbak_1m) assert df_1m.equals(dfbak_1m)

View File

@ -255,7 +255,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf):
assert edge.calculate() is False assert edge.calculate() is False
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False,
timerange=None, exchange=None, *args, **kwargs): timerange=None, exchange=None, *args, **kwargs):
hz = 0.1 hz = 0.1
base = 0.001 base = 0.001

View File

@ -1047,8 +1047,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
] ]
pair = 'ETH/BTC' pair = 'ETH/BTC'
async def mock_candle_hist(pair, ticker_interval, since_ms): async def mock_candle_hist(pair, timeframe, since_ms):
return pair, ticker_interval, tick return pair, timeframe, tick
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
@ -1107,7 +1107,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...", assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, timeframe {pairs[0][1]} ...",
caplog) caplog)
@ -1143,7 +1143,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
# exchange = Exchange(default_conf) # exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(), await async_ccxt_exception(mocker, default_conf, MagicMock(),
"_async_get_candle_history", "fetch_ohlcv", "_async_get_candle_history", "fetch_ohlcv",
pair='ABCD/BTC', ticker_interval=default_conf['ticker_interval']) pair='ABCD/BTC', timeframe=default_conf['ticker_interval'])
api_mock = MagicMock() api_mock = MagicMock()
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'):
@ -1586,8 +1586,9 @@ def test_name(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_trades_for_order(default_conf, mocker, exchange_name): def test_get_trades_for_order(default_conf, mocker, exchange_name):
order_id = 'ABCD-ABCD' order_id = 'ABCD-ABCD'
since = datetime(2018, 5, 5, tzinfo=timezone.utc) since = datetime(2018, 5, 5, 0, 0, 0)
default_conf["dry_run"] = False default_conf["dry_run"] = False
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
api_mock = MagicMock() api_mock = MagicMock()
@ -1623,7 +1624,8 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name):
assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC' assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC'
# Same test twice, hardcoded number and doing the same calculation # Same test twice, hardcoded number and doing the same calculation
assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000 assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000
assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000 assert api_mock.fetch_my_trades.call_args[0][1] == int(since.replace(
tzinfo=timezone.utc).timestamp() - 5) * 1000
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'get_trades_for_order', 'fetch_my_trades', 'get_trades_for_order', 'fetch_my_trades',

View File

@ -7,7 +7,7 @@ from freqtrade.exchange import timeframe_to_minutes
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
ticker_start_time = arrow.get(2018, 10, 3) ticker_start_time = arrow.get(2018, 10, 3)
tests_ticker_interval = '1h' tests_timeframe = '1h'
class BTrade(NamedTuple): class BTrade(NamedTuple):
@ -36,7 +36,7 @@ class BTContainer(NamedTuple):
def _get_frame_time_from_offset(offset): def _get_frame_time_from_offset(offset):
return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_ticker_interval)) return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_timeframe))
).datetime ).datetime

View File

@ -9,7 +9,7 @@ from freqtrade.optimize.backtesting import Backtesting
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from tests.conftest import patch_exchange from tests.conftest import patch_exchange
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
_get_frame_time_from_offset, tests_ticker_interval) _get_frame_time_from_offset, tests_timeframe)
# Test 0: Sell with signal sell in candle 3 # Test 0: Sell with signal sell in candle 3
# Test with Stop-loss at 1% # Test with Stop-loss at 1%
@ -293,7 +293,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
""" """
default_conf["stoploss"] = data.stop_loss default_conf["stoploss"] = data.stop_loss
default_conf["minimal_roi"] = data.roi default_conf["minimal_roi"] = data.roi
default_conf["ticker_interval"] = tests_ticker_interval default_conf["ticker_interval"] = tests_timeframe
default_conf["trailing_stop"] = data.trailing_stop default_conf["trailing_stop"] = data.trailing_stop
default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached
# Only add this to configuration If it's necessary # Only add this to configuration If it's necessary

View File

@ -50,7 +50,7 @@ def trim_dictlist(dict_list, num):
def load_data_test(what, testdatadir): def load_data_test(what, testdatadir):
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
pair = history.load_tickerdata_file(testdatadir, ticker_interval='1m', pair = history.load_tickerdata_file(testdatadir, timeframe='1m',
pair='UNITTEST/BTC', timerange=timerange) pair='UNITTEST/BTC', timerange=timerange)
datalen = len(pair) datalen = len(pair)
@ -116,7 +116,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
assert len(results) == num_results assert len(results) == num_results
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False,
timerange=None, exchange=None, live=False, *args, **kwargs): timerange=None, exchange=None, live=False, *args, **kwargs):
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC", pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC",
@ -126,14 +126,14 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
# use for mock ccxt.fetch_ohlvc' # use for mock ccxt.fetch_ohlvc'
def _load_pair_as_ticks(pair, tickfreq): def _load_pair_as_ticks(pair, tickfreq):
ticks = history.load_tickerdata_file(None, ticker_interval=tickfreq, pair=pair) ticks = history.load_tickerdata_file(None, timeframe=tickfreq, pair=pair)
ticks = ticks[-201:] ticks = ticks[-201:]
return ticks return ticks
# FIX: fixturize this? # FIX: fixturize this?
def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record=None): def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record=None):
data = history.load_data(datadir=datadir, ticker_interval='1m', pairs=[pair]) data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair])
data = trim_dictlist(data, -201) data = trim_dictlist(data, -201)
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(conf) backtesting = Backtesting(conf)
@ -184,9 +184,9 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'backtesting'
] ]
config = setup_configuration(get_args(args), RunMode.BACKTEST) config = setup_configuration(get_args(args), RunMode.BACKTEST)
@ -217,10 +217,10 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
) )
args = [ args = [
'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
@ -269,9 +269,9 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'backtesting'
] ]
with pytest.raises(DependencyException, match=r'.*stake amount.*'): with pytest.raises(DependencyException, match=r'.*stake amount.*'):
@ -286,9 +286,9 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'backtesting'
] ]
args = get_args(args) args = get_args(args)
start_backtesting(args) start_backtesting(args)
@ -307,7 +307,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert backtesting.ticker_interval == '5m' assert backtesting.timeframe == '5m'
assert callable(backtesting.strategy.tickerdata_to_dataframe) assert callable(backtesting.strategy.tickerdata_to_dataframe)
assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_buy)
assert callable(backtesting.strategy.advise_sell) assert callable(backtesting.strategy.advise_sell)
@ -522,7 +522,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
pair = 'UNITTEST/BTC' pair = 'UNITTEST/BTC'
timerange = TimeRange('date', None, 1517227800, 0) timerange = TimeRange('date', None, 1517227800, 0)
data = history.load_data(datadir=testdatadir, ticker_interval='5m', pairs=['UNITTEST/BTC'], data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange) timerange=timerange)
data_processed = backtesting.strategy.tickerdata_to_dataframe(data) data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timeframe(data_processed) min_date, max_date = get_timeframe(data_processed)
@ -576,9 +576,9 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) -
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
# Run a backtesting for an exiting 1min ticker_interval # Run a backtesting for an exiting 1min timeframe
timerange = TimeRange.parse_timerange('1510688220-1510700340') timerange = TimeRange.parse_timerange('1510688220-1510700340')
data = history.load_data(datadir=testdatadir, ticker_interval='1m', pairs=['UNITTEST/BTC'], data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'],
timerange=timerange) timerange=timerange)
processed = backtesting.strategy.tickerdata_to_dataframe(data) processed = backtesting.strategy.tickerdata_to_dataframe(data)
min_date, max_date = get_timeframe(processed) min_date, max_date = get_timeframe(processed)
@ -688,7 +688,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
patch_exchange(mocker) patch_exchange(mocker)
pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC']
data = history.load_data(datadir=testdatadir, ticker_interval='5m', pairs=pairs) data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=pairs)
# Only use 500 lines to increase performance # Only use 500 lines to increase performance
data = trim_dictlist(data, -500) data = trim_dictlist(data, -500)
@ -817,10 +817,10 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'--datadir', str(testdatadir), '--datadir', str(testdatadir),
'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--timerange', '1510694220-1510700340', '--timerange', '1510694220-1510700340',
'--enable-position-stacking', '--enable-position-stacking',
@ -866,9 +866,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--datadir', str(testdatadir), '--datadir', str(testdatadir),
'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--timerange', '1510694220-1510700340', '--timerange', '1510694220-1510700340',
'--enable-position-stacking', '--enable-position-stacking',

View File

@ -15,9 +15,9 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'edge'
] ]
config = setup_configuration(get_args(args), RunMode.EDGE) config = setup_configuration(get_args(args), RunMode.EDGE)
@ -45,10 +45,10 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
) )
args = [ args = [
'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'edge',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--timerange', ':100', '--timerange', ':100',
'--stoplosses=-0.01,-0.10,-0.001' '--stoplosses=-0.01,-0.10,-0.001'
@ -79,9 +79,9 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, edge_conf) patched_configuration_load_config_file(mocker, edge_conf)
args = [ args = [
'edge',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'edge'
] ]
args = get_args(args) args = get_args(args)
start_edge(args) start_edge(args)

View File

@ -26,7 +26,10 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def hyperopt(default_conf, mocker): def hyperopt(default_conf, mocker):
default_conf.update({'spaces': ['default']}) default_conf.update({
'spaces': ['default'],
'hyperopt': 'DefaultHyperOpt',
})
patch_exchange(mocker) patch_exchange(mocker)
return Hyperopt(default_conf) return Hyperopt(default_conf)
@ -69,8 +72,9 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = [ args = [
'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'hyperopt' '--hyperopt', 'DefaultHyperOpt',
] ]
config = setup_configuration(get_args(args), RunMode.HYPEROPT) config = setup_configuration(get_args(args), RunMode.HYPEROPT)
@ -100,9 +104,10 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
) )
args = [ args = [
'--config', 'config.json',
'--datadir', '/foo/bar',
'hyperopt', 'hyperopt',
'--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt',
'--datadir', '/foo/bar',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--timerange', ':100', '--timerange', ':100',
'--enable-position-stacking', '--enable-position-stacking',
@ -157,7 +162,8 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt', 'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
MagicMock(return_value=hyperopt(default_conf)) MagicMock(return_value=hyperopt(default_conf))
) )
x = HyperOptResolver(default_conf, ).hyperopt default_conf.update({'hyperopt': 'DefaultHyperOpt'})
x = HyperOptResolver(default_conf).hyperopt
assert not hasattr(x, 'populate_indicators') assert not hasattr(x, 'populate_indicators')
assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_buy_trend')
assert not hasattr(x, 'populate_sell_trend') assert not hasattr(x, 'populate_sell_trend')
@ -174,7 +180,15 @@ def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None:
default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) default_conf.update({'hyperopt': "NonExistingHyperoptClass"})
with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'):
HyperOptResolver(default_conf, ).hyperopt HyperOptResolver(default_conf).hyperopt
def test_hyperoptresolver_noname(default_conf):
default_conf['hyperopt'] = ''
with pytest.raises(OperationalException,
match="No Hyperopt set. Please use `--hyperopt` to specify "
"the Hyperopt class to use."):
HyperOptResolver(default_conf)
def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
@ -184,7 +198,7 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss',
MagicMock(return_value=hl) MagicMock(return_value=hl)
) )
x = HyperOptLossResolver(default_conf, ).hyperoptloss x = HyperOptLossResolver(default_conf).hyperoptloss
assert hasattr(x, "hyperopt_loss_function") assert hasattr(x, "hyperopt_loss_function")
@ -192,7 +206,7 @@ def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None:
default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) default_conf.update({'hyperopt_loss': "NonExistingLossClass"})
with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'):
HyperOptLossResolver(default_conf, ).hyperopt HyperOptLossResolver(default_conf).hyperopt
def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None: def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None:
@ -203,8 +217,9 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None
patch_exchange(mocker) patch_exchange(mocker)
args = [ args = [
'--config', 'config.json',
'hyperopt', 'hyperopt',
'--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt',
'--epochs', '5' '--epochs', '5'
] ]
args = get_args(args) args = get_args(args)
@ -220,8 +235,9 @@ def test_start(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
args = [ args = [
'--config', 'config.json',
'hyperopt', 'hyperopt',
'--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt',
'--epochs', '5' '--epochs', '5'
] ]
args = get_args(args) args = get_args(args)
@ -242,8 +258,9 @@ def test_start_no_data(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
args = [ args = [
'--config', 'config.json',
'hyperopt', 'hyperopt',
'--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt',
'--epochs', '5' '--epochs', '5'
] ]
args = get_args(args) args = get_args(args)
@ -258,8 +275,9 @@ def test_start_filelock(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
args = [ args = [
'--config', 'config.json',
'hyperopt', 'hyperopt',
'--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt',
'--epochs', '5' '--epochs', '5'
] ]
args = get_args(args) args = get_args(args)
@ -412,6 +430,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'default', 'spaces': 'default',
@ -539,10 +558,12 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
def test_generate_optimizer(mocker, default_conf) -> None: def test_generate_optimizer(mocker, default_conf) -> None:
default_conf.update({'config': 'config.json.example'}) default_conf.update({'config': 'config.json.example',
default_conf.update({'timerange': None}) 'hyperopt': 'DefaultHyperOpt',
default_conf.update({'spaces': 'all'}) 'timerange': None,
default_conf.update({'hyperopt_min_trades': 1}) 'spaces': 'all',
'hyperopt_min_trades': 1,
})
trades = [ trades = [
('TRX/BTC', 0.023117, 0.000233, 100) ('TRX/BTC', 0.023117, 0.000233, 100)
@ -610,6 +631,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
def test_clean_hyperopt(mocker, default_conf, caplog): def test_clean_hyperopt(mocker, default_conf, caplog):
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'default', 'spaces': 'default',
@ -626,6 +648,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog):
def test_continue_hyperopt(mocker, default_conf, caplog): def test_continue_hyperopt(mocker, default_conf, caplog):
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'default', 'spaces': 'default',
@ -656,6 +679,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'all', 'spaces': 'all',
@ -732,6 +756,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'roi stoploss', 'spaces': 'roi stoploss',
@ -771,6 +796,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'roi stoploss', 'spaces': 'roi stoploss',
@ -813,6 +839,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'all', 'spaces': 'all',
@ -847,6 +874,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'buy', 'spaces': 'buy',
@ -893,6 +921,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'sell', 'spaces': 'sell',
@ -941,6 +970,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'hyperopt': 'DefaultHyperOpt',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': space, 'spaces': space,

View File

@ -2,11 +2,13 @@
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.resolvers import PairListResolver from freqtrade.resolvers import PairListResolver
from tests.conftest import get_patched_freqtradebot from freqtrade.pairlist.pairlistmanager import PairListManager
import pytest from tests.conftest import get_patched_freqtradebot, log_has_re
# whitelist, blacklist # whitelist, blacklist
@ -24,25 +26,39 @@ def whitelist_conf(default_conf):
default_conf['exchange']['pair_blacklist'] = [ default_conf['exchange']['pair_blacklist'] = [
'BLK/BTC' 'BLK/BTC'
] ]
default_conf['pairlist'] = {'method': 'StaticPairList', default_conf['pairlists'] = [
'config': {'number_assets': 3} {
} "method": "VolumePairList",
"number_assets": 5,
"sort_key": "quoteVolume",
},
]
return default_conf return default_conf
@pytest.fixture(scope="function")
def static_pl_conf(whitelist_conf):
whitelist_conf['pairlists'] = [
{
"method": "StaticPairList",
},
]
return whitelist_conf
def test_load_pairlist_noexist(mocker, markets, default_conf): def test_load_pairlist_noexist(mocker, markets, default_conf):
freqtradebot = get_patched_freqtradebot(mocker, default_conf) bot = 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)
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('NonexistingPairList', freqtradebot, default_conf).pairlist PairListResolver('NonexistingPairList', bot.exchange, plm, default_conf, {}, 1)
def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf): def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) freqtradebot = 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() freqtradebot.pairlists.refresh_pairlist()
@ -51,50 +67,60 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf):
# 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(freqtradebot.pairlists.whitelist)
# Ensure config dict hasn't been changed # Ensure config dict hasn't been changed
assert (whitelist_conf['exchange']['pair_whitelist'] == assert (static_pl_conf['exchange']['pair_whitelist'] ==
freqtradebot.config['exchange']['pair_whitelist']) freqtradebot.config['exchange']['pair_whitelist'])
def test_refresh_pairlists(mocker, markets, whitelist_conf): def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch.multiple(
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) 'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
markets=PropertyMock(return_value=markets),
)
freqtradebot.pairlists.refresh_pairlist() freqtradebot.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(freqtradebot.pairlists.whitelist)
assert whitelist_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist assert static_pl_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist
def test_refresh_pairlist_dynamic(mocker, markets, tickers, whitelist_conf): def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
whitelist_conf['pairlist'] = {'method': 'VolumePairList',
'config': {'number_assets': 5}
}
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
get_tickers=tickers, get_tickers=tickers,
exchange_has=MagicMock(return_value=True) exchange_has=MagicMock(return_value=True),
) )
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) bot = get_patched_freqtradebot(mocker, whitelist_conf)
# Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=shitcoinmarkets),
)
# argument: use the whitelist dynamically by exchange-volume # argument: use the whitelist dynamically by exchange-volume
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']
freqtradebot.pairlists.refresh_pairlist() bot.pairlists.refresh_pairlist()
assert whitelist == freqtradebot.pairlists.whitelist assert whitelist == bot.pairlists.whitelist
whitelist_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {}
}
]
whitelist_conf['pairlist'] = {'method': 'VolumePairList',
'config': {}
}
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"'):
PairListResolver('VolumePairList', freqtradebot, whitelist_conf).pairlist PairListManager(bot.exchange, whitelist_conf)
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
)
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) freqtradebot = 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))
@ -107,35 +133,75 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
assert set(whitelist) == set(pairslist) assert set(whitelist) == set(pairslist)
@pytest.mark.parametrize("precision_filter,base_currency,key,whitelist_result", [ @pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [
(False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
(False, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']),
(False, "USDT", "quoteVolume", ['ETH/USDT']), # Different sorting depending on quote or bid volume
(False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
(True, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC"]), "BTC", ['HOT/BTC', 'FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']),
(True, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC"]) ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"USDT", ['ETH/USDT']),
# No pair for ETH ...
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"ETH", []),
# Precisionfilter and quote volume
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']),
# Precisionfilter bid
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
{"method": "PrecisionFilter"}], "BTC", ['FUEL/BTC', 'LTC/BTC', 'TKN/BTC', 'ETH/BTC']),
# PriceFilter and VolumePairList
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.03}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'FUEL/BTC']),
# Hot is removed by precision_filter, Fuel by low_price_filter.
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.02}
], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
# StaticPairlist Only
([{"method": "StaticPairList"},
], "BTC", ['ETH/BTC', 'TKN/BTC']),
# Static Pairlist before VolumePairList - sorting changes
([{"method": "StaticPairList"},
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
], "BTC", ['TKN/BTC', 'ETH/BTC']),
]) ])
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers, base_currency, key, def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
whitelist_result, precision_filter) -> None: pairlists, base_currency, whitelist_result,
whitelist_conf['pairlist']['method'] = 'VolumePairList' caplog) -> None:
whitelist_conf['pairlists'] = pairlists
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)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, p, r: round(r, 8))
freqtrade.pairlists._precision_filter = precision_filter mocker.patch.multiple('freqtrade.exchange.Exchange',
get_tickers=tickers,
markets=PropertyMock(return_value=shitcoinmarkets),
)
freqtrade.config['stake_currency'] = base_currency freqtrade.config['stake_currency'] = base_currency
whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key) freqtrade.pairlists.refresh_pairlist()
assert sorted(whitelist) == sorted(whitelist_result) whitelist = freqtrade.pairlists.whitelist
assert whitelist == whitelist_result
for pairlist in pairlists:
if pairlist['method'] == 'PrecisionFilter':
assert log_has_re(r'^Removed .* from whitelist, because stop price .* '
r'would be <= stop limit.*', caplog)
if pairlist['method'] == 'PriceFilter':
assert log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', 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['pairlist'] = {'method': 'VolumePairList', default_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {'number_assets': 10} 'config': {'number_assets': 10}
} }]
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) mocker.patch.multiple('freqtrade.exchange.Exchange',
get_tickers=tickers,
exchange_has=MagicMock(return_value=False),
)
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
get_patched_freqtradebot(mocker, default_conf) get_patched_freqtradebot(mocker, default_conf)
@ -143,13 +209,15 @@ def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) @pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
whitelist_conf['pairlist']['method'] = pairlist whitelist_conf['pairlists'][0]['method'] = pairlist
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) mocker.patch.multiple('freqtrade.exchange.Exchange',
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True)
)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
assert freqtrade.pairlists.name == pairlist assert freqtrade.pairlists.name_list == [pairlist]
assert pairlist in freqtrade.pairlists.short_desc() assert pairlist in str(freqtrade.pairlists.short_desc())
assert isinstance(freqtrade.pairlists.whitelist, list) assert isinstance(freqtrade.pairlists.whitelist, list)
assert isinstance(freqtrade.pairlists.blacklist, list) assert isinstance(freqtrade.pairlists.blacklist, list)
@ -157,20 +225,75 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) @pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
@pytest.mark.parametrize("whitelist,log_message", [ @pytest.mark.parametrize("whitelist,log_message", [
(['ETH/BTC', 'TKN/BTC'], ""), (['ETH/BTC', 'TKN/BTC'], ""),
(['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), # TRX/ETH wrong stake # TRX/ETH not in markets
(['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BCH/BTC not available (['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"),
(['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "is not compatible with exchange"), # BLK/BTC in blacklist # wrong stake
(['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") # BTT/BTC is inactive (['ETH/BTC', 'TKN/BTC', 'ETH/USDT'], "is not compatible with your stake currency"),
# BCH/BTC not available
(['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"),
# BLK/BTC in blacklist
(['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "in your blacklist. Removing "),
# BTT/BTC is inactive
(['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active")
]) ])
def test_validate_whitelist(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog,
log_message): log_message, tickers):
whitelist_conf['pairlist']['method'] = pairlist whitelist_conf['pairlists'][0]['method'] = pairlist
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) mocker.patch.multiple('freqtrade.exchange.Exchange',
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
caplog.clear() caplog.clear()
new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) # Assign starting whitelist
new_whitelist = freqtrade.pairlists._pairlists[0]._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
def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf):
whitelist_conf['pairlists'][0].update({"sort_key": "asdf"})
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
with pytest.raises(OperationalException,
match=r"key asdf not in .*"):
get_patched_freqtradebot(mocker, whitelist_conf)
def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
bot = get_patched_freqtradebot(mocker, whitelist_conf)
assert bot.pairlists._pairlists[0]._last_refresh == 0
assert tickers.call_count == 0
bot.pairlists.refresh_pairlist()
assert tickers.call_count == 1
assert bot.pairlists._pairlists[0]._last_refresh != 0
lrf = bot.pairlists._pairlists[0]._last_refresh
bot.pairlists.refresh_pairlist()
assert tickers.call_count == 1
# Time should not be updated.
assert bot.pairlists._pairlists[0]._last_refresh == lrf
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
del whitelist_conf['pairlists'][0]['method']
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
with pytest.raises(OperationalException,
match=r"No Pairlist defined!"):
get_patched_freqtradebot(mocker, whitelist_conf)
assert log_has_re("No method in .*", caplog)
whitelist_conf['pairlists'] = []
with pytest.raises(OperationalException,
match=r"No Pairlist defined!"):
get_patched_freqtradebot(mocker, whitelist_conf)

View File

@ -96,6 +96,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
mocker.patch.multiple(
'freqtrade.rpc.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -109,22 +114,34 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*no active order*'): with pytest.raises(RPCException, match=r'.*no active order*'):
rpc._rpc_status_table() rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
freqtradebot.create_trades() freqtradebot.create_trades()
result = rpc._rpc_status_table()
assert 'instantly' in result['Since'].all() result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'ETH/BTC' in result['Pair'].all() assert "Since" in headers
assert '-0.59%' in result['Profit'].all() assert "Pair" in headers
assert 'instantly' == result[0][2]
assert 'ETH/BTC' == result[0][1]
assert '-0.59%' == result[0][3]
# Test with fiatconvert
rpc._fiat_converter = CryptoToFiatConverter()
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert "Since" in headers
assert "Pair" in headers
assert 'instantly' == result[0][2]
assert 'ETH/BTC' == result[0][1]
assert '-0.59% (-0.09)' == result[0][3]
mocker.patch('freqtrade.exchange.Exchange.get_ticker', mocker.patch('freqtrade.exchange.Exchange.get_ticker',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
# invalidate ticker cache # invalidate ticker cache
rpc._freqtrade.exchange._cached_ticker = {} rpc._freqtrade.exchange._cached_ticker = {}
result = rpc._rpc_status_table() result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' in result['Since'].all() assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result['Pair'].all() assert 'ETH/BTC' == result[0][1]
assert 'nan%' in result['Profit'].all() assert 'nan%' == result[0][3]
def test_rpc_daily_profit(default_conf, update, ticker, fee, def test_rpc_daily_profit(default_conf, update, ticker, fee,
@ -719,21 +736,23 @@ def test_rpc_whitelist(mocker, default_conf) -> None:
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
ret = rpc._rpc_whitelist() ret = rpc._rpc_whitelist()
assert ret['method'] == 'StaticPairList' assert len(ret['method']) == 1
assert 'StaticPairList' in ret['method']
assert ret['whitelist'] == default_conf['exchange']['pair_whitelist'] assert ret['whitelist'] == default_conf['exchange']['pair_whitelist']
def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
default_conf['pairlist'] = {'method': 'VolumePairList', default_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {'number_assets': 4} 'number_assets': 4,
} }]
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
ret = rpc._rpc_whitelist() ret = rpc._rpc_whitelist()
assert ret['method'] == 'VolumePairList' assert len(ret['method']) == 1
assert 'VolumePairList' in ret['method']
assert ret['length'] == 4 assert ret['length'] == 4
assert ret['whitelist'] == default_conf['exchange']['pair_whitelist'] assert ret['whitelist'] == default_conf['exchange']['pair_whitelist']
@ -744,13 +763,14 @@ def test_rpc_blacklist(mocker, default_conf) -> None:
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
ret = rpc._rpc_blacklist(None) ret = rpc._rpc_blacklist(None)
assert ret['method'] == 'StaticPairList' assert len(ret['method']) == 1
assert 'StaticPairList' in ret['method']
assert len(ret['blacklist']) == 2 assert len(ret['blacklist']) == 2
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC']
ret = rpc._rpc_blacklist(["ETH/BTC"]) ret = rpc._rpc_blacklist(["ETH/BTC"])
assert ret['method'] == 'StaticPairList' assert 'StaticPairList' in ret['method']
assert len(ret['blacklist']) == 3 assert len(ret['blacklist']) == 3
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist'] assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC'] assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC']

View File

@ -64,6 +64,10 @@ 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")
assert_response(rc)
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)
@ -280,6 +284,18 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
assert rc.json["max"] == 1.0 assert rc.json["max"] == 1.0
def test_api_show_config(botclient, mocker):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
rc = client_get(client, f"{BASE_URI}/show_config")
assert_response(rc)
assert 'dry_run' in rc.json
assert rc.json['exchange'] == 'bittrex'
assert rc.json['ticker_interval'] == '5m'
assert not rc.json['trailing_stop']
def test_api_daily(botclient, mocker, ticker, fee, markets): def test_api_daily(botclient, mocker, ticker, fee, markets):
ftbot, client = botclient ftbot, client = botclient
patch_get_signal(ftbot, (True, False)) patch_get_signal(ftbot, (True, False))
@ -413,8 +429,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
) )
rc = client_get(client, f"{BASE_URI}/status") rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc, 502) assert_response(rc, 200)
assert rc.json == {'error': 'Error querying _status: no active trade'} assert rc.json == []
ftbot.create_trades() ftbot.create_trades()
rc = client_get(client, f"{BASE_URI}/status") rc = client_get(client, f"{BASE_URI}/status")
@ -456,7 +472,7 @@ def test_api_blacklist(botclient, mocker):
assert_response(rc) assert_response(rc)
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"], assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"],
"length": 2, "length": 2,
"method": "StaticPairList"} "method": ["StaticPairList"]}
# Add ETH/BTC to blacklist # Add ETH/BTC to blacklist
rc = client_post(client, f"{BASE_URI}/blacklist", rc = client_post(client, f"{BASE_URI}/blacklist",
@ -464,7 +480,7 @@ def test_api_blacklist(botclient, mocker):
assert_response(rc) assert_response(rc)
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"], assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
"length": 3, "length": 3,
"method": "StaticPairList"} "method": ["StaticPairList"]}
def test_api_whitelist(botclient): def test_api_whitelist(botclient):
@ -474,7 +490,7 @@ def test_api_whitelist(botclient):
assert_response(rc) assert_response(rc)
assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'],
"length": 4, "length": 4,
"method": "StaticPairList"} "method": ["StaticPairList"]}
def test_api_forcebuy(botclient, mocker, fee): def test_api_forcebuy(botclient, mocker, fee):

View File

@ -1,5 +1,5 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import time
import logging import logging
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -176,6 +176,8 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None:
"listen_port": "8080"} "listen_port": "8080"}
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
# Sleep to allow the thread to start
time.sleep(0.5)
assert log_has('Enabling rpc.api_server', caplog) assert log_has('Enabling rpc.api_server', caplog)
assert len(rpc_manager.registered_modules) == 1 assert len(rpc_manager.registered_modules) == 1
assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules] assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules]

View File

@ -73,7 +73,7 @@ def test_init(default_conf, mocker, caplog) -> None:
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \ "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
"['performance'], ['daily'], ['count'], ['reload_conf'], " \ "['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " \
"['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]" "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]"
assert log_has(message_str, caplog) assert log_has(message_str, caplog)
@ -1050,8 +1050,8 @@ def test_whitelist_static(default_conf, update, mocker) -> None:
telegram._whitelist(update=update, context=MagicMock()) telegram._whitelist(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert ('Using whitelist `StaticPairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' assert ("Using whitelist `['StaticPairList']` with 4 pairs\n"
in msg_mock.call_args_list[0][0][0]) "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0])
def test_whitelist_dynamic(default_conf, update, mocker) -> None: def test_whitelist_dynamic(default_conf, update, mocker) -> None:
@ -1062,17 +1062,17 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None:
_send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
default_conf['pairlist'] = {'method': 'VolumePairList', default_conf['pairlists'] = [{'method': 'VolumePairList',
'config': {'number_assets': 4} 'number_assets': 4
} }]
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._whitelist(update=update, context=MagicMock()) telegram._whitelist(update=update, context=MagicMock())
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert ('Using whitelist `VolumePairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' assert ("Using whitelist `['VolumePairList']` with 4 pairs\n"
in msg_mock.call_args_list[0][0][0]) "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0])
def test_blacklist_static(default_conf, update, mocker) -> None: def test_blacklist_static(default_conf, update, mocker) -> None:
@ -1174,6 +1174,23 @@ def test_version_handle(default_conf, update, mocker) -> None:
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
def test_show_config_handle(default_conf, update, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram._show_config(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0]
assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0]
def test_send_msg_buy_notification(default_conf, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker) -> None:
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(

View File

@ -54,21 +54,30 @@ def test_load_strategy_base64(result, caplog, default_conf):
def test_load_strategy_invalid_directory(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf):
default_conf['strategy'] = 'SampleStrategy'
resolver = StrategyResolver(default_conf) resolver = StrategyResolver(default_conf)
extra_dir = Path.cwd() / 'some/path' extra_dir = Path.cwd() / 'some/path'
resolver._load_strategy('SampleStrategy', config=default_conf, extra_dir=extra_dir) resolver._load_strategy('SampleStrategy', config=default_conf, extra_dir=extra_dir)
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_not_found_strategy(default_conf): def test_load_not_found_strategy(default_conf):
strategy = StrategyResolver(default_conf) default_conf['strategy'] = 'NotFoundStrategy'
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r"Impossible to load Strategy 'NotFoundStrategy'. " match=r"Impossible to load Strategy 'NotFoundStrategy'. "
r"This class does not exist or contains Python code errors."): r"This class does not exist or contains Python code errors."):
strategy._load_strategy(strategy_name='NotFoundStrategy', config=default_conf) StrategyResolver(default_conf)
def test_load_strategy_noname(default_conf):
default_conf['strategy'] = ''
with pytest.raises(OperationalException,
match="No strategy set. Please use `--strategy` to specify "
"the strategy class to use."):
StrategyResolver(default_conf)
def test_strategy(result, default_conf): def test_strategy(result, default_conf):

View File

@ -11,7 +11,7 @@ from freqtrade.configuration.cli_options import check_int_positive
# Parse common command-line-arguments. Used for all tools # Parse common command-line-arguments. Used for all tools
def test_parse_args_none() -> None: def test_parse_args_none() -> None:
arguments = Arguments([]) arguments = Arguments(['trade'])
assert isinstance(arguments, Arguments) assert isinstance(arguments, Arguments)
x = arguments.get_parsed_arg() x = arguments.get_parsed_arg()
assert isinstance(x, dict) assert isinstance(x, dict)
@ -19,7 +19,7 @@ def test_parse_args_none() -> None:
def test_parse_args_defaults() -> None: def test_parse_args_defaults() -> None:
args = Arguments([]).get_parsed_arg() args = Arguments(['trade']).get_parsed_arg()
assert args["config"] == ['config.json'] assert args["config"] == ['config.json']
assert args["strategy_path"] is None assert args["strategy_path"] is None
assert args["datadir"] is None assert args["datadir"] is None
@ -27,27 +27,27 @@ def test_parse_args_defaults() -> None:
def test_parse_args_config() -> None: def test_parse_args_config() -> None:
args = Arguments(['-c', '/dev/null']).get_parsed_arg() args = Arguments(['trade', '-c', '/dev/null']).get_parsed_arg()
assert args["config"] == ['/dev/null'] assert args["config"] == ['/dev/null']
args = Arguments(['--config', '/dev/null']).get_parsed_arg() args = Arguments(['trade', '--config', '/dev/null']).get_parsed_arg()
assert args["config"] == ['/dev/null'] assert args["config"] == ['/dev/null']
args = Arguments(['--config', '/dev/null', args = Arguments(['trade', '--config', '/dev/null',
'--config', '/dev/zero'],).get_parsed_arg() '--config', '/dev/zero'],).get_parsed_arg()
assert args["config"] == ['/dev/null', '/dev/zero'] assert args["config"] == ['/dev/null', '/dev/zero']
def test_parse_args_db_url() -> None: def test_parse_args_db_url() -> None:
args = Arguments(['--db-url', 'sqlite:///test.sqlite']).get_parsed_arg() args = Arguments(['trade', '--db-url', 'sqlite:///test.sqlite']).get_parsed_arg()
assert args["db_url"] == 'sqlite:///test.sqlite' assert args["db_url"] == 'sqlite:///test.sqlite'
def test_parse_args_verbose() -> None: def test_parse_args_verbose() -> None:
args = Arguments(['-v']).get_parsed_arg() args = Arguments(['trade', '-v']).get_parsed_arg()
assert args["verbosity"] == 1 assert args["verbosity"] == 1
args = Arguments(['--verbose']).get_parsed_arg() args = Arguments(['trade', '--verbose']).get_parsed_arg()
assert args["verbosity"] == 1 assert args["verbosity"] == 1
@ -69,7 +69,7 @@ def test_parse_args_invalid() -> None:
def test_parse_args_strategy() -> None: def test_parse_args_strategy() -> None:
args = Arguments(['--strategy', 'SomeStrategy']).get_parsed_arg() args = Arguments(['trade', '--strategy', 'SomeStrategy']).get_parsed_arg()
assert args["strategy"] == 'SomeStrategy' assert args["strategy"] == 'SomeStrategy'
@ -79,7 +79,7 @@ def test_parse_args_strategy_invalid() -> None:
def test_parse_args_strategy_path() -> None: def test_parse_args_strategy_path() -> None:
args = Arguments(['--strategy-path', '/some/path']).get_parsed_arg() args = Arguments(['trade', '--strategy-path', '/some/path']).get_parsed_arg()
assert args["strategy_path"] == '/some/path' assert args["strategy_path"] == '/some/path'
@ -98,8 +98,8 @@ def test_parse_args_backtesting_invalid() -> None:
def test_parse_args_backtesting_custom() -> None: def test_parse_args_backtesting_custom() -> None:
args = [ args = [
'-c', 'test_conf.json',
'backtesting', 'backtesting',
'-c', 'test_conf.json',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'DefaultStrategy',
@ -108,7 +108,7 @@ def test_parse_args_backtesting_custom() -> None:
call_args = Arguments(args).get_parsed_arg() call_args = Arguments(args).get_parsed_arg()
assert call_args["config"] == ['test_conf.json'] assert call_args["config"] == ['test_conf.json']
assert call_args["verbosity"] == 0 assert call_args["verbosity"] == 0
assert call_args["subparser"] == 'backtesting' assert call_args["command"] == 'backtesting'
assert call_args["func"] is not None assert call_args["func"] is not None
assert call_args["ticker_interval"] == '1m' assert call_args["ticker_interval"] == '1m'
assert type(call_args["strategy_list"]) is list assert type(call_args["strategy_list"]) is list
@ -117,8 +117,8 @@ def test_parse_args_backtesting_custom() -> None:
def test_parse_args_hyperopt_custom() -> None: def test_parse_args_hyperopt_custom() -> None:
args = [ args = [
'-c', 'test_conf.json',
'hyperopt', 'hyperopt',
'-c', 'test_conf.json',
'--epochs', '20', '--epochs', '20',
'--spaces', 'buy' '--spaces', 'buy'
] ]
@ -126,7 +126,7 @@ def test_parse_args_hyperopt_custom() -> None:
assert call_args["config"] == ['test_conf.json'] assert call_args["config"] == ['test_conf.json']
assert call_args["epochs"] == 20 assert call_args["epochs"] == 20
assert call_args["verbosity"] == 0 assert call_args["verbosity"] == 0
assert call_args["subparser"] == 'hyperopt' assert call_args["command"] == 'hyperopt'
assert call_args["spaces"] == ['buy'] assert call_args["spaces"] == ['buy']
assert call_args["func"] is not None assert call_args["func"] is not None
assert callable(call_args["func"]) assert callable(call_args["func"])
@ -134,8 +134,8 @@ def test_parse_args_hyperopt_custom() -> None:
def test_download_data_options() -> None: def test_download_data_options() -> None:
args = [ args = [
'--datadir', 'datadir/directory',
'download-data', 'download-data',
'--datadir', 'datadir/directory',
'--pairs-file', 'file_with_pairs', '--pairs-file', 'file_with_pairs',
'--days', '30', '--days', '30',
'--exchange', 'binance' '--exchange', 'binance'
@ -150,8 +150,8 @@ def test_download_data_options() -> None:
def test_plot_dataframe_options() -> None: def test_plot_dataframe_options() -> None:
args = [ args = [
'-c', 'config.json.example',
'plot-dataframe', 'plot-dataframe',
'-c', 'config.json.example',
'--indicators1', 'sma10', 'sma100', '--indicators1', 'sma10', 'sma100',
'--indicators2', 'macd', 'fastd', 'fastk', '--indicators2', 'macd', 'fastd', 'fastk',
'--plot-limit', '30', '--plot-limit', '30',
@ -186,7 +186,7 @@ def test_config_notallowed(mocker) -> None:
] ]
pargs = Arguments(args).get_parsed_arg() pargs = Arguments(args).get_parsed_arg()
assert pargs["config"] is None assert "config" not in pargs
# When file exists: # When file exists:
mocker.patch.object(Path, "is_file", MagicMock(return_value=True)) mocker.patch.object(Path, "is_file", MagicMock(return_value=True))
@ -195,7 +195,7 @@ def test_config_notallowed(mocker) -> None:
] ]
pargs = Arguments(args).get_parsed_arg() pargs = Arguments(args).get_parsed_arg()
# config is not added even if it exists, since create-userdir is in the notallowed list # config is not added even if it exists, since create-userdir is in the notallowed list
assert pargs["config"] is None assert "config" not in pargs
def test_config_notrequired(mocker) -> None: def test_config_notrequired(mocker) -> None:

View File

@ -68,7 +68,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
def test__args_to_config(caplog): def test__args_to_config(caplog):
arg_list = ['--strategy-path', 'TestTest'] arg_list = ['trade', '--strategy-path', 'TestTest']
args = Arguments(arg_list).get_parsed_arg() args = Arguments(arg_list).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = {} config = {}
@ -96,7 +96,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
default_conf['max_open_trades'] = 0 default_conf['max_open_trades'] = 0
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([]).get_parsed_arg() args = Arguments(['trade']).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -121,7 +121,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
configsmock configsmock
) )
arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] arg_list = ['trade', '-c', 'test_conf.json', '--config', 'test2_conf.json', ]
args = Arguments(arg_list).get_parsed_arg() args = Arguments(arg_list).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -187,7 +187,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
default_conf['max_open_trades'] = -1 default_conf['max_open_trades'] = -1
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([]).get_parsed_arg() args = Arguments(['trade']).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -211,11 +211,10 @@ def test_load_config_file_exception(mocker) -> None:
def test_load_config(default_conf, mocker) -> None: def test_load_config(default_conf, mocker) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([]).get_parsed_arg() args = Arguments(['trade']).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert validated_conf.get('strategy') == 'DefaultStrategy'
assert validated_conf.get('strategy_path') is None assert validated_conf.get('strategy_path') is None
assert 'edge' not in validated_conf assert 'edge' not in validated_conf
@ -224,6 +223,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
arglist = [ arglist = [
'trade',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path', '--strategy-path', '/some/path',
'--db-url', 'sqlite:///someurl', '--db-url', 'sqlite:///someurl',
@ -243,6 +243,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
patched_configuration_load_config_file(mocker, conf) patched_configuration_load_config_file(mocker, conf)
arglist = [ arglist = [
'trade',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
@ -259,6 +260,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
patched_configuration_load_config_file(mocker, conf) patched_configuration_load_config_file(mocker, conf)
arglist = [ arglist = [
'trade',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
@ -275,6 +277,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
patched_configuration_load_config_file(mocker, conf) patched_configuration_load_config_file(mocker, conf)
arglist = [ arglist = [
'trade',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
@ -293,6 +296,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
patched_configuration_load_config_file(mocker, conf) patched_configuration_load_config_file(mocker, conf)
arglist = [ arglist = [
'trade',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
@ -303,6 +307,23 @@ def test_load_config_with_params(default_conf, mocker) -> None:
assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL
@pytest.mark.parametrize("config_value,expected,arglist", [
(True, True, ['trade', '--dry-run']), # Leave config untouched
(False, True, ['trade', '--dry-run']), # Override config untouched
(False, False, ['trade']), # Leave config untouched
(True, True, ['trade']), # Leave config untouched
])
def test_load_dry_run(default_conf, mocker, config_value, expected, arglist) -> None:
default_conf['dry_run'] = config_value
patched_configuration_load_config_file(mocker, default_conf)
configuration = Configuration(Arguments(arglist).get_parsed_arg())
validated_conf = configuration.load_config()
assert validated_conf.get('dry_run') is expected
def test_load_custom_strategy(default_conf, mocker) -> None: def test_load_custom_strategy(default_conf, mocker) -> None:
default_conf.update({ default_conf.update({
'strategy': 'CustomStrategy', 'strategy': 'CustomStrategy',
@ -310,7 +331,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
}) })
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([]).get_parsed_arg() args = Arguments(['trade']).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -322,6 +343,7 @@ def test_show_info(default_conf, mocker, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
arglist = [ arglist = [
'trade',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--db-url', 'sqlite:///tmp/testdb', '--db-url', 'sqlite:///tmp/testdb',
] ]
@ -338,9 +360,9 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
arglist = [ arglist = [
'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'backtesting'
] ]
args = Arguments(arglist).get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
@ -376,11 +398,11 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
lambda x, *args, **kwargs: Path(x) lambda x, *args, **kwargs: Path(x)
) )
arglist = [ arglist = [
'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--userdir', "/tmp/freqtrade", '--userdir', "/tmp/freqtrade",
'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
@ -427,8 +449,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
arglist = [ arglist = [
'--config', 'config.json',
'backtesting', 'backtesting',
'--config', 'config.json',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--export', '/bar/foo', '--export', '/bar/foo',
'--strategy-list', '--strategy-list',
@ -568,7 +590,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
# Prevent setting loggers # Prevent setting loggers
mocker.patch('freqtrade.loggers._set_loggers', MagicMock) mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
arglist = ['-vvv'] arglist = ['trade', '-vvv']
args = Arguments(arglist).get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@ -620,7 +642,7 @@ def test_set_logfile(default_conf, mocker):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
arglist = [ arglist = [
'--logfile', 'test_file.log', 'trade', '--logfile', 'test_file.log',
] ]
args = Arguments(arglist).get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@ -636,7 +658,7 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
default_conf['forcebuy_enable'] = True default_conf['forcebuy_enable'] = True
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
args = Arguments([]).get_parsed_arg() args = Arguments(['trade']).get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -755,9 +777,9 @@ def test_validate_whitelist(default_conf):
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf.update({"pairlist": { conf.update({"pairlists": [{
"method": "VolumePairList", "method": "VolumePairList",
}}) }]})
# Dynamic whitelist should not care about pair_whitelist # Dynamic whitelist should not care about pair_whitelist
validate_config_consistency(conf) validate_config_consistency(conf)
del conf['exchange']['pair_whitelist'] del conf['exchange']['pair_whitelist']
@ -847,8 +869,8 @@ def test_pairlist_resolving():
def test_pairlist_resolving_with_config(mocker, default_conf): def test_pairlist_resolving_with_config(mocker, default_conf):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
arglist = [ arglist = [
'--config', 'config.json',
'download-data', 'download-data',
'--config', 'config.json',
] ]
args = Arguments(arglist).get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
@ -861,8 +883,8 @@ def test_pairlist_resolving_with_config(mocker, default_conf):
# Override pairs # Override pairs
arglist = [ arglist = [
'--config', 'config.json',
'download-data', 'download-data',
'--config', 'config.json',
'--pairs', 'ETH/BTC', 'XRP/BTC', '--pairs', 'ETH/BTC', 'XRP/BTC',
] ]
@ -883,8 +905,8 @@ def test_pairlist_resolving_with_config_pl(mocker, default_conf):
mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock()))
arglist = [ arglist = [
'--config', 'config.json',
'download-data', 'download-data',
'--config', 'config.json',
'--pairs-file', 'pairs.json', '--pairs-file', 'pairs.json',
] ]
@ -905,8 +927,8 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf):
mocker.patch.object(Path, "exists", MagicMock(return_value=False)) mocker.patch.object(Path, "exists", MagicMock(return_value=False))
arglist = [ arglist = [
'--config', 'config.json',
'download-data', 'download-data',
'--config', 'config.json',
'--pairs-file', 'pairs.json', '--pairs-file', 'pairs.json',
] ]
@ -975,6 +997,18 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca
assert default_conf[setting[0]][setting[1]] == setting[5] assert default_conf[setting[0]][setting[1]] == setting[5]
def test_process_deprecated_setting_pairlists(mocker, default_conf, caplog):
patched_configuration_load_config_file(mocker, default_conf)
default_conf.update({'pairlist': {
'method': 'VolumePairList',
'config': {'precision_filter': True}
}})
process_temporary_deprecated_settings(default_conf)
assert log_has_re(r'DEPRECATED.*precision_filter.*', caplog)
assert log_has_re(r'DEPRECATED.*in pairlist is deprecated and must be moved*', caplog)
def test_check_conflicting_settings(mocker, default_conf, caplog): def test_check_conflicting_settings(mocker, default_conf, caplog):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)

12
tests/test_docs.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# Test Documentation boxes -
# !!! <TYPE>: is not allowed!
# !!! <TYPE> "title" - Title needs to be quoted!
grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*
if [ $? -ne 0 ]; then
echo "Docs test success."
exit 0
fi
echo "Docs test failed."
exit 1

View File

@ -1804,7 +1804,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade, def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
fee, mocker) -> None: fee, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -2089,6 +2089,29 @@ def test_handle_timedout_limit_buy(mocker, default_conf, limit_buy_order) -> Non
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_order) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
cancel_order_mock = MagicMock(return_value={})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf)
Trade.session = MagicMock()
trade = MagicMock()
limit_buy_order['remaining'] = limit_buy_order['amount']
assert freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
assert cancel_order_mock.call_count == 1
cancel_order_mock.reset_mock()
limit_buy_order['amount'] = 2
assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order)
assert cancel_order_mock.call_count == 1
def test_handle_timedout_limit_sell(mocker, default_conf) -> None: def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)

View File

@ -11,10 +11,16 @@ from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main from freqtrade.main import main
from freqtrade.state import State from freqtrade.state import State
from freqtrade.worker import Worker from freqtrade.worker import Worker
from tests.conftest import (log_has, patch_exchange, from tests.conftest import (log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file) patched_configuration_load_config_file)
def test_parse_args_None(caplog) -> None:
with pytest.raises(SystemExit):
main([])
assert log_has_re(r"Usage of Freqtrade requires a subcommand.*", caplog)
def test_parse_args_backtesting(mocker) -> None: def test_parse_args_backtesting(mocker) -> None:
""" """
Test that main() can start backtesting and also ensure we can pass some specific arguments Test that main() can start backtesting and also ensure we can pass some specific arguments
@ -29,7 +35,7 @@ def test_parse_args_backtesting(mocker) -> None:
call_args = backtesting_mock.call_args[0][0] call_args = backtesting_mock.call_args[0][0]
assert call_args["config"] == ['config.json'] assert call_args["config"] == ['config.json']
assert call_args["verbosity"] == 0 assert call_args["verbosity"] == 0
assert call_args["subparser"] == 'backtesting' assert call_args["command"] == 'backtesting'
assert call_args["func"] is not None assert call_args["func"] is not None
assert callable(call_args["func"]) assert callable(call_args["func"])
assert call_args["ticker_interval"] is None assert call_args["ticker_interval"] is None
@ -45,7 +51,7 @@ def test_main_start_hyperopt(mocker) -> None:
call_args = hyperopt_mock.call_args[0][0] call_args = hyperopt_mock.call_args[0][0]
assert call_args["config"] == ['config.json'] assert call_args["config"] == ['config.json']
assert call_args["verbosity"] == 0 assert call_args["verbosity"] == 0
assert call_args["subparser"] == 'hyperopt' assert call_args["command"] == 'hyperopt'
assert call_args["func"] is not None assert call_args["func"] is not None
assert callable(call_args["func"]) assert callable(call_args["func"])
@ -58,7 +64,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
args = ['-c', 'config.json.example'] args = ['trade', '-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
@ -75,7 +81,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
args = ['-c', 'config.json.example'] args = ['trade', '-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
@ -95,7 +101,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
args = ['-c', 'config.json.example'] args = ['trade', '-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
@ -114,15 +120,15 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
OperationalException("Oh snap!")]) OperationalException("Oh snap!")])
mocker.patch('freqtrade.worker.Worker._worker', worker_mock) mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock()) reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
args = Arguments(['-c', 'config.json.example']).get_parsed_arg() args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg()
worker = Worker(args=args, config=default_conf) worker = Worker(args=args, config=default_conf)
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(['-c', 'config.json.example']) main(['trade', '-c', 'config.json.example'])
assert log_has('Using config: config.json.example ...', caplog) assert log_has('Using config: config.json.example ...', caplog)
assert worker_mock.call_count == 4 assert worker_mock.call_count == 4
@ -141,7 +147,7 @@ def test_reconfigure(mocker, default_conf) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
args = Arguments(['-c', 'config.json.example']).get_parsed_arg() args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg()
worker = Worker(args=args, config=default_conf) worker = Worker(args=args, config=default_conf)
freqtrade = worker.freqtrade freqtrade = worker.freqtrade

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