Merge branch 'develop' into hyperopt-trailing-space
This commit is contained in:
commit
e7ddd81251
230
.github/workflows/ci.yml
vendored
Normal file
230
.github/workflows/ci.yml
vendored
Normal 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 }}
|
||||
|
18
.github/workflows/docker_update_readme.yml
vendored
Normal file
18
.github/workflows/docker_update_readme.yml
vendored
Normal 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
|
||||
|
16
.travis.yml
16
.travis.yml
@ -24,15 +24,15 @@ jobs:
|
||||
script:
|
||||
- pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||
# Allow failure for coveralls
|
||||
- coveralls || true
|
||||
# - coveralls || true
|
||||
name: pytest
|
||||
- script:
|
||||
- cp config.json.example config.json
|
||||
- freqtrade --datadir tests/testdata backtesting
|
||||
- freqtrade backtesting --datadir tests/testdata --strategy DefaultStrategy
|
||||
name: backtest
|
||||
- script:
|
||||
- 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
|
||||
- script: flake8
|
||||
name: flake8
|
||||
@ -45,11 +45,11 @@ jobs:
|
||||
- script: mypy freqtrade scripts
|
||||
name: mypy
|
||||
|
||||
- stage: docker
|
||||
if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron))
|
||||
script:
|
||||
- build_helpers/publish_docker.sh
|
||||
name: "Build and test and push docker image"
|
||||
# - stage: docker
|
||||
# if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron))
|
||||
# script:
|
||||
# - build_helpers/publish_docker.sh
|
||||
# name: "Build and test and push docker image"
|
||||
|
||||
notifications:
|
||||
slack:
|
||||
|
@ -24,3 +24,5 @@ RUN pip install numpy --no-cache-dir \
|
||||
COPY . /freqtrade/
|
||||
RUN pip install -e . --no-cache-dir
|
||||
ENTRYPOINT ["freqtrade"]
|
||||
# Default to trade mode
|
||||
CMD [ "trade" ]
|
||||
|
@ -38,3 +38,4 @@ RUN ~/berryconda3/bin/pip install -e . --no-cache-dir
|
||||
RUN [ "cross-build-end" ]
|
||||
|
||||
ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"]
|
||||
CMD [ "trade" ]
|
||||
|
@ -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/).
|
||||
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Bot commands
|
||||
@ -106,7 +105,7 @@ optional arguments:
|
||||
|
||||
### 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
|
||||
- `/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.
|
||||
- `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
|
||||
|
||||
### Help / Slack
|
||||
|
BIN
build_helpers/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl
Normal file
Binary file not shown.
8
build_helpers/install_windows.ps1
Normal file
8
build_helpers/install_windows.ps1
Normal 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 .
|
@ -1,17 +1,17 @@
|
||||
#!/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
|
||||
echo "${TRAVIS_COMMIT} ${TRAVIS_COMMIT_MESSAGE}" > freqtrade_commit
|
||||
echo "${GITHUB_SHA}" > freqtrade_commit
|
||||
|
||||
if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then
|
||||
echo "event ${TRAVIS_EVENT_TYPE}: full rebuild - skipping cache"
|
||||
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
|
||||
echo "event ${GITHUB_EVENT_NAME}: full rebuild - skipping cache"
|
||||
docker build -t freqtrade:${TAG} .
|
||||
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
|
||||
docker pull ${IMAGE_NAME}:${TAG}
|
||||
docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} .
|
||||
@ -23,7 +23,7 @@ if [ $? -ne 0 ]; then
|
||||
fi
|
||||
|
||||
# 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
|
||||
echo "failed running backtest"
|
||||
@ -38,12 +38,12 @@ if [ $? -ne 0 ]; then
|
||||
fi
|
||||
|
||||
# Tag as latest for develop builds
|
||||
if [ "${TRAVIS_BRANCH}" = "develop" ]; then
|
||||
if [ "${GITHUB_REF}" = "develop" ]; then
|
||||
docker tag freqtrade:$TAG ${IMAGE_NAME}:latest
|
||||
fi
|
||||
|
||||
# Login
|
||||
echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin
|
||||
docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "failed login"
|
||||
|
@ -52,6 +52,9 @@
|
||||
"DOGE/BTC"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
@ -68,7 +71,7 @@
|
||||
"remove_pumps": false
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"token": "your_telegram_token",
|
||||
"chat_id": "your_telegram_chat_id"
|
||||
},
|
||||
|
@ -54,6 +54,9 @@
|
||||
"BNB/BTC"
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
|
@ -50,14 +50,18 @@
|
||||
"buy": "gtc",
|
||||
"sell": "gtc"
|
||||
},
|
||||
"pairlist": {
|
||||
"method": "VolumePairList",
|
||||
"config": {
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"},
|
||||
{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"precision_filter": false
|
||||
"refresh_period": 1800
|
||||
},
|
||||
{"method": "PrecisionFilter"},
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.01
|
||||
}
|
||||
},
|
||||
],
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
"sandbox": false,
|
||||
|
@ -46,6 +46,9 @@
|
||||
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
|
@ -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.
|
||||
|
||||
!!! 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:
|
||||
|
||||
```bash
|
||||
|
@ -45,7 +45,7 @@ freqtrade --datadir user_data/data/bittrex-20180101 backtesting
|
||||
#### With a (custom) strategy file
|
||||
|
||||
```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.
|
||||
|
@ -5,20 +5,18 @@ This page explains the different parameters of the bot and how to run it.
|
||||
!!! Note
|
||||
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
|
||||
|
||||
|
||||
## Bot commands
|
||||
|
||||
```
|
||||
usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
||||
[--userdir PATH] [-s NAME] [--strategy-path PATH]
|
||||
[--db-url PATH] [--sd-notify]
|
||||
{backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
|
||||
usage: freqtrade [-h] [-V]
|
||||
{trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
|
||||
...
|
||||
|
||||
Free, open source crypto trading bot
|
||||
|
||||
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.
|
||||
edge Edge module.
|
||||
hyperopt Hyperopt module.
|
||||
@ -32,6 +30,27 @@ positional arguments:
|
||||
|
||||
optional arguments:
|
||||
-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).
|
||||
--logfile FILE Log to the file specified.
|
||||
-V, --version show program's version number and exit
|
||||
@ -43,15 +62,12 @@ optional arguments:
|
||||
Path to directory with historical backtesting data.
|
||||
--userdir PATH, --user-data-dir PATH
|
||||
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?
|
||||
@ -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:
|
||||
|
||||
```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
|
||||
@ -79,13 +95,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua
|
||||
require them):
|
||||
|
||||
```bash
|
||||
freqtrade -c ./config.json
|
||||
freqtrade trade -c ./config.json
|
||||
```
|
||||
|
||||
and specify both configuration files when running in the normal Live Trade Mode:
|
||||
|
||||
```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
|
||||
@ -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:
|
||||
|
||||
```bash
|
||||
freqtrade --strategy AwesomeStrategy
|
||||
freqtrade trade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
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!):
|
||||
|
||||
```bash
|
||||
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||
freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||
```
|
||||
|
||||
#### 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:
|
||||
|
||||
```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
|
||||
@ -173,8 +189,10 @@ freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
|
||||
Backtesting also uses the config specified via `-c/--config`.
|
||||
|
||||
```
|
||||
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||
[--max_open_trades INT]
|
||||
usage: freqtrade backtesting [-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] [--fee FLOAT]
|
||||
[--eps] [--dmmp]
|
||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||
@ -211,11 +229,29 @@ optional arguments:
|
||||
--export EXPORT Export backtest results, argument are: trades.
|
||||
Example: `--export=trades`
|
||||
--export-filename PATH
|
||||
Save backtest results to the file with this filename
|
||||
(default: `user_data/backtest_results/backtest-
|
||||
result.json`). Requires `--export` to be set as well.
|
||||
Example: `--export-filename=user_data/backtest_results
|
||||
/backtest_today.json`
|
||||
Save backtest results to the file with this filename.
|
||||
Requires `--export` to be set as well. Example:
|
||||
`--export-filename=user_data/backtest_results/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.
|
||||
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
|
||||
|
||||
@ -231,12 +267,14 @@ To optimize your strategy, you can use hyperopt parameter hyperoptimization
|
||||
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]
|
||||
[--stake_amount STAKE_AMOUNT] [--fee FLOAT]
|
||||
[--customhyperopt NAME] [--hyperopt-path PATH]
|
||||
[--eps] [-e INT]
|
||||
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
||||
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
|
||||
[-e INT]
|
||||
[--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
|
||||
[--dmmp] [--print-all] [--no-color] [--print-json]
|
||||
[-j JOBS] [--random-state INT] [--min-trades INT]
|
||||
[--continue] [--hyperopt-loss NAME]
|
||||
@ -254,16 +292,15 @@ optional arguments:
|
||||
Specify stake_amount.
|
||||
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
|
||||
entry and exit).
|
||||
--customhyperopt NAME
|
||||
Specify hyperopt class name (default:
|
||||
`DefaultHyperOpt`).
|
||||
--hyperopt-path PATH Specify additional lookup path for Hyperopts and
|
||||
--hyperopt NAME Specify hyperopt class name which will be used by the
|
||||
bot.
|
||||
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
|
||||
Hyperopt Loss functions.
|
||||
--eps, --enable-position-stacking
|
||||
Allow buying the same pair multiple times (position
|
||||
stacking).
|
||||
-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
|
||||
list. Default: `all`.
|
||||
--dmmp, --disable-max-market-positions
|
||||
@ -292,8 +329,27 @@ optional arguments:
|
||||
generate completely different results, since the
|
||||
target for optimization is different. Built-in
|
||||
Hyperopt-loss-functions are: DefaultHyperOptLoss,
|
||||
OnlyProfitHyperOptLoss, SharpeHyperOptLoss.(default:
|
||||
OnlyProfitHyperOptLoss, SharpeHyperOptLoss (default:
|
||||
`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
|
||||
@ -301,7 +357,9 @@ optional arguments:
|
||||
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]
|
||||
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
|
||||
|
||||
@ -324,6 +382,24 @@ optional arguments:
|
||||
(without any space). Example:
|
||||
`--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).
|
||||
|
@ -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.
|
||||
| `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.
|
||||
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists).
|
||||
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
|
||||
| `pairlists` | StaticPairList | Define one or more pairlists to be used. [More information below](#dynamic-pairlists).
|
||||
| `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.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`.
|
||||
| `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.
|
||||
| `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).
|
||||
| `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.
|
||||
@ -357,13 +356,6 @@ For example, to test the order type `FOK` with Kraken, and modify candle_limit t
|
||||
!!! Warning
|
||||
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?
|
||||
|
||||
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"
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
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,
|
||||
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
|
||||
|
||||
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",
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
!!! Note
|
||||
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.
|
||||
|
||||
@ -501,14 +538,13 @@ export HTTPS_PROXY="http://addr:port"
|
||||
freqtrade
|
||||
```
|
||||
|
||||
|
||||
### Embedding Strategies
|
||||
## Embedding Strategies
|
||||
|
||||
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,
|
||||
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
|
||||
|
||||
|
@ -78,10 +78,8 @@ freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --d
|
||||
!!! 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.
|
||||
|
||||
### 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.
|
||||
!!! Note "Kraken user"
|
||||
Kraken users should read [this](exchanges.md#historic-kraken-data) before starting to download data.
|
||||
|
||||
## Next step
|
||||
|
||||
|
@ -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.
|
||||
|
||||
#### Install
|
||||
|
||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
* [docker](https://docs.docker.com/install/)
|
||||
* [docker-compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
#### Starting the bot
|
||||
##### Use the develop dockerfile
|
||||
|
||||
``` bash
|
||||
rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml
|
||||
```
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
##### Starting
|
||||
@ -62,9 +65,11 @@ rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml
|
||||
``` bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png)
|
||||
|
||||
##### Rebuilding
|
||||
|
||||
``` bash
|
||||
docker-compose build
|
||||
```
|
||||
@ -77,8 +82,8 @@ that can be effected by `docker-compose up` or `docker-compose run freqtrade_dev
|
||||
``` 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
|
||||
|
||||
@ -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).
|
||||
|
||||
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
|
||||
self._freqtrade = freqtrade
|
||||
self._exchange = exchange
|
||||
self._pairlistmanager = pairlistmanager
|
||||
self._config = config
|
||||
self._whitelist = self._config['exchange']['pair_whitelist']
|
||||
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
||||
self._pairlistconfig = pairlistconfig
|
||||
self._pairlist_pos = pairlist_pos
|
||||
```
|
||||
|
||||
|
||||
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"`.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
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
|
||||
|
||||
``` python
|
||||
def refresh_pairlist(self) -> None:
|
||||
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||
# Generate dynamic whitelist
|
||||
pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key)
|
||||
# Validate whitelist to only have active market pairs
|
||||
self._whitelist = self._validate_whitelist(pairs)[:self._number_pairs]
|
||||
pairs = self._calculate_pairlist(pairlist, tickers)
|
||||
return pairs
|
||||
```
|
||||
|
||||
#### _gen_pair_whitelist
|
||||
|
||||
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)
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
This part of the documentation is aimed at maintainers, and shows how to create a release.
|
||||
|
@ -160,7 +160,7 @@ docker run -d \
|
||||
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||
-v ~/.freqtrade/user_data/:/freqtrade/user_data \
|
||||
-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
|
||||
@ -202,7 +202,7 @@ docker run -d \
|
||||
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||
-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.
|
||||
|
@ -235,7 +235,7 @@ An example of its output:
|
||||
### Update cached pairs with the latest data
|
||||
|
||||
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
|
||||
|
||||
|
63
docs/exchanges.md
Normal file
63
docs/exchanges.md
Normal 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
|
||||
```
|
@ -4,7 +4,7 @@
|
||||
|
||||
### 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:
|
||||
|
||||
@ -48,12 +48,8 @@ You can use the `/forcesell all` command from Telegram.
|
||||
### I get the message "RESTRICTED_MARKET"
|
||||
|
||||
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.
|
||||
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.
|
||||
Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information.
|
||||
|
||||
### How do I search the bot logs for something?
|
||||
|
||||
|
@ -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.
|
||||
|
||||
```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.
|
||||
@ -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.
|
||||
|
||||
```bash
|
||||
freqtrade --strategy SampleStrategy hyperopt --customhyperopt SampleHyperopt
|
||||
freqtrade hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt
|
||||
```
|
||||
|
||||
### Running Hyperopt with Smaller Search Space
|
||||
|
@ -26,24 +26,32 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E
|
||||
|
||||
## Quick start
|
||||
|
||||
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to 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
|
||||
```
|
||||
Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot.
|
||||
|
||||
!!! Note
|
||||
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
|
||||
$ ./setup.sh
|
||||
@ -56,25 +64,25 @@ usage:
|
||||
|
||||
** --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`
|
||||
* Setup your virtualenv
|
||||
* 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 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 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 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.
|
||||
|
||||
```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.
|
||||
|
@ -23,13 +23,15 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three
|
||||
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 ...]]
|
||||
[--indicators2 INDICATORS2 [INDICATORS2 ...]]
|
||||
[--plot-limit INT] [--db-url PATH]
|
||||
[--trade-source {DB,file}] [--export EXPORT]
|
||||
[--export-filename PATH]
|
||||
[--timerange TIMERANGE]
|
||||
[--timerange TIMERANGE] [-i TICKER_INTERVAL]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@ -62,6 +64,28 @@ optional arguments:
|
||||
/backtest_today.json`
|
||||
--timerange TIMERANGE
|
||||
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.
|
||||
|
||||
``` 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
|
||||
@ -91,25 +115,25 @@ freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma
|
||||
To plot multiple pairs, separate them with a space:
|
||||
|
||||
``` 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)
|
||||
|
||||
``` 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`:
|
||||
|
||||
``` 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>`
|
||||
|
||||
``` 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
|
||||
@ -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:
|
||||
|
||||
```
|
||||
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]
|
||||
[--export-filename PATH] [--db-url PATH]
|
||||
[--trade-source {DB,file}]
|
||||
[--trade-source {DB,file}] [-i TICKER_INTERVAL]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
@ -159,6 +184,22 @@ optional arguments:
|
||||
--trade-source {DB,file}
|
||||
Specify the source for trades (Can be DB or 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.
|
||||
|
||||
```
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
mkdocs-material==4.4.3
|
||||
mkdocs-material==4.5.0
|
||||
mdx_truly_sane_lists==1.2
|
||||
|
@ -22,7 +22,14 @@ Sample configuration:
|
||||
!!! Danger "Password selection"
|
||||
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.
|
||||
|
||||
@ -58,7 +65,7 @@ docker run -d \
|
||||
-v ~/.freqtrade/user_data/:/freqtrade/user_data \
|
||||
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||
-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"
|
||||
@ -99,6 +106,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
||||
| `stop` | | Stops the trader
|
||||
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `reload_conf` | | Reloads the configuration file
|
||||
| `show_config` | | Shows part of the current configuration with relevant settings to operation
|
||||
| `status` | | Lists all open trades
|
||||
| `count` | | Displays number of trades used and available
|
||||
| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||
@ -165,6 +173,10 @@ reload_conf
|
||||
Reload configuration
|
||||
:returns: json object
|
||||
|
||||
show_config
|
||||
Returns part of the configuration, relevant for trading operations.
|
||||
:return: json object containing the version
|
||||
|
||||
start
|
||||
Start the bot if it's in stopped state.
|
||||
:returns: json object
|
||||
|
@ -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)
|
||||
|
||||
```bash
|
||||
freqtrade --strategy AwesomeStrategy
|
||||
freqtrade trade --strategy AwesomeStrategy
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```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)
|
||||
@ -314,9 +314,9 @@ Please always check the mode of operation to select the correct method to get da
|
||||
#### Possible options for DataProvider
|
||||
|
||||
- `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.
|
||||
- `historic_ohlcv(pair, ticker_interval)` - 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).
|
||||
- `ohlcv(pair, timeframe)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame.
|
||||
- `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk.
|
||||
- `get_pair_dataframe(pair, timeframe)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
|
||||
- `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries.
|
||||
- `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure.
|
||||
- `runmode` - Property containing the current runmode.
|
||||
@ -327,7 +327,7 @@ Please always check the mode of operation to select the correct method to get da
|
||||
if self.dp:
|
||||
inf_pair, inf_timeframe = self.informative_pairs()[0]
|
||||
informative = self.dp.get_pair_dataframe(pair=inf_pair,
|
||||
ticker_interval=inf_timeframe)
|
||||
timeframe=inf_timeframe)
|
||||
```
|
||||
|
||||
!!! 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`
|
||||
|
||||
```bash
|
||||
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||
freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory
|
||||
```
|
||||
|
||||
### Common mistakes when developing strategies
|
||||
|
@ -10,7 +10,7 @@ from pathlib import Path
|
||||
# Customize these according to your needs.
|
||||
|
||||
# Define some constants
|
||||
ticker_interval = "5m"
|
||||
timeframe = "5m"
|
||||
# Name of the strategy class
|
||||
strategy_name = 'SampleStrategy'
|
||||
# Path to user data
|
||||
@ -29,7 +29,7 @@ pair = "BTC_USDT"
|
||||
from freqtrade.data.history import load_pair_history
|
||||
|
||||
candles = load_pair_history(datadir=data_location,
|
||||
ticker_interval=ticker_interval,
|
||||
timeframe=timeframe,
|
||||
pair=pair)
|
||||
|
||||
# Confirm success
|
||||
|
@ -53,6 +53,7 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/stop` | | Stops the trader
|
||||
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||
| `/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 table` | | List all open trades in a table format
|
||||
| `/count` | | Displays number of trades used and available
|
||||
|
@ -6,7 +6,7 @@ After=network.target
|
||||
# Set WorkingDirectory and ExecStart to your file paths accordingly
|
||||
# NOTE: %h will be resolved to /home/<username>
|
||||
WorkingDirectory=%h/freqtrade
|
||||
ExecStart=/usr/bin/freqtrade
|
||||
ExecStart=/usr/bin/freqtrade trade
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
|
@ -6,7 +6,7 @@ After=network.target
|
||||
# Set WorkingDirectory and ExecStart to your file paths accordingly
|
||||
# NOTE: %h will be resolved to /home/<username>
|
||||
WorkingDirectory=%h/freqtrade
|
||||
ExecStart=/usr/bin/freqtrade --sd-notify
|
||||
ExecStart=/usr/bin/freqtrade trade --sd-notify
|
||||
|
||||
Restart=always
|
||||
#Restart=on-failure
|
||||
|
@ -13,7 +13,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat
|
||||
|
||||
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",
|
||||
"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",
|
||||
"timeframes", "erase"]
|
||||
|
||||
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
|
||||
"trade_source", "export", "exportfilename", "timerange", "ticker_interval"]
|
||||
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
||||
"db_url", "trade_source", "export", "exportfilename",
|
||||
"timerange", "ticker_interval"]
|
||||
|
||||
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
|
||||
"trade_source", "ticker_interval"]
|
||||
@ -61,11 +62,6 @@ class Arguments:
|
||||
def __init__(self, args: Optional[List[str]]) -> None:
|
||||
self.args = args
|
||||
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]:
|
||||
"""
|
||||
@ -73,7 +69,7 @@ class Arguments:
|
||||
:return: List[str] List of arguments
|
||||
"""
|
||||
if self._parsed_arg is None:
|
||||
self._load_args()
|
||||
self._build_subcommands()
|
||||
self._parsed_arg = self._parse_args()
|
||||
|
||||
return vars(self._parsed_arg)
|
||||
@ -84,22 +80,17 @@ class Arguments:
|
||||
"""
|
||||
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
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
if (parsed_arg.config is None
|
||||
and subparser not in NO_CONF_ALLOWED
|
||||
and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file()
|
||||
or (subparser not in NO_CONF_REQURIED))):
|
||||
if ('config' in parsed_arg and parsed_arg.config is None and
|
||||
((Path.cwd() / constants.DEFAULT_CONFIG).is_file() or
|
||||
not ('command' in parsed_arg and parsed_arg.command in NO_CONF_REQURIED))):
|
||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
|
||||
def _build_args(self, optionlist, parser=None):
|
||||
parser = parser or self.parser
|
||||
def _build_args(self, optionlist, parser):
|
||||
|
||||
for val in optionlist:
|
||||
opt = AVAILABLE_CLI_OPTIONS[val]
|
||||
@ -110,38 +101,68 @@ class Arguments:
|
||||
Builds and attaches all subcommands.
|
||||
: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.utils import (start_create_userdir, start_download_data,
|
||||
start_list_exchanges, start_list_timeframes,
|
||||
start_list_markets)
|
||||
start_list_exchanges, 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
|
||||
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)
|
||||
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
|
||||
|
||||
# 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)
|
||||
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
|
||||
|
||||
# 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)
|
||||
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
|
||||
|
||||
# add create-userdir subcommand
|
||||
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)
|
||||
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
|
||||
|
||||
# Add list-exchanges subcommand
|
||||
list_exchanges_cmd = subparsers.add_parser(
|
||||
'list-exchanges',
|
||||
help='Print available exchanges.'
|
||||
help='Print available exchanges.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
||||
@ -149,7 +170,8 @@ class Arguments:
|
||||
# Add list-timeframes subcommand
|
||||
list_timeframes_cmd = subparsers.add_parser(
|
||||
'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)
|
||||
self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd)
|
||||
@ -157,7 +179,8 @@ class Arguments:
|
||||
# Add list-markets subcommand
|
||||
list_markets_cmd = subparsers.add_parser(
|
||||
'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))
|
||||
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd)
|
||||
@ -165,7 +188,8 @@ class Arguments:
|
||||
# Add list-pairs subcommand
|
||||
list_pairs_cmd = subparsers.add_parser(
|
||||
'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))
|
||||
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd)
|
||||
@ -173,16 +197,17 @@ class Arguments:
|
||||
# Add download-data subcommand
|
||||
download_data_cmd = subparsers.add_parser(
|
||||
'download-data',
|
||||
help='Download backtesting data.'
|
||||
help='Download backtesting data.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
download_data_cmd.set_defaults(func=start_download_data)
|
||||
self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd)
|
||||
|
||||
# Add Plotting subcommand
|
||||
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
|
||||
plot_dataframe_cmd = subparsers.add_parser(
|
||||
'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)
|
||||
self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd)
|
||||
@ -190,7 +215,8 @@ class Arguments:
|
||||
# Plot profit
|
||||
plot_profit_cmd = subparsers.add_parser(
|
||||
'plot-profit',
|
||||
help='Generate plot showing profits.'
|
||||
help='Generate plot showing profits.',
|
||||
parents=[_common_parser],
|
||||
)
|
||||
plot_profit_cmd.set_defaults(func=start_plot_profit)
|
||||
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)
|
||||
|
@ -65,9 +65,8 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
# Main options
|
||||
"strategy": Arg(
|
||||
'-s', '--strategy',
|
||||
help='Specify strategy class name (default: `%(default)s`).',
|
||||
help='Specify strategy class name which will be used by the bot.',
|
||||
metavar='NAME',
|
||||
default='DefaultStrategy',
|
||||
),
|
||||
"strategy_path": Arg(
|
||||
'--strategy-path',
|
||||
@ -86,6 +85,11 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
help='Notify systemd service manager.',
|
||||
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
|
||||
"ticker_interval": Arg(
|
||||
'-i', '--ticker-interval',
|
||||
@ -136,7 +140,7 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
"exportfilename": Arg(
|
||||
'--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. '
|
||||
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
|
||||
metavar='PATH',
|
||||
@ -156,14 +160,13 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
),
|
||||
# Hyperopt
|
||||
"hyperopt": Arg(
|
||||
'--customhyperopt',
|
||||
help='Specify hyperopt class name (default: `%(default)s`).',
|
||||
'--hyperopt',
|
||||
help='Specify hyperopt class name which will be used by the bot.',
|
||||
metavar='NAME',
|
||||
default=constants.DEFAULT_HYPEROPT,
|
||||
),
|
||||
"hyperopt_path": Arg(
|
||||
'--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',
|
||||
),
|
||||
"epochs": Arg(
|
||||
|
@ -122,6 +122,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
||||
RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]:
|
||||
return
|
||||
|
||||
if (conf.get('pairlist', {}).get('method', 'StaticPairList') == 'StaticPairList'
|
||||
and not conf.get('exchange', {}).get('pair_whitelist')):
|
||||
raise OperationalException("StaticPairList requires pair_whitelist to be set.")
|
||||
for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]):
|
||||
if (pl.get('method') == 'StaticPairList'
|
||||
and not conf.get('exchange', {}).get('pair_whitelist')):
|
||||
raise OperationalException("StaticPairList requires pair_whitelist to be set.")
|
||||
|
@ -81,6 +81,9 @@ class Configuration:
|
||||
if 'ask_strategy' not in config:
|
||||
config['ask_strategy'] = {}
|
||||
|
||||
if 'pairlists' not in config:
|
||||
config['pairlists'] = []
|
||||
|
||||
# validate configuration before returning
|
||||
logger.info('Validating configuration ...')
|
||||
validate_config_schema(config)
|
||||
@ -93,7 +96,7 @@ class Configuration:
|
||||
:return: Configuration dictionary
|
||||
"""
|
||||
# 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
|
||||
config['original_config'] = deepcopy(config)
|
||||
@ -153,7 +156,7 @@ class Configuration:
|
||||
self._process_logging_options(config)
|
||||
|
||||
# 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")})
|
||||
|
||||
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"]:
|
||||
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:
|
||||
"""
|
||||
Extract information for sys.argv and load directory configurations
|
||||
|
@ -57,3 +57,19 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
|
||||
'experimental', 'sell_profit_only')
|
||||
process_deprecated_setting(config, 'ask_strategy', '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'})
|
||||
|
@ -39,12 +39,12 @@ class TimeRange:
|
||||
if self.startts:
|
||||
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:
|
||||
"""
|
||||
Adjust startts by <startup_candles> candles.
|
||||
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 min_date: Minimum data date loaded. Key kriterium to decide if start-time
|
||||
has to be moved
|
||||
@ -55,7 +55,7 @@ class TimeRange:
|
||||
# If no startts was defined, or backtest-data starts at the defined backtest-date
|
||||
logger.warning("Moving start-date by %s candles to account for startup time.",
|
||||
startup_candles)
|
||||
self.startts = (min_date.timestamp + ticker_interval_secs * startup_candles)
|
||||
self.startts = (min_date.timestamp + timeframe_secs * startup_candles)
|
||||
self.starttype = 'date'
|
||||
|
||||
@staticmethod
|
||||
|
@ -9,8 +9,6 @@ PROCESS_THROTTLE_SECS = 5 # sec
|
||||
DEFAULT_TICKER_INTERVAL = 5 # min
|
||||
HYPEROPT_EPOCH = 100 # epochs
|
||||
RETRY_TIMEOUT = 30 # sec
|
||||
DEFAULT_STRATEGY = 'DefaultStrategy'
|
||||
DEFAULT_HYPEROPT = 'DefaultHyperOpt'
|
||||
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
|
||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||
@ -20,11 +18,11 @@ REQUIRED_ORDERTIF = ['buy', 'sell']
|
||||
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter']
|
||||
DRY_RUN_WALLET = 999.9
|
||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||
|
||||
TICKER_INTERVALS = [
|
||||
TIMEFRAMES = [
|
||||
'1m', '3m', '5m', '15m', '30m',
|
||||
'1h', '2h', '4h', '6h', '8h', '12h',
|
||||
'1d', '3d', '1w',
|
||||
@ -57,7 +55,7 @@ CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'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_amount': {
|
||||
"type": ["number", "string"],
|
||||
@ -151,13 +149,16 @@ CONF_SCHEMA = {
|
||||
'block_bad_exchanges': {'type': 'boolean'}
|
||||
}
|
||||
},
|
||||
'pairlist': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
|
||||
'config': {'type': 'object'}
|
||||
},
|
||||
'required': ['method']
|
||||
'pairlists': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
|
||||
'config': {'type': 'object'}
|
||||
},
|
||||
'required': ['method'],
|
||||
}
|
||||
},
|
||||
'telegram': {
|
||||
'type': 'object',
|
||||
|
@ -7,7 +7,7 @@ from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytz
|
||||
from datetime import timezone
|
||||
|
||||
from freqtrade import persistence
|
||||
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"]
|
||||
|
||||
trades = pd.DataFrame([(t.pair,
|
||||
t.open_date.replace(tzinfo=pytz.UTC),
|
||||
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None,
|
||||
t.open_date.replace(tzinfo=timezone.utc),
|
||||
t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None,
|
||||
t.calc_profit(), t.calc_profit_percent(),
|
||||
t.open_rate, t.close_rate, t.amount,
|
||||
(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.
|
||||
"""
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
ticker_minutes = timeframe_to_minutes(timeframe)
|
||||
# Resample to ticker_interval to make sure trades match candles
|
||||
_trades_sum = trades.resample(f'{ticker_minutes}min', on='close_time')[['profitperc']].sum()
|
||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||
# Resample to timeframe to make sure trades match candles
|
||||
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time')[['profitperc']].sum()
|
||||
df.loc[:, col_name] = _trades_sum.cumsum()
|
||||
# Set first value to 0
|
||||
df.loc[df.iloc[0].name, col_name] = 0
|
||||
|
@ -10,13 +10,13 @@ from pandas import DataFrame, to_datetime
|
||||
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,
|
||||
drop_incomplete: bool = True) -> 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_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 fill_missing: fill up missing candles with 0 candles
|
||||
(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')
|
||||
|
||||
if fill_missing:
|
||||
return ohlcv_fill_up_missing_data(frame, ticker_interval, pair)
|
||||
return ohlcv_fill_up_missing_data(frame, timeframe, pair)
|
||||
else:
|
||||
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,
|
||||
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',
|
||||
'volume': 'sum'
|
||||
}
|
||||
ticker_minutes = timeframe_to_minutes(ticker_interval)
|
||||
ticker_minutes = timeframe_to_minutes(timeframe)
|
||||
# Resample to create "NAN" values
|
||||
df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict)
|
||||
|
||||
|
@ -37,52 +37,53 @@ class DataProvider:
|
||||
@property
|
||||
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.
|
||||
"""
|
||||
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
|
||||
Please use the `available_pairs` method to verify which pairs are currently cached.
|
||||
: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.
|
||||
Use False only for read-only operations (where the dataframe is not modified)
|
||||
"""
|
||||
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)
|
||||
else:
|
||||
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
|
||||
: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,
|
||||
ticker_interval=ticker_interval or self._config['ticker_interval'],
|
||||
timeframe=timeframe or self._config['ticker_interval'],
|
||||
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
|
||||
on the runmode.
|
||||
: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):
|
||||
# Get live ohlcv data.
|
||||
data = self.ohlcv(pair=pair, ticker_interval=ticker_interval)
|
||||
data = self.ohlcv(pair=pair, timeframe=timeframe)
|
||||
else:
|
||||
# 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:
|
||||
logger.warning(f"No data found for ({pair}, {ticker_interval}).")
|
||||
logger.warning(f"No data found for ({pair}, {timeframe}).")
|
||||
return data
|
||||
|
||||
def market(self, pair: str) -> Optional[Dict[str, Any]]:
|
||||
|
@ -9,12 +9,11 @@ Includes:
|
||||
import logging
|
||||
import operator
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
import pytz
|
||||
from pandas import DataFrame
|
||||
|
||||
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]
|
||||
|
||||
|
||||
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
|
||||
: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':
|
||||
start = datetime.fromtimestamp(timerange.startts, tz=pytz.utc)
|
||||
df = df.loc[df['date'] >= start, :]
|
||||
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
|
||||
df = df.loc[df[df_date_col] >= start, :]
|
||||
if timerange.stoptype == 'date':
|
||||
stop = datetime.fromtimestamp(timerange.stopts, tz=pytz.utc)
|
||||
df = df.loc[df['date'] <= stop, :]
|
||||
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
|
||||
df = df.loc[df[df_date_col] <= stop, :]
|
||||
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]:
|
||||
"""
|
||||
Load a pair from file, either .json.gz or .json
|
||||
: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)
|
||||
if not pairdata:
|
||||
return []
|
||||
@ -81,11 +84,11 @@ def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: 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
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@ -122,7 +125,7 @@ def _validate_pairdata(pair, pairdata, timerange: TimeRange):
|
||||
|
||||
|
||||
def load_pair_history(pair: str,
|
||||
ticker_interval: str,
|
||||
timeframe: str,
|
||||
datadir: Path,
|
||||
timerange: Optional[TimeRange] = None,
|
||||
refresh_pairs: bool = False,
|
||||
@ -134,7 +137,7 @@ def load_pair_history(pair: str,
|
||||
"""
|
||||
Loads cached ticker history for the given pair.
|
||||
: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 timerange: Limit data to be loaded to this timerange
|
||||
:param refresh_pairs: Refresh pairs from exchange.
|
||||
@ -148,34 +151,34 @@ def load_pair_history(pair: str,
|
||||
|
||||
timerange_startup = deepcopy(timerange)
|
||||
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
|
||||
if refresh_pairs:
|
||||
download_pair_history(datadir=datadir,
|
||||
exchange=exchange,
|
||||
pair=pair,
|
||||
ticker_interval=ticker_interval,
|
||||
timeframe=timeframe,
|
||||
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 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,
|
||||
drop_incomplete=drop_incomplete)
|
||||
else:
|
||||
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'
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def load_data(datadir: Path,
|
||||
ticker_interval: str,
|
||||
timeframe: str,
|
||||
pairs: List[str],
|
||||
refresh_pairs: bool = False,
|
||||
exchange: Optional[Exchange] = None,
|
||||
@ -187,7 +190,7 @@ def load_data(datadir: Path,
|
||||
"""
|
||||
Loads ticker history data for a list of pairs
|
||||
: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 refresh_pairs: Refresh pairs from exchange.
|
||||
(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} ...')
|
||||
|
||||
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,
|
||||
refresh_pairs=refresh_pairs,
|
||||
exchange=exchange,
|
||||
@ -221,9 +224,9 @@ def load_data(datadir: Path,
|
||||
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("/", "_")
|
||||
filename = datadir.joinpath(f'{pair_s}-{ticker_interval}.json')
|
||||
filename = datadir.joinpath(f'{pair_s}-{timeframe}.json')
|
||||
return filename
|
||||
|
||||
|
||||
@ -233,7 +236,7 @@ def pair_trades_filename(datadir: Path, pair: str) -> Path:
|
||||
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],
|
||||
Optional[int]]:
|
||||
"""
|
||||
@ -251,12 +254,12 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: st
|
||||
if timerange.starttype == 'date':
|
||||
since_ms = timerange.startts * 1000
|
||||
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
|
||||
|
||||
# read the cached file
|
||||
# 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
|
||||
if data:
|
||||
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,
|
||||
exchange: Optional[Exchange],
|
||||
pair: str,
|
||||
ticker_interval: str = '5m',
|
||||
timeframe: str = '5m',
|
||||
timerange: Optional[TimeRange] = None) -> bool:
|
||||
"""
|
||||
Download the latest ticker intervals from the exchange for the pair passed in parameters
|
||||
The data is downloaded starting from the last correct ticker interval data that
|
||||
Download latest candles from the exchange for the pair and timeframe passed in parameters
|
||||
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,
|
||||
the full data will be redownloaded
|
||||
|
||||
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
||||
|
||||
:param pair: pair to download
|
||||
:param ticker_interval: ticker interval
|
||||
:param timeframe: Ticker Timeframe (e.g 5m)
|
||||
:param timerange: range of time to download
|
||||
:return: bool with success state
|
||||
"""
|
||||
@ -299,17 +302,17 @@ def download_pair_history(datadir: Path,
|
||||
|
||||
try:
|
||||
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}.'
|
||||
)
|
||||
|
||||
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 End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
||||
|
||||
# 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
|
||||
else
|
||||
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 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
|
||||
|
||||
except Exception as e:
|
||||
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}'
|
||||
)
|
||||
return False
|
||||
@ -344,17 +347,17 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
||||
pairs_not_available.append(pair)
|
||||
logger.info(f"Skipping pair {pair}...")
|
||||
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():
|
||||
logger.info(
|
||||
f'Deleting existing data for pair {pair}, interval {ticker_interval}.')
|
||||
f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
||||
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,
|
||||
pair=pair, ticker_interval=str(ticker_interval),
|
||||
pair=pair, timeframe=str(timeframe),
|
||||
timerange=timerange)
|
||||
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,
|
||||
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.
|
||||
|
||||
@ -468,10 +471,10 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime,
|
||||
:param pair: pair used for log output.
|
||||
:param min_date: start-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
|
||||
expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins)
|
||||
# total difference in minutes / timeframe-minutes
|
||||
expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_mins)
|
||||
found_missing = False
|
||||
dflen = len(data)
|
||||
if dflen < expected_frames:
|
||||
|
@ -97,7 +97,7 @@ class Edge:
|
||||
data = history.load_data(
|
||||
datadir=Path(self.config['datadir']),
|
||||
pairs=pairs,
|
||||
ticker_interval=self.strategy.ticker_interval,
|
||||
timeframe=self.strategy.ticker_interval,
|
||||
refresh_pairs=self._refresh_pairs,
|
||||
exchange=self.exchange,
|
||||
timerange=self._timerange,
|
||||
|
@ -15,3 +15,4 @@ from freqtrade.exchange.exchange import (market_is_active, # noqa: F401
|
||||
symbol_is_pair)
|
||||
from freqtrade.exchange.kraken import Kraken # noqa: F401
|
||||
from freqtrade.exchange.binance import Binance # noqa: F401
|
||||
from freqtrade.exchange.bibox import Bibox # noqa: F401
|
||||
|
22
freqtrade/exchange/bibox.py
Normal file
22
freqtrade/exchange/bibox.py
Normal 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}}
|
@ -30,6 +30,9 @@ class Exchange:
|
||||
|
||||
_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)
|
||||
_params: Dict = {}
|
||||
|
||||
@ -91,10 +94,17 @@ class Exchange:
|
||||
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
|
||||
|
||||
# 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(
|
||||
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(
|
||||
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)
|
||||
|
||||
@ -536,40 +546,40 @@ class Exchange:
|
||||
logger.info("returning cached ticker-data for %s", 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:
|
||||
"""
|
||||
Gets candle history using asyncio and returns the list of candles.
|
||||
Handles all async doing.
|
||||
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
|
||||
: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
|
||||
:returns List of tickers
|
||||
"""
|
||||
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))
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str,
|
||||
ticker_interval: str,
|
||||
timeframe: str,
|
||||
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(
|
||||
"one_call: %s msecs (%s)",
|
||||
one_call,
|
||||
arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True)
|
||||
)
|
||||
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)]
|
||||
|
||||
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||
|
||||
# Combine tickers
|
||||
data: List = []
|
||||
for p, ticker_interval, ticker in tickers:
|
||||
for p, timeframe, ticker in tickers:
|
||||
if p == pair:
|
||||
data.extend(ticker)
|
||||
# Sort data again after extending the result - above calls return in "async order"
|
||||
@ -589,14 +599,14 @@ class Exchange:
|
||||
input_coroutines = []
|
||||
|
||||
# Gather coroutines to run
|
||||
for pair, ticker_interval in set(pair_list):
|
||||
if (not ((pair, ticker_interval) in self._klines)
|
||||
or self._now_is_time_to_refresh(pair, ticker_interval)):
|
||||
input_coroutines.append(self._async_get_candle_history(pair, ticker_interval))
|
||||
for pair, timeframe in set(pair_list):
|
||||
if (not ((pair, timeframe) in self._klines)
|
||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||
input_coroutines.append(self._async_get_candle_history(pair, timeframe))
|
||||
else:
|
||||
logger.debug(
|
||||
"Using cached ohlcv data for pair %s, interval %s ...",
|
||||
pair, ticker_interval
|
||||
"Using cached ohlcv data for pair %s, timeframe %s ...",
|
||||
pair, timeframe
|
||||
)
|
||||
|
||||
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__)
|
||||
continue
|
||||
pair = res[0]
|
||||
ticker_interval = res[1]
|
||||
timeframe = res[1]
|
||||
ticks = res[2]
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
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
|
||||
self._klines[(pair, ticker_interval)] = parse_ticker_dataframe(
|
||||
ticks, ticker_interval, pair=pair, fill_missing=True,
|
||||
self._klines[(pair, timeframe)] = parse_ticker_dataframe(
|
||||
ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
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
|
||||
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)
|
||||
|
||||
@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]:
|
||||
"""
|
||||
Asynchronously gets candle histories using fetch_ohlcv
|
||||
returns tuple: (pair, ticker_interval, ohlcv_list)
|
||||
returns tuple: (pair, timeframe, ohlcv_list)
|
||||
"""
|
||||
try:
|
||||
# fetch ohlcv asynchronously
|
||||
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
|
||||
logger.debug(
|
||||
"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)
|
||||
|
||||
# Because some exchange sort Tickers ASC and other DESC.
|
||||
@ -653,9 +663,9 @@ class Exchange:
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
except IndexError:
|
||||
logger.exception("Error loading %s. Result was %s.", pair, data)
|
||||
return pair, ticker_interval, []
|
||||
logger.debug("Done fetching pair %s, interval %s ...", pair, ticker_interval)
|
||||
return pair, ticker_interval, data
|
||||
return pair, timeframe, []
|
||||
logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
|
||||
return pair, timeframe, data
|
||||
|
||||
except ccxt.NotSupported as e:
|
||||
raise OperationalException(
|
||||
@ -802,7 +812,6 @@ class Exchange:
|
||||
Handles all async doing.
|
||||
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
|
||||
:param pair: Pair to download
|
||||
:param ticker_interval: Interval to get
|
||||
:param since: Timestamp in milliseconds to get history from
|
||||
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
|
||||
:param from_id: Download data starting with ID (if id is known)
|
||||
@ -875,6 +884,22 @@ class Exchange:
|
||||
|
||||
@retrier
|
||||
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']:
|
||||
return []
|
||||
if not self.exchange_has('fetchMyTrades'):
|
||||
@ -882,7 +907,8 @@ class Exchange:
|
||||
try:
|
||||
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||
# 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]
|
||||
|
||||
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)]
|
||||
|
||||
|
||||
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
|
||||
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
|
||||
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.
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
|
@ -20,9 +20,9 @@ from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import (ExchangeResolver, PairListResolver,
|
||||
StrategyResolver)
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||
from freqtrade.state import State
|
||||
from freqtrade.strategy.interface import IStrategy, SellType
|
||||
from freqtrade.wallets import Wallets
|
||||
@ -70,8 +70,7 @@ class FreqtradeBot:
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.wallets = self.wallets
|
||||
|
||||
pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList')
|
||||
self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
|
||||
# Initializing Edge only if enabled
|
||||
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
||||
@ -139,10 +138,9 @@ class FreqtradeBot:
|
||||
if len(trades) < self.config['max_open_trades']:
|
||||
self.process_maybe_execute_buys()
|
||||
|
||||
if 'unfilledtimeout' in self.config:
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
Trade.session.flush()
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
Trade.session.flush()
|
||||
|
||||
if (self.heartbeat_interval
|
||||
and (arrow.utcnow().timestamp - self._heartbeat_msg > self.heartbeat_interval)):
|
||||
@ -756,23 +754,28 @@ class FreqtradeBot:
|
||||
return True
|
||||
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:
|
||||
"""
|
||||
Check if any orders are timed out and cancel if neccessary
|
||||
:param timeoutvalue: Number of minutes until order is considered timed out
|
||||
: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():
|
||||
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:
|
||||
continue
|
||||
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
@ -782,23 +785,20 @@ class FreqtradeBot:
|
||||
trade,
|
||||
traceback.format_exc())
|
||||
continue
|
||||
ordertime = arrow.get(order['datetime']).datetime
|
||||
|
||||
# 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()
|
||||
continue
|
||||
|
||||
if ((order['side'] == 'buy' and order['status'] == 'canceled')
|
||||
or (order['status'] == 'open'
|
||||
and order['side'] == 'buy' and ordertime < buy_timeout_threshold)):
|
||||
or (self._check_timed_out('buy', order))):
|
||||
|
||||
self.handle_timedout_limit_buy(trade, order)
|
||||
self.wallets.update()
|
||||
|
||||
elif ((order['side'] == 'sell' and order['status'] == 'canceled')
|
||||
or (order['status'] == 'open'
|
||||
and order['side'] == 'sell' and ordertime < sell_timeout_threshold)):
|
||||
or (self._check_timed_out('sell', order))):
|
||||
self.handle_timedout_limit_sell(trade, order)
|
||||
self.wallets.update()
|
||||
|
||||
@ -813,7 +813,8 @@ class FreqtradeBot:
|
||||
})
|
||||
|
||||
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
|
||||
"""
|
||||
reason = "cancelled due to timeout"
|
||||
@ -824,18 +825,22 @@ class FreqtradeBot:
|
||||
corder = order
|
||||
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
|
||||
self.handle_buy_order_full_cancel(trade, reason)
|
||||
return True
|
||||
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
# 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
|
||||
# verify if fees were taken from amount to avoid problems during selling
|
||||
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):
|
||||
trade.amount = new_amount
|
||||
# Fee was applied, so set to 0
|
||||
|
@ -15,7 +15,6 @@ from typing import Any, List
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.configuration import Arguments
|
||||
from freqtrade.worker import Worker
|
||||
|
||||
|
||||
logger = logging.getLogger('freqtrade')
|
||||
@ -28,21 +27,23 @@ def main(sysargv: List[str] = None) -> None:
|
||||
"""
|
||||
|
||||
return_code: Any = 1
|
||||
worker = None
|
||||
try:
|
||||
arguments = Arguments(sysargv)
|
||||
args = arguments.get_parsed_arg()
|
||||
|
||||
# A subcommand has been issued.
|
||||
# Means if Backtesting or Hyperopt have been called we exit the bot
|
||||
# Call subcommand.
|
||||
if 'func' in args:
|
||||
args['func'](args)
|
||||
# TODO: fetch return_code as returned by the command function here
|
||||
return_code = 0
|
||||
return_code = args['func'](args)
|
||||
else:
|
||||
# Load and run worker
|
||||
worker = Worker(args)
|
||||
worker.run()
|
||||
# No subcommand was issued.
|
||||
raise OperationalException(
|
||||
"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:
|
||||
return_code = e
|
||||
@ -55,8 +56,6 @@ def main(sysargv: List[str] = None) -> None:
|
||||
except Exception:
|
||||
logger.exception('Fatal exception!')
|
||||
finally:
|
||||
if worker:
|
||||
worker.exit()
|
||||
sys.exit(return_code)
|
||||
|
||||
|
||||
|
@ -78,7 +78,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None:
|
||||
except Timeout:
|
||||
logger.info("Another running instance of freqtrade Hyperopt detected.")
|
||||
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.")
|
||||
logger.info("Quitting now.")
|
||||
# TODO: return False here in order to help freqtrade to exit
|
||||
|
@ -83,8 +83,8 @@ class Backtesting:
|
||||
if "ticker_interval" not in self.config:
|
||||
raise OperationalException("Ticker-interval needs to be set in either configuration "
|
||||
"or as cli argument `--ticker-interval 5m`")
|
||||
self.ticker_interval = str(self.config.get('ticker_interval'))
|
||||
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
|
||||
self.timeframe = str(self.config.get('ticker_interval'))
|
||||
self.timeframe_mins = timeframe_to_minutes(self.timeframe)
|
||||
|
||||
# Get maximum required startup period
|
||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||
@ -108,7 +108,7 @@ class Backtesting:
|
||||
data = history.load_data(
|
||||
datadir=Path(self.config['datadir']),
|
||||
pairs=self.config['exchange']['pair_whitelist'],
|
||||
ticker_interval=self.ticker_interval,
|
||||
timeframe=self.timeframe,
|
||||
timerange=timerange,
|
||||
startup_candles=self.required_startup,
|
||||
fail_without_data=True,
|
||||
@ -121,7 +121,7 @@ class Backtesting:
|
||||
min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days
|
||||
)
|
||||
# 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)
|
||||
|
||||
return data, timerange
|
||||
@ -375,7 +375,7 @@ class Backtesting:
|
||||
lock_pair_until: Dict = {}
|
||||
# Indexes per pair, so some pairs are allowed to have a missing start.
|
||||
indexes: Dict = {}
|
||||
tmp = start_date + timedelta(minutes=self.ticker_interval_mins)
|
||||
tmp = start_date + timedelta(minutes=self.timeframe_mins)
|
||||
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
while tmp < end_date:
|
||||
@ -427,7 +427,7 @@ class Backtesting:
|
||||
lock_pair_until[pair] = end_date.datetime
|
||||
|
||||
# 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)
|
||||
|
||||
def start(self) -> None:
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""
|
||||
IHyperOpt interface
|
||||
This module defines the interface to apply for hyperopts
|
||||
This module defines the interface to apply for hyperopt
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
@ -27,8 +27,8 @@ def _format_exception_message(method: str, space: str) -> str:
|
||||
|
||||
class IHyperOpt(ABC):
|
||||
"""
|
||||
Interface for freqtrade hyperopts
|
||||
Defines the mandatory structure must follow any custom hyperopts
|
||||
Interface for freqtrade hyperopt
|
||||
Defines the mandatory structure must follow any custom hyperopt
|
||||
|
||||
Class attributes you can use:
|
||||
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_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
|
||||
# ticker_interval used by the bot:
|
||||
# timeframe used by the bot:
|
||||
#
|
||||
# * 'roi_t' (limits for the time intervals in the ROI tables) components
|
||||
# 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()
|
||||
# method for the 5m ticker interval.
|
||||
roi_t_scale = ticker_interval_mins / 5
|
||||
roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5)
|
||||
roi_t_scale = timeframe_mins / 5
|
||||
roi_p_scale = math.log1p(timeframe_mins) / math.log1p(5)
|
||||
roi_limits = {
|
||||
'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha),
|
||||
'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha),
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""
|
||||
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
|
||||
@ -11,7 +11,7 @@ from pandas import DataFrame
|
||||
|
||||
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.)
|
||||
"""
|
||||
ticker_interval: str
|
||||
|
@ -5,22 +5,31 @@ Provides lists as configured in config.json
|
||||
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
|
||||
from freqtrade.exchange import market_is_active
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IPairList(ABC):
|
||||
|
||||
def __init__(self, freqtrade, config: dict) -> None:
|
||||
self._freqtrade = freqtrade
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
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._whitelist = self._config['exchange']['pair_whitelist']
|
||||
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
||||
self._pairlistconfig = pairlistconfig
|
||||
self._pairlist_pos = pairlist_pos
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@ -30,21 +39,13 @@ class IPairList(ABC):
|
||||
"""
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def whitelist(self) -> List[str]:
|
||||
@abstractproperty
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Has the current whitelist
|
||||
-> no need to overwrite in subclasses
|
||||
Boolean property defining if tickers are necessary.
|
||||
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
|
||||
def short_desc(self) -> str:
|
||||
@ -54,36 +55,62 @@ class IPairList(ABC):
|
||||
"""
|
||||
|
||||
@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
|
||||
: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
|
||||
: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
|
||||
black_listed
|
||||
"""
|
||||
markets = self._freqtrade.exchange.markets
|
||||
markets = self._exchange.markets
|
||||
|
||||
sanitized_whitelist = set()
|
||||
for pair in whitelist:
|
||||
# pair is not in the generated dynamic market, or in the blacklist ... ignore it
|
||||
if (pair in self.blacklist or pair not in markets
|
||||
or not pair.endswith(self._config['stake_currency'])):
|
||||
sanitized_whitelist: List[str] = []
|
||||
for pair in pairlist:
|
||||
# pair is not in the generated dynamic market or has the wrong stake currency
|
||||
if pair not in markets:
|
||||
logger.warning(f"Pair {pair} is not compatible with exchange "
|
||||
f"{self._freqtrade.exchange.name} or contained in "
|
||||
f"your blacklist. Removing it from whitelist..")
|
||||
f"{self._exchange.name}. Removing it from whitelist..")
|
||||
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
|
||||
market = markets[pair]
|
||||
if not market_is_active(market):
|
||||
logger.info(f"Ignoring {pair} from whitelist. Market is not active.")
|
||||
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
|
||||
return list(sanitized_whitelist)
|
||||
return sanitized_whitelist
|
||||
|
62
freqtrade/pairlist/PrecisionFilter.py
Normal file
62
freqtrade/pairlist/PrecisionFilter.py
Normal 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
|
69
freqtrade/pairlist/PriceFilter.py
Normal file
69
freqtrade/pairlist/PriceFilter.py
Normal 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
|
@ -5,6 +5,7 @@ Provides lists as configured in config.json
|
||||
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
@ -13,18 +14,28 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class StaticPairList(IPairList):
|
||||
|
||||
def __init__(self, freqtrade, config: dict) -> None:
|
||||
super().__init__(freqtrade, config)
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return False
|
||||
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
Short whitelist method description - used for startup-messages
|
||||
-> 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'])
|
||||
|
@ -5,11 +5,12 @@ Provides lists as configured in config.json
|
||||
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
from cachetools import TTLCache, cached
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
|
||||
@ -17,18 +18,19 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
|
||||
|
||||
class VolumePairList(IPairList):
|
||||
|
||||
def __init__(self, freqtrade, config: dict) -> None:
|
||||
super().__init__(freqtrade, config)
|
||||
self._whitelistconf = self._config.get('pairlist', {}).get('config')
|
||||
if 'number_assets' not in self._whitelistconf:
|
||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
if 'number_assets' not in self._pairlistconfig:
|
||||
raise OperationalException(
|
||||
f'`number_assets` not specified. Please check your configuration '
|
||||
'for "pairlist.config.number_assets"')
|
||||
self._number_pairs = self._whitelistconf['number_assets']
|
||||
self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume')
|
||||
self._precision_filter = self._whitelistconf.get('precision_filter', False)
|
||||
self._number_pairs = self._pairlistconfig['number_assets']
|
||||
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
|
||||
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(
|
||||
'Exchange does not support dynamic whitelist.'
|
||||
'Please edit your config and restart the bot'
|
||||
@ -36,6 +38,16 @@ class VolumePairList(IPairList):
|
||||
if not self._validate_keys(self._sort_key):
|
||||
raise OperationalException(
|
||||
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):
|
||||
return key in SORT_VALUES
|
||||
@ -43,54 +55,54 @@ class VolumePairList(IPairList):
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
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
|
||||
-> Please overwrite in subclasses
|
||||
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
|
||||
"""
|
||||
# Generate dynamic whitelist
|
||||
self._whitelist = self._gen_pair_whitelist(
|
||||
self._config['stake_currency'], self._sort_key)
|
||||
if self._last_refresh + self.refresh_period < datetime.now().timestamp():
|
||||
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, base_currency: str, key: str) -> List[str]:
|
||||
def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str, key: str) -> List[str]:
|
||||
"""
|
||||
Updates the whitelist with with a dynamically generated list
|
||||
:param base_currency: base currency as str
|
||||
:param key: sort key (defaults to 'quoteVolume')
|
||||
:param tickers: Tickers (from exchange.get_tickers()).
|
||||
:return: List of pairs
|
||||
"""
|
||||
|
||||
tickers = self._freqtrade.exchange.get_tickers()
|
||||
# check length so that we make sure that '/' is actually in the string
|
||||
tickers = [v for k, v in tickers.items()
|
||||
if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency
|
||||
and v[key] is not None)]
|
||||
sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key])
|
||||
if self._pairlist_pos == 0:
|
||||
# If VolumePairList is the first in the list, use fresh pairlist
|
||||
# check length so that we make sure that '/' is actually in the string
|
||||
filtered_tickers = [v for k, v in tickers.items()
|
||||
if (len(k.split('/')) == 2 and k.split('/')[1] == base_currency
|
||||
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
|
||||
valid_pairs = self._validate_whitelist([s['symbol'] for s in sorted_tickers])
|
||||
valid_tickers = [t for t in sorted_tickers if t["symbol"] in valid_pairs]
|
||||
|
||||
if self._freqtrade.strategy.stoploss is not None and self._precision_filter:
|
||||
|
||||
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]}")
|
||||
pairs = self._whitelist_for_active_markets([s['symbol'] for s in sorted_tickers])
|
||||
pairs = self._verify_blacklist(pairs)
|
||||
# Limit to X number of pairs
|
||||
pairs = pairs[:self._number_pairs]
|
||||
logger.info(f"Searching {self._number_pairs} pairs: {pairs}")
|
||||
|
||||
return pairs
|
||||
|
95
freqtrade/pairlist/pairlistmanager.py
Normal file
95
freqtrade/pairlist/pairlistmanager.py
Normal 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
|
@ -39,7 +39,7 @@ def init_plotscript(config):
|
||||
tickers = history.load_data(
|
||||
datadir=Path(str(config.get("datadir"))),
|
||||
pairs=pairs,
|
||||
ticker_interval=config.get('ticker_interval', '5m'),
|
||||
timeframe=config.get('ticker_interval', '5m'),
|
||||
timerange=timerange,
|
||||
)
|
||||
|
||||
@ -47,7 +47,7 @@ def init_plotscript(config):
|
||||
db_url=config.get('db_url'),
|
||||
exportfilename=config.get('exportfilename'),
|
||||
)
|
||||
|
||||
trades = history.trim_dataframe(trades, timerange, 'open_time')
|
||||
return {"tickers": tickers,
|
||||
"trades": trades,
|
||||
"pairs": pairs,
|
||||
@ -300,12 +300,12 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
|
||||
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("/", "_")
|
||||
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)
|
||||
|
||||
@ -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
|
||||
:param fig: Plotly Figure to plot
|
||||
:param pair: Pair to plot (used as filename and Plot title)
|
||||
:param ticker_interval: Used as part of the filename
|
||||
:param filename: Name to store the file as
|
||||
:param directory: Directory to store the file in
|
||||
:param auto_open: Automatically open files saved
|
||||
:return: None
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
plot_elements = init_plotscript(config)
|
||||
trades = load_trades(config['trade_source'],
|
||||
db_url=str(config.get('db_url')),
|
||||
exportfilename=str(config.get('exportfilename')),
|
||||
)
|
||||
trades = plot_elements['trades']
|
||||
# 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.
|
||||
# this could be useful to gauge the overall market trend
|
||||
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"],
|
||||
|
@ -1,14 +1,14 @@
|
||||
# pragma pylint: disable=attribute-defined-outside-init
|
||||
|
||||
"""
|
||||
This module load custom hyperopts
|
||||
This module load custom hyperopt
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
|
||||
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_loss_interface import IHyperOptLoss
|
||||
from freqtrade.resolvers import IResolver
|
||||
@ -20,7 +20,6 @@ class HyperOptResolver(IResolver):
|
||||
"""
|
||||
This class contains all the logic to load custom hyperopt class
|
||||
"""
|
||||
|
||||
__slots__ = ['hyperopt']
|
||||
|
||||
def __init__(self, config: Dict) -> None:
|
||||
@ -28,9 +27,12 @@ class HyperOptResolver(IResolver):
|
||||
Load the custom class from config parameter
|
||||
: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,
|
||||
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
|
||||
"""
|
||||
|
||||
__slots__ = ['hyperoptloss']
|
||||
|
||||
def __init__(self, config: Dict = None) -> None:
|
||||
def __init__(self, config: Dict) -> None:
|
||||
"""
|
||||
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
|
||||
hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
|
||||
# Verify the hyperopt_loss is in the configuration, otherwise fallback to the
|
||||
# default hyperopt loss
|
||||
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
|
||||
|
||||
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
|
||||
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
|
||||
|
||||
if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'):
|
||||
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(
|
||||
self, hyper_loss_name: str, config: Dict,
|
||||
|
@ -17,13 +17,13 @@ class IResolver:
|
||||
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]:
|
||||
|
||||
abs_paths = [
|
||||
config['user_data_dir'].joinpath(user_subdir),
|
||||
current_path,
|
||||
]
|
||||
abs_paths: List[Path] = [current_path]
|
||||
|
||||
if user_subdir:
|
||||
abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir))
|
||||
|
||||
if extra_dir:
|
||||
# Add extra directory to the top of the search paths
|
||||
|
@ -20,13 +20,18 @@ class PairListResolver(IResolver):
|
||||
|
||||
__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
|
||||
:param config: configuration dictionary or None
|
||||
"""
|
||||
self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'freqtrade': freqtrade,
|
||||
'config': config})
|
||||
self.pairlist = self._load_pairlist(pairlist_name, config,
|
||||
kwargs={'exchange': exchange,
|
||||
'pairlistmanager': pairlistmanager,
|
||||
'config': config,
|
||||
'pairlistconfig': pairlistconfig,
|
||||
'pairlist_pos': pairlist_pos})
|
||||
|
||||
def _load_pairlist(
|
||||
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()
|
||||
|
||||
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,
|
||||
object_name=pairlist_name, kwargs=kwargs)
|
||||
|
@ -32,8 +32,11 @@ class StrategyResolver(IResolver):
|
||||
"""
|
||||
config = config or {}
|
||||
|
||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
||||
if not config.get('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,
|
||||
config=config,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
|
@ -169,6 +169,10 @@ class ApiServer(RPC):
|
||||
view_func=self._status, methods=['GET'])
|
||||
self.app.add_url_rule(f'{BASE_URI}/version', 'version',
|
||||
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
|
||||
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()
|
||||
return self.rest_dump(msg)
|
||||
|
||||
@rpc_catch_errors
|
||||
def _ping(self):
|
||||
"""
|
||||
simple poing version
|
||||
"""
|
||||
return self.rest_dump({"status": "pong"})
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
def _version(self):
|
||||
@ -232,6 +243,14 @@ class ApiServer(RPC):
|
||||
"""
|
||||
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
|
||||
@rpc_catch_errors
|
||||
def _reload_conf(self):
|
||||
@ -265,7 +284,7 @@ class ApiServer(RPC):
|
||||
|
||||
stats = self._rpc_daily_profit(timescale,
|
||||
self._config['stake_currency'],
|
||||
self._config['fiat_display_currency']
|
||||
self._config.get('fiat_display_currency', '')
|
||||
)
|
||||
|
||||
return self.rest_dump(stats)
|
||||
@ -321,8 +340,11 @@ class ApiServer(RPC):
|
||||
|
||||
Returns the current status of the trades in json format
|
||||
"""
|
||||
results = self._rpc_trade_status()
|
||||
return self.rest_dump(results)
|
||||
try:
|
||||
results = self._rpc_trade_status()
|
||||
return self.rest_dump(results)
|
||||
except RPCException:
|
||||
return self.rest_dump([])
|
||||
|
||||
@require_login
|
||||
@rpc_catch_errors
|
||||
|
@ -3,16 +3,15 @@ This module contains class to define a RPC communications
|
||||
"""
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from datetime import timedelta, datetime, date
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
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
|
||||
from numpy import mean, NAN
|
||||
from pandas import DataFrame
|
||||
from numpy import NAN, mean
|
||||
|
||||
from freqtrade import TemporaryError, DependencyException
|
||||
from freqtrade import DependencyException, TemporaryError
|
||||
from freqtrade.misc import shorten_date
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
@ -81,6 +80,29 @@ class RPC:
|
||||
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||
""" 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]]:
|
||||
"""
|
||||
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)
|
||||
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()
|
||||
if not trades:
|
||||
raise RPCException('no active order')
|
||||
@ -130,17 +152,28 @@ class RPC:
|
||||
except DependencyException:
|
||||
current_rate = NAN
|
||||
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([
|
||||
trade.id,
|
||||
trade.pair,
|
||||
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']
|
||||
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
||||
df_statuses = df_statuses.set_index(columns[0])
|
||||
return df_statuses
|
||||
columns = ['ID', 'Pair', 'Since', profitcol]
|
||||
return trades_list, columns
|
||||
|
||||
def _rpc_daily_profit(
|
||||
self, timescale: int,
|
||||
@ -219,7 +252,7 @@ class RPC:
|
||||
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
||||
|
||||
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)
|
||||
|
||||
@ -452,7 +485,7 @@ class RPC:
|
||||
|
||||
def _rpc_whitelist(self) -> Dict:
|
||||
""" 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),
|
||||
'whitelist': self._freqtrade.active_pair_whitelist
|
||||
}
|
||||
@ -467,7 +500,7 @@ class RPC:
|
||||
and pair not in self._freqtrade.pairlists.blacklist):
|
||||
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),
|
||||
'blacklist': self._freqtrade.pairlists.blacklist,
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ class Telegram(RPC):
|
||||
CommandHandler('daily', self._daily),
|
||||
CommandHandler('count', self._count),
|
||||
CommandHandler('reload_conf', self._reload_conf),
|
||||
CommandHandler('show_config', self._show_config),
|
||||
CommandHandler('stopbuy', self._stopbuy),
|
||||
CommandHandler('whitelist', self._whitelist),
|
||||
CommandHandler('blacklist', self._blacklist),
|
||||
@ -234,8 +235,9 @@ class Telegram(RPC):
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
df_statuses = self._rpc_status_table()
|
||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||
statlist, head = self._rpc_status_table(self._config['stake_currency'],
|
||||
self._config.get('fiat_display_currency', ''))
|
||||
message = tabulate(statlist, headers=head, tablefmt='simple')
|
||||
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
@ -549,6 +551,7 @@ class Telegram(RPC):
|
||||
"*/balance:* `Show account balance per currency`\n" \
|
||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
|
||||
"*/reload_conf:* `Reload configuration file` \n" \
|
||||
"*/show_config:* `Show running configuration` \n" \
|
||||
"*/whitelist:* `Show current whitelist` \n" \
|
||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
|
||||
"to the blacklist.` \n" \
|
||||
@ -569,6 +572,26 @@ class Telegram(RPC):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Send given markdown message
|
||||
|
@ -109,8 +109,8 @@ class IStrategy(ABC):
|
||||
# Class level variables (intentional) containing
|
||||
# the dataprovider (dp) (access to other candles, historic data, ...)
|
||||
# and wallets - access to the current balance.
|
||||
dp: DataProvider
|
||||
wallets: Wallets
|
||||
dp: Optional[DataProvider] = None
|
||||
wallets: Optional[Wallets] = None
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
self.config = config
|
||||
|
@ -39,6 +39,25 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
|
||||
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:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
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()
|
||||
:return: None
|
||||
"""
|
||||
|
@ -16,6 +16,7 @@ nav:
|
||||
- Hyperopt: hyperopt.md
|
||||
- Edge Positioning: edge.md
|
||||
- Utility Subcommands: utils.md
|
||||
- Exchange-specific Notes: exchanges.md
|
||||
- FAQ: faq.md
|
||||
- Data Analysis:
|
||||
- Jupyter Notebooks: data-analysis.md
|
||||
|
@ -1,23 +1,23 @@
|
||||
# requirements without requirements installable via conda
|
||||
# mainly used for Raspberry pi installs
|
||||
ccxt==1.19.14
|
||||
SQLAlchemy==1.3.10
|
||||
ccxt==1.19.54
|
||||
SQLAlchemy==1.3.11
|
||||
python-telegram-bot==12.2.0
|
||||
arrow==0.15.4
|
||||
cachetools==3.1.1
|
||||
requests==2.22.0
|
||||
urllib3==1.25.6
|
||||
urllib3==1.25.7
|
||||
wrapt==1.11.2
|
||||
jsonschema==3.1.1
|
||||
TA-Lib==0.4.17
|
||||
tabulate==0.8.5
|
||||
tabulate==0.8.6
|
||||
coinmarketcap==5.0.3
|
||||
|
||||
# find first, C search in arrays
|
||||
py_find_1st==1.1.4
|
||||
|
||||
# Load ticker files 30% faster
|
||||
python-rapidjson==0.8.0
|
||||
python-rapidjson==0.9.1
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
@ -6,9 +6,9 @@
|
||||
coveralls==1.8.2
|
||||
flake8==3.7.9
|
||||
flake8-type-annotations==0.1.0
|
||||
flake8-tidy-imports==3.0.0
|
||||
flake8-tidy-imports==3.1.0
|
||||
mypy==0.740
|
||||
pytest==5.2.2
|
||||
pytest==5.2.4
|
||||
pytest-asyncio==0.10.0
|
||||
pytest-cov==2.8.1
|
||||
pytest-mock==1.11.2
|
||||
|
@ -2,7 +2,7 @@
|
||||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.3.1
|
||||
scipy==1.3.2
|
||||
scikit-learn==0.21.3
|
||||
scikit-optimize==0.5.2
|
||||
filelock==3.0.12
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==4.2.1
|
||||
plotly==4.3.0
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Load common requirements
|
||||
-r requirements-common.txt
|
||||
|
||||
numpy==1.17.3
|
||||
numpy==1.17.4
|
||||
pandas==0.25.3
|
||||
|
@ -8,12 +8,15 @@ so it can be used as a standalone script.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import inspect
|
||||
from urllib.parse import urlencode, urlparse, urlunparse
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlencode, urlparse, urlunparse
|
||||
|
||||
import rapidjson
|
||||
import requests
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
@ -63,100 +66,106 @@ class FtRestClient():
|
||||
return self._call("POST", apipath, params=params, data=data)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the bot if it's in stopped state.
|
||||
"""Start the bot if it's in the stopped state.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._post("start")
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the bot. Use start to restart
|
||||
"""Stop the bot. Use `start` to restart.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._post("stop")
|
||||
|
||||
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 self._post("stopbuy")
|
||||
|
||||
def reload_conf(self):
|
||||
"""
|
||||
Reload configuration
|
||||
"""Reload configuration.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._post("reload_conf")
|
||||
|
||||
def balance(self):
|
||||
"""
|
||||
Get the account balance
|
||||
"""Get the account balance.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("balance")
|
||||
|
||||
def count(self):
|
||||
"""
|
||||
Returns the amount of open trades
|
||||
"""Return the amount of open trades.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("count")
|
||||
|
||||
def daily(self, days=None):
|
||||
"""
|
||||
Returns the amount of open trades
|
||||
"""Return the amount of open trades.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("daily", params={"timescale": days} if days else None)
|
||||
|
||||
def edge(self):
|
||||
"""
|
||||
Returns information about edge
|
||||
"""Return information about edge.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("edge")
|
||||
|
||||
def profit(self):
|
||||
"""
|
||||
Returns the profit summary
|
||||
"""Return the profit summary.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("profit")
|
||||
|
||||
def performance(self):
|
||||
"""
|
||||
Returns the performance of the different coins
|
||||
"""Return the performance of the different coins.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("performance")
|
||||
|
||||
def status(self):
|
||||
"""
|
||||
Get the status of open trades
|
||||
"""Get the status of open trades.
|
||||
|
||||
:return: json object
|
||||
"""
|
||||
return self._get("status")
|
||||
|
||||
def version(self):
|
||||
"""
|
||||
Returns the version of the bot
|
||||
"""Return the version of the bot.
|
||||
|
||||
:return: json object containing the 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 self._get("whitelist")
|
||||
|
||||
def blacklist(self, *args):
|
||||
"""
|
||||
Show the current blacklist
|
||||
"""Show the current blacklist.
|
||||
|
||||
:param add: List of coins to add (example: "BNB/BTC")
|
||||
:return: json object
|
||||
"""
|
||||
@ -166,8 +175,8 @@ class FtRestClient():
|
||||
return self._post("blacklist", data={"blacklist": args})
|
||||
|
||||
def forcebuy(self, pair, price=None):
|
||||
"""
|
||||
Buy an asset
|
||||
"""Buy an asset.
|
||||
|
||||
:param pair: Pair to buy (ETH/BTC)
|
||||
:param price: Optional - price to buy
|
||||
:return: json object of the trade
|
||||
@ -178,8 +187,8 @@ class FtRestClient():
|
||||
return self._post("forcebuy", data=data)
|
||||
|
||||
def forcesell(self, tradeid):
|
||||
"""
|
||||
Force-sell a trade
|
||||
"""Force-sell a trade.
|
||||
|
||||
:param tradeid: Id of the trade (can be received via status command)
|
||||
:return: json object
|
||||
"""
|
||||
@ -190,7 +199,9 @@ class FtRestClient():
|
||||
def add_arguments():
|
||||
parser = argparse.ArgumentParser()
|
||||
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',
|
||||
help='Show possible methods with this client',
|
||||
@ -221,24 +232,29 @@ def load_config(configfile):
|
||||
file = Path(configfile)
|
||||
if file.is_file():
|
||||
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 {}
|
||||
else:
|
||||
logger.warning(f"Could not load config file {file}.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def print_commands():
|
||||
# Print dynamic help for the different commands using the commands doc-strings
|
||||
client = FtRestClient(None)
|
||||
print("Possible commands:")
|
||||
print("Possible commands:\n")
|
||||
for x, y in inspect.getmembers(client):
|
||||
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):
|
||||
|
||||
if args.get("help"):
|
||||
if args.get("show"):
|
||||
print_commands()
|
||||
sys.exit()
|
||||
|
||||
config = load_config(args["config"])
|
||||
url = config.get("api_server", {}).get("server_url", "127.0.0.1")
|
||||
|
@ -242,6 +242,9 @@ def default_conf(testdatadir):
|
||||
"HOT/BTC",
|
||||
]
|
||||
},
|
||||
"pairlists": [
|
||||
{"method": "StaticPairList"}
|
||||
],
|
||||
"telegram": {
|
||||
"enabled": True,
|
||||
"token": "token",
|
||||
@ -252,6 +255,7 @@ def default_conf(testdatadir):
|
||||
"db_url": "sqlite://",
|
||||
"user_data_dir": Path("user_data"),
|
||||
"verbosity": 3,
|
||||
"strategy": "DefaultStrategy"
|
||||
}
|
||||
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
|
||||
def markets_empty():
|
||||
return MagicMock(return_value=[])
|
||||
@ -866,6 +936,50 @@ def tickers():
|
||||
'quoteVolume': 1215.14489611,
|
||||
'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': {
|
||||
'symbol': 'ETH/USDT',
|
||||
'timestamp': 1522014804118,
|
||||
|
@ -56,7 +56,7 @@ def test_extract_trades_of_period(testdatadir):
|
||||
# 2018-11-14 06:07:00
|
||||
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)
|
||||
|
||||
trades = DataFrame(
|
||||
@ -122,7 +122,7 @@ def test_combine_tickers_with_mean(testdatadir):
|
||||
pairs = ["ETH/BTC", "ADA/BTC"]
|
||||
tickers = load_data(datadir=testdatadir,
|
||||
pairs=pairs,
|
||||
ticker_interval='5m'
|
||||
timeframe='5m'
|
||||
)
|
||||
df = combine_tickers_with_mean(tickers)
|
||||
assert isinstance(df, DataFrame)
|
||||
@ -136,7 +136,7 @@ def test_create_cum_profit(testdatadir):
|
||||
bt_data = load_backtest_data(filename)
|
||||
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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
cum_profits = create_cum_profit(df.set_index('date'),
|
||||
|
@ -23,7 +23,7 @@ def test_parse_ticker_dataframe(ticker_history_list, caplog):
|
||||
|
||||
def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
|
||||
data = load_pair_history(datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
timeframe='1m',
|
||||
pair='UNITTEST/BTC',
|
||||
fill_up_missing=False)
|
||||
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):
|
||||
ticker_interval = '5m'
|
||||
timeframe = '5m'
|
||||
ticks = [[
|
||||
1511686200000, # 8:50:00
|
||||
8.794e-05, # open
|
||||
@ -78,10 +78,10 @@ def test_ohlcv_fill_up_missing_data2(caplog):
|
||||
]
|
||||
|
||||
# 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
|
||||
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
|
||||
# 3rd candle has been filled
|
||||
row = data2.loc[2, :]
|
||||
@ -99,7 +99,7 @@ def test_ohlcv_fill_up_missing_data2(caplog):
|
||||
|
||||
|
||||
def test_ohlcv_drop_incomplete(caplog):
|
||||
ticker_interval = '1d'
|
||||
timeframe = '1d'
|
||||
ticks = [[
|
||||
1559750400000, # 2019-06-04
|
||||
8.794e-05, # open
|
||||
@ -134,13 +134,13 @@ def test_ohlcv_drop_incomplete(caplog):
|
||||
]
|
||||
]
|
||||
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)
|
||||
assert len(data) == 4
|
||||
assert not log_has("Dropping last candle", caplog)
|
||||
|
||||
# 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)
|
||||
assert len(data) == 3
|
||||
|
||||
|
@ -9,32 +9,32 @@ from tests.conftest import get_patched_exchange
|
||||
|
||||
def test_ohlcv(mocker, default_conf, ticker_history):
|
||||
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._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
||||
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
||||
exchange._klines[("XRP/BTC", timeframe)] = ticker_history
|
||||
exchange._klines[("UNITTEST/BTC", timeframe)] = ticker_history
|
||||
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.DRY_RUN
|
||||
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval))
|
||||
assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||
assert dp.ohlcv("UNITTEST/BTC", ticker_interval) is not ticker_history
|
||||
assert dp.ohlcv("UNITTEST/BTC", ticker_interval, copy=False) is ticker_history
|
||||
assert not dp.ohlcv("UNITTEST/BTC", ticker_interval).empty
|
||||
assert dp.ohlcv("NONESENSE/AAA", ticker_interval).empty
|
||||
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe))
|
||||
assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame)
|
||||
assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ticker_history
|
||||
assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ticker_history
|
||||
assert not dp.ohlcv("UNITTEST/BTC", timeframe).empty
|
||||
assert dp.ohlcv("NONESENSE/AAA", timeframe).empty
|
||||
|
||||
# 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
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
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
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
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):
|
||||
@ -45,7 +45,7 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history):
|
||||
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
||||
assert isinstance(data, DataFrame)
|
||||
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):
|
||||
|
@ -64,20 +64,20 @@ def _clean_test_file(file: Path) -> 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 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
|
||||
)
|
||||
|
||||
|
||||
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 ld is None
|
||||
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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
file = testdatadir / 'UNITTEST_BTC-1m.json'
|
||||
_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 not log_has(
|
||||
'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',
|
||||
MagicMock(return_value=None))
|
||||
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,
|
||||
startup_candles=20,
|
||||
)
|
||||
@ -122,28 +122,28 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog,
|
||||
_backup_file(file)
|
||||
# do not download a new pair if refresh_pairs isn't set
|
||||
history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
timeframe='1m',
|
||||
pair='MEME/BTC')
|
||||
assert not file.is_file()
|
||||
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
|
||||
)
|
||||
|
||||
# download a new pair if refresh_pairs is set
|
||||
history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
timeframe='1m',
|
||||
refresh_pairs=True,
|
||||
exchange=exchange,
|
||||
pair='MEME/BTC')
|
||||
assert file.is_file()
|
||||
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
|
||||
)
|
||||
with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'):
|
||||
history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
timeframe='1m',
|
||||
refresh_pairs=True,
|
||||
exchange=None,
|
||||
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,
|
||||
pair='MEME/BTC',
|
||||
ticker_interval='1m')
|
||||
timeframe='1m')
|
||||
assert download_pair_history(datadir=testdatadir, exchange=exchange,
|
||||
pair='CFI/BTC',
|
||||
ticker_interval='1m')
|
||||
timeframe='1m')
|
||||
assert not exchange._pairs_last_refresh_time
|
||||
assert file1_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,
|
||||
pair='MEME/BTC',
|
||||
ticker_interval='5m')
|
||||
timeframe='5m')
|
||||
assert download_pair_history(datadir=testdatadir, exchange=exchange,
|
||||
pair='CFI/BTC',
|
||||
ticker_interval='5m')
|
||||
timeframe='5m')
|
||||
assert not exchange._pairs_last_refresh_time
|
||||
assert file1_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)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
|
||||
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", ticker_interval='3m')
|
||||
download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m')
|
||||
download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m')
|
||||
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,
|
||||
pair='MEME/BTC',
|
||||
ticker_interval='1m')
|
||||
timeframe='1m')
|
||||
# clean files freshly downloaded
|
||||
_clean_test_file(file1_1)
|
||||
_clean_test_file(file1_5)
|
||||
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
|
||||
)
|
||||
|
||||
@ -369,7 +369,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
|
||||
caplog.clear()
|
||||
start = arrow.get('2018-01-10T00: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'],
|
||||
timerange=TimeRange('date', 'date',
|
||||
start.timestamp, end.timestamp))
|
||||
@ -390,7 +390,7 @@ def test_init(default_conf, mocker) -> None:
|
||||
exchange=exchange,
|
||||
pairs=[],
|
||||
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:
|
||||
data = history.load_data(
|
||||
datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
timeframe='1m',
|
||||
pairs=['UNITTEST/BTC']
|
||||
)['UNITTEST/BTC']
|
||||
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(
|
||||
history.load_data(
|
||||
datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
timeframe='1m',
|
||||
pairs=['UNITTEST/BTC']
|
||||
)
|
||||
)
|
||||
@ -533,7 +533,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
|
||||
data = strategy.tickerdata_to_dataframe(
|
||||
history.load_data(
|
||||
datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
timeframe='1m',
|
||||
pairs=['UNITTEST/BTC'],
|
||||
fill_up_missing=False
|
||||
)
|
||||
@ -556,7 +556,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
|
||||
data = strategy.tickerdata_to_dataframe(
|
||||
history.load_data(
|
||||
datadir=testdatadir,
|
||||
ticker_interval='5m',
|
||||
timeframe='5m',
|
||||
pairs=['UNITTEST/BTC'],
|
||||
timerange=timerange
|
||||
)
|
||||
@ -669,10 +669,10 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
|
||||
file5 = testdatadir / 'XRP_ETH-5m.json'
|
||||
# Compare downloaded dataset with converted dataset
|
||||
dfbak_1m = history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval="1m",
|
||||
timeframe="1m",
|
||||
pair=pair)
|
||||
dfbak_5m = history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval="5m",
|
||||
timeframe="5m",
|
||||
pair=pair)
|
||||
|
||||
_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)
|
||||
# Load new data
|
||||
df_1m = history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval="1m",
|
||||
timeframe="1m",
|
||||
pair=pair)
|
||||
df_5m = history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval="5m",
|
||||
timeframe="5m",
|
||||
pair=pair)
|
||||
|
||||
assert df_1m.equals(dfbak_1m)
|
||||
|
@ -255,7 +255,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf):
|
||||
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):
|
||||
hz = 0.1
|
||||
base = 0.001
|
||||
|
@ -1047,8 +1047,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
|
||||
]
|
||||
pair = 'ETH/BTC'
|
||||
|
||||
async def mock_candle_hist(pair, ticker_interval, since_ms):
|
||||
return pair, ticker_interval, tick
|
||||
async def mock_candle_hist(pair, timeframe, since_ms):
|
||||
return pair, timeframe, tick
|
||||
|
||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||
# 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')])
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@ -1143,7 +1143,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
|
||||
# exchange = Exchange(default_conf)
|
||||
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||
"_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()
|
||||
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)
|
||||
def test_get_trades_for_order(default_conf, mocker, exchange_name):
|
||||
|
||||
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
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||
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'
|
||||
# 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] == 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,
|
||||
'get_trades_for_order', 'fetch_my_trades',
|
||||
|
@ -7,7 +7,7 @@ from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.strategy.interface import SellType
|
||||
|
||||
ticker_start_time = arrow.get(2018, 10, 3)
|
||||
tests_ticker_interval = '1h'
|
||||
tests_timeframe = '1h'
|
||||
|
||||
|
||||
class BTrade(NamedTuple):
|
||||
@ -36,7 +36,7 @@ class BTContainer(NamedTuple):
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -9,7 +9,7 @@ from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from tests.conftest import patch_exchange
|
||||
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 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["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_only_offset_is_reached"] = data.trailing_only_offset_is_reached
|
||||
# Only add this to configuration If it's necessary
|
||||
|
@ -50,7 +50,7 @@ def trim_dictlist(dict_list, num):
|
||||
|
||||
def load_data_test(what, testdatadir):
|
||||
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)
|
||||
datalen = len(pair)
|
||||
|
||||
@ -116,7 +116,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
|
||||
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):
|
||||
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
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'
|
||||
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:]
|
||||
return ticks
|
||||
|
||||
|
||||
# FIX: fixturize this?
|
||||
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)
|
||||
patch_exchange(mocker)
|
||||
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)
|
||||
|
||||
args = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.BACKTEST)
|
||||
@ -217,10 +217,10 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
|
||||
)
|
||||
|
||||
args = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', '/foo/bar',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--enable-position-stacking',
|
||||
'--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)
|
||||
|
||||
args = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
args = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
args = get_args(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))
|
||||
backtesting = Backtesting(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.advise_buy)
|
||||
assert callable(backtesting.strategy.advise_sell)
|
||||
@ -522,7 +522,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
|
||||
backtesting = Backtesting(default_conf)
|
||||
pair = 'UNITTEST/BTC'
|
||||
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)
|
||||
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
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)
|
||||
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')
|
||||
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)
|
||||
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
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)
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
args = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', str(testdatadir),
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--timerange', '1510694220-1510700340',
|
||||
'--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)
|
||||
|
||||
args = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--datadir', str(testdatadir),
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--timerange', '1510694220-1510700340',
|
||||
'--enable-position-stacking',
|
||||
|
@ -15,9 +15,9 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
'edge',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'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 = [
|
||||
'edge',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', '/foo/bar',
|
||||
'edge',
|
||||
'--ticker-interval', '1m',
|
||||
'--timerange', ':100',
|
||||
'--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)
|
||||
|
||||
args = [
|
||||
'edge',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'edge'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_edge(args)
|
||||
|
@ -26,7 +26,10 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def hyperopt(default_conf, mocker):
|
||||
default_conf.update({'spaces': ['default']})
|
||||
default_conf.update({
|
||||
'spaces': ['default'],
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
})
|
||||
patch_exchange(mocker)
|
||||
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)
|
||||
|
||||
args = [
|
||||
'hyperopt',
|
||||
'--config', 'config.json',
|
||||
'hyperopt'
|
||||
'--hyperopt', 'DefaultHyperOpt',
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.HYPEROPT)
|
||||
@ -100,9 +104,10 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
|
||||
)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--datadir', '/foo/bar',
|
||||
'hyperopt',
|
||||
'--config', 'config.json',
|
||||
'--hyperopt', 'DefaultHyperOpt',
|
||||
'--datadir', '/foo/bar',
|
||||
'--ticker-interval', '1m',
|
||||
'--timerange', ':100',
|
||||
'--enable-position-stacking',
|
||||
@ -157,7 +162,8 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
|
||||
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_buy_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"})
|
||||
|
||||
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:
|
||||
@ -184,7 +198,7 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss',
|
||||
MagicMock(return_value=hl)
|
||||
)
|
||||
x = HyperOptLossResolver(default_conf, ).hyperoptloss
|
||||
x = HyperOptLossResolver(default_conf).hyperoptloss
|
||||
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"})
|
||||
|
||||
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:
|
||||
@ -203,8 +217,9 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--config', 'config.json',
|
||||
'--hyperopt', 'DefaultHyperOpt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
@ -220,8 +235,9 @@ def test_start(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--config', 'config.json',
|
||||
'--hyperopt', 'DefaultHyperOpt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
@ -242,8 +258,9 @@ def test_start_no_data(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--config', 'config.json',
|
||||
'--hyperopt', 'DefaultHyperOpt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
@ -258,8 +275,9 @@ def test_start_filelock(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--config', 'config.json',
|
||||
'--hyperopt', 'DefaultHyperOpt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
@ -412,6 +430,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'default',
|
||||
@ -539,10 +558,12 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
|
||||
|
||||
|
||||
def test_generate_optimizer(mocker, default_conf) -> None:
|
||||
default_conf.update({'config': 'config.json.example'})
|
||||
default_conf.update({'timerange': None})
|
||||
default_conf.update({'spaces': 'all'})
|
||||
default_conf.update({'hyperopt_min_trades': 1})
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
'hyperopt_min_trades': 1,
|
||||
})
|
||||
|
||||
trades = [
|
||||
('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):
|
||||
patch_exchange(mocker)
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'default',
|
||||
@ -626,6 +648,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog):
|
||||
def test_continue_hyperopt(mocker, default_conf, caplog):
|
||||
patch_exchange(mocker)
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'default',
|
||||
@ -656,6 +679,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
@ -732,6 +756,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'roi stoploss',
|
||||
@ -771,6 +796,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'roi stoploss',
|
||||
@ -813,6 +839,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
@ -847,6 +874,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'buy',
|
||||
@ -893,6 +921,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'sell',
|
||||
@ -941,6 +970,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'hyperopt': 'DefaultHyperOpt',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': space,
|
||||
|
@ -2,11 +2,13 @@
|
||||
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.constants import AVAILABLE_PAIRLISTS
|
||||
from freqtrade.resolvers import PairListResolver
|
||||
from tests.conftest import get_patched_freqtradebot
|
||||
import pytest
|
||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||
from tests.conftest import get_patched_freqtradebot, log_has_re
|
||||
|
||||
# whitelist, blacklist
|
||||
|
||||
@ -24,25 +26,39 @@ def whitelist_conf(default_conf):
|
||||
default_conf['exchange']['pair_blacklist'] = [
|
||||
'BLK/BTC'
|
||||
]
|
||||
default_conf['pairlist'] = {'method': 'StaticPairList',
|
||||
'config': {'number_assets': 3}
|
||||
}
|
||||
|
||||
default_conf['pairlists'] = [
|
||||
{
|
||||
"method": "VolumePairList",
|
||||
"number_assets": 5,
|
||||
"sort_key": "quoteVolume",
|
||||
},
|
||||
]
|
||||
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):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
bot = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
plm = PairListManager(bot.exchange, default_conf)
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Impossible to load Pairlist 'NonexistingPairList'. "
|
||||
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))
|
||||
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
|
||||
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||
# 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'])
|
||||
|
||||
|
||||
def test_refresh_pairlists(mocker, markets, whitelist_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, static_pl_conf)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
markets=PropertyMock(return_value=markets),
|
||||
)
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
# Ensure all except those in whitelist are removed
|
||||
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):
|
||||
whitelist_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 5}
|
||||
}
|
||||
def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf):
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
markets=PropertyMock(return_value=markets),
|
||||
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
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']
|
||||
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,
|
||||
match=r'`number_assets` not specified. Please check your configuration '
|
||||
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):
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
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)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("precision_filter,base_currency,key,whitelist_result", [
|
||||
(False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
|
||||
(False, "BTC", "bidVolume", ['LTC/BTC', 'TKN/BTC', 'ETH/BTC']),
|
||||
(False, "USDT", "quoteVolume", ['ETH/USDT']),
|
||||
(False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange
|
||||
(True, "BTC", "quoteVolume", ["LTC/BTC", "ETH/BTC", "TKN/BTC"]),
|
||||
(True, "BTC", "bidVolume", ["LTC/BTC", "TKN/BTC", "ETH/BTC"])
|
||||
@pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
||||
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']),
|
||||
# Different sorting depending on quote or bid volume
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
||||
"BTC", ['HOT/BTC', 'FUEL/BTC', '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,
|
||||
whitelist_result, precision_filter) -> None:
|
||||
whitelist_conf['pairlist']['method'] = 'VolumePairList'
|
||||
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||
pairlists, base_currency, whitelist_result,
|
||||
caplog) -> None:
|
||||
whitelist_conf['pairlists'] = pairlists
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
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
|
||||
whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key)
|
||||
assert sorted(whitelist) == sorted(whitelist_result)
|
||||
freqtrade.pairlists.refresh_pairlist()
|
||||
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:
|
||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 10}
|
||||
}
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList',
|
||||
'config': {'number_assets': 10}
|
||||
}]
|
||||
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
get_tickers=tickers,
|
||||
exchange_has=MagicMock(return_value=False),
|
||||
)
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
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)
|
||||
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
|
||||
whitelist_conf['pairlist']['method'] = pairlist
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
whitelist_conf['pairlists'][0]['method'] = pairlist
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True)
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
|
||||
assert freqtrade.pairlists.name == pairlist
|
||||
assert pairlist in freqtrade.pairlists.short_desc()
|
||||
assert freqtrade.pairlists.name_list == [pairlist]
|
||||
assert pairlist in str(freqtrade.pairlists.short_desc())
|
||||
assert isinstance(freqtrade.pairlists.whitelist, 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("whitelist,log_message", [
|
||||
(['ETH/BTC', 'TKN/BTC'], ""),
|
||||
(['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), # TRX/ETH wrong stake
|
||||
(['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BCH/BTC not available
|
||||
(['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "is not compatible with exchange"), # BLK/BTC in blacklist
|
||||
(['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") # BTT/BTC is inactive
|
||||
# TRX/ETH not in markets
|
||||
(['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"),
|
||||
# wrong stake
|
||||
(['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,
|
||||
log_message):
|
||||
whitelist_conf['pairlist']['method'] = pairlist
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog,
|
||||
log_message, tickers):
|
||||
whitelist_conf['pairlists'][0]['method'] = pairlist
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
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 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)
|
||||
|
@ -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:
|
||||
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.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
@ -109,22 +114,34 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
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()
|
||||
result = rpc._rpc_status_table()
|
||||
assert 'instantly' in result['Since'].all()
|
||||
assert 'ETH/BTC' in result['Pair'].all()
|
||||
assert '-0.59%' in result['Profit'].all()
|
||||
|
||||
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%' == 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',
|
||||
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||
# invalidate ticker cache
|
||||
rpc._freqtrade.exchange._cached_ticker = {}
|
||||
result = rpc._rpc_status_table()
|
||||
assert 'instantly' in result['Since'].all()
|
||||
assert 'ETH/BTC' in result['Pair'].all()
|
||||
assert 'nan%' in result['Profit'].all()
|
||||
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
assert 'instantly' == result[0][2]
|
||||
assert 'ETH/BTC' == result[0][1]
|
||||
assert 'nan%' == result[0][3]
|
||||
|
||||
|
||||
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)
|
||||
rpc = RPC(freqtradebot)
|
||||
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']
|
||||
|
||||
|
||||
def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
|
||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 4}
|
||||
}
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList',
|
||||
'number_assets': 4,
|
||||
}]
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
ret = rpc._rpc_whitelist()
|
||||
assert ret['method'] == 'VolumePairList'
|
||||
assert len(ret['method']) == 1
|
||||
assert 'VolumePairList' in ret['method']
|
||||
assert ret['length'] == 4
|
||||
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)
|
||||
rpc = RPC(freqtradebot)
|
||||
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 ret['blacklist'] == default_conf['exchange']['pair_blacklist']
|
||||
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC']
|
||||
|
||||
ret = rpc._rpc_blacklist(["ETH/BTC"])
|
||||
assert ret['method'] == 'StaticPairList'
|
||||
assert 'StaticPairList' in ret['method']
|
||||
assert len(ret['blacklist']) == 3
|
||||
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
|
||||
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC']
|
||||
|
@ -64,6 +64,10 @@ def test_api_not_found(botclient):
|
||||
|
||||
def test_api_unauthorized(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
|
||||
rc = client.get(f"{BASE_URI}/version")
|
||||
assert_response(rc, 401)
|
||||
@ -280,6 +284,18 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
|
||||
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):
|
||||
ftbot, client = botclient
|
||||
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")
|
||||
assert_response(rc, 502)
|
||||
assert rc.json == {'error': 'Error querying _status: no active trade'}
|
||||
assert_response(rc, 200)
|
||||
assert rc.json == []
|
||||
|
||||
ftbot.create_trades()
|
||||
rc = client_get(client, f"{BASE_URI}/status")
|
||||
@ -456,7 +472,7 @@ def test_api_blacklist(botclient, mocker):
|
||||
assert_response(rc)
|
||||
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"],
|
||||
"length": 2,
|
||||
"method": "StaticPairList"}
|
||||
"method": ["StaticPairList"]}
|
||||
|
||||
# Add ETH/BTC to blacklist
|
||||
rc = client_post(client, f"{BASE_URI}/blacklist",
|
||||
@ -464,7 +480,7 @@ def test_api_blacklist(botclient, mocker):
|
||||
assert_response(rc)
|
||||
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
|
||||
"length": 3,
|
||||
"method": "StaticPairList"}
|
||||
"method": ["StaticPairList"]}
|
||||
|
||||
|
||||
def test_api_whitelist(botclient):
|
||||
@ -474,7 +490,7 @@ def test_api_whitelist(botclient):
|
||||
assert_response(rc)
|
||||
assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'],
|
||||
"length": 4,
|
||||
"method": "StaticPairList"}
|
||||
"method": ["StaticPairList"]}
|
||||
|
||||
|
||||
def test_api_forcebuy(botclient, mocker, fee):
|
||||
|
@ -1,5 +1,5 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
import time
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@ -176,6 +176,8 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None:
|
||||
"listen_port": "8080"}
|
||||
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 len(rpc_manager.registered_modules) == 1
|
||||
assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules]
|
||||
|
@ -73,7 +73,7 @@ def test_init(default_conf, mocker, caplog) -> None:
|
||||
|
||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
|
||||
"['performance'], ['daily'], ['count'], ['reload_conf'], " \
|
||||
"['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " \
|
||||
"['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]"
|
||||
|
||||
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())
|
||||
assert msg_mock.call_count == 1
|
||||
assert ('Using whitelist `StaticPairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`'
|
||||
in msg_mock.call_args_list[0][0][0])
|
||||
assert ("Using whitelist `['StaticPairList']` with 4 pairs\n"
|
||||
"`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:
|
||||
@ -1062,17 +1062,17 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None:
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 4}
|
||||
}
|
||||
default_conf['pairlists'] = [{'method': 'VolumePairList',
|
||||
'number_assets': 4
|
||||
}]
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
telegram._whitelist(update=update, context=MagicMock())
|
||||
assert msg_mock.call_count == 1
|
||||
assert ('Using whitelist `VolumePairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`'
|
||||
in msg_mock.call_args_list[0][0][0])
|
||||
assert ("Using whitelist `['VolumePairList']` with 4 pairs\n"
|
||||
"`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:
|
||||
@ -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]
|
||||
|
||||
|
||||
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:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
|
@ -54,21 +54,30 @@ def test_load_strategy_base64(result, caplog, default_conf):
|
||||
|
||||
|
||||
def test_load_strategy_invalid_directory(result, caplog, default_conf):
|
||||
default_conf['strategy'] = 'SampleStrategy'
|
||||
resolver = StrategyResolver(default_conf)
|
||||
extra_dir = Path.cwd() / 'some/path'
|
||||
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 '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):
|
||||
strategy = StrategyResolver(default_conf)
|
||||
default_conf['strategy'] = 'NotFoundStrategy'
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Impossible to load Strategy 'NotFoundStrategy'. "
|
||||
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):
|
||||
|
@ -11,7 +11,7 @@ from freqtrade.configuration.cli_options import check_int_positive
|
||||
|
||||
# Parse common command-line-arguments. Used for all tools
|
||||
def test_parse_args_none() -> None:
|
||||
arguments = Arguments([])
|
||||
arguments = Arguments(['trade'])
|
||||
assert isinstance(arguments, Arguments)
|
||||
x = arguments.get_parsed_arg()
|
||||
assert isinstance(x, dict)
|
||||
@ -19,7 +19,7 @@ def test_parse_args_none() -> 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["strategy_path"] is None
|
||||
assert args["datadir"] is None
|
||||
@ -27,27 +27,27 @@ def test_parse_args_defaults() -> 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']
|
||||
|
||||
args = Arguments(['--config', '/dev/null']).get_parsed_arg()
|
||||
args = Arguments(['trade', '--config', '/dev/null']).get_parsed_arg()
|
||||
assert args["config"] == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null',
|
||||
args = Arguments(['trade', '--config', '/dev/null',
|
||||
'--config', '/dev/zero'],).get_parsed_arg()
|
||||
assert args["config"] == ['/dev/null', '/dev/zero']
|
||||
|
||||
|
||||
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'
|
||||
|
||||
|
||||
def test_parse_args_verbose() -> None:
|
||||
args = Arguments(['-v']).get_parsed_arg()
|
||||
args = Arguments(['trade', '-v']).get_parsed_arg()
|
||||
assert args["verbosity"] == 1
|
||||
|
||||
args = Arguments(['--verbose']).get_parsed_arg()
|
||||
args = Arguments(['trade', '--verbose']).get_parsed_arg()
|
||||
assert args["verbosity"] == 1
|
||||
|
||||
|
||||
@ -69,7 +69,7 @@ def test_parse_args_invalid() -> 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'
|
||||
|
||||
|
||||
@ -79,7 +79,7 @@ def test_parse_args_strategy_invalid() -> 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'
|
||||
|
||||
|
||||
@ -98,8 +98,8 @@ def test_parse_args_backtesting_invalid() -> None:
|
||||
|
||||
def test_parse_args_backtesting_custom() -> None:
|
||||
args = [
|
||||
'-c', 'test_conf.json',
|
||||
'backtesting',
|
||||
'-c', 'test_conf.json',
|
||||
'--ticker-interval', '1m',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
@ -108,7 +108,7 @@ def test_parse_args_backtesting_custom() -> None:
|
||||
call_args = Arguments(args).get_parsed_arg()
|
||||
assert call_args["config"] == ['test_conf.json']
|
||||
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["ticker_interval"] == '1m'
|
||||
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:
|
||||
args = [
|
||||
'-c', 'test_conf.json',
|
||||
'hyperopt',
|
||||
'-c', 'test_conf.json',
|
||||
'--epochs', '20',
|
||||
'--spaces', 'buy'
|
||||
]
|
||||
@ -126,7 +126,7 @@ def test_parse_args_hyperopt_custom() -> None:
|
||||
assert call_args["config"] == ['test_conf.json']
|
||||
assert call_args["epochs"] == 20
|
||||
assert call_args["verbosity"] == 0
|
||||
assert call_args["subparser"] == 'hyperopt'
|
||||
assert call_args["command"] == 'hyperopt'
|
||||
assert call_args["spaces"] == ['buy']
|
||||
assert call_args["func"] is not None
|
||||
assert callable(call_args["func"])
|
||||
@ -134,8 +134,8 @@ def test_parse_args_hyperopt_custom() -> None:
|
||||
|
||||
def test_download_data_options() -> None:
|
||||
args = [
|
||||
'--datadir', 'datadir/directory',
|
||||
'download-data',
|
||||
'--datadir', 'datadir/directory',
|
||||
'--pairs-file', 'file_with_pairs',
|
||||
'--days', '30',
|
||||
'--exchange', 'binance'
|
||||
@ -150,8 +150,8 @@ def test_download_data_options() -> None:
|
||||
|
||||
def test_plot_dataframe_options() -> None:
|
||||
args = [
|
||||
'-c', 'config.json.example',
|
||||
'plot-dataframe',
|
||||
'-c', 'config.json.example',
|
||||
'--indicators1', 'sma10', 'sma100',
|
||||
'--indicators2', 'macd', 'fastd', 'fastk',
|
||||
'--plot-limit', '30',
|
||||
@ -186,7 +186,7 @@ def test_config_notallowed(mocker) -> None:
|
||||
]
|
||||
pargs = Arguments(args).get_parsed_arg()
|
||||
|
||||
assert pargs["config"] is None
|
||||
assert "config" not in pargs
|
||||
|
||||
# When file exists:
|
||||
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()
|
||||
# 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:
|
||||
|
@ -68,7 +68,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
|
||||
def test__args_to_config(caplog):
|
||||
|
||||
arg_list = ['--strategy-path', 'TestTest']
|
||||
arg_list = ['trade', '--strategy-path', 'TestTest']
|
||||
args = Arguments(arg_list).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
config = {}
|
||||
@ -96,7 +96,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||
default_conf['max_open_trades'] = 0
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
args = Arguments(['trade']).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@ -121,7 +121,7 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
||||
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()
|
||||
configuration = Configuration(args)
|
||||
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
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
args = Arguments(['trade']).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
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:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
args = Arguments(['trade']).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
||||
assert validated_conf.get('strategy_path') is None
|
||||
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)
|
||||
|
||||
arglist = [
|
||||
'trade',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path',
|
||||
'--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)
|
||||
|
||||
arglist = [
|
||||
'trade',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--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)
|
||||
|
||||
arglist = [
|
||||
'trade',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--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)
|
||||
|
||||
arglist = [
|
||||
'trade',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--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)
|
||||
|
||||
arglist = [
|
||||
'trade',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--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
|
||||
|
||||
|
||||
@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:
|
||||
default_conf.update({
|
||||
'strategy': 'CustomStrategy',
|
||||
@ -310,7 +331,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
})
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
args = Arguments(['trade']).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
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)
|
||||
|
||||
arglist = [
|
||||
'trade',
|
||||
'--strategy', 'TestStrategy',
|
||||
'--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)
|
||||
|
||||
arglist = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
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)
|
||||
)
|
||||
arglist = [
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', '/foo/bar',
|
||||
'--userdir', "/tmp/freqtrade",
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--enable-position-stacking',
|
||||
'--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)
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'backtesting',
|
||||
'--config', 'config.json',
|
||||
'--ticker-interval', '1m',
|
||||
'--export', '/bar/foo',
|
||||
'--strategy-list',
|
||||
@ -568,7 +590,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||
|
||||
# Prevent setting loggers
|
||||
mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
|
||||
arglist = ['-vvv']
|
||||
arglist = ['trade', '-vvv']
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
@ -620,7 +642,7 @@ def test_set_logfile(default_conf, mocker):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
arglist = [
|
||||
'--logfile', 'test_file.log',
|
||||
'trade', '--logfile', 'test_file.log',
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
@ -636,7 +658,7 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
args = Arguments(['trade']).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
@ -755,9 +777,9 @@ def test_validate_whitelist(default_conf):
|
||||
|
||||
conf = deepcopy(default_conf)
|
||||
|
||||
conf.update({"pairlist": {
|
||||
conf.update({"pairlists": [{
|
||||
"method": "VolumePairList",
|
||||
}})
|
||||
}]})
|
||||
# Dynamic whitelist should not care about pair_whitelist
|
||||
validate_config_consistency(conf)
|
||||
del conf['exchange']['pair_whitelist']
|
||||
@ -847,8 +869,8 @@ def test_pairlist_resolving():
|
||||
def test_pairlist_resolving_with_config(mocker, default_conf):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'download-data',
|
||||
'--config', 'config.json',
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
@ -861,8 +883,8 @@ def test_pairlist_resolving_with_config(mocker, default_conf):
|
||||
|
||||
# Override pairs
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'download-data',
|
||||
'--config', 'config.json',
|
||||
'--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()))
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'download-data',
|
||||
'--config', 'config.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))
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'download-data',
|
||||
'--config', 'config.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]
|
||||
|
||||
|
||||
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):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
|
12
tests/test_docs.sh
Executable file
12
tests/test_docs.sh
Executable 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
|
@ -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,
|
||||
fee, mocker) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
cancel_order_mock = MagicMock()
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'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
|
||||
|
||||
|
||||
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:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
|
@ -11,10 +11,16 @@ from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.main import main
|
||||
from freqtrade.state import State
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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]
|
||||
assert call_args["config"] == ['config.json']
|
||||
assert call_args["verbosity"] == 0
|
||||
assert call_args["subparser"] == 'backtesting'
|
||||
assert call_args["command"] == 'backtesting'
|
||||
assert call_args["func"] is not None
|
||||
assert callable(call_args["func"])
|
||||
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]
|
||||
assert call_args["config"] == ['config.json']
|
||||
assert call_args["verbosity"] == 0
|
||||
assert call_args["subparser"] == 'hyperopt'
|
||||
assert call_args["command"] == 'hyperopt'
|
||||
assert call_args["func"] is not None
|
||||
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.persistence.init', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
args = ['trade', '-c', 'config.json.example']
|
||||
|
||||
# Test Main + the KeyboardInterrupt exception
|
||||
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.persistence.init', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
args = ['trade', '-c', 'config.json.example']
|
||||
|
||||
# Test Main + the KeyboardInterrupt exception
|
||||
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.persistence.init', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
args = ['trade', '-c', 'config.json.example']
|
||||
|
||||
# Test Main + the KeyboardInterrupt exception
|
||||
with pytest.raises(SystemExit):
|
||||
@ -114,15 +120,15 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||
OperationalException("Oh snap!")])
|
||||
mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
|
||||
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.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)
|
||||
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 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.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)
|
||||
freqtrade = worker.freqtrade
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user